package graphdb

import (
	"context"
	"iter"
	"net"
	"time"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/wire"
	"github.com/lightningnetwork/lnd/batch"
	"github.com/lightningnetwork/lnd/graph/db/models"
	"github.com/lightningnetwork/lnd/lnwire"
	"github.com/lightningnetwork/lnd/routing/route"
)

// NodeTraverser is an abstract read only interface that provides information
// about nodes and their edges. The interface is about providing fast read-only
// access to the graph and so if a cache is available, it should be used.
type NodeTraverser interface {
	// ForEachNodeDirectedChannel calls the callback for every channel of
	// the given node.
	ForEachNodeDirectedChannel(ctx context.Context, nodePub route.Vertex,
		cb func(channel *DirectedChannel) error, reset func()) error

	// FetchNodeFeatures returns the features of the given node.
	FetchNodeFeatures(ctx context.Context,
		nodePub route.Vertex) (*lnwire.FeatureVector, error)
}

// Store represents the main interface for the channel graph database for all
// channels and nodes gossiped via the V1 gossip protocol as defined in BOLT 7.
type Store interface { //nolint:interfacebloat
	// ForEachNodeDirectedChannel calls the callback for every channel of
	// the given node.
	ForEachNodeDirectedChannel(ctx context.Context, v lnwire.GossipVersion,
		nodePub route.Vertex, cb func(channel *DirectedChannel) error,
		reset func()) error

	// FetchNodeFeatures returns the features of the given node.
	FetchNodeFeatures(ctx context.Context, v lnwire.GossipVersion,
		nodePub route.Vertex) (*lnwire.FeatureVector, error)

	// AddNode adds a vertex/node to the graph database. If the
	// node is not in the database from before, this will add a new,
	// unconnected one to the graph. If it is present from before, this will
	// update that node's information. Note that this method is expected to
	// only be called to update an already present node from a node
	// announcement, or to insert a node found in a channel update.
	AddNode(ctx context.Context, node *models.Node,
		op ...batch.SchedulerOption) error

	// AddrsForNode returns all known addresses for the target node public
	// key that the graph DB is aware of. The returned boolean indicates if
	// the given node is unknown to the graph DB or not.
	AddrsForNode(ctx context.Context, v lnwire.GossipVersion,
		nodePub *btcec.PublicKey) (bool, []net.Addr, error)

	// ForEachSourceNodeChannel iterates through all channels of the source
	// node, executing the passed callback on each. The call-back is
	// provided with the channel's outpoint, whether we have a policy for
	// the channel and the channel peer's node information.
	ForEachSourceNodeChannel(ctx context.Context, v lnwire.GossipVersion,
		cb func(chanPoint wire.OutPoint, havePolicy bool,
			otherNode *models.Node) error,
		reset func()) error

	// ForEachNodeChannel iterates through all channels of the given node,
	// executing the passed callback with an edge info structure and the
	// policies of each end of the channel. The first edge policy is the
	// outgoing edge *to* the connecting node, while the second is the
	// incoming edge *from* the connecting node. If the callback returns an
	// error, then the iteration is halted with the error propagated back up
	// to the caller.
	//
	// Unknown policies are passed into the callback as nil values.
	ForEachNodeChannel(ctx context.Context, v lnwire.GossipVersion,
		nodePub route.Vertex, cb func(*models.ChannelEdgeInfo,
			*models.ChannelEdgePolicy,
			*models.ChannelEdgePolicy) error, reset func()) error

	// ForEachNodeCached is similar to forEachNode, but it returns
	// DirectedChannel data to the call-back. If withAddrs is true, then
	// the call-back will also be provided with the addresses associated
	// with the node. The address retrieval will likely result in an
	// additional round-trip to the database, so it should only be used if
	// the addresses are actually needed.
	//
	// NOTE: The callback contents MUST not be modified.
	ForEachNodeCached(ctx context.Context, v lnwire.GossipVersion,
		withAddrs bool,
		cb func(ctx context.Context, node route.Vertex,
			addrs []net.Addr,
			chans map[uint64]*DirectedChannel) error,
		reset func()) error

	// ForEachNode iterates through all the stored vertices/nodes in the
	// graph, executing the passed callback with each node encountered. If
	// the callback returns an error, then the transaction is aborted and
	// the iteration stops early.
	ForEachNode(ctx context.Context, v lnwire.GossipVersion,
		cb func(*models.Node) error, reset func()) error

	// ForEachNodeCacheable iterates through all the stored vertices/nodes
	// in the graph, executing the passed callback with each node
	// encountered. If the callback returns an error, then the transaction
	// is aborted and the iteration stops early.
	ForEachNodeCacheable(ctx context.Context, v lnwire.GossipVersion,
		cb func(route.Vertex, *lnwire.FeatureVector) error,
		reset func()) error

	// LookupAlias attempts to return the alias as advertised by the target
	// node.
	LookupAlias(ctx context.Context, v lnwire.GossipVersion,
		pub *btcec.PublicKey) (string, error)

	// DeleteNode starts a new database transaction to remove a
	// vertex/node from the database according to the node's public key.
	DeleteNode(ctx context.Context, v lnwire.GossipVersion,
		nodePub route.Vertex) error

	// NodeUpdatesInHorizon returns all the known lightning nodes which have
	// updates within the passed range for the given gossip version. For v1
	// gossip, the range is time-based with [start, end) per BOLT 07. This
	// method can be used by two nodes to quickly determine if they have
	// the same set of up to date node announcements.
	NodeUpdatesInHorizon(ctx context.Context, v lnwire.GossipVersion,
		r NodeUpdateRange,
		opts ...IteratorOption) iter.Seq2[*models.Node, error]

	// FetchNode attempts to look up a target node by its identity
	// public key. If the node isn't found in the database, then
	// ErrGraphNodeNotFound is returned.
	FetchNode(ctx context.Context, v lnwire.GossipVersion,
		nodePub route.Vertex) (*models.Node, error)

	// HasV1Node determines if the graph has a vertex identified by
	// the target node identity public key in the V1 graph. If the node
	// exists in the database, a timestamp of when the data for the node
	// was lasted updated is returned along with a true boolean. Otherwise,
	// an empty time.Time is returned with a false boolean.
	// This is specific to the V1 graph since only V1 node announcements
	// use timestamps for their latest update timestamp.
	HasV1Node(ctx context.Context, nodePub [33]byte) (time.Time, bool,
		error)

	// HasNode determines if the graph has a vertex identified by
	// the target node identity public key.
	HasNode(ctx context.Context, v lnwire.GossipVersion,
		nodePub [33]byte) (bool, error)

	// IsPublicNode is a helper method that determines whether the node with
	// the given public key is seen as a public node in the graph from the
	// graph's source node's point of view.
	IsPublicNode(ctx context.Context, v lnwire.GossipVersion,
		pubKey [33]byte) (bool, error)

	// GraphSession will provide the call-back with access to a
	// NodeTraverser instance which can be used to perform queries against
	// the channel graph.
	GraphSession(ctx context.Context,
		cb func(graph NodeTraverser) error, reset func()) error

	// ForEachChannel iterates through all the channel edges stored within
	// the graph and invokes the passed callback for each edge. The callback
	// takes two edges as since this is a directed graph, both the in/out
	// edges are visited. If the callback returns an error, then the
	// transaction is aborted and the iteration stops early.
	//
	// NOTE: If an edge can't be found, or wasn't advertised, then a nil
	// pointer for that particular channel edge routing policy will be
	// passed into the callback.
	//
	// TODO(elle): add a cross-version iteration API and make this iterate
	// over all versions.
	ForEachChannel(ctx context.Context, v lnwire.GossipVersion,
		cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy,
			*models.ChannelEdgePolicy) error, reset func()) error

	// ForEachChannelCacheable iterates through all the channel edges stored
	// within the graph and invokes the passed callback for each edge. The
	// callback takes two edges as since this is a directed graph, both the
	// in/out edges are visited. If the callback returns an error, then the
	// transaction is aborted and the iteration stops early.
	//
	// NOTE: If an edge can't be found, or wasn't advertised, then a nil
	// pointer for that particular channel edge routing policy will be
	// passed into the callback.
	//
	// NOTE: this method is like ForEachChannel but fetches only the data
	// required for the graph cache.
	ForEachChannelCacheable(ctx context.Context, v lnwire.GossipVersion,
		cb func(*models.CachedEdgeInfo, *models.CachedEdgePolicy,
			*models.CachedEdgePolicy) error, reset func()) error

	// DisabledChannelIDs returns the channel ids of disabled channels.
	// A channel is disabled when two of the associated ChanelEdgePolicies
	// have their disabled bit on.
	DisabledChannelIDs(ctx context.Context,
		v lnwire.GossipVersion) ([]uint64, error)

	// AddChannelEdge adds a new (undirected, blank) edge to the graph
	// database. An undirected edge from the two target nodes are created.
	// The information stored denotes the static attributes of the channel,
	// such as the channelID, the keys involved in creation of the channel,
	// and the set of features that the channel supports. The chanPoint and
	// chanID are used to uniquely identify the edge globally within the
	// database.
	AddChannelEdge(ctx context.Context, edge *models.ChannelEdgeInfo,
		op ...batch.SchedulerOption) error

	// HasV1ChannelEdge returns true if the database knows of a channel edge
	// with the passed channel ID, and false otherwise. If an edge with that
	// ID is found within the graph, then two time stamps representing the
	// last time the edge was updated for both directed edges are returned
	// along with the boolean. If it is not found, then the zombie index is
	// checked and its result is returned as the second boolean.
	HasV1ChannelEdge(ctx context.Context, chanID uint64) (
		time.Time, time.Time, bool, bool, error)

	// HasChannelEdge returns true if the database knows of a channel edge
	// with the passed channel ID and gossip version, and false otherwise.
	// If it is not found, then the zombie index is checked and its result
	// is returned as the second boolean.
	HasChannelEdge(ctx context.Context, v lnwire.GossipVersion,
		chanID uint64) (bool, bool, error)

	// DeleteChannelEdges removes edges with the given channel IDs from the
	// database and marks them as zombies. This ensures that we're unable to
	// re-add it to our database once again. If an edge does not exist
	// within the database, then ErrEdgeNotFound will be returned. If
	// strictZombiePruning is true, then when we mark these edges as
	// zombies, we'll set up the keys such that we require the node that
	// failed to send the fresh update to be the one that resurrects the
	// channel from its zombie state. The markZombie bool denotes whether
	// to mark the channel as a zombie.
	DeleteChannelEdges(ctx context.Context, v lnwire.GossipVersion,
		strictZombiePruning, markZombie bool, chanIDs ...uint64) (
		[]*models.ChannelEdgeInfo, error)

	// AddEdgeProof sets the proof of an existing edge in the graph
	// database.
	AddEdgeProof(ctx context.Context, chanID lnwire.ShortChannelID,
		proof *models.ChannelAuthProof) error

	// ChannelID attempt to lookup the 8-byte compact channel ID which maps
	// to the passed channel point (outpoint). If the passed channel doesn't
	// exist within the database, then ErrEdgeNotFound is returned.
	ChannelID(ctx context.Context, v lnwire.GossipVersion,
		chanPoint *wire.OutPoint) (uint64, error)

	// HighestChanID returns the "highest" known channel ID in the channel
	// graph. This represents the "newest" channel from the PoV of the
	// chain. This method can be used by peers to quickly determine if
	// they're graphs are in sync.
	HighestChanID(ctx context.Context, v lnwire.GossipVersion) (
		uint64, error)

	// ChanUpdatesInHorizon returns all the known channel edges which have
	// at least one edge update within the specified range for the given
	// gossip version. For v1 gossip, the range is time-based with
	// [start, end) per BOLT 07.
	ChanUpdatesInHorizon(ctx context.Context, v lnwire.GossipVersion,
		r ChanUpdateRange,
		opts ...IteratorOption) iter.Seq2[ChannelEdge, error]

	// FilterKnownChanIDs takes a set of channel IDs for a given gossip
	// version and returns the subset of chan ID's that we don't know and
	// are not known zombies of the passed set. In other words, we perform
	// a set difference of our set of chan ID's and the ones passed in.
	// This method can be used by callers to determine the set of channels
	// another peer knows of that we don't. The ChannelUpdateInfos for the
	// known zombies is also returned.
	FilterKnownChanIDs(ctx context.Context, v lnwire.GossipVersion,
		chansInfo []ChannelUpdateInfo) ([]uint64, []ChannelUpdateInfo,
		error)

	// FilterChannelRange returns the channel ID's of all known channels
	// which were mined in a block height within the passed range for the
	// given gossip version. The channel IDs are grouped by their common
	// block height. This method can be used to quickly share with a peer
	// the set of channels we know of within a particular range to catch
	// them up after a period of time offline. If withTimestamps is true
	// then the timestamp info of the latest received channel update
	// messages of the channel will be included in the response.
	FilterChannelRange(ctx context.Context, v lnwire.GossipVersion,
		startHeight, endHeight uint32,
		withTimestamps bool) ([]BlockChannelRange, error)

	// FetchChanInfos returns the set of channel edges that correspond to
	// the passed channel ID's. If an edge is the query is unknown to the
	// database, it will skipped and the result will contain only those
	// edges that exist at the time of the query. This can be used to
	// respond to peer queries that are seeking to fill in gaps in their
	// view of the channel graph.
	FetchChanInfos(ctx context.Context, v lnwire.GossipVersion,
		chanIDs []uint64) ([]ChannelEdge, error)

	// FetchChannelEdgesByOutpoint attempts to lookup the two directed edges
	// for the channel identified by the funding outpoint. If the channel
	// can't be found, then ErrEdgeNotFound is returned. A struct which
	// houses the general information for the channel itself is returned as
	// well as two structs that contain the routing policies for the channel
	// in either direction.
	FetchChannelEdgesByOutpoint(ctx context.Context,
		v lnwire.GossipVersion, op *wire.OutPoint) (
		*models.ChannelEdgeInfo, *models.ChannelEdgePolicy,
		*models.ChannelEdgePolicy, error)

	// FetchChannelEdgesByID attempts to lookup the two directed edges for
	// the channel identified by the channel ID. If the channel can't be
	// found, then ErrEdgeNotFound is returned. A struct which houses the
	// general information for the channel itself is returned as well as
	// two structs that contain the routing policies for the channel in
	// either direction.
	//
	// ErrZombieEdge can be returned if the edge is currently marked as a
	// zombie within the database. In this case, the ChannelEdgePolicy's
	// will be nil, and the ChannelEdgeInfo will only include the public
	// keys of each node.
	FetchChannelEdgesByID(ctx context.Context, v lnwire.GossipVersion,
		chanID uint64) (
		*models.ChannelEdgeInfo, *models.ChannelEdgePolicy,
		*models.ChannelEdgePolicy, error)

	// ChannelView returns the verifiable edge information for each active
	// channel within the known channel graph for the given gossip version.
	// The set of UTXO's (along with their scripts) returned are the ones
	// that need to be watched on chain to detect channel closes on the
	// resident blockchain.
	ChannelView(ctx context.Context, v lnwire.GossipVersion) ([]EdgePoint,
		error)

	// MarkEdgeZombie attempts to mark a channel identified by its channel
	// ID as a zombie for the given gossip version. This method is used on
	// an ad-hoc basis, when channels need to be marked as zombies outside
	// the normal pruning cycle.
	MarkEdgeZombie(ctx context.Context, v lnwire.GossipVersion,
		chanID uint64, pubKey1, pubKey2 [33]byte) error

	// MarkEdgeLive clears an edge from our zombie index for the given
	// gossip version, deeming it as live.
	MarkEdgeLive(ctx context.Context, v lnwire.GossipVersion,
		chanID uint64) error

	// IsZombieEdge returns whether the edge is considered zombie. If it is
	// a zombie, then the two node public keys corresponding to this edge
	// are also returned.
	IsZombieEdge(ctx context.Context, v lnwire.GossipVersion,
		chanID uint64) (bool, [33]byte, [33]byte, error)

	// NumZombies returns the current number of zombie channels in the
	// graph.
	NumZombies(ctx context.Context, v lnwire.GossipVersion) (uint64, error)

	// PutClosedScid stores a SCID for a closed channel in the database.
	// This is so that we can ignore channel announcements that we know to
	// be closed without having to validate them and fetch a block.
	PutClosedScid(ctx context.Context, scid lnwire.ShortChannelID) error

	// IsClosedScid checks whether a channel identified by the passed in
	// scid is closed. This helps avoid having to perform expensive
	// validation checks.
	IsClosedScid(ctx context.Context,
		scid lnwire.ShortChannelID) (bool, error)

	// UpdateEdgePolicy updates the edge routing policy for a single
	// directed edge within the database for the referenced channel. The
	// `flags` attribute within the ChannelEdgePolicy determines which of
	// the directed edges are being updated. If the flag is 1, then the
	// first node's information is being updated, otherwise it's the second
	// node's information. The node ordering is determined by the
	// lexicographical ordering of the identity public keys of the nodes on
	// either side of the channel.
	UpdateEdgePolicy(ctx context.Context, edge *models.ChannelEdgePolicy,
		op ...batch.SchedulerOption) (route.Vertex, route.Vertex, error)

	// SourceNode returns the source node of the graph. The source node is
	// treated as the center node within a star-graph. This method may be
	// used to kick off a path finding algorithm in order to explore the
	// reachability of another node based off the source node.
	SourceNode(ctx context.Context, v lnwire.GossipVersion) (*models.Node,
		error)

	// SetSourceNode sets the source node within the graph database. The
	// source node is to be used as the center of a star-graph within path
	// finding algorithms.
	SetSourceNode(ctx context.Context,
		node *models.Node) error

	// PruneTip returns the block height and hash of the latest block that
	// has been used to prune channels in the graph. Knowing the "prune tip"
	// allows callers to tell if the graph is currently in sync with the
	// current best known UTXO state.
	PruneTip(ctx context.Context) (*chainhash.Hash, uint32, error)

	// PruneGraphNodes is a garbage collection method which attempts to
	// prune out any nodes from the channel graph that are currently
	// unconnected. This ensures that we only maintain a graph of reachable
	// nodes. In the event that a pruned node gains more channels, it will
	// be re-added back to the graph.
	PruneGraphNodes(ctx context.Context) ([]route.Vertex, error)

	// PruneGraph prunes newly closed channels from the channel graph in
	// response to a new block being solved on the network. Any transactions
	// which spend the funding output of any known channels within he graph
	// will be deleted. Additionally, the "prune tip", or the last block
	// which has been used to prune the graph is stored so callers can
	// ensure the graph is fully in sync with the current UTXO state. A
	// slice of channels that have been closed by the target block along
	// with any pruned nodes are returned if the function succeeds without
	// error.
	PruneGraph(ctx context.Context, spentOutputs []*wire.OutPoint,
		blockHash *chainhash.Hash, blockHeight uint32) (
		[]*models.ChannelEdgeInfo, []route.Vertex, error)

	// DisconnectBlockAtHeight is used to indicate that the block specified
	// by the passed height has been disconnected from the main chain. This
	// will "rewind" the graph back to the height below, deleting channels
	// that are no longer confirmed from the graph. The prune log will be
	// set to the last prune height valid for the remaining chain.
	// Channels that were removed from the graph resulting from the
	// disconnected block are returned.
	DisconnectBlockAtHeight(ctx context.Context,
		height uint32) ([]*models.ChannelEdgeInfo, error)
}
