package chainntnfs

import (
	"fmt"
	"sync"

	"github.com/btcsuite/btcd/btcjson"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/wire"
)

// 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"
	}
}

// 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.
// TODO(roasbeef): all events should have a Cancel() method to free up the
// resource
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. 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. The 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) (*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. 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.
	// The heightHint denotes the earliest height in the blockchain in
	// which the target output could have been created.
	//
	// 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.
	RegisterBlockEpochNtfn(*BlockEpoch) (*BlockEpochEvent, error)

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

	// Stops the concrete ChainNotifier. Once stopped, the ChainNotifier
	// should disallow any future requests from potential clients.
	// Additionally, all pending client notifications will be cancelled
	// 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
}

// 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.
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.
	Confirmed chan *TxConfirmation // MUST be buffered.

	// Updates is a channel that will sent upon, at every incremental
	// confirmation, how many confirmations are left to declare the
	// transaction as fully confirmed.
	Updates chan uint32 // MUST be buffered.

	// TODO(roasbeef): all goroutines on ln channel updates should also
	// have a struct chan that's closed if funding gets re-org out. Need
	// to sync, to request another confirmation event ntfn, then re-open
	// channel after confs.

	NegativeConf chan int32 // MUST be buffered.
}

// 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
}

// 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.
	Spend <-chan *SpendDetail // MUST be buffered.

	// 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()
}

// 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
}

// 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.
	Epochs <-chan *BlockEpoch // MUST be buffered.

	// Cancel is a closure that should be executed by the caller in the
	// case that they wish to abandon their registered spend 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: %v",
				reorgHash, err)
		}
		chainHeader, err := chainConn.GetBlockHeader(&chainHash)
		if err != nil {
			return 0, fmt.Errorf("unable to get header for hash=%v: %v",
				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: %v",
			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: %v", err)
	}

	return missedBlocks, nil
}

// RewindChain handles internal state updates for the notifier's TxConfNotifier
// 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, txConfNotifier *TxConfNotifier,
	currBestBlock BlockEpoch, targetHeight int32) (BlockEpoch, error) {

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

	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)
		}

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

		err = txConfNotifier.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
	}
	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, txConfNotifier *TxConfNotifier,
	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, txConfNotifier,
			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)
		}
		missedBlocks = append(missedBlocks,
			BlockEpoch{Hash: hash, Height: height})
	}

	return missedBlocks, nil
}
