package chainntnfs

import (
	"bytes"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
	"sync"

	"github.com/btcsuite/btcd/blockchain"
	"github.com/btcsuite/btcd/btcjson"
	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/wire"
	"github.com/lightningnetwork/lnd/fn"
)

var (
	// ErrChainNotifierShuttingDown is used when we are trying to
	// measure a spend notification when notifier is already stopped.
	ErrChainNotifierShuttingDown = errors.New("chain notifier shutting down")
)

// TxConfStatus denotes the status of a transaction's lookup.
type TxConfStatus uint8

const (
	// TxFoundMempool denotes that the transaction was found within the
	// backend node's mempool.
	TxFoundMempool TxConfStatus = iota

	// TxFoundIndex denotes that the transaction was found within the
	// backend node's txindex.
	TxFoundIndex

	// TxNotFoundIndex denotes that the transaction was not found within the
	// backend node's txindex.
	TxNotFoundIndex

	// TxFoundManually denotes that the transaction was found within the
	// chain by scanning for it manually.
	TxFoundManually

	// TxNotFoundManually denotes that the transaction was not found within
	// the chain by scanning for it manually.
	TxNotFoundManually
)

// String returns the string representation of the TxConfStatus.
func (t TxConfStatus) String() string {
	switch t {
	case TxFoundMempool:
		return "TxFoundMempool"

	case TxFoundIndex:
		return "TxFoundIndex"

	case TxNotFoundIndex:
		return "TxNotFoundIndex"

	case TxFoundManually:
		return "TxFoundManually"

	case TxNotFoundManually:
		return "TxNotFoundManually"

	default:
		return "unknown"
	}
}

// notifierOptions is a set of functional options that allow callers to further
// modify the type of chain event notifications they receive.
type notifierOptions struct {
	// includeBlock if true, then the dispatched confirmation notification
	// will include the block that mined the transaction.
	includeBlock bool
}

// defaultNotifierOptions returns the set of default options for the notifier.
func defaultNotifierOptions() *notifierOptions {
	return &notifierOptions{}
}

// NotifierOption is a functional option that allows a caller to modify the
// events received from the notifier.
type NotifierOption func(*notifierOptions)

// WithIncludeBlock is an optional argument that allows the calelr to specify
// that the block that mined a transaction should be included in the response.
func WithIncludeBlock() NotifierOption {
	return func(o *notifierOptions) {
		o.includeBlock = true
	}
}

// ChainNotifier represents a trusted source to receive notifications concerning
// targeted events on the Bitcoin blockchain. The interface specification is
// intentionally general in order to support a wide array of chain notification
// implementations such as: btcd's websockets notifications, Bitcoin Core's
// ZeroMQ notifications, various Bitcoin API services, Electrum servers, etc.
//
// Concrete implementations of ChainNotifier should be able to support multiple
// concurrent client requests, as well as multiple concurrent notification events.
type ChainNotifier interface {
	// RegisterConfirmationsNtfn registers an intent to be notified once
	// txid reaches numConfs confirmations. We also pass in the pkScript as
	// the default light client instead needs to match on scripts created in
	// the block. If a nil txid is passed in, then not only should we match
	// on the script, but we should also dispatch once the transaction
	// containing the script reaches numConfs confirmations. This can be
	// useful in instances where we only know the script in advance, but not
	// the transaction containing it.
	//
	// The returned ConfirmationEvent should properly notify the client once
	// the specified number of confirmations has been reached for the txid,
	// as well as if the original tx gets re-org'd out of the mainchain. The
	// heightHint parameter is provided as a convenience to light clients.
	// It heightHint denotes the earliest height in the blockchain in which
	// the target txid _could_ have been included in the chain. This can be
	// used to bound the search space when checking to see if a notification
	// can immediately be dispatched due to historical data.
	//
	// NOTE: Dispatching notifications to multiple clients subscribed to
	// the same (txid, numConfs) tuple MUST be supported.
	RegisterConfirmationsNtfn(txid *chainhash.Hash, pkScript []byte,
		numConfs, heightHint uint32,
		opts ...NotifierOption) (*ConfirmationEvent, error)

	// RegisterSpendNtfn registers an intent to be notified once the target
	// outpoint is successfully spent within a transaction. The script that
	// the outpoint creates must also be specified. This allows this
	// interface to be implemented by BIP 158-like filtering. If a nil
	// outpoint is passed in, then not only should we match on the script,
	// but we should also dispatch once a transaction spends the output
	// containing said script. This can be useful in instances where we only
	// know the script in advance, but not the outpoint itself.
	//
	// The returned SpendEvent will receive a send on the 'Spend'
	// transaction once a transaction spending the input is detected on the
	// blockchain. The heightHint parameter is provided as a convenience to
	// light clients. It denotes the earliest height in the blockchain in
	// which the target output could have been spent.
	//
	// NOTE: The notification should only be triggered when the spending
	// transaction receives a single confirmation.
	//
	// NOTE: Dispatching notifications to multiple clients subscribed to a
	// spend of the same outpoint MUST be supported.
	RegisterSpendNtfn(outpoint *wire.OutPoint, pkScript []byte,
		heightHint uint32) (*SpendEvent, error)

	// RegisterBlockEpochNtfn registers an intent to be notified of each
	// new block connected to the tip of the main chain. The returned
	// BlockEpochEvent struct contains a channel which will be sent upon
	// for each new block discovered.
	//
	// Clients have the option of passing in their best known block.
	// If they specify a block, the ChainNotifier checks whether the client
	// is behind on blocks. If they are, the ChainNotifier sends a backlog
	// of block notifications for the missed blocks. If they do not provide
	// one, then a notification will be dispatched immediately for the
	// current tip of the chain upon a successful registration.
	RegisterBlockEpochNtfn(*BlockEpoch) (*BlockEpochEvent, error)

	// Start the ChainNotifier. Once started, the implementation should be
	// ready, and able to receive notification registrations from clients.
	Start() error

	// Started returns true if this instance has been started, and false otherwise.
	Started() bool

	// Stops the concrete ChainNotifier. Once stopped, the ChainNotifier
	// should disallow any future requests from potential clients.
	// Additionally, all pending client notifications will be canceled
	// by closing the related channels on the *Event's.
	Stop() error
}

// TxConfirmation carries some additional block-level details of the exact
// block that specified transactions was confirmed within.
type TxConfirmation struct {
	// BlockHash is the hash of the block that confirmed the original
	// transition.
	BlockHash *chainhash.Hash

	// BlockHeight is the height of the block in which the transaction was
	// confirmed within.
	BlockHeight uint32

	// TxIndex is the index within the block of the ultimate confirmed
	// transaction.
	TxIndex uint32

	// Tx is the transaction for which the notification was requested for.
	Tx *wire.MsgTx

	// Block is the block that contains the transaction referenced above.
	//
	// NOTE: This is only specified if the confirmation request opts to
	// have the response include the block itself.
	Block *wire.MsgBlock
}

// ConfirmationEvent encapsulates a confirmation notification. With this struct,
// callers can be notified of: the instance the target txid reaches the targeted
// number of confirmations, how many confirmations are left for the target txid
// to be fully confirmed at every new block height, and also in the event that
// the original txid becomes disconnected from the blockchain as a result of a
// re-org.
//
// Once the txid reaches the specified number of confirmations, the 'Confirmed'
// channel will be sent upon fulfilling the notification.
//
// If the event that the original transaction becomes re-org'd out of the main
// chain, the 'NegativeConf' will be sent upon with a value representing the
// depth of the re-org.
//
// NOTE: If the caller wishes to cancel their registered spend notification,
// the Cancel closure MUST be called.
type ConfirmationEvent struct {
	// Confirmed is a channel that will be sent upon once the transaction
	// has been fully confirmed. The struct sent will contain all the
	// details of the channel's confirmation.
	//
	// NOTE: This channel must be buffered.
	Confirmed chan *TxConfirmation

	// Updates is a channel that will sent upon, at every incremental
	// confirmation, how many confirmations are left to declare the
	// transaction as fully confirmed.
	//
	// NOTE: This channel must be buffered with the number of required
	// confirmations.
	Updates chan uint32

	// NegativeConf is a channel that will be sent upon if the transaction
	// confirms, but is later reorged out of the chain. The integer sent
	// through the channel represents the reorg depth.
	//
	// NOTE: This channel must be buffered.
	NegativeConf chan int32

	// Done is a channel that gets sent upon once the confirmation request
	// is no longer under the risk of being reorged out of the chain.
	//
	// NOTE: This channel must be buffered.
	Done chan struct{}

	// Cancel is a closure that should be executed by the caller in the case
	// that they wish to prematurely abandon their registered confirmation
	// notification.
	Cancel func()
}

// NewConfirmationEvent constructs a new ConfirmationEvent with newly opened
// channels.
func NewConfirmationEvent(numConfs uint32, cancel func()) *ConfirmationEvent {
	return &ConfirmationEvent{
		Confirmed:    make(chan *TxConfirmation, 1),
		Updates:      make(chan uint32, numConfs),
		NegativeConf: make(chan int32, 1),
		Done:         make(chan struct{}, 1),
		Cancel:       cancel,
	}
}

// SpendDetail contains details pertaining to a spent output. This struct itself
// is the spentness notification. It includes the original outpoint which triggered
// the notification, the hash of the transaction spending the output, the
// spending transaction itself, and finally the input index which spent the
// target output.
type SpendDetail struct {
	SpentOutPoint     *wire.OutPoint
	SpenderTxHash     *chainhash.Hash
	SpendingTx        *wire.MsgTx
	SpenderInputIndex uint32
	SpendingHeight    int32
}

// HasSpenderWitness returns true if the spending transaction has non-empty
// witness.
func (s *SpendDetail) HasSpenderWitness() bool {
	tx := s.SpendingTx

	// If there are no inputs, then there is no witness.
	if len(tx.TxIn) == 0 {
		return false
	}

	// If the spender input index is larger than the number of inputs, then
	// we don't have a witness and this is an error case so we log it.
	if uint32(len(tx.TxIn)) <= s.SpenderInputIndex {
		Log.Errorf("SpenderInputIndex %d is out of range for tx %v",
			s.SpenderInputIndex, tx.TxHash())

		return false
	}

	// If the witness is empty, then there is no witness.
	if len(tx.TxIn[s.SpenderInputIndex].Witness) == 0 {
		return false
	}

	// If the witness is non-empty, then we have a witness.
	return true
}

// String returns a string representation of SpendDetail.
func (s *SpendDetail) String() string {
	return fmt.Sprintf("%v[%d] spending %v at height=%v", s.SpenderTxHash,
		s.SpenderInputIndex, s.SpentOutPoint, s.SpendingHeight)
}

// SpendEvent encapsulates a spentness notification. Its only field 'Spend' will
// be sent upon once the target output passed into RegisterSpendNtfn has been
// spent on the blockchain.
//
// NOTE: If the caller wishes to cancel their registered spend notification,
// the Cancel closure MUST be called.
type SpendEvent struct {
	// Spend is a receive only channel which will be sent upon once the
	// target outpoint has been spent.
	//
	// NOTE: This channel must be buffered.
	Spend chan *SpendDetail

	// Reorg is a channel that will be sent upon once we detect the spending
	// transaction of the outpoint in question has been reorged out of the
	// chain.
	//
	// NOTE: This channel must be buffered.
	Reorg chan struct{}

	// Done is a channel that gets sent upon once the confirmation request
	// is no longer under the risk of being reorged out of the chain.
	//
	// NOTE: This channel must be buffered.
	Done chan struct{}

	// Cancel is a closure that should be executed by the caller in the case
	// that they wish to prematurely abandon their registered spend
	// notification.
	Cancel func()
}

// NewSpendEvent constructs a new SpendEvent with newly opened channels.
func NewSpendEvent(cancel func()) *SpendEvent {
	return &SpendEvent{
		Spend:  make(chan *SpendDetail, 1),
		Reorg:  make(chan struct{}, 1),
		Done:   make(chan struct{}, 1),
		Cancel: cancel,
	}
}

// BlockEpoch represents metadata concerning each new block connected to the
// main chain.
type BlockEpoch struct {
	// Hash is the block hash of the latest block to be added to the tip of
	// the main chain.
	Hash *chainhash.Hash

	// Height is the height of the latest block to be added to the tip of
	// the main chain.
	Height int32

	// BlockHeader is the block header of this new height.
	BlockHeader *wire.BlockHeader
}

// BlockEpochEvent encapsulates an on-going stream of block epoch
// notifications. Its only field 'Epochs' will be sent upon for each new block
// connected to the main-chain.
//
// NOTE: If the caller wishes to cancel their registered block epoch
// notification, the Cancel closure MUST be called.
type BlockEpochEvent struct {
	// Epochs is a receive only channel that will be sent upon each time a
	// new block is connected to the end of the main chain.
	//
	// NOTE: This channel must be buffered.
	Epochs <-chan *BlockEpoch

	// Cancel is a closure that should be executed by the caller in the case
	// that they wish to abandon their registered block epochs notification.
	Cancel func()
}

// NotifierDriver represents a "driver" for a particular interface. A driver is
// identified by a globally unique string identifier along with a 'New()'
// method which is responsible for initializing a particular ChainNotifier
// concrete implementation.
type NotifierDriver struct {
	// NotifierType is a string which uniquely identifies the ChainNotifier
	// that this driver, drives.
	NotifierType string

	// New creates a new instance of a concrete ChainNotifier
	// implementation given a variadic set up arguments. The function takes
	// a variadic number of interface parameters in order to provide
	// initialization flexibility, thereby accommodating several potential
	// ChainNotifier implementations.
	New func(args ...interface{}) (ChainNotifier, error)
}

var (
	notifiers   = make(map[string]*NotifierDriver)
	registerMtx sync.Mutex
)

// RegisteredNotifiers returns a slice of all currently registered notifiers.
//
// NOTE: This function is safe for concurrent access.
func RegisteredNotifiers() []*NotifierDriver {
	registerMtx.Lock()
	defer registerMtx.Unlock()

	drivers := make([]*NotifierDriver, 0, len(notifiers))
	for _, driver := range notifiers {
		drivers = append(drivers, driver)
	}

	return drivers
}

// RegisterNotifier registers a NotifierDriver which is capable of driving a
// concrete ChainNotifier interface. In the case that this driver has already
// been registered, an error is returned.
//
// NOTE: This function is safe for concurrent access.
func RegisterNotifier(driver *NotifierDriver) error {
	registerMtx.Lock()
	defer registerMtx.Unlock()

	if _, ok := notifiers[driver.NotifierType]; ok {
		return fmt.Errorf("notifier already registered")
	}

	notifiers[driver.NotifierType] = driver

	return nil
}

// SupportedNotifiers returns a slice of strings that represent the database
// drivers that have been registered and are therefore supported.
//
// NOTE: This function is safe for concurrent access.
func SupportedNotifiers() []string {
	registerMtx.Lock()
	defer registerMtx.Unlock()

	supportedNotifiers := make([]string, 0, len(notifiers))
	for driverName := range notifiers {
		supportedNotifiers = append(supportedNotifiers, driverName)
	}

	return supportedNotifiers
}

// ChainConn enables notifiers to pass in their chain backend to interface
// functions that require it.
type ChainConn interface {
	// GetBlockHeader returns the block header for a hash.
	GetBlockHeader(blockHash *chainhash.Hash) (*wire.BlockHeader, error)

	// GetBlockHeaderVerbose returns the verbose block header for a hash.
	GetBlockHeaderVerbose(blockHash *chainhash.Hash) (
		*btcjson.GetBlockHeaderVerboseResult, error)

	// GetBlockHash returns the hash from a block height.
	GetBlockHash(blockHeight int64) (*chainhash.Hash, error)
}

// GetCommonBlockAncestorHeight takes in:
// (1) the hash of a block that has been reorged out of the main chain
// (2) the hash of the block of the same height from the main chain
// It returns the height of the nearest common ancestor between the two hashes,
// or an error
func GetCommonBlockAncestorHeight(chainConn ChainConn, reorgHash,
	chainHash chainhash.Hash) (int32, error) {

	for reorgHash != chainHash {
		reorgHeader, err := chainConn.GetBlockHeader(&reorgHash)
		if err != nil {
			return 0, fmt.Errorf("unable to get header for "+
				"hash=%v: %w", reorgHash, err)
		}
		chainHeader, err := chainConn.GetBlockHeader(&chainHash)
		if err != nil {
			return 0, fmt.Errorf("unable to get header for "+
				"hash=%v: %w", chainHash, err)
		}
		reorgHash = reorgHeader.PrevBlock
		chainHash = chainHeader.PrevBlock
	}

	verboseHeader, err := chainConn.GetBlockHeaderVerbose(&chainHash)
	if err != nil {
		return 0, fmt.Errorf("unable to get verbose header for "+
			"hash=%v: %w", chainHash, err)
	}

	return verboseHeader.Height, nil
}

// GetClientMissedBlocks uses a client's best block to determine what blocks
// it missed being notified about, and returns them in a slice. Its
// backendStoresReorgs parameter tells it whether or not the notifier's
// chainConn stores information about blocks that have been reorged out of the
// chain, which allows GetClientMissedBlocks to find out whether the client's
// best block has been reorged out of the chain, rewind to the common ancestor
// and return blocks starting right after the common ancestor.
func GetClientMissedBlocks(chainConn ChainConn, clientBestBlock *BlockEpoch,
	notifierBestHeight int32, backendStoresReorgs bool) ([]BlockEpoch, error) {

	startingHeight := clientBestBlock.Height
	if backendStoresReorgs {
		// If a reorg causes the client's best hash to be incorrect,
		// retrieve the closest common ancestor and dispatch
		// notifications from there.
		hashAtBestHeight, err := chainConn.GetBlockHash(
			int64(clientBestBlock.Height))
		if err != nil {
			return nil, fmt.Errorf("unable to find blockhash for "+
				"height=%d: %v", clientBestBlock.Height, err)
		}

		startingHeight, err = GetCommonBlockAncestorHeight(
			chainConn, *clientBestBlock.Hash, *hashAtBestHeight,
		)
		if err != nil {
			return nil, fmt.Errorf("unable to find common ancestor: "+
				"%v", err)
		}
	}

	// We want to start dispatching historical notifications from the block
	// right after the client's best block, to avoid a redundant notification.
	missedBlocks, err := getMissedBlocks(
		chainConn, startingHeight+1, notifierBestHeight+1,
	)
	if err != nil {
		return nil, fmt.Errorf("unable to get missed blocks: %w", err)
	}

	return missedBlocks, nil
}

// RewindChain handles internal state updates for the notifier's TxNotifier. It
// has no effect if given a height greater than or equal to our current best
// known height. It returns the new best block for the notifier.
func RewindChain(chainConn ChainConn, txNotifier *TxNotifier,
	currBestBlock BlockEpoch, targetHeight int32) (BlockEpoch, error) {

	newBestBlock := BlockEpoch{
		Height:      currBestBlock.Height,
		Hash:        currBestBlock.Hash,
		BlockHeader: currBestBlock.BlockHeader,
	}

	for height := currBestBlock.Height; height > targetHeight; height-- {
		hash, err := chainConn.GetBlockHash(int64(height - 1))
		if err != nil {
			return newBestBlock, fmt.Errorf("unable to "+
				"find blockhash for disconnected height=%d: %v",
				height, err)
		}
		header, err := chainConn.GetBlockHeader(hash)
		if err != nil {
			return newBestBlock, fmt.Errorf("unable to get block "+
				"header for height=%v", height-1)
		}

		Log.Infof("Block disconnected from main chain: "+
			"height=%v, sha=%v", height, newBestBlock.Hash)

		err = txNotifier.DisconnectTip(uint32(height))
		if err != nil {
			return newBestBlock, fmt.Errorf("unable to "+
				" disconnect tip for height=%d: %v",
				height, err)
		}
		newBestBlock.Height = height - 1
		newBestBlock.Hash = hash
		newBestBlock.BlockHeader = header
	}

	return newBestBlock, nil
}

// HandleMissedBlocks is called when the chain backend for a notifier misses a
// series of blocks, handling a reorg if necessary. Its backendStoresReorgs
// parameter tells it whether or not the notifier's chainConn stores
// information about blocks that have been reorged out of the chain, which allows
// HandleMissedBlocks to check whether the notifier's best block has been
// reorged out, and rewind the chain accordingly. It returns the best block for
// the notifier and a slice of the missed blocks. The new best block needs to be
// returned in case a chain rewind occurs and partially completes before
// erroring. In the case where there is no rewind, the notifier's
// current best block is returned.
func HandleMissedBlocks(chainConn ChainConn, txNotifier *TxNotifier,
	currBestBlock BlockEpoch, newHeight int32,
	backendStoresReorgs bool) (BlockEpoch, []BlockEpoch, error) {

	startingHeight := currBestBlock.Height

	if backendStoresReorgs {
		// If a reorg causes our best hash to be incorrect, rewind the
		// chain so our best block is set to the closest common
		// ancestor, then dispatch notifications from there.
		hashAtBestHeight, err := chainConn.GetBlockHash(
			int64(currBestBlock.Height),
		)
		if err != nil {
			return currBestBlock, nil, fmt.Errorf("unable to find "+
				"blockhash for height=%d: %v",
				currBestBlock.Height, err)
		}

		startingHeight, err = GetCommonBlockAncestorHeight(
			chainConn, *currBestBlock.Hash, *hashAtBestHeight,
		)
		if err != nil {
			return currBestBlock, nil, fmt.Errorf("unable to find "+
				"common ancestor: %v", err)
		}

		currBestBlock, err = RewindChain(
			chainConn, txNotifier, currBestBlock, startingHeight,
		)
		if err != nil {
			return currBestBlock, nil, fmt.Errorf("unable to "+
				"rewind chain: %v", err)
		}
	}

	// We want to start dispatching historical notifications from the block
	// right after our best block, to avoid a redundant notification.
	missedBlocks, err := getMissedBlocks(chainConn, startingHeight+1, newHeight)
	if err != nil {
		return currBestBlock, nil, fmt.Errorf("unable to get missed "+
			"blocks: %v", err)
	}

	return currBestBlock, missedBlocks, nil
}

// getMissedBlocks returns a slice of blocks: [startingHeight, endingHeight)
// fetched from the chain.
func getMissedBlocks(chainConn ChainConn, startingHeight,
	endingHeight int32) ([]BlockEpoch, error) {

	numMissedBlocks := endingHeight - startingHeight
	if numMissedBlocks < 0 {
		return nil, fmt.Errorf("starting height %d is greater than "+
			"ending height %d", startingHeight, endingHeight)
	}

	missedBlocks := make([]BlockEpoch, 0, numMissedBlocks)
	for height := startingHeight; height < endingHeight; height++ {
		hash, err := chainConn.GetBlockHash(int64(height))
		if err != nil {
			return nil, fmt.Errorf("unable to find blockhash for "+
				"height=%d: %v", height, err)
		}
		header, err := chainConn.GetBlockHeader(hash)
		if err != nil {
			return nil, fmt.Errorf("unable to find block header "+
				"for height=%d: %v", height, err)
		}

		missedBlocks = append(
			missedBlocks,
			BlockEpoch{
				Hash:        hash,
				Height:      height,
				BlockHeader: header,
			},
		)
	}

	return missedBlocks, nil
}

// TxIndexConn abstracts an RPC backend with txindex enabled.
type TxIndexConn interface {
	// GetRawTransactionVerbose returns the transaction identified by the
	// passed chain hash, and returns additional information such as the
	// block that the transaction confirmed.
	GetRawTransactionVerbose(*chainhash.Hash) (*btcjson.TxRawResult, error)

	// GetBlock returns the block identified by the chain hash.
	GetBlock(*chainhash.Hash) (*wire.MsgBlock, error)
}

// ConfDetailsFromTxIndex looks up whether a transaction is already included in
// a block in the active chain by using the backend node's transaction index.
// If the transaction is found its TxConfStatus is returned. If it was found in
// the mempool this will be TxFoundMempool, if it is found in a block this will
// be TxFoundIndex. Otherwise TxNotFoundIndex is returned. If the tx is found
// in a block its confirmation details are also returned.
func ConfDetailsFromTxIndex(chainConn TxIndexConn, r ConfRequest,
	txNotFoundErr string) (*TxConfirmation, TxConfStatus, error) {

	// If the transaction has some or all of its confirmations required,
	// then we may be able to dispatch it immediately.
	rawTxRes, err := chainConn.GetRawTransactionVerbose(&r.TxID)
	if err != nil {
		// If the transaction lookup was successful, but it wasn't found
		// within the index itself, then we can exit early. We'll also
		// need to look at the error message returned as the error code
		// is used for multiple errors.
		jsonErr, ok := err.(*btcjson.RPCError)
		if ok && jsonErr.Code == btcjson.ErrRPCNoTxInfo &&
			strings.Contains(jsonErr.Message, txNotFoundErr) {

			return nil, TxNotFoundIndex, nil
		}

		return nil, TxNotFoundIndex,
			fmt.Errorf("unable to query for txid %v: %w",
				r.TxID, err)
	}

	// Deserialize the hex-encoded transaction to include it in the
	// confirmation details.
	rawTx, err := hex.DecodeString(rawTxRes.Hex)
	if err != nil {
		return nil, TxNotFoundIndex,
			fmt.Errorf("unable to deserialize tx %v: %w",
				r.TxID, err)
	}
	var tx wire.MsgTx
	if err := tx.Deserialize(bytes.NewReader(rawTx)); err != nil {
		return nil, TxNotFoundIndex,
			fmt.Errorf("unable to deserialize tx %v: %w",
				r.TxID, err)
	}

	// Ensure the transaction matches our confirmation request in terms of
	// txid and pkscript.
	if !r.MatchesTx(&tx) {
		return nil, TxNotFoundIndex,
			fmt.Errorf("unable to locate tx %v", r.TxID)
	}

	// Make sure we actually retrieved a transaction that is included in a
	// block. If not, the transaction must be unconfirmed (in the mempool),
	// and we'll return TxFoundMempool together with a nil TxConfirmation.
	if rawTxRes.BlockHash == "" {
		return nil, TxFoundMempool, nil
	}

	// As we need to fully populate the returned TxConfirmation struct,
	// grab the block in which the transaction was confirmed so we can
	// locate its exact index within the block.
	blockHash, err := chainhash.NewHashFromStr(rawTxRes.BlockHash)
	if err != nil {
		return nil, TxNotFoundIndex,
			fmt.Errorf("unable to get block hash %v for "+
				"historical dispatch: %w", rawTxRes.BlockHash,
				err)
	}
	block, err := chainConn.GetBlock(blockHash)
	if err != nil {
		return nil, TxNotFoundIndex,
			fmt.Errorf("unable to get block with hash %v for "+
				"historical dispatch: %w", blockHash, err)
	}

	// In the modern chain (the only one we really care about for LN), the
	// coinbase transaction of all blocks will include the block height.
	// Therefore we can save another query, and just use that height
	// directly.
	blockHeight, err := blockchain.ExtractCoinbaseHeight(
		btcutil.NewTx(block.Transactions[0]),
	)
	if err != nil {
		return nil, TxNotFoundIndex, fmt.Errorf("unable to extract "+
			"coinbase height: %w", err)
	}

	// If the block was obtained, locate the transaction's index within the
	// block so we can give the subscriber full confirmation details.
	for txIndex, blockTx := range block.Transactions {
		if blockTx.TxHash() != r.TxID {
			continue
		}

		return &TxConfirmation{
			Tx:          tx.Copy(),
			BlockHash:   blockHash,
			BlockHeight: uint32(blockHeight),
			TxIndex:     uint32(txIndex),
			Block:       block,
		}, TxFoundIndex, nil
	}

	// We return an error because we should have found the transaction
	// within the block, but didn't.
	return nil, TxNotFoundIndex, fmt.Errorf("unable to locate "+
		"tx %v in block %v", r.TxID, blockHash)
}

// SpendHintCache is an interface whose duty is to cache spend hints for
// outpoints. A spend hint is defined as the earliest height in the chain at
// which an outpoint could have been spent within.
type SpendHintCache interface {
	// CommitSpendHint commits a spend hint for the outpoints to the cache.
	CommitSpendHint(height uint32, spendRequests ...SpendRequest) error

	// QuerySpendHint returns the latest spend hint for an outpoint.
	// ErrSpendHintNotFound is returned if a spend hint does not exist
	// within the cache for the outpoint.
	QuerySpendHint(spendRequest SpendRequest) (uint32, error)

	// PurgeSpendHint removes the spend hint for the outpoints from the
	// cache.
	PurgeSpendHint(spendRequests ...SpendRequest) error
}

// ConfirmHintCache is an interface whose duty is to cache confirm hints for
// transactions. A confirm hint is defined as the earliest height in the chain
// at which a transaction could have been included in a block.
type ConfirmHintCache interface {
	// CommitConfirmHint commits a confirm hint for the transactions to the
	// cache.
	CommitConfirmHint(height uint32, confRequests ...ConfRequest) error

	// QueryConfirmHint returns the latest confirm hint for a transaction
	// hash. ErrConfirmHintNotFound is returned if a confirm hint does not
	// exist within the cache for the transaction hash.
	QueryConfirmHint(confRequest ConfRequest) (uint32, error)

	// PurgeConfirmHint removes the confirm hint for the transactions from
	// the cache.
	PurgeConfirmHint(confRequests ...ConfRequest) error
}

// MempoolWatcher defines an interface that allows the caller to query
// information in the mempool.
type MempoolWatcher interface {
	// SubscribeMempoolSpent allows the caller to register a subscription
	// to watch for a spend of an outpoint in the mempool.The event will be
	// dispatched once the outpoint is spent in the mempool.
	SubscribeMempoolSpent(op wire.OutPoint) (*MempoolSpendEvent, error)

	// CancelMempoolSpendEvent allows the caller to cancel a subscription to
	// watch for a spend of an outpoint in the mempool.
	CancelMempoolSpendEvent(sub *MempoolSpendEvent)

	// LookupInputMempoolSpend looks up the mempool to find a spending tx
	// which spends the given outpoint. A fn.None is returned if it's not
	// found.
	LookupInputMempoolSpend(op wire.OutPoint) fn.Option[wire.MsgTx]
}
