package liquidity

import (
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/btcsuite/btcd/btcutil"
	"github.com/lightninglabs/lndclient"
	clientrpc "github.com/lightninglabs/loop/looprpc"
	"github.com/lightninglabs/loop/swap"
	"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/lightningnetwork/lnd/routing/route"
)

var (
	// defaultParameters contains the default parameters that we start our
	// liquidity manager with.
	defaultParameters = Parameters{
		AutoFeeBudget:             defaultBudget,
		AutoFeeRefreshPeriod:      defaultBudgetRefreshPeriod,
		AutoloopBudgetLastRefresh: time.Now(),
		DestAddr:                  nil,
		MaxAutoInFlight:           defaultMaxInFlight,
		ChannelRules:              make(map[lnwire.ShortChannelID]*SwapRule),
		PeerRules:                 make(map[route.Vertex]*SwapRule),
		FailureBackOff:            defaultFailureBackoff,
		SweepConfTarget:           defaultConfTarget,
		HtlcConfTarget:            defaultHtlcConfTarget,
		FeeLimit:                  defaultFeePortion(),
		FastSwapPublication:       true,
	}
)

const InfiniteDuration = (24 * 31 * 12 * 100) * time.Hour

// Parameters is a set of parameters provided by the user which guide
// how we assess liquidity.
type Parameters struct {
	// Autoloop enables automatic dispatch of swaps.
	Autoloop bool

	// DestAddr is the address to be used for sweeping the on-chain HTLC
	// that is related with a loop out.
	DestAddr btcutil.Address

	// An alternative destination address source for the swap. This field
	// represents the name of the account in the backing lnd instance.
	// Refer to lnd's wallet import functions for reference.
	Account string

	// The address type of the account specified in the account field.
	AccountAddrType walletrpc.AddressType

	// AutoFeeBudget is the total amount we allow to be spent on
	// automatically dispatched swaps. Once this budget has been used, we
	// will stop dispatching swaps until the budget is refreshed.
	AutoFeeBudget btcutil.Amount

	// AutoFeeRefreshPeriod is the amount of time that must pass before the
	// auto fee budget is refreshed.
	AutoFeeRefreshPeriod time.Duration

	// AutoloopBudgetLastRefresh is the last time at which we refreshed
	// our budget.
	AutoloopBudgetLastRefresh time.Time

	// MaxAutoInFlight is the maximum number of in-flight automatically
	// dispatched swaps we allow.
	MaxAutoInFlight int

	// FailureBackOff is the amount of time that we require passes after a
	// channel has been part of a failed loop out swap before we suggest
	// using it again.
	// TODO(carla): add exponential backoff
	FailureBackOff time.Duration

	// SweepConfTarget is the number of blocks we aim to confirm our sweep
	// transaction in. This value affects the on chain fees we will pay.
	SweepConfTarget int32

	// HtlcConfTarget is the confirmation target that we use for publishing
	// loop in swap htlcs on chain.
	HtlcConfTarget int32

	// FeeLimit controls the fee limit we place on swaps.
	FeeLimit FeeLimit

	// ClientRestrictions are the restrictions placed on swap size by the
	// client.
	ClientRestrictions Restrictions

	// ChannelRules maps a short channel ID to a rule that describes how we
	// would like liquidity to be managed. These rules and PeerRules are
	// exclusively set to prevent overlap between peer and channel rules.
	ChannelRules map[lnwire.ShortChannelID]*SwapRule

	// PeerRules maps a peer's pubkey to a rule that applies to all the
	// channels that we have with the peer collectively. These rules and
	// ChannelRules are exclusively set to prevent overlap between peer
	// and channel rules map to avoid ambiguity.
	PeerRules map[route.Vertex]*SwapRule

	// CustomPaymentCheckInterval is an optional custom interval to use when
	// checking an autoloop loop out payments' payment status.
	CustomPaymentCheckInterval time.Duration

	// EasyAutoloop is a boolean that indicates whether we should use the
	// easy autoloop feature.
	EasyAutoloop bool

	// EasyAutoloopTarget is the target amount of liquidity that we want to
	// maintain in our channels.
	EasyAutoloopTarget btcutil.Amount

	// EasyAutoloopExcludedPeers is an optional list of peers that should be
	// excluded from being selected for easy autoloop swaps.
	EasyAutoloopExcludedPeers []route.Vertex

	// AssetAutoloopParams maps an asset id hex encoded string to its
	// easy autoloop parameters.
	AssetAutoloopParams map[string]AssetParams

	// FastSwapPublication controls publication deadline for new loop out
	// swaps. If set to true, the deadline is set to immediate publication.
	// If set to false, the deadline is set to 30 minutes.
	FastSwapPublication bool
}

// AssetParams define the asset specific autoloop parameters.
type AssetParams struct {
	// EnableEasyOut is a boolean that indicates whether we should use the
	// easy autoloop feature for this asset.
	EnableEasyOut bool

	// LocalTargetAssetAmount is the target amount of liquidity that we
	// want to maintain in our channels.
	LocalTargetAssetAmount uint64
}

// String returns the string representation of our parameters.
func (p Parameters) String() string {
	ruleList := make([]string, 0, len(p.ChannelRules)+len(p.PeerRules))

	for channel, rule := range p.ChannelRules {
		ruleList = append(
			ruleList, fmt.Sprintf("Channel: %v: %v", channel, rule),
		)
	}

	for peer, rule := range p.PeerRules {
		ruleList = append(
			ruleList, fmt.Sprintf("Peer: %v: %v", peer, rule),
		)
	}

	return fmt.Sprintf("rules: %v, failure backoff: %v, sweep "+
		"sweep conf target: %v, htlc conf target: %v,fees: %v, "+
		"auto budget: %v, budget refresh: %v, max auto in flight: %v, "+
		"minimum swap size=%v, maximum swap size=%v",
		strings.Join(ruleList, ","), p.FailureBackOff,
		p.SweepConfTarget, p.HtlcConfTarget, p.FeeLimit,
		p.AutoFeeBudget, p.AutoFeeRefreshPeriod, p.MaxAutoInFlight,
		p.ClientRestrictions.Minimum, p.ClientRestrictions.Maximum)
}

// haveRules returns a boolean indicating whether we have any rules configured.
func (p Parameters) haveRules() bool {
	if len(p.ChannelRules) != 0 {
		return true
	}

	if len(p.PeerRules) != 0 {
		return true
	}

	return false
}

// validate checks whether a set of parameters is valid. Our set of currently
// open channels are required to check that there is no overlap between the
// rules set on a per-peer level, and those set for specific channels. We can't
// allow both, because then we're trying to cater for two separate liquidity
// goals on the same channel. Since we use short channel ID, we don't need to
// worry about pending channels (users would need to work very hard to get the
// short channel ID for a pending channel). Likewise, we don't care about closed
// channels, since there is no action that may occur on them, and we want to
// allow peer-level rules to be set once a channel which had a specific rule
// has been closed. It takes the minimum confirmations we allow for sweep
// confirmation target as a parameter.
// TODO(carla): prune channels that have been closed from rules.
func (p Parameters) validate(minConfs int32, openChans []lndclient.ChannelInfo,
	server *Restrictions) error {

	// First, we check that the rules on a per peer and per channel do not
	// overlap, since this could lead to contractions.
	for _, channel := range openChans {
		// If we don't have a rule for the peer, there's no way we have
		// an overlap between this peer and the channel.
		_, ok := p.PeerRules[channel.PubKeyBytes]
		if !ok {
			continue
		}

		shortID := lnwire.NewShortChanIDFromInt(channel.ChannelID)
		_, ok = p.ChannelRules[shortID]
		if ok {
			log.Debugf("Rules for peer: %v and its channel: %v "+
				"can't both be set", channel.PubKeyBytes, shortID)

			return ErrExclusiveRules
		}
	}

	for channel, rule := range p.ChannelRules {
		if channel.ToUint64() == 0 {
			return ErrZeroChannelID
		}

		if rule.Type == swap.TypeIn {
			return errors.New("channel level rules not supported for " +
				"loop in swaps, only peer-level rules allowed")
		}

		if err := rule.validate(); err != nil {
			return fmt.Errorf("channel: %v has invalid rule: %v",
				channel.ToUint64(), err)
		}
	}

	for peer, rule := range p.PeerRules {
		if err := rule.validate(); err != nil {
			return fmt.Errorf("peer: %v has invalid rule: %v",
				peer, err)
		}
	}

	// Check that our confirmation target is above our required minimum.
	if p.SweepConfTarget < minConfs {
		return fmt.Errorf("confirmation target must be at least: %v",
			minConfs)
	}

	if p.HtlcConfTarget < 1 {
		return fmt.Errorf("htlc confirmation target must be > 0")
	}

	if err := p.FeeLimit.validate(); err != nil {
		return err
	}

	if p.AutoFeeBudget < 0 {
		return ErrNegativeBudget
	}

	if p.MaxAutoInFlight <= 0 {
		return ErrZeroInFlight
	}

	// Destination address and account cannot be set at the same time.
	if p.DestAddr != nil && len(p.DestAddr.String()) > 0 &&
		len(p.Account) > 0 {

		return ErrAmbiguousDestAddr
	}

	// If an account is specified the respective address type must be
	// specified as well, or both must be unset.
	if len(p.Account) == 0 !=
		(p.AccountAddrType == walletrpc.AddressType_UNKNOWN) {

		return ErrAccountAndAddrType
	}

	err := validateRestrictions(server, &p.ClientRestrictions)
	if err != nil {
		return err
	}

	return nil
}

// validateRestrictions checks that client restrictions fall within the server's
// restrictions.
func validateRestrictions(server, client *Restrictions) error {
	zeroMin := client.Minimum == 0
	zeroMax := client.Maximum == 0

	if zeroMin && zeroMax {
		return nil
	}

	// If we have a non-zero maximum, we need to ensure it is greater than
	// our minimum (which is fine if min is zero), and does not exceed the
	// server's maximum.
	if !zeroMax {
		if client.Minimum > client.Maximum {
			return ErrMinimumExceedsMaximumAmt
		}

		if client.Maximum > server.Maximum {
			return ErrMaxExceedsServer
		}
	}

	if zeroMin {
		return nil
	}

	// If the client set a minimum, ensure it is at least equal to the
	// server's limit.
	if client.Minimum < server.Minimum {
		return ErrMinLessThanServer
	}

	return nil
}

// cloneParameters creates a deep clone of a parameters struct so that callers
// cannot mutate our parameters. Although our parameters struct itself is not
// a reference, we still need to clone the contents of maps.
func cloneParameters(params Parameters) Parameters {
	paramCopy := params
	paramCopy.ChannelRules = make(
		map[lnwire.ShortChannelID]*SwapRule,
		len(params.ChannelRules),
	)

	for channel, rule := range params.ChannelRules {
		ruleCopy := *rule
		paramCopy.ChannelRules[channel] = &ruleCopy
	}

	paramCopy.PeerRules = make(
		map[route.Vertex]*SwapRule,
		len(params.PeerRules),
	)

	for peer, rule := range params.PeerRules {
		ruleCopy := *rule
		paramCopy.PeerRules[peer] = &ruleCopy
	}

	return paramCopy
}

// rpcToFee converts the values provided over rpc to a fee limit interface,
// failing if an inconsistent set of fields are set.
func rpcToFee(req *clientrpc.LiquidityParameters) (FeeLimit, error) {
	// Check which fee limit type we have values set for. If any fields
	// relevant to our individual categories are set, we count that type
	// as set.
	isFeePPM := req.FeePpm != 0
	isCategories := req.MaxSwapFeePpm != 0 || req.MaxRoutingFeePpm != 0 ||
		req.MaxPrepayRoutingFeePpm != 0 || req.MaxMinerFeeSat != 0 ||
		req.MaxPrepaySat != 0 || req.SweepFeeRateSatPerVbyte != 0

	switch {
	case isFeePPM && isCategories:
		return nil, errors.New("set either fee ppm, or individual " +
			"fee categories")
	case isFeePPM:
		return NewFeePortion(req.FeePpm), nil

	case isCategories:
		satPerKVbyte := chainfee.SatPerKVByte(
			req.SweepFeeRateSatPerVbyte * 1000,
		)

		return NewFeeCategoryLimit(
			req.MaxSwapFeePpm,
			req.MaxRoutingFeePpm,
			req.MaxPrepayRoutingFeePpm,
			btcutil.Amount(req.MaxMinerFeeSat),
			btcutil.Amount(req.MaxPrepaySat),
			satPerKVbyte.FeePerKWeight(),
		), nil

	default:
		return nil, errors.New("no fee categories set")
	}
}

// rpcToRule switches on rpc rule type to convert to our rule interface.
func rpcToRule(rule *clientrpc.LiquidityRule) (*SwapRule, error) {
	swapType := swap.TypeOut
	if rule.SwapType == clientrpc.SwapType_LOOP_IN {
		swapType = swap.TypeIn
	}

	switch rule.Type {
	case clientrpc.LiquidityRuleType_UNKNOWN:
		return nil, fmt.Errorf("rule type field must be set")

	case clientrpc.LiquidityRuleType_THRESHOLD:
		return &SwapRule{
			ThresholdRule: NewThresholdRule(
				int(rule.IncomingThreshold),
				int(rule.OutgoingThreshold),
			),
			Type: swapType,
		}, nil

	default:
		return nil, fmt.Errorf("unknown rule: %T", rule)
	}
}

// rpcToParameters takes a `LiquidityParameters` and creates a `Parameters`
// from it.
func RpcToParameters(req *clientrpc.LiquidityParameters) (*Parameters,
	error) {

	feeLimit, err := rpcToFee(req)
	if err != nil {
		return nil, err
	}

	var destaddr btcutil.Address
	if len(req.AutoloopDestAddress) != 0 {
		if req.AutoloopDestAddress == "default" {
			destaddr = nil
		} else {
			destaddr, err = btcutil.DecodeAddress(
				req.AutoloopDestAddress, nil,
			)
			if err != nil {
				return nil, err
			}
		}
	}

	addrType := walletrpc.AddressType_UNKNOWN
	if req.AccountAddrType == clientrpc.AddressType_TAPROOT_PUBKEY {
		addrType = walletrpc.AddressType_TAPROOT_PUBKEY
	}

	easyAssetParams := make(map[string]AssetParams)
	for asset, params := range req.EasyAssetParams {
		easyAssetParams[asset] = AssetParams{
			EnableEasyOut:          params.Enabled,
			LocalTargetAssetAmount: params.LocalTargetAssetAmt,
		}
	}

	params := &Parameters{
		FeeLimit:        feeLimit,
		SweepConfTarget: req.SweepConfTarget,
		FailureBackOff: time.Duration(req.FailureBackoffSec) *
			time.Second,
		Autoloop: req.Autoloop,
		AutoloopBudgetLastRefresh: time.Unix(
			int64(req.AutoloopBudgetLastRefresh), 0,
		),
		DestAddr:        destaddr,
		Account:         req.Account,
		AccountAddrType: addrType,
		AutoFeeBudget:   btcutil.Amount(req.AutoloopBudgetSat),
		MaxAutoInFlight: int(req.AutoMaxInFlight),
		ChannelRules: make(
			map[lnwire.ShortChannelID]*SwapRule,
		),
		PeerRules: make(
			map[route.Vertex]*SwapRule,
		),
		ClientRestrictions: Restrictions{
			Minimum: btcutil.Amount(req.MinSwapAmount),
			Maximum: btcutil.Amount(req.MaxSwapAmount),
		},
		HtlcConfTarget: req.HtlcConfTarget,
		EasyAutoloop:   req.EasyAutoloop,
		EasyAutoloopTarget: btcutil.Amount(
			req.EasyAutoloopLocalTargetSat,
		),
		AssetAutoloopParams: easyAssetParams,
		FastSwapPublication: req.FastSwapPublication,
	}

	if req.AutoloopBudgetRefreshPeriodSec != 0 {
		params.AutoFeeRefreshPeriod =
			time.Duration(req.AutoloopBudgetRefreshPeriodSec) *
				time.Second
	}

	// Map excluded peers for easy autoloop, if any.
	excludedPeersRPC := req.GetEasyAutoloopExcludedPeers()
	params.EasyAutoloopExcludedPeers = make(
		[]route.Vertex, 0, len(excludedPeersRPC),
	)
	for _, p := range excludedPeersRPC {
		v, err := route.NewVertexFromBytes(p)
		if err != nil {
			return nil, err
		}
		params.EasyAutoloopExcludedPeers = append(
			params.EasyAutoloopExcludedPeers, v,
		)
	}

	// If an old-style budget was written to storage then express it by
	// using the new auto budget parameters. If the newly added parameters
	// have the 0 default value, but a budget was defined that means the
	// client is using the old style budget parameters.
	if req.AutoloopBudgetRefreshPeriodSec == 0 &&
		req.AutoloopBudgetSat != 0 {

		params.AutoFeeRefreshPeriod = InfiniteDuration
		params.AutoloopBudgetLastRefresh = time.Unix(
			int64(req.AutoloopBudgetStartSec), 0)
	}

	for _, rule := range req.Rules {
		peerRule := rule.Pubkey != nil
		chanRule := rule.ChannelId != 0

		liquidityRule, err := rpcToRule(rule)
		if err != nil {
			return nil, err
		}

		switch {
		case peerRule && chanRule:
			return nil, fmt.Errorf("cannot set channel: %v and "+
				"peer: %v fields in rule", rule.ChannelId,
				rule.Pubkey)

		case peerRule:
			pubkey, err := route.NewVertexFromBytes(rule.Pubkey)
			if err != nil {
				return nil, err
			}

			if _, ok := params.PeerRules[pubkey]; ok {
				return nil, fmt.Errorf("multiple rules set "+
					"for peer: %v", pubkey)
			}

			params.PeerRules[pubkey] = liquidityRule

		case chanRule:
			shortID := lnwire.NewShortChanIDFromInt(rule.ChannelId)

			if _, ok := params.ChannelRules[shortID]; ok {
				return nil, fmt.Errorf("multiple rules set "+
					"for channel: %v", shortID)
			}

			params.ChannelRules[shortID] = liquidityRule

		default:
			return nil, errors.New("please set channel id or " +
				"pubkey for rule")
		}
	}

	return params, nil
}

// ParametersToRpc takes a `Parameters` and creates a `LiquidityParameters`
// from it.
func ParametersToRpc(cfg Parameters) (*clientrpc.LiquidityParameters,
	error) {

	totalRules := len(cfg.ChannelRules) + len(cfg.PeerRules)

	var destaddr string
	if cfg.DestAddr != nil {
		destaddr = cfg.DestAddr.String()
	}

	var addrType clientrpc.AddressType
	switch cfg.AccountAddrType {
	case walletrpc.AddressType_TAPROOT_PUBKEY:
		addrType = clientrpc.AddressType_TAPROOT_PUBKEY

	default:
		addrType = clientrpc.AddressType_ADDRESS_TYPE_UNKNOWN
	}

	easyAssetMap := make(
		map[string]*clientrpc.EasyAssetAutoloopParams,
		len(cfg.AssetAutoloopParams),
	)

	for asset, params := range cfg.AssetAutoloopParams {
		easyAssetMap[asset] = &clientrpc.EasyAssetAutoloopParams{
			Enabled:             params.EnableEasyOut,
			LocalTargetAssetAmt: params.LocalTargetAssetAmount,
		}
	}

	rpcCfg := &clientrpc.LiquidityParameters{
		SweepConfTarget:   cfg.SweepConfTarget,
		FailureBackoffSec: uint64(cfg.FailureBackOff.Seconds()),
		Autoloop:          cfg.Autoloop,
		AutoloopBudgetSat: uint64(cfg.AutoFeeBudget),
		AutoloopBudgetRefreshPeriodSec: uint64(
			cfg.AutoFeeRefreshPeriod.Seconds(),
		),
		AutoloopBudgetLastRefresh: uint64(
			cfg.AutoloopBudgetLastRefresh.Unix(),
		),
		AutoMaxInFlight:     uint64(cfg.MaxAutoInFlight),
		AutoloopDestAddress: destaddr,
		Rules: make(
			[]*clientrpc.LiquidityRule, 0, totalRules,
		),
		MinSwapAmount: uint64(
			cfg.ClientRestrictions.Minimum,
		),
		MaxSwapAmount: uint64(
			cfg.ClientRestrictions.Maximum,
		),
		HtlcConfTarget:             cfg.HtlcConfTarget,
		EasyAutoloop:               cfg.EasyAutoloop,
		EasyAutoloopLocalTargetSat: uint64(cfg.EasyAutoloopTarget),
		Account:                    cfg.Account,
		AccountAddrType:            addrType,
		EasyAssetParams:            easyAssetMap,
		FastSwapPublication:        cfg.FastSwapPublication,
	}
	// Set excluded peers for easy autoloop.
	rpcCfg.EasyAutoloopExcludedPeers = make(
		[][]byte, 0, len(cfg.EasyAutoloopExcludedPeers),
	)
	for _, v := range cfg.EasyAutoloopExcludedPeers {
		rpcCfg.EasyAutoloopExcludedPeers = append(
			rpcCfg.EasyAutoloopExcludedPeers, v[:],
		)
	}

	switch f := cfg.FeeLimit.(type) {
	case *FeeCategoryLimit:
		satPerByte := f.SweepFeeRateLimit.FeePerKVByte() / 1000

		rpcCfg.SweepFeeRateSatPerVbyte = uint64(satPerByte)

		rpcCfg.MaxMinerFeeSat = uint64(f.MaximumMinerFee)
		rpcCfg.MaxSwapFeePpm = f.MaximumSwapFeePPM
		rpcCfg.MaxRoutingFeePpm = f.MaximumRoutingFeePPM
		rpcCfg.MaxPrepayRoutingFeePpm = f.MaximumPrepayRoutingFeePPM
		rpcCfg.MaxPrepaySat = uint64(f.MaximumPrepay)

	case *FeePortion:
		rpcCfg.FeePpm = f.PartsPerMillion

	default:
		return nil, fmt.Errorf("unknown fee limit: %T", cfg.FeeLimit)
	}

	for channel, rule := range cfg.ChannelRules {
		rpcRule := newRPCRule(channel.ToUint64(), nil, rule)
		rpcCfg.Rules = append(rpcCfg.Rules, rpcRule)
	}

	for peer, rule := range cfg.PeerRules {
		rpcRule := newRPCRule(0, peer[:], rule)
		rpcCfg.Rules = append(rpcCfg.Rules, rpcRule)
	}

	return rpcCfg, nil
}

// newRPCRule is a helper function that creates a `LiquidityRule` based on the
// provided `SwapRule` for the given channelID or peer.
func newRPCRule(channelID uint64, peer []byte,
	rule *SwapRule) *clientrpc.LiquidityRule {

	rpcRule := &clientrpc.LiquidityRule{
		ChannelId:         channelID,
		Pubkey:            peer,
		Type:              clientrpc.LiquidityRuleType_THRESHOLD,
		IncomingThreshold: uint32(rule.MinimumIncoming),
		OutgoingThreshold: uint32(rule.MinimumOutgoing),
		SwapType:          clientrpc.SwapType_LOOP_OUT,
	}

	if rule.Type == swap.TypeIn {
		rpcRule.SwapType = clientrpc.SwapType_LOOP_IN
	}

	return rpcRule
}
