package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/btcsuite/btcd/btcutil"
	"github.com/lightninglabs/loop"
	"github.com/lightninglabs/loop/looprpc"
	"github.com/lightninglabs/taproot-assets/rfqmath"
	"github.com/lightninglabs/taproot-assets/rpcutils"
	"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/lightningnetwork/lnd/routing/route"
	"github.com/urfave/cli/v3"
)

var quoteCommand = &cli.Command{
	Name:     "quote",
	Usage:    "get a quote for the cost of a swap",
	Commands: []*cli.Command{quoteInCommand, quoteOutCommand},
}

var quoteInCommand = &cli.Command{
	Name:      "in",
	Usage:     "get a quote for the cost of a loop in swap",
	ArgsUsage: "amt",
	Description: "Allows to determine the cost of a swap up front." +
		"Either specify an amount or deposit outpoints.",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name: lastHopFlag.Name,
			Usage: "the pubkey of the last hop to use for the " +
				"quote",
		},
		confTargetFlag,
		verboseFlag,
		privateFlag,
		routeHintsFlag,
		&cli.StringSliceFlag{
			Name: "deposit_outpoint",
			Usage: "one or more static address deposit outpoints " +
				"to quote for. Deposit outpoints are not to " +
				"be used in combination with an amount. Each" +
				"additional outpoint can be added by " +
				"specifying --deposit_outpoint tx_id:idx",
		},
	},
	Action: quoteIn,
}

func quoteIn(ctx context.Context, cmd *cli.Command) error {
	// Show command help if the incorrect number arguments was provided.
	if cmd.NArg() != 1 && !cmd.IsSet("deposit_outpoint") {
		return showCommandHelp(ctx, cmd)
	}

	var (
		manualAmt        btcutil.Amount
		depositAmt       btcutil.Amount
		depositOutpoints []string
		err              error
	)

	if cmd.NArg() == 1 {
		args := cmd.Args()
		manualAmt, err = parseAmt(args.First())
		if err != nil {
			return err
		}
	}

	client, cleanup, err := getClient(cmd)
	if err != nil {
		return err
	}
	defer cleanup()

	// Private and routehints are mutually exclusive as setting private
	// means we retrieve our own routehints from the connected node.
	hints, err := validateRouteHints(cmd)
	if err != nil {
		return err
	}

	if cmd.IsSet("deposit_outpoint") {
		depositOutpoints = cmd.StringSlice("deposit_outpoint")
		depositAmt, err = depositAmount(ctx, client, depositOutpoints)
		if err != nil {
			return err
		}
	}

	quoteReq := &looprpc.QuoteRequest{
		Amt:              int64(manualAmt),
		ConfTarget:       int32(cmd.Uint64("conf_target")),
		LoopInRouteHints: hints,
		Private:          cmd.Bool(privateFlag.Name),
		DepositOutpoints: depositOutpoints,
	}

	if cmd.IsSet(lastHopFlag.Name) {
		lastHopVertex, err := route.NewVertexFromStr(
			cmd.String(lastHopFlag.Name),
		)
		if err != nil {
			return err
		}

		quoteReq.LoopInLastHop = lastHopVertex[:]
	}

	quoteResp, err := client.GetLoopInQuote(ctx, quoteReq)
	if err != nil {
		return err
	}

	// For loop in, the fee estimation is handed to lnd which tries to
	// construct a real transaction to sample realistic fees to pay to the
	// HTLC. If the wallet doesn't have enough funds to create this TX, we
	// don't want to fail the quote. But the user should still be informed
	// why the fee shows as -1.
	if quoteResp.HtlcPublishFeeSat == int64(loop.MinerFeeEstimationFailed) {
		_, _ = fmt.Fprintf(os.Stderr, "Warning: Miner fee estimation "+
			"not possible, lnd has insufficient funds to "+
			"create a sample transaction for selected "+
			"amount.\n")
	}

	// If the user specified static address deposits, we quoted for their
	// total value and need to display that value instead of the manually
	// selected one.
	if manualAmt == 0 {
		quoteReq.Amt = int64(depositAmt)
	}
	printQuoteInResp(quoteReq, quoteResp, cmd.Bool("verbose"))
	return nil
}

func depositAmount(ctx context.Context, client looprpc.SwapClientClient,
	depositOutpoints []string) (btcutil.Amount, error) {

	addressSummary, err := client.ListStaticAddressDeposits(
		ctx, &looprpc.ListStaticAddressDepositsRequest{
			Outpoints: depositOutpoints,
		},
	)
	if err != nil {
		return 0, err
	}

	var depositAmt btcutil.Amount
	for _, deposit := range addressSummary.FilteredDeposits {
		depositAmt += btcutil.Amount(deposit.Value)
	}

	return depositAmt, nil
}

var quoteOutCommand = &cli.Command{
	Name:        "out",
	Usage:       "get a quote for the cost of a loop out swap",
	ArgsUsage:   "amt",
	Description: "Allows to determine the cost of a swap up front",
	Flags: []cli.Flag{
		&cli.Uint64Flag{
			Name: "conf_target",
			Usage: "the number of blocks from the swap " +
				"initiation height that the on-chain HTLC " +
				"should be swept within in a Loop Out",
			Value: uint64(loop.DefaultSweepConfTarget),
		},
		&cli.BoolFlag{
			Name: "fast",
			Usage: "Indicate you want to swap immediately, " +
				"paying potentially a higher fee. If not " +
				"set the swap server might choose to wait up " +
				"to 30 minutes before publishing the swap " +
				"HTLC on-chain, to save on chain fees. Not " +
				"setting this flag might result in a lower " +
				"swap fee.",
		},
		verboseFlag,
	},
	Action: quoteOut,
}

func quoteOut(ctx context.Context, cmd *cli.Command) error {
	// Show command help if the incorrect number arguments was provided.
	if cmd.NArg() != 1 {
		return showCommandHelp(ctx, cmd)
	}

	args := cmd.Args()
	amt, err := parseAmt(args.First())
	if err != nil {
		return err
	}

	client, cleanup, err := getClient(cmd)
	if err != nil {
		return err
	}
	defer cleanup()

	fast := cmd.Bool("fast")
	swapDeadline := time.Now()
	if !fast {
		swapDeadline = time.Now().Add(defaultSwapWaitTime)
	}

	quoteReq := &looprpc.QuoteRequest{
		Amt:                     int64(amt),
		ConfTarget:              int32(cmd.Uint64("conf_target")),
		SwapPublicationDeadline: uint64(swapDeadline.Unix()),
	}
	quoteResp, err := client.LoopOutQuote(ctx, quoteReq)
	if err != nil {
		return err
	}

	printQuoteOutResp(quoteReq, quoteResp, cmd.Bool("verbose"))
	return nil
}

func printQuoteInResp(req *looprpc.QuoteRequest,
	resp *looprpc.InQuoteResponse, verbose bool) {

	totalFee := resp.HtlcPublishFeeSat + resp.SwapFeeSat
	amt := req.Amt
	if amt == 0 {
		amt = resp.QuotedAmt
	}

	if req.DepositOutpoints != nil {
		fmt.Printf(satAmtFmt, "Previously deposited on-chain:",
			amt)
	} else {
		fmt.Printf(satAmtFmt, "Send on-chain:", amt)
	}
	fmt.Printf(satAmtFmt, "Receive off-chain:", amt-totalFee)

	switch {
	case req.ExternalHtlc && !verbose:
		// If it's external, then we don't know the miner fee hence the
		// total cost.
		fmt.Printf(satAmtFmt, "Loop service fee:", resp.SwapFeeSat)

	case req.ExternalHtlc && verbose:
		fmt.Printf(satAmtFmt, "Loop service fee:", resp.SwapFeeSat)
		fmt.Println()
		fmt.Printf(blkFmt, "CLTV expiry delta:", resp.CltvDelta)

	case verbose:
		fmt.Println()
		fmt.Printf(
			satAmtFmt, "Estimated on-chain fee:",
			resp.HtlcPublishFeeSat,
		)
		fmt.Printf(satAmtFmt, "Loop service fee:", resp.SwapFeeSat)
		fmt.Printf(satAmtFmt, "Estimated total fee:", totalFee)
		fmt.Println()
		fmt.Printf(blkFmt, "Conf target:", resp.ConfTarget)
		fmt.Printf(blkFmt, "CLTV expiry delta:", resp.CltvDelta)
	default:
		fmt.Printf(satAmtFmt, "Estimated total fee:", totalFee)
	}
}

func printQuoteOutResp(req *looprpc.QuoteRequest,
	resp *looprpc.OutQuoteResponse, verbose bool) {

	totalFee := resp.HtlcSweepFeeSat + resp.SwapFeeSat

	if resp.AssetRfqInfo != nil {
		assetAmtSwap, err := getAssetAmt(
			req.Amt, resp.AssetRfqInfo.SwapAssetRate,
		)
		if err != nil {
			fmt.Printf("Error converting asset amount: %v\n", err)
			return
		}
		exchangeRate := float64(assetAmtSwap) / float64(req.Amt)
		fmt.Printf(assetAmtFmt, "Send off-chain:",
			assetAmtSwap, resp.AssetRfqInfo.AssetName)
		fmt.Printf(rateFmt, "Exchange rate:",
			exchangeRate, resp.AssetRfqInfo.AssetName)
		fmt.Printf(assetAmtFmt, "Limit Send off-chain:",
			resp.AssetRfqInfo.MaxSwapAssetAmt,
			resp.AssetRfqInfo.AssetName)
	} else {
		fmt.Printf(satAmtFmt, "Send off-chain:", req.Amt)
	}

	fmt.Printf(satAmtFmt, "Receive on-chain:", req.Amt-totalFee)

	if !verbose {
		fmt.Printf(satAmtFmt, "Estimated total fee:", totalFee)
		return
	}

	fmt.Println()
	fmt.Printf(satAmtFmt, "Estimated on-chain fee:", resp.HtlcSweepFeeSat)
	fmt.Printf(satAmtFmt, "Loop service fee:", resp.SwapFeeSat)
	fmt.Printf(satAmtFmt, "Estimated total fee:", totalFee)
	fmt.Println()
	if resp.AssetRfqInfo != nil {
		assetAmtPrepay, err := getAssetAmt(
			resp.PrepayAmtSat, resp.AssetRfqInfo.PrepayAssetRate,
		)
		if err != nil {
			fmt.Printf("Error converting asset amount: %v\n", err)
			return
		}
		fmt.Printf(assetAmtFmt, "No show penalty (prepay):",
			assetAmtPrepay,
			resp.AssetRfqInfo.AssetName)
		fmt.Printf(assetAmtFmt, "Limit no show penalty (prepay):",
			resp.AssetRfqInfo.MaxPrepayAssetAmt,
			resp.AssetRfqInfo.AssetName)
	} else {
		fmt.Printf(satAmtFmt, "No show penalty (prepay):",
			resp.PrepayAmtSat)
	}
	fmt.Printf(blkFmt, "Conf target:", resp.ConfTarget)
	fmt.Printf(blkFmt, "CLTV expiry delta:", resp.CltvDelta)
	fmt.Printf("%-38s %s\n",
		"Publication deadline:",
		time.Unix(int64(req.SwapPublicationDeadline), 0),
	)
}

// getAssetAmt returns the asset amount for the given amount in satoshis and
// the asset rate.
func getAssetAmt(amt int64, assetRate *looprpc.FixedPoint) (
	uint64, error) {

	askAssetRate, err := unmarshalFixedPoint(assetRate)
	if err != nil {
		return 0, err
	}

	msatAmt := lnwire.MilliSatoshi((amt * 1000))

	assetAmt := rfqmath.MilliSatoshiToUnits(msatAmt, *askAssetRate)

	return assetAmt.ToUint64(), nil
}

// unmarshalFixedPoint converts an RPC FixedPoint to a BigIntFixedPoint.
func unmarshalFixedPoint(fp *looprpc.FixedPoint) (*rfqmath.BigIntFixedPoint,
	error) {

	// convert the looprpc.FixedPoint to a rfqrpc.FixedPoint
	rfqrpcFP := &rfqrpc.FixedPoint{
		Coefficient: fp.Coefficient,
		Scale:       fp.Scale,
	}

	return rpcutils.UnmarshalRfqFixedPoint(rfqrpcFP)
}
