package main

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"

	"github.com/lightninglabs/loop/liquidity"
	"github.com/lightninglabs/loop/looprpc"
	"github.com/lightningnetwork/lnd/routing/route"
	"github.com/urfave/cli/v3"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

var getLiquidityParamsCommand = &cli.Command{
	Name:  "getparams",
	Usage: "show liquidity manager parameters",
	Description: "Displays the current set of parameters that are set " +
		"for the liquidity manager.",
	Action: getParams,
}

func getParams(ctx context.Context, cmd *cli.Command) error {
	client, cleanup, err := getClient(cmd)
	if err != nil {
		return err
	}
	defer cleanup()

	cfg, err := client.GetLiquidityParams(
		ctx, &looprpc.GetLiquidityParamsRequest{},
	)
	if err != nil {
		return err
	}

	printRespJSON(cfg)

	return nil
}

var setLiquidityRuleCommand = &cli.Command{
	Name:        "setrule",
	Usage:       "set liquidity manager rule for a channel/peer",
	Description: "Update or remove the liquidity rule for a channel/peer.",
	ArgsUsage:   "{shortchanid | peerpubkey}",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name: "type",
			Usage: "the type of swap to perform, set to 'out' " +
				"for acquiring inbound liquidity or 'in' for " +
				"acquiring outbound liquidity.",
			Value: "out",
		},

		&cli.IntFlag{
			Name: "incoming_threshold",
			Usage: "the minimum percentage of incoming liquidity " +
				"to total capacity beneath which to " +
				"recommend loop out to acquire incoming.",
		},
		&cli.IntFlag{
			Name: "outgoing_threshold",
			Usage: "the minimum percentage of outbound liquidity " +
				"that we do not want to drop below.",
		},
		&cli.BoolFlag{
			Name: "clear",
			Usage: "remove the rule currently set for the " +
				"channel/peer.",
		},
	},
	Action: setRule,
}

func setRule(ctx context.Context, cmd *cli.Command) error {
	// We require that a channel ID is set for this rule update.
	if cmd.NArg() != 1 {
		return fmt.Errorf("please set a channel id or peer pubkey " +
			"for the rule update")
	}

	var (
		pubkey     route.Vertex
		pubkeyRule bool
	)
	chanID, err := strconv.ParseUint(cmd.Args().First(), 10, 64)
	if err != nil {
		pubkey, err = route.NewVertexFromStr(cmd.Args().First())
		if err != nil {
			return fmt.Errorf("please provide a valid pubkey: "+
				"%v, or short channel ID", err)
		}
		pubkeyRule = true
	}

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

	// We need to set the full set of current parameters every time we call
	// SetParameters. To allow users to set only individual fields on the
	// cli, we look up our current params, then update individual values.
	params, err := client.GetLiquidityParams(
		ctx, &looprpc.GetLiquidityParamsRequest{},
	)
	if err != nil {
		return err
	}

	var (
		inboundSet  = cmd.IsSet("incoming_threshold")
		outboundSet = cmd.IsSet("outgoing_threshold")
		ruleSet     bool
		otherRules  []*looprpc.LiquidityRule
	)

	// Run through our current set of rules and check whether we have a rule
	// currently set for this channel or peer. We also track a slice
	// containing all of the rules we currently have set for other channels,
	// and peers because we want to leave these rules untouched.
	for _, rule := range params.Rules {
		var (
			channelRuleSet = rule.ChannelId != 0 &&
				rule.ChannelId == chanID

			peerRuleSet = rule.Pubkey != nil && bytes.Equal(
				rule.Pubkey, pubkey[:],
			)
		)

		if channelRuleSet || peerRuleSet {
			ruleSet = true
		} else {
			otherRules = append(otherRules, rule)
		}
	}

	// If we want to clear the rule for this channel, check that we had a
	// rule set in the first place, and set our parameters to the current
	// set excluding the channel specified.
	if cmd.IsSet("clear") {
		if !ruleSet {
			return fmt.Errorf("cannot clear channel: %v, no rule "+
				"set at present", chanID)
		}

		if inboundSet || outboundSet {
			return fmt.Errorf("do not set other flags with clear " +
				"flag")
		}

		params.Rules = otherRules
		_, err = client.SetLiquidityParams(
			ctx,
			&looprpc.SetLiquidityParamsRequest{
				Parameters: params,
			},
		)
		return err
	}

	// If we are setting a rule for this channel (not clearing it), check
	// that at least one value is set.
	if !inboundSet && !outboundSet {
		return fmt.Errorf("provide at least one flag to set rules or " +
			"use the --clear flag to remove rules")
	}

	// Create a new rule which will be used to overwrite our current rule.
	newRule := &looprpc.LiquidityRule{
		ChannelId: chanID,
		Type:      looprpc.LiquidityRuleType_THRESHOLD,
	}
	if cmd.IsSet("type") {
		switch cmd.String("type") {
		case "in":
			newRule.SwapType = looprpc.SwapType_LOOP_IN

		case "out":
			newRule.SwapType = looprpc.SwapType_LOOP_OUT

		default:
			return errors.New("please set type to in or out")
		}
	}

	if pubkeyRule {
		newRule.Pubkey = pubkey[:]
	}

	if inboundSet {
		newRule.IncomingThreshold = uint32(
			cmd.Int("incoming_threshold"),
		)
	}

	if outboundSet {
		newRule.OutgoingThreshold = uint32(
			cmd.Int("outgoing_threshold"),
		)
	}

	// Just set the rules on our current set of parameters and leave the
	// other values untouched.
	otherRules = append(otherRules, newRule)
	params.Rules = otherRules

	// Update our parameters to the existing set, plus our new rule.
	_, err = client.SetLiquidityParams(
		ctx,
		&looprpc.SetLiquidityParamsRequest{
			Parameters: params,
		},
	)

	return err
}

var setParamsCommand = &cli.Command{
	Name:  "setparams",
	Usage: "update the parameters set for the liquidity manager",
	Description: "Updates the parameters set for the liquidity manager. " +
		"Note the parameters are persisted in db to save the trouble " +
		"of setting them again upon loopd restart. To get the default" +
		"values, use `getparams` before any `setparams`.",
	Flags: []cli.Flag{
		&cli.IntFlag{
			Name: "sweeplimit",
			Usage: "the limit placed on our estimated sweep fee " +
				"in sat/vByte.",
		},
		&cli.Float64Flag{
			Name: "feepercent",
			Usage: "the maximum percentage of swap amount to be " +
				"used across all fee categories",
		},
		&cli.Float64Flag{
			Name: "maxswapfee",
			Usage: "the maximum percentage of swap volume we are " +
				"willing to pay in server fees.",
		},
		&cli.Float64Flag{
			Name: "maxroutingfee",
			Usage: "the maximum percentage of off-chain payment " +
				"volume that we are willing to pay in routing" +
				"fees.",
		},
		&cli.Float64Flag{
			Name: "maxprepayfee",
			Usage: "the maximum percentage of off-chain prepay " +
				"volume that we are willing to pay in " +
				"routing fees.",
		},
		&cli.Uint64Flag{
			Name: "maxprepay",
			Usage: "the maximum no-show (prepay) in satoshis that " +
				"swap suggestions should be limited to.",
		},
		&cli.Uint64Flag{
			Name: "maxminer",
			Usage: "the maximum miner fee in satoshis that swap " +
				"suggestions should be limited to.",
		},
		&cli.IntFlag{
			Name: "sweepconf",
			Usage: "the number of blocks from htlc height that " +
				"swap suggestion sweeps should target, used " +
				"to estimate max miner fee.",
		},
		&cli.Uint64Flag{
			Name: "failurebackoff",
			Usage: "the amount of time, in seconds, that " +
				"should pass before a channel that " +
				"previously had a failed swap will be " +
				"included in suggestions.",
		},
		&cli.BoolFlag{
			Name: "autoloop",
			Usage: "set to true to enable automated dispatch " +
				"of swaps, limited to the budget set by " +
				"autobudget.",
		},
		&cli.StringFlag{
			Name: "destaddr",
			Usage: "custom address to be used as destination for " +
				"autoloop loop out, set to \"default\" in " +
				"order to revert to default behavior.",
		},
		&cli.StringFlag{
			Name: "account",
			Usage: "the name of the account to generate a new " +
				"address from. You can list the names of " +
				"valid accounts in your backing lnd " +
				"instance with \"lncli wallet accounts list\".",
			Value: "",
		},
		&cli.StringFlag{
			Name: "account_addr_type",
			Usage: "the address type of the extended public key " +
				"specified in account. Currently only " +
				"pay-to-taproot-pubkey(p2tr) is supported",
			Value: "p2tr",
		},
		&cli.Uint64Flag{
			Name: "autobudget",
			Usage: "the maximum amount of fees in satoshis that " +
				"automatically dispatched loop out swaps may " +
				"spend.",
		},
		&cli.DurationFlag{
			Name: "autobudgetrefreshperiod",
			Usage: "the time period over which the automated " +
				"loop budget is refreshed.",
		},
		&cli.Uint64Flag{
			Name: "autoinflight",
			Usage: "the maximum number of automatically " +
				"dispatched swaps that we allow to be in " +
				"flight.",
		},
		&cli.Uint64Flag{
			Name: "minamt",
			Usage: "the minimum amount in satoshis that the " +
				"autoloop client will dispatch per-swap.",
		},
		&cli.Uint64Flag{
			Name: "maxamt",
			Usage: "the maximum amount in satoshis that the " +
				"autoloop client will dispatch per-swap.",
		},
		&cli.IntFlag{
			Name: "htlc_conf",
			Usage: "the confirmation target for loop in on-chain " +
				"htlcs.",
		},
		&cli.BoolFlag{
			Name: "easyautoloop",
			Usage: "set to true to enable easy autoloop, which " +
				"will automatically dispatch swaps in order " +
				"to meet the target local balance.",
		},
		&cli.Uint64Flag{
			Name: "localbalancesat",
			Usage: "the target size of total local balance in " +
				"satoshis, used by easy autoloop.",
		},
		&cli.StringSliceFlag{
			Name: "easyautoloop_excludepeer",
			Usage: "list of peer pubkeys (hex) to exclude from " +
				"easy autoloop channel selection; repeat " +
				"--easyautoloop_excludepeer for multiple peers",
		},
		&cli.BoolFlag{
			Name: "easyatutoloop_includeallpeers",
			Usage: "include all peers back into easy autoloop by " +
				"clearing the exclusion list. It cannot be " +
				"combined with --easyautoloop_excludepeer",
		},
		&cli.BoolFlag{
			Name: "asset_easyautoloop",
			Usage: "set to true to enable asset easy autoloop, which " +
				"will automatically dispatch asset swaps in order " +
				"to meet the target local balance.",
		},
		&cli.StringFlag{
			Name: "asset_id",
			Usage: "If set to a valid asset ID, the easyautoloop " +
				"and localbalancesat flags will be set for the " +
				"specified asset.",
		},
		&cli.Uint64Flag{
			Name: "asset_localbalance",
			Usage: "the target size of total local balance in " +
				"asset units, used by asset easy autoloop.",
		},
		&cli.BoolFlag{
			Name: "fast",
			Usage: "if set new swaps are expected to be " +
				"published immediately, paying a potentially " +
				"higher fee. If not set the swap server " +
				"might choose to wait up to 30 minutes " +
				"before publishing swap HTLCs on-chain, to " +
				"save on chain fees. Not setting this flag " +
				"therefore might result in a lower swap fees",
		},
	},
	Action: setParams,
}

func setParams(ctx context.Context, cmd *cli.Command) error {
	client, cleanup, err := getClient(cmd)
	if err != nil {
		return err
	}
	defer cleanup()

	// We need to set the full set of current parameters every time we call
	// SetParameters. To allow users to set only individual fields on the
	// cli, we look up our current params, then update individual values.
	params, err := client.GetLiquidityParams(
		ctx, &looprpc.GetLiquidityParamsRequest{},
	)
	if err != nil {
		return err
	}

	var flagSet, categoriesSet, feePercentSet bool

	// Update our existing parameters with the values provided by cli flags.
	// Our fee categories and fee percentage are exclusive, so track which
	// flags are set to ensure that we don't have nonsensical overlap.
	if cmd.IsSet("maxswapfee") {
		feeRate := cmd.Float64("maxswapfee")
		params.MaxSwapFeePpm, err = ppmFromPercentage(feeRate)
		if err != nil {
			return err
		}

		flagSet = true
		categoriesSet = true
	}

	if cmd.IsSet("sweeplimit") {
		satPerVByte := cmd.Int("sweeplimit")
		params.SweepFeeRateSatPerVbyte = uint64(satPerVByte)

		flagSet = true
		categoriesSet = true
	}

	if cmd.IsSet("feepercent") {
		feeRate := cmd.Float64("feepercent")
		params.FeePpm, err = ppmFromPercentage(feeRate)
		if err != nil {
			return err
		}

		flagSet = true
		feePercentSet = true
	}

	if cmd.IsSet("maxroutingfee") {
		feeRate := cmd.Float64("maxroutingfee")
		params.MaxRoutingFeePpm, err = ppmFromPercentage(feeRate)
		if err != nil {
			return err
		}

		flagSet = true
		categoriesSet = true
	}

	if cmd.IsSet("maxprepayfee") {
		feeRate := cmd.Float64("maxprepayfee")
		params.MaxPrepayRoutingFeePpm, err = ppmFromPercentage(feeRate)
		if err != nil {
			return err
		}

		flagSet = true
		categoriesSet = true
	}

	if cmd.IsSet("maxprepay") {
		params.MaxPrepaySat = cmd.Uint64("maxprepay")
		flagSet = true
		categoriesSet = true
	}

	if cmd.IsSet("maxminer") {
		params.MaxMinerFeeSat = cmd.Uint64("maxminer")
		flagSet = true
		categoriesSet = true
	}

	if cmd.IsSet("sweepconf") {
		params.SweepConfTarget = int32(cmd.Int("sweepconf"))
		flagSet = true
	}

	if cmd.IsSet("failurebackoff") {
		params.FailureBackoffSec = cmd.Uint64("failurebackoff")
		flagSet = true
	}

	if cmd.IsSet("autoloop") {
		params.Autoloop = cmd.Bool("autoloop")
		flagSet = true
	}

	if cmd.IsSet("autobudget") {
		params.AutoloopBudgetSat = cmd.Uint64("autobudget")
		flagSet = true
	}

	switch {
	case cmd.IsSet("destaddr") && cmd.IsSet("account"):
		return fmt.Errorf("cannot set destaddr and account at the " +
			"same time")

	case cmd.IsSet("destaddr"):
		params.AutoloopDestAddress = cmd.String("destaddr")
		params.Account = ""
		flagSet = true

	case cmd.IsSet("account") != cmd.IsSet("account_addr_type"):
		return liquidity.ErrAccountAndAddrType

	case cmd.IsSet("account"):
		params.Account = cmd.String("account")
		params.AutoloopDestAddress = ""
		flagSet = true
	}

	if cmd.IsSet("account_addr_type") {
		switch cmd.String("account_addr_type") {
		case "p2tr":
			params.AccountAddrType = looprpc.AddressType_TAPROOT_PUBKEY

		default:
			return fmt.Errorf("unknown account address type")
		}
	}

	if cmd.IsSet("autobudgetrefreshperiod") {
		params.AutoloopBudgetRefreshPeriodSec =
			uint64(cmd.Duration("autobudgetrefreshperiod").Seconds())
		flagSet = true
	}

	if cmd.IsSet("autoinflight") {
		params.AutoMaxInFlight = cmd.Uint64("autoinflight")
		flagSet = true
	}

	if cmd.IsSet("minamt") {
		params.MinSwapAmount = cmd.Uint64("minamt")
		flagSet = true
	}

	if cmd.IsSet("maxamt") {
		params.MaxSwapAmount = cmd.Uint64("maxamt")
		flagSet = true
	}

	if cmd.IsSet("htlc_conf") {
		params.HtlcConfTarget = int32(cmd.Int("htlc_conf"))
		flagSet = true
	}

	// If we are setting easy autoloop parameters, we need to ensure that
	// the asset ID is set, and that we have a valid entry in our params
	// map.
	if cmd.IsSet("asset_id") {
		if params.EasyAssetParams == nil {
			params.EasyAssetParams = make(
				map[string]*looprpc.EasyAssetAutoloopParams,
			)
		}
		if _, ok := params.EasyAssetParams[cmd.String("asset_id")]; !ok { //nolint:lll
			params.EasyAssetParams[cmd.String("asset_id")] =
				&looprpc.EasyAssetAutoloopParams{}
		}
	}

	if cmd.IsSet("easyautoloop") {
		params.EasyAutoloop = cmd.Bool("easyautoloop")
		flagSet = true
	}

	if cmd.IsSet("localbalancesat") {
		params.EasyAutoloopLocalTargetSat = cmd.Uint64("localbalancesat")
		flagSet = true
	}

	// If easyatutoloop_includeallpeers is set, clear the entire exclusion
	// list.
	if cmd.IsSet("easyatutoloop_includeallpeers") {
		if cmd.IsSet("easyautoloop_excludepeer") {
			return fmt.Errorf("easyatutoloop_includeallpeers " +
				"cannot be used with " +
				"--easyautoloop_excludepeer")
		}
		params.EasyAutoloopExcludedPeers = nil
		flagSet = true
	}

	if cmd.IsSet("easyautoloop_excludepeer") {
		peers := cmd.StringSlice("easyautoloop_excludepeer")
		// Reset and set according to a provided list.
		params.EasyAutoloopExcludedPeers = make([][]byte, 0, len(peers))
		for _, s := range peers {
			s = strings.TrimSpace(s)
			if s == "" {
				continue
			}
			v, err := route.NewVertexFromStr(s)
			if err != nil {
				return fmt.Errorf("invalid peer pubkey "+
					"%s: %v", s, err)
			}
			params.EasyAutoloopExcludedPeers = append(
				params.EasyAutoloopExcludedPeers, v[:],
			)
		}
		flagSet = true
	}

	if cmd.IsSet("asset_easyautoloop") {
		if !cmd.IsSet("asset_id") {
			return fmt.Errorf("asset_id must be set to use " +
				"asset_easyautoloop")
		}
		params.EasyAssetParams[cmd.String("asset_id")].
			Enabled = cmd.Bool("asset_easyautoloop")
		flagSet = true
	}

	if cmd.IsSet("asset_localbalance") {
		if !cmd.IsSet("asset_id") {
			return fmt.Errorf("asset_id must be set to use " +
				"asset_localbalance")
		}
		params.EasyAssetParams[cmd.String("asset_id")].
			LocalTargetAssetAmt = cmd.Uint64("asset_localbalance")
		flagSet = true
	}

	if cmd.IsSet("fast") {
		params.FastSwapPublication = true
	}

	if !flagSet {
		return fmt.Errorf("at least one flag required to set params")
	}

	switch {
	// Fail if fee params for both types of fee limit are set, since they
	// cannot be used in conjunction.
	case feePercentSet && categoriesSet:
		return fmt.Errorf("feepercent cannot be set with specific " +
			"fee category flags")

	// If we are updating to fee percentage, we unset all other fee related
	// params so that users do not need to manually unset them.
	case feePercentSet:
		params.SweepFeeRateSatPerVbyte = 0
		params.MaxMinerFeeSat = 0
		params.MaxPrepayRoutingFeePpm = 0
		params.MaxPrepaySat = 0
		params.MaxRoutingFeePpm = 0
		params.MaxSwapFeePpm = 0

	// If we are setting any of our fee categories, unset fee percentage
	// so that it does not need to be manually updated.
	case categoriesSet:
		params.FeePpm = 0
	}
	// Update our parameters to our mutated values.
	_, err = client.SetLiquidityParams(
		ctx, &looprpc.SetLiquidityParamsRequest{
			Parameters: params,
		},
	)

	return err
}

// ppmFromPercentage converts a percentage, expressed as a float, to parts
// per million.
func ppmFromPercentage(percentage float64) (uint64, error) {
	if percentage <= 0 || percentage >= 100 {
		return 0, fmt.Errorf("fee percentage must be in (0;100)")
	}

	return uint64(percentage / 100 * liquidity.FeeBase), nil
}

var suggestSwapCommand = &cli.Command{
	Name:  "suggestswaps",
	Usage: "show a list of suggested swaps",
	Description: "Displays a list of suggested swaps that aim to obtain " +
		"the liquidity balance as specified by the rules set in " +
		"the liquidity manager.",
	Action: suggestSwap,
}

func suggestSwap(ctx context.Context, cmd *cli.Command) error {
	client, cleanup, err := getClient(cmd)
	if err != nil {
		return err
	}
	defer cleanup()

	resp, err := client.SuggestSwaps(
		ctx, &looprpc.SuggestSwapsRequest{},
	)
	if err == nil {
		printRespJSON(resp)
		return nil
	}

	// If we got an error because no rules are set, we want to display a
	// friendly message.
	rpcErr, ok := status.FromError(err)
	if !ok {
		return err
	}

	if rpcErr.Code() != codes.FailedPrecondition {
		return err
	}

	return errors.New("no rules set for autolooper, please set rules " +
		"using the setrule command")
}
