package routing

import (
	"bytes"
	"container/heap"
	"errors"
	"fmt"
	"math"
	"sort"
	"time"

	"github.com/btcsuite/btcd/btcutil"
	sphinx "github.com/lightningnetwork/lightning-onion"
	"github.com/lightningnetwork/lnd/feature"
	"github.com/lightningnetwork/lnd/fn"
	graphdb "github.com/lightningnetwork/lnd/graph/db"
	"github.com/lightningnetwork/lnd/graph/db/models"
	"github.com/lightningnetwork/lnd/lnutils"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/lightningnetwork/lnd/record"
	"github.com/lightningnetwork/lnd/routing/route"
)

const (
	// infinity is used as a starting distance in our shortest path search.
	infinity = math.MaxInt64

	// RiskFactorBillionths controls the influence of time lock delta
	// of a channel on route selection. It is expressed as billionths
	// of msat per msat sent through the channel per time lock delta
	// block. See edgeWeight function below for more details.
	// The chosen value is based on the previous incorrect weight function
	// 1 + timelock + fee * fee. In this function, the fee penalty
	// diminishes the time lock penalty for all but the smallest amounts.
	// To not change the behaviour of path finding too drastically, a
	// relatively small value is chosen which is still big enough to give
	// some effect with smaller time lock values. The value may need
	// tweaking and/or be made configurable in the future.
	RiskFactorBillionths = 15

	// estimatedNodeCount is used to preallocate the path finding structures
	// to avoid resizing and copies. It should be number on the same order as
	// the number of active nodes in the network.
	estimatedNodeCount = 10000

	// fakeHopHintCapacity is the capacity we assume for hop hint channels.
	// This is a high number, which expresses that a hop hint channel should
	// be able to route payments.
	fakeHopHintCapacity = btcutil.Amount(10 * btcutil.SatoshiPerBitcoin)
)

// pathFinder defines the interface of a path finding algorithm.
type pathFinder = func(g *graphParams, r *RestrictParams,
	cfg *PathFindingConfig, self, source, target route.Vertex,
	amt lnwire.MilliSatoshi, timePref float64, finalHtlcExpiry int32) (
	[]*unifiedEdge, float64, error)

var (
	// DefaultEstimator is the default estimator used for computing
	// probabilities in pathfinding.
	DefaultEstimator = AprioriEstimatorName

	// DefaultAttemptCost is the default fixed virtual cost in path finding
	// of a failed payment attempt. It is used to trade off potentially
	// better routes against their probability of succeeding.
	DefaultAttemptCost = lnwire.NewMSatFromSatoshis(100)

	// DefaultAttemptCostPPM is the default proportional virtual cost in
	// path finding weight units of executing a payment attempt that fails.
	// It is used to trade off potentially better routes against their
	// probability of succeeding. This parameter is expressed in parts per
	// million of the payment amount.
	//
	// It is impossible to pick a perfect default value. The current value
	// of 0.1% is based on the idea that a transaction fee of 1% is within
	// reasonable territory and that a payment shouldn't need more than 10
	// attempts.
	DefaultAttemptCostPPM = int64(1000)

	// DefaultMinRouteProbability is the default minimum probability for routes
	// returned from findPath.
	DefaultMinRouteProbability = float64(0.01)

	// DefaultAprioriHopProbability is the default a priori probability for
	// a hop.
	DefaultAprioriHopProbability = float64(0.6)
)

// edgePolicyWithSource is a helper struct to keep track of the source node
// of a channel edge. ChannelEdgePolicy only contains to destination node
// of the edge.
type edgePolicyWithSource struct {
	sourceNode route.Vertex
	edge       AdditionalEdge
}

// finalHopParams encapsulates various parameters for route construction that
// apply to the final hop in a route. These features include basic payment data
// such as amounts and cltvs, as well as more complex features like destination
// custom records and payment address.
type finalHopParams struct {
	amt      lnwire.MilliSatoshi
	totalAmt lnwire.MilliSatoshi

	// cltvDelta is the final hop's minimum CLTV expiry delta.
	//
	// NOTE that in the case of paying to a blinded path, this value will
	// be set to a duplicate of the blinded path's accumulated CLTV value.
	// We would then only need to use this value in the case where the
	// introduction node of the path is also the destination node.
	cltvDelta uint16

	records     record.CustomSet
	paymentAddr fn.Option[[32]byte]

	// metadata is additional data that is sent along with the payment to
	// the payee.
	metadata []byte
}

// newRoute constructs a route using the provided path and final hop constraints.
// Any destination specific fields from the final hop params  will be attached
// assuming the destination's feature vector signals support, otherwise this
// method will fail.  If the route is too long, or the selected path cannot
// support the fully payment including fees, then a non-nil error is returned.
// If the route is to a blinded path, the blindedPath parameter is used to
// back fill additional fields that are required for a blinded payment. This is
// done in a separate pass to keep our route construction simple, as blinded
// paths require zero expiry and amount values for intermediate hops (which
// makes calculating the totals during route construction difficult if we
// include blinded paths on the first pass).
//
// NOTE: The passed slice of unified edges MUST be sorted in forward order: from
// the source to the target node of the path finding attempt. It is assumed that
// any feature vectors on all hops have been validated for transitive
// dependencies.
// NOTE: If a non-nil blinded path is provided it is assumed to have been
// validated by the caller.
func newRoute(sourceVertex route.Vertex,
	pathEdges []*unifiedEdge, currentHeight uint32, finalHop finalHopParams,
	blindedPathSet *BlindedPaymentPathSet) (*route.Route, error) {

	var (
		hops []*route.Hop

		// totalTimeLock will accumulate the cumulative time lock
		// across the entire route. This value represents how long the
		// sender will need to wait in the *worst* case.
		totalTimeLock = currentHeight

		// nextIncomingAmount is the amount that will need to flow into
		// the *next* hop. Since we're going to be walking the route
		// backwards below, this next hop gets closer and closer to the
		// sender of the payment.
		nextIncomingAmount lnwire.MilliSatoshi

		blindedPayment *BlindedPayment
	)

	pathLength := len(pathEdges)
	for i := pathLength - 1; i >= 0; i-- {
		// Now we'll start to calculate the items within the per-hop
		// payload for the hop this edge is leading to.
		edge := pathEdges[i].policy

		// If this is an edge from a blinded path and the
		// blindedPayment variable has not been set yet, then set it now
		// by extracting the corresponding blinded payment from the
		// edge.
		isBlindedEdge := pathEdges[i].blindedPayment != nil
		if isBlindedEdge && blindedPayment == nil {
			blindedPayment = pathEdges[i].blindedPayment
		}

		// We'll calculate the amounts, timelocks, and fees for each hop
		// in the route. The base case is the final hop which includes
		// their amount and timelocks. These values will accumulate
		// contributions from the preceding hops back to the sender as
		// we compute the route in reverse.
		var (
			amtToForward        lnwire.MilliSatoshi
			fee                 int64
			totalAmtMsatBlinded lnwire.MilliSatoshi
			outgoingTimeLock    uint32
			customRecords       record.CustomSet
			mpp                 *record.MPP
			metadata            []byte
		)

		// Define a helper function that checks this edge's feature
		// vector for support for a given feature. We assume at this
		// point that the feature vectors transitive dependencies have
		// been validated.
		supports := func(feature lnwire.FeatureBit) bool {
			// If this edge comes from router hints, the features
			// could be nil.
			if edge.ToNodeFeatures == nil {
				return false
			}
			return edge.ToNodeFeatures.HasFeature(feature)
		}

		if i == len(pathEdges)-1 {
			// If this is the last hop, then the hop payload will
			// contain the exact amount. In BOLT #4: Onion Routing
			// Protocol / "Payload for the Last Node", this is
			// detailed.
			amtToForward = finalHop.amt

			// Fee is not part of the hop payload, but only used for
			// reporting through RPC. Set to zero for the final hop.
			fee = 0

			if blindedPathSet == nil {
				totalTimeLock += uint32(finalHop.cltvDelta)
			} else {
				totalTimeLock += uint32(
					blindedPathSet.FinalCLTVDelta(),
				)
			}
			outgoingTimeLock = totalTimeLock

			// Attach any custom records to the final hop.
			customRecords = finalHop.records

			// If we're attaching a payment addr but the receiver
			// doesn't support both TLV and payment addrs, fail.
			payAddr := supports(lnwire.PaymentAddrOptional)
			if !payAddr && finalHop.paymentAddr.IsSome() {
				return nil, errors.New("cannot attach " +
					"payment addr")
			}

			// Otherwise attach the mpp record if it exists.
			// TODO(halseth): move this to payment life cycle,
			// where AMP options are set.
			finalHop.paymentAddr.WhenSome(func(addr [32]byte) {
				mpp = record.NewMPP(finalHop.totalAmt, addr)
			})

			metadata = finalHop.metadata

			if blindedPathSet != nil {
				totalAmtMsatBlinded = finalHop.totalAmt
			}
		} else {
			// The amount that the current hop needs to forward is
			// equal to the incoming amount of the next hop.
			amtToForward = nextIncomingAmount

			// The fee that needs to be paid to the current hop is
			// based on the amount that this hop needs to forward
			// and its policy for the outgoing channel. This policy
			// is stored as part of the incoming channel of
			// the next hop.
			outboundFee := pathEdges[i+1].policy.ComputeFee(
				amtToForward,
			)

			inboundFee := pathEdges[i].inboundFees.CalcFee(
				amtToForward + outboundFee,
			)

			fee = int64(outboundFee) + inboundFee
			if fee < 0 {
				fee = 0
			}

			// We'll take the total timelock of the preceding hop as
			// the outgoing timelock or this hop. Then we'll
			// increment the total timelock incurred by this hop.
			outgoingTimeLock = totalTimeLock
			totalTimeLock += uint32(
				pathEdges[i+1].policy.TimeLockDelta,
			)
		}

		// Since we're traversing the path backwards atm, we prepend
		// each new hop such that, the final slice of hops will be in
		// the forwards order.
		currentHop := &route.Hop{
			PubKeyBytes:      edge.ToNodePubKey(),
			ChannelID:        edge.ChannelID,
			AmtToForward:     amtToForward,
			OutgoingTimeLock: outgoingTimeLock,
			CustomRecords:    customRecords,
			MPP:              mpp,
			Metadata:         metadata,
			TotalAmtMsat:     totalAmtMsatBlinded,
		}

		hops = append([]*route.Hop{currentHop}, hops...)

		// Finally, we update the amount that needs to flow into the
		// *next* hop, which is the amount this hop needs to forward,
		// accounting for the fee that it takes.
		nextIncomingAmount = amtToForward + lnwire.MilliSatoshi(fee)
	}

	// If we are creating a route to a blinded path, we need to add some
	// additional data to the route that is required for blinded forwarding.
	// We do another pass on our edges to append this data.
	if blindedPathSet != nil {
		// If the passed in BlindedPaymentPathSet is non-nil but no
		// edge had a BlindedPayment attached, it means that the path
		// chosen was an introduction-node-only path. So in this case,
		// we can assume the relevant payment is the only one in the
		// payment set.
		if blindedPayment == nil {
			var err error
			blindedPayment, err = blindedPathSet.IntroNodeOnlyPath()
			if err != nil {
				return nil, err
			}
		}

		var (
			inBlindedRoute bool
			dataIndex      = 0

			blindedPath = blindedPayment.BlindedPath
			numHops     = len(blindedPath.BlindedHops)
			realFinal   = blindedPath.BlindedHops[numHops-1].
					BlindedNodePub

			introVertex = route.NewVertex(
				blindedPath.IntroductionPoint,
			)
		)

		for i, hop := range hops {
			// Once we locate our introduction node, we know that
			// every hop after this is part of the blinded route.
			if bytes.Equal(hop.PubKeyBytes[:], introVertex[:]) {
				inBlindedRoute = true
				hop.BlindingPoint = blindedPath.BlindingPoint
			}

			// We don't need to modify edges outside of our blinded
			// route.
			if !inBlindedRoute {
				continue
			}

			payload := blindedPath.BlindedHops[dataIndex].CipherText
			hop.EncryptedData = payload

			// All of the hops in a blinded route *except* the
			// final hop should have zero amounts / time locks.
			if i != len(hops)-1 {
				hop.AmtToForward = 0
				hop.OutgoingTimeLock = 0
			} else {
				// For the final hop, we swap out the pub key
				// bytes to the original destination node pub
				// key for that payment path.
				hop.PubKeyBytes = route.NewVertex(realFinal)
			}

			dataIndex++
		}
	}

	// With the base routing data expressed as hops, build the full route
	newRoute, err := route.NewRouteFromHops(
		nextIncomingAmount, totalTimeLock, route.Vertex(sourceVertex),
		hops,
	)
	if err != nil {
		return nil, err
	}

	return newRoute, nil
}

// edgeWeight computes the weight of an edge. This value is used when searching
// for the shortest path within the channel graph between two nodes. Weight is
// is the fee itself plus a time lock penalty added to it. This benefits
// channels with shorter time lock deltas and shorter (hops) routes in general.
// RiskFactor controls the influence of time lock on route selection. This is
// currently a fixed value, but might be configurable in the future.
func edgeWeight(lockedAmt lnwire.MilliSatoshi, fee lnwire.MilliSatoshi,
	timeLockDelta uint16) int64 {
	// timeLockPenalty is the penalty for the time lock delta of this channel.
	// It is controlled by RiskFactorBillionths and scales proportional
	// to the amount that will pass through channel. Rationale is that it if
	// a twice as large amount gets locked up, it is twice as bad.
	timeLockPenalty := int64(lockedAmt) * int64(timeLockDelta) *
		RiskFactorBillionths / 1000000000

	return int64(fee) + timeLockPenalty
}

// graphParams wraps the set of graph parameters passed to findPath.
type graphParams struct {
	// graph is the ChannelGraph to be used during path finding.
	graph Graph

	// additionalEdges is an optional set of edges that should be
	// considered during path finding, that is not already found in the
	// channel graph. These can either be private edges for bolt 11 invoices
	// or blinded edges when a payment to a blinded path is made.
	additionalEdges map[route.Vertex][]AdditionalEdge

	// bandwidthHints is an interface that provides bandwidth hints that
	// can provide a better estimate of the current channel bandwidth than
	// what is found in the graph. It will override the capacities and
	// disabled flags found in the graph for local channels when doing
	// path finding if it has updated values for that channel. In
	// particular, it should be set to the current available sending
	// bandwidth for active local channels, and 0 for inactive channels.
	bandwidthHints bandwidthHints
}

// RestrictParams wraps the set of restrictions passed to findPath that the
// found path must adhere to.
type RestrictParams struct {
	// ProbabilitySource is a callback that is expected to return the
	// success probability of traversing the channel from the node.
	ProbabilitySource func(route.Vertex, route.Vertex,
		lnwire.MilliSatoshi, btcutil.Amount) float64

	// FeeLimit is a maximum fee amount allowed to be used on the path from
	// the source to the target.
	FeeLimit lnwire.MilliSatoshi

	// OutgoingChannelIDs is the list of channels that are allowed for the
	// first hop. If nil, any channel may be used.
	OutgoingChannelIDs []uint64

	// LastHop is the pubkey of the last node before the final destination
	// is reached. If nil, any node may be used.
	LastHop *route.Vertex

	// CltvLimit is the maximum time lock of the route excluding the final
	// ctlv. After path finding is complete, the caller needs to increase
	// all cltv expiry heights with the required final cltv delta.
	CltvLimit uint32

	// DestCustomRecords contains the custom records to drop off at the
	// final hop, if any.
	DestCustomRecords record.CustomSet

	// DestFeatures is a feature vector describing what the final hop
	// supports. If none are provided, pathfinding will try to inspect any
	// features on the node announcement instead.
	DestFeatures *lnwire.FeatureVector

	// PaymentAddr is a random 32-byte value generated by the receiver to
	// mitigate probing vectors and payment sniping attacks on overpaid
	// invoices.
	PaymentAddr fn.Option[[32]byte]

	// Amp signals to the pathfinder that this payment is an AMP payment
	// and therefore it needs to account for additional AMP data in the
	// final hop payload size calculation.
	Amp *AMPOptions

	// Metadata is additional data that is sent along with the payment to
	// the payee.
	Metadata []byte

	// BlindedPaymentPathSet is necessary to determine the hop size of the
	// last/exit hop.
	BlindedPaymentPathSet *BlindedPaymentPathSet

	// FirstHopCustomRecords includes any records that should be included in
	// the update_add_htlc message towards our peer.
	FirstHopCustomRecords lnwire.CustomRecords
}

// PathFindingConfig defines global parameters that control the trade-off in
// path finding between fees and probability.
type PathFindingConfig struct {
	// AttemptCost is the fixed virtual cost in path finding of a failed
	// payment attempt. It is used to trade off potentially better routes
	// against their probability of succeeding.
	AttemptCost lnwire.MilliSatoshi

	// AttemptCostPPM is the proportional virtual cost in path finding of a
	// failed payment attempt. It is used to trade off potentially better
	// routes against their probability of succeeding. This parameter is
	// expressed in parts per million of the total payment amount.
	AttemptCostPPM int64

	// MinProbability defines the minimum success probability of the
	// returned route.
	MinProbability float64
}

// getOutgoingBalance returns the maximum available balance in any of the
// channels of the given node. The second return parameters is the total
// available balance.
func getOutgoingBalance(node route.Vertex, outgoingChans map[uint64]struct{},
	bandwidthHints bandwidthHints,
	g Graph) (lnwire.MilliSatoshi, lnwire.MilliSatoshi, error) {

	var max, total lnwire.MilliSatoshi
	cb := func(channel *graphdb.DirectedChannel) error {
		if !channel.OutPolicySet {
			return nil
		}

		chanID := channel.ChannelID

		// Enforce outgoing channel restriction.
		if outgoingChans != nil {
			if _, ok := outgoingChans[chanID]; !ok {
				return nil
			}
		}

		bandwidth, ok := bandwidthHints.availableChanBandwidth(
			chanID, 0,
		)

		// If the bandwidth is not available, use the channel capacity.
		// This can happen when a channel is added to the graph after
		// we've already queried the bandwidth hints.
		if !ok {
			bandwidth = lnwire.NewMSatFromSatoshis(channel.Capacity)
		}

		if bandwidth > max {
			max = bandwidth
		}

		var overflow bool
		total, overflow = overflowSafeAdd(total, bandwidth)
		if overflow {
			// If the current total and the bandwidth would
			// overflow the maximum value, we set the total to the
			// maximum value. Which is more milli-satoshis than are
			// in existence anyway, so the actual value is
			// irrelevant.
			total = lnwire.MilliSatoshi(math.MaxUint64)
		}

		return nil
	}

	// Iterate over all channels of the to node.
	err := g.ForEachNodeChannel(node, cb)
	if err != nil {
		return 0, 0, err
	}
	return max, total, err
}

// findPath attempts to find a path from the source node within the ChannelGraph
// to the target node that's capable of supporting a payment of `amt` value. The
// current approach implemented is modified version of Dijkstra's algorithm to
// find a single shortest path between the source node and the destination. The
// distance metric used for edges is related to the time-lock+fee costs along a
// particular edge. If a path is found, this function returns a slice of
// ChannelHop structs which encoded the chosen path from the target to the
// source. The search is performed backwards from destination node back to
// source. This is to properly accumulate fees that need to be paid along the
// path and accurately check the amount to forward at every node against the
// available bandwidth.
func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
	self, source, target route.Vertex, amt lnwire.MilliSatoshi,
	timePref float64, finalHtlcExpiry int32) ([]*unifiedEdge, float64,
	error) {

	// Pathfinding can be a significant portion of the total payment
	// latency, especially on low-powered devices. Log several metrics to
	// aid in the analysis performance problems in this area.
	start := time.Now()
	nodesVisited := 0
	edgesExpanded := 0
	defer func() {
		timeElapsed := time.Since(start)
		log.Debugf("Pathfinding perf metrics: nodes=%v, edges=%v, "+
			"time=%v", nodesVisited, edgesExpanded, timeElapsed)
	}()

	// If no destination features are provided, we will load what features
	// we have for the target node from our graph.
	features := r.DestFeatures
	if features == nil {
		var err error
		features, err = g.graph.FetchNodeFeatures(target)
		if err != nil {
			return nil, 0, err
		}
	}

	// Ensure that the destination's features don't include unknown
	// required features.
	err := feature.ValidateRequired(features)
	if err != nil {
		log.Warnf("Pathfinding destination node features: %v", err)
		return nil, 0, errUnknownRequiredFeature
	}

	// Ensure that all transitive dependencies are set.
	err = feature.ValidateDeps(features)
	if err != nil {
		log.Warnf("Pathfinding destination node features: %v", err)
		return nil, 0, errMissingDependentFeature
	}

	// Now that we know the feature vector is well-formed, we'll proceed in
	// checking that it supports the features we need. If the caller has a
	// payment address to attach, check that our destination feature vector
	// supports them.
	if r.PaymentAddr.IsSome() &&
		!features.HasFeature(lnwire.PaymentAddrOptional) {

		return nil, 0, errNoPaymentAddr
	}

	// Set up outgoing channel map for quicker access.
	var outgoingChanMap map[uint64]struct{}
	if len(r.OutgoingChannelIDs) > 0 {
		outgoingChanMap = make(map[uint64]struct{})
		for _, outChan := range r.OutgoingChannelIDs {
			outgoingChanMap[outChan] = struct{}{}
		}
	}

	// If we are routing from ourselves, check that we have enough local
	// balance available.
	if source == self {
		max, total, err := getOutgoingBalance(
			self, outgoingChanMap, g.bandwidthHints, g.graph,
		)
		if err != nil {
			return nil, 0, err
		}

		// If the total outgoing balance isn't sufficient, it will be
		// impossible to complete the payment.
		if total < amt {
			log.Warnf("Not enough outbound balance to send "+
				"htlc of amount: %v, only have local "+
				"balance: %v", amt, total)

			return nil, 0, errInsufficientBalance
		}

		// If there is only not enough capacity on a single route, it
		// may still be possible to complete the payment by splitting.
		if max < amt {
			return nil, 0, errNoPathFound
		}
	}

	// First we'll initialize an empty heap which'll help us to quickly
	// locate the next edge we should visit next during our graph
	// traversal.
	nodeHeap := newDistanceHeap(estimatedNodeCount)

	// Holds the current best distance for a given node.
	distance := make(map[route.Vertex]*nodeWithDist, estimatedNodeCount)

	additionalEdgesWithSrc := make(map[route.Vertex][]*edgePolicyWithSource)
	for vertex, additionalEdges := range g.additionalEdges {
		// Edges connected to self are always included in the graph,
		// therefore can be skipped. This prevents us from trying
		// routes to malformed hop hints.
		if vertex == self {
			continue
		}

		// Build reverse lookup to find incoming edges. Needed because
		// search is taken place from target to source.
		for _, additionalEdge := range additionalEdges {
			outgoingEdgePolicy := additionalEdge.EdgePolicy()
			toVertex := outgoingEdgePolicy.ToNodePubKey()

			incomingEdgePolicy := &edgePolicyWithSource{
				sourceNode: vertex,
				edge:       additionalEdge,
			}

			additionalEdgesWithSrc[toVertex] =
				append(additionalEdgesWithSrc[toVertex],
					incomingEdgePolicy)
		}
	}

	// The payload size of the final hop differ from intermediate hops
	// and depends on whether the destination is blinded or not.
	lastHopPayloadSize := lastHopPayloadSize(r, finalHtlcExpiry, amt)

	// We can't always assume that the end destination is publicly
	// advertised to the network so we'll manually include the target node.
	// The target node charges no fee. Distance is set to 0, because this is
	// the starting point of the graph traversal. We are searching backwards
	// to get the fees first time right and correctly match channel
	// bandwidth.
	//
	// Don't record the initial partial path in the distance map and reserve
	// that key for the source key in the case we route to ourselves.
	partialPath := &nodeWithDist{
		dist:              0,
		weight:            0,
		node:              target,
		netAmountReceived: amt,
		incomingCltv:      finalHtlcExpiry,
		probability:       1,
		routingInfoSize:   lastHopPayloadSize,
	}

	// Calculate the absolute cltv limit. Use uint64 to prevent an overflow
	// if the cltv limit is MaxUint32.
	absoluteCltvLimit := uint64(r.CltvLimit) + uint64(finalHtlcExpiry)

	// Calculate the default attempt cost as configured globally.
	defaultAttemptCost := float64(
		cfg.AttemptCost +
			amt*lnwire.MilliSatoshi(cfg.AttemptCostPPM)/1000000,
	)

	// Validate time preference value.
	if math.Abs(timePref) > 1 {
		return nil, 0, fmt.Errorf("time preference %v out of range "+
			"[-1, 1]", timePref)
	}

	// Scale to avoid the extremes -1 and 1 which run into infinity issues.
	timePref *= 0.9

	// Apply time preference. At 0, the default attempt cost will
	// be used.
	absoluteAttemptCost := defaultAttemptCost * (1/(0.5-timePref/2) - 1)

	log.Debugf("Pathfinding absolute attempt cost: %v sats",
		absoluteAttemptCost/1000)

	// processEdge is a helper closure that will be used to make sure edges
	// satisfy our specific requirements.
	processEdge := func(fromVertex route.Vertex,
		edge *unifiedEdge, toNodeDist *nodeWithDist) {

		edgesExpanded++

		// Calculate inbound fee charged by "to" node. The exit hop
		// doesn't charge inbound fees. If the "to" node is the exit
		// hop, its inbound fees have already been set to zero by
		// nodeEdgeUnifier.
		inboundFee := edge.inboundFees.CalcFee(
			toNodeDist.netAmountReceived,
		)

		// Make sure that the node total fee is never negative.
		// Routing nodes treat a total fee that turns out
		// negative as a zero fee and pathfinding should do the
		// same.
		minInboundFee := -int64(toNodeDist.outboundFee)
		if inboundFee < minInboundFee {
			inboundFee = minInboundFee
		}

		// Calculate amount that the candidate node would have to send
		// out.
		amountToSend := toNodeDist.netAmountReceived +
			lnwire.MilliSatoshi(inboundFee)

		// Check if accumulated fees would exceed fee limit when this
		// node would be added to the path.
		totalFee := int64(amountToSend) - int64(amt)

		log.Trace(lnutils.NewLogClosure(func() string {
			return fmt.Sprintf(
				"Checking fromVertex (%v) with "+
					"minInboundFee=%v, inboundFee=%v, "+
					"amountToSend=%v, amt=%v, totalFee=%v",
				fromVertex, minInboundFee, inboundFee,
				amountToSend, amt, totalFee,
			)
		}))

		if totalFee > 0 && lnwire.MilliSatoshi(totalFee) > r.FeeLimit {
			return
		}

		// Request the success probability for this edge.
		edgeProbability := r.ProbabilitySource(
			fromVertex, toNodeDist.node, amountToSend,
			edge.capacity,
		)

		log.Trace(lnutils.NewLogClosure(func() string {
			return fmt.Sprintf("path finding probability: fromnode=%v,"+
				" tonode=%v, amt=%v, cap=%v, probability=%v",
				fromVertex, toNodeDist.node, amountToSend,
				edge.capacity, edgeProbability)
		}))

		// If the probability is zero, there is no point in trying.
		if edgeProbability == 0 {
			return
		}

		// Compute fee that fromVertex is charging. It is based on the
		// amount that needs to be sent to the next node in the route.
		//
		// Source node has no predecessor to pay a fee. Therefore set
		// fee to zero, because it should not be included in the fee
		// limit check and edge weight.
		//
		// Also determine the time lock delta that will be added to the
		// route if fromVertex is selected. If fromVertex is the source
		// node, no additional timelock is required.
		var (
			timeLockDelta uint16
			outboundFee   int64
		)

		if fromVertex != source {
			outboundFee = int64(
				edge.policy.ComputeFee(amountToSend),
			)
			timeLockDelta = edge.policy.TimeLockDelta
		}

		incomingCltv := toNodeDist.incomingCltv + int32(timeLockDelta)

		// Check that we are within our CLTV limit.
		if uint64(incomingCltv) > absoluteCltvLimit {
			return
		}

		// netAmountToReceive is the amount that the node that is added
		// to the distance map needs to receive from a (to be found)
		// previous node in the route. The inbound fee of the receiving
		// node is already subtracted from this value. The previous node
		// will need to pay the amount that this node forwards plus the
		// fee it charges plus this node's inbound fee.
		netAmountToReceive := amountToSend +
			lnwire.MilliSatoshi(outboundFee)

		// Calculate total probability of successfully reaching target
		// by multiplying the probabilities. Both this edge and the rest
		// of the route must succeed.
		probability := toNodeDist.probability * edgeProbability

		// If the probability is below the specified lower bound, we can
		// abandon this direction. Adding further nodes can only lower
		// the probability more.
		if probability < cfg.MinProbability {
			return
		}

		// Calculate the combined fee for this edge. Dijkstra does not
		// support negative edge weights. Because this fee feeds into
		// the edge weight calculation, we don't allow it to be
		// negative.
		signedFee := inboundFee + outboundFee
		fee := lnwire.MilliSatoshi(0)
		if signedFee > 0 {
			fee = lnwire.MilliSatoshi(signedFee)
		}

		// By adding fromVertex in the route, there will be an extra
		// weight composed of the fee that this node will charge and
		// the amount that will be locked for timeLockDelta blocks in
		// the HTLC that is handed out to fromVertex.
		weight := edgeWeight(amountToSend, fee, timeLockDelta)

		// Compute the tentative weight to this new channel/edge
		// which is the weight from our toNode to the target node
		// plus the weight of this edge.
		tempWeight := toNodeDist.weight + weight

		// Add an extra factor to the weight to take into account the
		// probability. Another reason why we rounded the fee up to zero
		// is to prevent a highly negative fee from cancelling out the
		// extra factor. We don't want an always-failing node to attract
		// traffic using a highly negative fee and escape penalization.
		tempDist := getProbabilityBasedDist(
			tempWeight, probability,
			absoluteAttemptCost,
		)

		// If there is already a best route stored, compare this
		// candidate route with the best route so far.
		current, ok := distance[fromVertex]
		if ok {
			// If this route is worse than what we already found,
			// skip this route.
			if tempDist > current.dist {
				return
			}

			// If the route is equally good and the probability
			// isn't better, skip this route. It is important to
			// also return if both cost and probability are equal,
			// because otherwise the algorithm could run into an
			// endless loop.
			probNotBetter := probability <= current.probability
			if tempDist == current.dist && probNotBetter {
				return
			}
		}

		// Calculate the total routing info size if this hop were to be
		// included. If we are coming from the source hop, the payload
		// size is zero, because the original htlc isn't in the onion
		// blob.
		var payloadSize uint64
		if fromVertex != source {
			// In case the unifiedEdge does not have a payload size
			// function supplied we request a graceful shutdown
			// because this should never happen.
			if edge.hopPayloadSizeFn == nil {
				log.Criticalf("No payload size function "+
					"available for edge=%v unable to "+
					"determine payload size: %v", edge,
					ErrNoPayLoadSizeFunc)

				return
			}

			payloadSize = edge.hopPayloadSizeFn(
				amountToSend,
				uint32(toNodeDist.incomingCltv),
				edge.policy.ChannelID,
			)
		}

		routingInfoSize := toNodeDist.routingInfoSize + payloadSize
		// Skip paths that would exceed the maximum routing info size.
		if routingInfoSize > sphinx.MaxPayloadSize {
			return
		}

		// All conditions are met and this new tentative distance is
		// better than the current best known distance to this node.
		// The new better distance is recorded, and also our "next hop"
		// map is populated with this edge.
		withDist := &nodeWithDist{
			dist:              tempDist,
			weight:            tempWeight,
			node:              fromVertex,
			netAmountReceived: netAmountToReceive,
			outboundFee:       lnwire.MilliSatoshi(outboundFee),
			incomingCltv:      incomingCltv,
			probability:       probability,
			nextHop:           edge,
			routingInfoSize:   routingInfoSize,
		}
		distance[fromVertex] = withDist

		// Either push withDist onto the heap if the node
		// represented by fromVertex is not already on the heap OR adjust
		// its position within the heap via heap.Fix.
		nodeHeap.PushOrFix(withDist)
	}

	// TODO(roasbeef): also add path caching
	//  * similar to route caching, but doesn't factor in the amount

	// Cache features because we visit nodes multiple times.
	featureCache := make(map[route.Vertex]*lnwire.FeatureVector)

	// getGraphFeatures returns (cached) node features from the graph.
	getGraphFeatures := func(node route.Vertex) (*lnwire.FeatureVector,
		error) {

		// Check cache for features of the fromNode.
		fromFeatures, ok := featureCache[node]
		if ok {
			return fromFeatures, nil
		}

		// Fetch node features fresh from the graph.
		fromFeatures, err := g.graph.FetchNodeFeatures(node)
		if err != nil {
			return nil, err
		}

		// Don't route through nodes that contain unknown required
		// features and mark as nil in the cache.
		err = feature.ValidateRequired(fromFeatures)
		if err != nil {
			featureCache[node] = nil
			return nil, nil
		}

		// Don't route through nodes that don't properly set all
		// transitive feature dependencies and mark as nil in the cache.
		err = feature.ValidateDeps(fromFeatures)
		if err != nil {
			featureCache[node] = nil
			return nil, nil
		}

		// Update cache.
		featureCache[node] = fromFeatures

		return fromFeatures, nil
	}

	routeToSelf := source == target
	for {
		nodesVisited++

		pivot := partialPath.node
		isExitHop := partialPath.nextHop == nil

		// Create unified policies for all incoming connections. Don't
		// use inbound fees for the exit hop.
		u := newNodeEdgeUnifier(
			self, pivot, !isExitHop, outgoingChanMap,
		)

		err := u.addGraphPolicies(g.graph)
		if err != nil {
			return nil, 0, err
		}

		// We add hop hints that were supplied externally.
		for _, reverseEdge := range additionalEdgesWithSrc[pivot] {
			// Assume zero inbound fees for route hints. If inbound
			// fees would apply, they couldn't be communicated in
			// bolt11 invoices currently.
			inboundFee := models.InboundFee{}

			// Hop hints don't contain a capacity. We set one here,
			// since a capacity is needed for probability
			// calculations. We set a high capacity to act as if
			// there is enough liquidity, otherwise the hint would
			// not have been added by a wallet.
			// We also pass the payload size function to the
			// graph data so that we calculate the exact payload
			// size when evaluating this hop for a route.
			u.addPolicy(
				reverseEdge.sourceNode,
				reverseEdge.edge.EdgePolicy(),
				inboundFee,
				fakeHopHintCapacity,
				reverseEdge.edge.IntermediatePayloadSize,
				reverseEdge.edge.BlindedPayment(),
			)
		}

		netAmountReceived := partialPath.netAmountReceived

		// Expand all connections using the optimal policy for each
		// connection.
		for fromNode, edgeUnifier := range u.edgeUnifiers {
			// The target node is not recorded in the distance map.
			// Therefore we need to have this check to prevent
			// creating a cycle. Only when we intend to route to
			// self, we allow this cycle to form. In that case we'll
			// also break out of the search loop below.
			if !routeToSelf && fromNode == target {
				continue
			}

			// Apply last hop restriction if set.
			if r.LastHop != nil &&
				pivot == target && fromNode != *r.LastHop {

				continue
			}

			edge := edgeUnifier.getEdge(
				netAmountReceived, g.bandwidthHints,
				partialPath.outboundFee,
			)

			if edge == nil {
				continue
			}

			// Get feature vector for fromNode.
			fromFeatures, err := getGraphFeatures(fromNode)
			if err != nil {
				return nil, 0, err
			}

			// If there are no valid features, skip this node.
			if fromFeatures == nil {
				continue
			}

			// Check if this candidate node is better than what we
			// already have.
			processEdge(fromNode, edge, partialPath)
		}

		if nodeHeap.Len() == 0 {
			break
		}

		// Fetch the node within the smallest distance from our source
		// from the heap.
		partialPath = heap.Pop(&nodeHeap).(*nodeWithDist)

		// If we've reached our source (or we don't have any incoming
		// edges), then we're done here and can exit the graph
		// traversal early.
		if partialPath.node == source {
			break
		}
	}

	// Use the distance map to unravel the forward path from source to
	// target.
	var pathEdges []*unifiedEdge
	currentNode := source
	for {
		// Determine the next hop forward using the next map.
		currentNodeWithDist, ok := distance[currentNode]
		if !ok {
			// If the node doesn't have a next hop it means we
			// didn't find a path.
			return nil, 0, errNoPathFound
		}

		// Add the next hop to the list of path edges.
		pathEdges = append(pathEdges, currentNodeWithDist.nextHop)

		// Advance current node.
		currentNode = currentNodeWithDist.nextHop.policy.ToNodePubKey()

		// Check stop condition at the end of this loop. This prevents
		// breaking out too soon for self-payments that have target set
		// to source.
		if currentNode == target {
			break
		}
	}

	// For the final hop, we'll set the node features to those determined
	// above. These are either taken from the destination features, e.g.
	// virtual or invoice features, or loaded as a fallback from the graph.
	// The transitive dependencies were already validated above, so no need
	// to do so now.
	//
	// NOTE: This may overwrite features loaded from the graph if
	// destination features were provided. This is fine though, since our
	// route construction does not care where the features are actually
	// taken from. In the future we may wish to do route construction within
	// findPath, and avoid using ChannelEdgePolicy altogether.
	pathEdges[len(pathEdges)-1].policy.ToNodeFeatures = features

	log.Debugf("Found route: probability=%v, hops=%v, fee=%v",
		distance[source].probability, len(pathEdges),
		distance[source].netAmountReceived-amt)

	return pathEdges, distance[source].probability, nil
}

// blindedPathRestrictions are a set of constraints to adhere to when
// choosing a set of blinded paths to this node.
type blindedPathRestrictions struct {
	// minNumHops is the minimum number of hops to include in a blinded
	// path. This doesn't include our node, so if the minimum is 1, then
	// the path will contain at minimum our node along with an introduction
	// node hop. A minimum of 0 will include paths where this node is the
	// introduction node and so should be used with caution.
	minNumHops uint8

	// maxNumHops is the maximum number of hops to include in a blinded
	// path. This doesn't include our node, so if the maximum is 1, then
	// the path will contain our node along with an introduction node hop.
	maxNumHops uint8

	// nodeOmissionSet holds a set of node IDs of nodes that we should
	// ignore during blinded path selection.
	nodeOmissionSet fn.Set[route.Vertex]
}

// blindedHop holds the information about a hop we have selected for a blinded
// path.
type blindedHop struct {
	vertex       route.Vertex
	channelID    uint64
	edgeCapacity btcutil.Amount
}

// findBlindedPaths does a depth first search from the target node to find a set
// of blinded paths to the target node given the set of restrictions. This
// function will select and return any candidate path. A candidate path is a
// path to the target node with a size determined by the given hop number
// constraints where all the nodes on the path signal the route blinding feature
// _and_ the introduction node for the path has more than one public channel.
// Any filtering of paths based on payment value or success probabilities is
// left to the caller.
func findBlindedPaths(g Graph, target route.Vertex,
	restrictions *blindedPathRestrictions) ([][]blindedHop, error) {

	// Sanity check the restrictions.
	if restrictions.minNumHops > restrictions.maxNumHops {
		return nil, fmt.Errorf("maximum number of blinded path hops "+
			"(%d) must be greater than or equal to the minimum "+
			"number of hops (%d)", restrictions.maxNumHops,
			restrictions.minNumHops)
	}

	// If the node is not the destination node, then it is required that the
	// node advertise the route blinding feature-bit in order for it to be
	// chosen as a node on the blinded path.
	supportsRouteBlinding := func(node route.Vertex) (bool, error) {
		if node == target {
			return true, nil
		}

		features, err := g.FetchNodeFeatures(node)
		if err != nil {
			return false, err
		}

		return features.HasFeature(lnwire.RouteBlindingOptional), nil
	}

	// This function will have some recursion. We will spin out from the
	// target node & append edges to the paths until we reach various exit
	// conditions such as: The maxHops number being reached or reaching
	// a node that doesn't have any other edges - in that final case, the
	// whole path should be ignored.
	paths, _, err := processNodeForBlindedPath(
		g, target, supportsRouteBlinding, nil, restrictions,
	)
	if err != nil {
		return nil, err
	}

	// Reverse each path so that the order is correct (from introduction
	// node to last hop node) and then append this node on as the
	// destination of each path.
	orderedPaths := make([][]blindedHop, len(paths))
	for i, path := range paths {
		sort.Slice(path, func(i, j int) bool {
			return j < i
		})

		orderedPaths[i] = append(path, blindedHop{vertex: target})
	}

	// Handle the special case that allows a blinded path with the
	// introduction node as the destination node.
	if restrictions.minNumHops == 0 {
		singleHopPath := [][]blindedHop{{{vertex: target}}}

		//nolint:makezero
		orderedPaths = append(
			orderedPaths, singleHopPath...,
		)
	}

	return orderedPaths, err
}

// processNodeForBlindedPath is a recursive function that traverses the graph
// in a depth first manner searching for a set of blinded paths to the given
// node.
func processNodeForBlindedPath(g Graph, node route.Vertex,
	supportsRouteBlinding func(vertex route.Vertex) (bool, error),
	alreadyVisited map[route.Vertex]bool,
	restrictions *blindedPathRestrictions) ([][]blindedHop, bool, error) {

	// If we have already visited the maximum number of hops, then this path
	// is complete and we can exit now.
	if len(alreadyVisited) > int(restrictions.maxNumHops) {
		return nil, false, nil
	}

	// If we have already visited this peer on this path, then we skip
	// processing it again.
	if alreadyVisited[node] {
		return nil, false, nil
	}

	// If we have explicitly been told to ignore this node for blinded paths
	// then we skip it too.
	if restrictions.nodeOmissionSet.Contains(node) {
		return nil, false, nil
	}

	supports, err := supportsRouteBlinding(node)
	if err != nil {
		return nil, false, err
	}
	if !supports {
		return nil, false, nil
	}

	// At this point, copy the alreadyVisited map.
	visited := make(map[route.Vertex]bool, len(alreadyVisited))
	for r := range alreadyVisited {
		visited[r] = true
	}

	// Add this node the visited set.
	visited[node] = true

	var (
		hopSets   [][]blindedHop
		chanCount int
	)

	// Now, iterate over the node's channels in search for paths to this
	// node that can be used for blinded paths
	err = g.ForEachNodeChannel(node,
		func(channel *graphdb.DirectedChannel) error {
			// Keep track of how many incoming channels this node
			// has. We only use a node as an introduction node if it
			// has channels other than the one that lead us to it.
			chanCount++

			// Process each channel peer to gather any paths that
			// lead to the peer.
			nextPaths, hasMoreChans, err := processNodeForBlindedPath( //nolint:lll
				g, channel.OtherNode, supportsRouteBlinding,
				visited, restrictions,
			)
			if err != nil {
				return err
			}

			hop := blindedHop{
				vertex:       channel.OtherNode,
				channelID:    channel.ChannelID,
				edgeCapacity: channel.Capacity,
			}

			// For each of the paths returned, unwrap them and
			// append this hop to them.
			for _, path := range nextPaths {
				hopSets = append(
					hopSets,
					append([]blindedHop{hop}, path...),
				)
			}

			// If this node does have channels other than the one
			// that lead to it, and if the hop count up to this node
			// meets the minHop requirement, then we also add a
			// path that starts at this node.
			if hasMoreChans &&
				len(visited) >= int(restrictions.minNumHops) {

				hopSets = append(hopSets, []blindedHop{hop})
			}

			return nil
		},
	)
	if err != nil {
		return nil, false, err
	}

	return hopSets, chanCount > 1, nil
}

// getProbabilityBasedDist converts a weight into a distance that takes into
// account the success probability and the (virtual) cost of a failed payment
// attempt.
//
// Derivation:
//
// Suppose there are two routes A and B with fees Fa and Fb and success
// probabilities Pa and Pb.
//
// Is the expected cost of trying route A first and then B lower than trying the
// other way around?
//
// The expected cost of A-then-B is: Pa*Fa + (1-Pa)*Pb*(c+Fb)
//
// The expected cost of B-then-A is: Pb*Fb + (1-Pb)*Pa*(c+Fa)
//
// In these equations, the term representing the case where both A and B fail is
// left out because its value would be the same in both cases.
//
// Pa*Fa + (1-Pa)*Pb*(c+Fb) < Pb*Fb + (1-Pb)*Pa*(c+Fa)
//
// Pa*Fa + Pb*c + Pb*Fb - Pa*Pb*c - Pa*Pb*Fb < Pb*Fb + Pa*c + Pa*Fa - Pa*Pb*c - Pa*Pb*Fa
//
// Removing terms that cancel out:
// Pb*c - Pa*Pb*Fb < Pa*c - Pa*Pb*Fa
//
// Divide by Pa*Pb:
// c/Pa - Fb < c/Pb - Fa
//
// Move terms around:
// Fa + c/Pa < Fb + c/Pb
//
// So the value of F + c/P can be used to compare routes.
func getProbabilityBasedDist(weight int64, probability float64,
	penalty float64) int64 {

	// Prevent divide by zero by returning early.
	if probability == 0 {
		return infinity
	}

	// Calculate distance.
	dist := float64(weight) + penalty/probability

	// Avoid cast if an overflow would occur. The maxFloat constant is
	// chosen to stay well below the maximum float64 value that is still
	// convertible to int64.
	const maxFloat = 9000000000000000000
	if dist > maxFloat {
		return infinity
	}

	return int64(dist)
}

// lastHopPayloadSize calculates the payload size of the final hop in a route.
// It depends on the tlv types which are present and also whether the hop is
// part of a blinded route or not.
func lastHopPayloadSize(r *RestrictParams, finalHtlcExpiry int32,
	amount lnwire.MilliSatoshi) uint64 {

	if r.BlindedPaymentPathSet != nil {
		paymentPath := r.BlindedPaymentPathSet.
			LargestLastHopPayloadPath()
		blindedPath := paymentPath.BlindedPath.BlindedHops
		blindedPoint := paymentPath.BlindedPath.BlindingPoint

		encryptedData := blindedPath[len(blindedPath)-1].CipherText
		finalHop := route.Hop{
			AmtToForward:     amount,
			OutgoingTimeLock: uint32(finalHtlcExpiry),
			EncryptedData:    encryptedData,
		}
		if len(blindedPath) == 1 {
			finalHop.BlindingPoint = blindedPoint
		}

		// The final hop does not have a short chanID set.
		return finalHop.PayloadSize(0)
	}

	var mpp *record.MPP
	r.PaymentAddr.WhenSome(func(addr [32]byte) {
		mpp = record.NewMPP(amount, addr)
	})

	var amp *record.AMP
	if r.Amp != nil {
		// The AMP payload is not easy accessible at this point but we
		// are only interested in the size of the payload so we just use
		// the AMP record dummy.
		amp = &record.MaxAmpPayLoadSize
	}

	finalHop := route.Hop{
		AmtToForward:     amount,
		OutgoingTimeLock: uint32(finalHtlcExpiry),
		CustomRecords:    r.DestCustomRecords,
		MPP:              mpp,
		AMP:              amp,
		Metadata:         r.Metadata,
	}

	// The final hop does not have a short chanID set.
	return finalHop.PayloadSize(0)
}

// overflowSafeAdd adds two MilliSatoshi values and returns the result. If an
// overflow could occur, zero is returned instead and the boolean is set to
// true.
func overflowSafeAdd(x, y lnwire.MilliSatoshi) (lnwire.MilliSatoshi, bool) {
	if y > math.MaxUint64-x {
		// Overflow would occur, return 0 and set overflow flag.
		return 0, true
	}

	return x + y, false
}
