package routing

import (
	"math"

	"github.com/btcsuite/btcd/btcutil"
	"github.com/lightningnetwork/lnd/channeldb"
	"github.com/lightningnetwork/lnd/channeldb/models"
	"github.com/lightningnetwork/lnd/lntypes"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/lightningnetwork/lnd/routing/route"
)

// nodeEdgeUnifier holds all edge unifiers for connections towards a node.
type nodeEdgeUnifier struct {
	// edgeUnifiers contains an edge unifier for every from node.
	edgeUnifiers map[route.Vertex]*edgeUnifier

	// sourceNode is the sender of a payment. The rules to pick the final
	// policy are different for local channels.
	sourceNode route.Vertex

	// toNode is the node for which the edge unifiers are instantiated.
	toNode route.Vertex

	// useInboundFees indicates whether to take inbound fees into account.
	useInboundFees bool

	// outChanRestr is an optional outgoing channel restriction for the
	// local channel to use.
	outChanRestr map[uint64]struct{}
}

// newNodeEdgeUnifier instantiates a new nodeEdgeUnifier object. Channel
// policies can be added to this object.
func newNodeEdgeUnifier(sourceNode, toNode route.Vertex, useInboundFees bool,
	outChanRestr map[uint64]struct{}) *nodeEdgeUnifier {

	return &nodeEdgeUnifier{
		edgeUnifiers:   make(map[route.Vertex]*edgeUnifier),
		toNode:         toNode,
		useInboundFees: useInboundFees,
		sourceNode:     sourceNode,
		outChanRestr:   outChanRestr,
	}
}

// addPolicy adds a single channel policy. Capacity may be zero if unknown
// (light clients). We expect a non-nil payload size function and will request a
// graceful shutdown if it is not provided as this indicates that edges are
// incorrectly specified.
func (u *nodeEdgeUnifier) addPolicy(fromNode route.Vertex,
	edge *models.CachedEdgePolicy, inboundFee models.InboundFee,
	capacity btcutil.Amount, hopPayloadSizeFn PayloadSizeFunc,
	blindedPayment *BlindedPayment) {

	localChan := fromNode == u.sourceNode

	// Skip channels if there is an outgoing channel restriction.
	if localChan && u.outChanRestr != nil {
		if _, ok := u.outChanRestr[edge.ChannelID]; !ok {
			return
		}
	}

	// Update the edgeUnifiers map.
	unifier, ok := u.edgeUnifiers[fromNode]
	if !ok {
		unifier = &edgeUnifier{
			localChan: localChan,
		}
		u.edgeUnifiers[fromNode] = unifier
	}

	// In case no payload size function was provided a graceful shutdown
	// is requested, because this function is not used as intended.
	if hopPayloadSizeFn == nil {
		log.Criticalf("No payloadsize function was provided for the "+
			"edge (chanid=%v) when adding it to the edge unifier "+
			"of node: %v", edge.ChannelID, fromNode)

		return
	}

	// Zero inbound fee for exit hops.
	if !u.useInboundFees {
		inboundFee = models.InboundFee{}
	}

	unifier.edges = append(unifier.edges, newUnifiedEdge(
		edge, capacity, inboundFee, hopPayloadSizeFn, blindedPayment,
	))
}

// addGraphPolicies adds all policies that are known for the toNode in the
// graph.
func (u *nodeEdgeUnifier) addGraphPolicies(g Graph) error {
	cb := func(channel *channeldb.DirectedChannel) error {
		// If there is no edge policy for this candidate node, skip.
		// Note that we are searching backwards so this node would have
		// come prior to the pivot node in the route.
		if channel.InPolicy == nil {
			return nil
		}

		// Add this policy to the corresponding edgeUnifier. We default
		// to the clear hop payload size function because
		// `addGraphPolicies` is only used for cleartext intermediate
		// hops in a route.
		inboundFee := models.NewInboundFeeFromWire(
			channel.InboundFee,
		)

		u.addPolicy(
			channel.OtherNode, channel.InPolicy, inboundFee,
			channel.Capacity, defaultHopPayloadSize, nil,
		)

		return nil
	}

	// Iterate over all channels of the to node.
	return g.ForEachNodeChannel(u.toNode, cb)
}

// unifiedEdge is the individual channel data that is kept inside an edgeUnifier
// object.
type unifiedEdge struct {
	policy      *models.CachedEdgePolicy
	capacity    btcutil.Amount
	inboundFees models.InboundFee

	// hopPayloadSize supplies an edge with the ability to calculate the
	// exact payload size if this edge would be included in a route. This
	// is needed because hops of a blinded path differ in their payload
	// structure compared to cleartext hops.
	hopPayloadSizeFn PayloadSizeFunc

	// blindedPayment if set, is the BlindedPayment that this edge was
	// derived from originally.
	blindedPayment *BlindedPayment
}

// newUnifiedEdge constructs a new unifiedEdge.
func newUnifiedEdge(policy *models.CachedEdgePolicy, capacity btcutil.Amount,
	inboundFees models.InboundFee, hopPayloadSizeFn PayloadSizeFunc,
	blindedPayment *BlindedPayment) *unifiedEdge {

	return &unifiedEdge{
		policy:           policy,
		capacity:         capacity,
		inboundFees:      inboundFees,
		hopPayloadSizeFn: hopPayloadSizeFn,
		blindedPayment:   blindedPayment,
	}
}

// amtInRange checks whether an amount falls within the valid range for a
// channel.
func (u *unifiedEdge) amtInRange(amt lnwire.MilliSatoshi) bool {
	// If the capacity is available (non-light clients), skip channels that
	// are too small.
	if u.capacity > 0 &&
		amt > lnwire.NewMSatFromSatoshis(u.capacity) {

		log.Tracef("Not enough capacity: amt=%v, capacity=%v",
			amt, u.capacity)
		return false
	}

	// Skip channels for which this htlc is too large.
	if u.policy.MessageFlags.HasMaxHtlc() &&
		amt > u.policy.MaxHTLC {

		log.Tracef("Exceeds policy's MaxHTLC: amt=%v, MaxHTLC=%v",
			amt, u.policy.MaxHTLC)
		return false
	}

	// Skip channels for which this htlc is too small.
	if amt < u.policy.MinHTLC {
		log.Tracef("below policy's MinHTLC: amt=%v, MinHTLC=%v",
			amt, u.policy.MinHTLC)
		return false
	}

	return true
}

// edgeUnifier is an object that covers all channels between a pair of nodes.
type edgeUnifier struct {
	edges     []*unifiedEdge
	localChan bool
}

// getEdge returns the optimal unified edge to use for this connection given a
// specific amount to send. It differentiates between local and network
// channels.
func (u *edgeUnifier) getEdge(netAmtReceived lnwire.MilliSatoshi,
	bandwidthHints bandwidthHints,
	nextOutFee lnwire.MilliSatoshi) *unifiedEdge {

	if u.localChan {
		return u.getEdgeLocal(
			netAmtReceived, bandwidthHints, nextOutFee,
		)
	}

	return u.getEdgeNetwork(netAmtReceived, nextOutFee)
}

// calcCappedInboundFee calculates the inbound fee for a channel, taking into
// account the total node fee for the "to" node.
func calcCappedInboundFee(edge *unifiedEdge, amt lnwire.MilliSatoshi,
	nextOutFee lnwire.MilliSatoshi) int64 {

	// Calculate the inbound fee charged for the amount that passes over the
	// channel.
	inboundFee := edge.inboundFees.CalcFee(amt)

	// Take into account that the total node fee cannot be negative.
	if inboundFee < -int64(nextOutFee) {
		inboundFee = -int64(nextOutFee)
	}

	return inboundFee
}

// getEdgeLocal returns the optimal unified edge to use for this local
// connection given a specific amount to send.
func (u *edgeUnifier) getEdgeLocal(netAmtReceived lnwire.MilliSatoshi,
	bandwidthHints bandwidthHints,
	nextOutFee lnwire.MilliSatoshi) *unifiedEdge {

	var (
		bestEdge     *unifiedEdge
		maxBandwidth lnwire.MilliSatoshi
	)

	for _, edge := range u.edges {
		// Calculate the inbound fee charged at the receiving node.
		inboundFee := calcCappedInboundFee(
			edge, netAmtReceived, nextOutFee,
		)

		// Add inbound fee to get to the amount that is sent over the
		// local channel.
		amt := netAmtReceived + lnwire.MilliSatoshi(inboundFee)

		// Check valid amount range for the channel. We skip this test
		// for payments with custom HTLC data, as the amount sent on
		// the BTC layer may differ from the amount that is actually
		// forwarded in custom channels.
		if bandwidthHints.firstHopCustomBlob().IsNone() &&
			!edge.amtInRange(amt) {

			log.Debugf("Amount %v not in range for edge %v",
				netAmtReceived, edge.policy.ChannelID)

			continue
		}

		// For local channels, there is no fee to pay or an extra time
		// lock. We only consider the currently available bandwidth for
		// channel selection. The disabled flag is ignored for local
		// channels.

		// Retrieve bandwidth for this local channel. If not
		// available, assume this channel has enough bandwidth.
		//
		// TODO(joostjager): Possibly change to skipping this
		// channel. The bandwidth hint is expected to be
		// available.
		bandwidth, ok := bandwidthHints.availableChanBandwidth(
			edge.policy.ChannelID, amt,
		)
		if !ok {
			log.Debugf("Cannot get bandwidth for edge %v, use max "+
				"instead", edge.policy.ChannelID)
			bandwidth = lnwire.MaxMilliSatoshi
		}

		// TODO(yy): if the above `!ok` is chosen, we'd have
		// `bandwidth` to be the max value, which will end up having
		// the `maxBandwidth` to be have the largest value and this
		// edge will be the chosen one. This is wrong in two ways,
		// 1. we need to understand why `availableChanBandwidth` cannot
		// find bandwidth for this edge as something is wrong with this
		// channel, and,
		// 2. this edge is likely NOT the local channel with the
		// highest available bandwidth.
		//
		// Skip channels that can't carry the payment.
		if amt > bandwidth {
			log.Debugf("Skipped edge %v: not enough bandwidth, "+
				"bandwidth=%v, amt=%v", edge.policy.ChannelID,
				bandwidth, amt)

			continue
		}

		// We pick the local channel with the highest available
		// bandwidth, to maximize the success probability. It can be
		// that the channel state changes between querying the bandwidth
		// hints and sending out the htlc.
		if bandwidth < maxBandwidth {
			log.Debugf("Skipped edge %v: not max bandwidth, "+
				"bandwidth=%v, maxBandwidth=%v",
				edge.policy.ChannelID, bandwidth, maxBandwidth)

			continue
		}
		maxBandwidth = bandwidth

		// Update best edge.
		bestEdge = newUnifiedEdge(
			edge.policy, edge.capacity, edge.inboundFees,
			edge.hopPayloadSizeFn, edge.blindedPayment,
		)
	}

	return bestEdge
}

// getEdgeNetwork returns the optimal unified edge to use for this connection
// given a specific amount to send. The goal is to return a unified edge with a
// policy that maximizes the probability of a successful forward in a non-strict
// forwarding context.
func (u *edgeUnifier) getEdgeNetwork(netAmtReceived lnwire.MilliSatoshi,
	nextOutFee lnwire.MilliSatoshi) *unifiedEdge {

	var (
		bestPolicy       *unifiedEdge
		maxFee           int64 = math.MinInt64
		maxTimelock      uint16
		maxCapMsat       lnwire.MilliSatoshi
		hopPayloadSizeFn PayloadSizeFunc
	)

	for _, edge := range u.edges {
		// Calculate the inbound fee charged at the receiving node.
		inboundFee := calcCappedInboundFee(
			edge, netAmtReceived, nextOutFee,
		)

		// Add inbound fee to get to the amount that is sent over the
		// channel.
		amt := netAmtReceived + lnwire.MilliSatoshi(inboundFee)

		// Check valid amount range for the channel.
		if !edge.amtInRange(amt) {
			log.Debugf("Amount %v not in range for edge %v",
				amt, edge.policy.ChannelID)
			continue
		}

		// For network channels, skip the disabled ones.
		edgeFlags := edge.policy.ChannelFlags
		isDisabled := edgeFlags&lnwire.ChanUpdateDisabled != 0
		if isDisabled {
			log.Debugf("Skipped edge %v due to it being disabled",
				edge.policy.ChannelID)
			continue
		}

		// Track the maximal capacity for usable channels. If we don't
		// know the capacity, we fall back to MaxHTLC.
		capMsat := lnwire.NewMSatFromSatoshis(edge.capacity)
		if capMsat == 0 && edge.policy.MessageFlags.HasMaxHtlc() {
			log.Tracef("No capacity available for channel %v, "+
				"using MaxHtlcMsat (%v) as a fallback.",
				edge.policy.ChannelID, edge.policy.MaxHTLC)

			capMsat = edge.policy.MaxHTLC
		}
		maxCapMsat = lntypes.Max(capMsat, maxCapMsat)

		// Track the maximum time lock of all channels that are
		// candidate for non-strict forwarding at the routing node.
		maxTimelock = lntypes.Max(
			maxTimelock, edge.policy.TimeLockDelta,
		)

		outboundFee := int64(edge.policy.ComputeFee(amt))
		fee := outboundFee + inboundFee

		// Use the policy that results in the highest fee for this
		// specific amount.
		if fee < maxFee {
			log.Debugf("Skipped edge %v due to it produces less "+
				"fee: fee=%v, maxFee=%v",
				edge.policy.ChannelID, fee, maxFee)

			continue
		}
		maxFee = fee

		bestPolicy = newUnifiedEdge(
			edge.policy, 0, edge.inboundFees, nil,
			edge.blindedPayment,
		)

		// The payload size function for edges to a connected peer is
		// always the same hence there is not need to find the maximum.
		// This also counts for blinded edges where we only have one
		// edge to a blinded peer.
		hopPayloadSizeFn = edge.hopPayloadSizeFn
	}

	// Return early if no channel matches.
	if bestPolicy == nil {
		return nil
	}

	// We have already picked the highest fee that could be required for
	// non-strict forwarding. To also cover the case where a lower fee
	// channel requires a longer time lock, we modify the policy by setting
	// the maximum encountered time lock. Note that this results in a
	// synthetic policy that is not actually present on the routing node.
	//
	// The reason we do this, is that we try to maximize the chance that we
	// get forwarded. Because we penalize pair-wise, there won't be a second
	// chance for this node pair. But this is all only needed for nodes that
	// have distinct policies for channels to the same peer.
	policyCopy := *bestPolicy.policy
	policyCopy.TimeLockDelta = maxTimelock
	modifiedEdge := newUnifiedEdge(
		&policyCopy, maxCapMsat.ToSatoshis(), bestPolicy.inboundFees,
		hopPayloadSizeFn, bestPolicy.blindedPayment,
	)

	return modifiedEdge
}

// minAmt returns the minimum amount that can be forwarded on this connection.
func (u *edgeUnifier) minAmt() lnwire.MilliSatoshi {
	min := lnwire.MaxMilliSatoshi
	for _, edge := range u.edges {
		min = lntypes.Min(min, edge.policy.MinHTLC)
	}

	return min
}
