package test

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"sync"
	"time"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/btcutil/psbt"
	"github.com/btcsuite/btcd/chaincfg"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/wire"
	"github.com/btcsuite/btcwallet/waddrmgr"
	"github.com/btcsuite/btcwallet/wtxmgr"
	"github.com/lightninglabs/lndclient"
	"github.com/lightningnetwork/lnd/keychain"
	"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
	"github.com/lightningnetwork/lnd/lnwallet"
	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)

// DefaultMockFee is the default value we use for fee estimates when no values
// are set for specific conf targets.
var DefaultMockFee = chainfee.SatPerKWeight(10000)

type mockWalletKit struct {
	lndclient.WalletKitClient

	lnd      *LndMockServices
	keyIndex int32

	feeEstimateLock sync.Mutex
	feeEstimates    map[int32]chainfee.SatPerKWeight
	minRelayFee     chainfee.SatPerKWeight

	// listUnspent holds test UTXOs to be returned by ListUnspent.
	listUnspent []*lnwallet.Utxo
}

var _ lndclient.WalletKitClient = (*mockWalletKit)(nil)

func (m *mockWalletKit) RawClientWithMacAuth(
	ctx context.Context) (context.Context, time.Duration,
	walletrpc.WalletKitClient) {

	return ctx, 0, nil
}

func (m *mockWalletKit) ListUnspent(ctx context.Context, minConfs,
	maxConfs int32, opts ...lndclient.ListUnspentOption) (
	[]*lnwallet.Utxo, error) {

	return m.listUnspent, nil
}

func (m *mockWalletKit) ListLeases(
	context.Context) ([]lndclient.LeaseDescriptor, error) {

	return nil, nil
}

func (m *mockWalletKit) LeaseOutput(ctx context.Context, lockID wtxmgr.LockID,
	op wire.OutPoint, duration time.Duration) (time.Time, error) {

	return time.Now(), nil
}

func (m *mockWalletKit) ReleaseOutput(ctx context.Context,
	lockID wtxmgr.LockID, op wire.OutPoint) error {

	return nil
}

func (m *mockWalletKit) DeriveNextKey(ctx context.Context, family int32) (
	*keychain.KeyDescriptor, error) {

	index := m.keyIndex

	_, pubKey := CreateKey(index)
	m.keyIndex++

	return &keychain.KeyDescriptor{
		KeyLocator: keychain.KeyLocator{
			Family: keychain.KeyFamily(family),
			Index:  uint32(index),
		},
		PubKey: pubKey,
	}, nil
}

func (m *mockWalletKit) DeriveKey(ctx context.Context, in *keychain.KeyLocator) (
	*keychain.KeyDescriptor, error) {

	_, pubKey := CreateKey(int32(in.Index))

	return &keychain.KeyDescriptor{
		KeyLocator: *in,
		PubKey:     pubKey,
	}, nil
}

func (m *mockWalletKit) NextAddr(context.Context, string, walletrpc.AddressType,
	bool) (btcutil.Address, error) {

	addr, err := btcutil.NewAddressWitnessPubKeyHash(
		make([]byte, 20), &chaincfg.TestNet3Params,
	)
	if err != nil {
		return nil, err
	}
	return addr, nil
}

func (m *mockWalletKit) PublishTransaction(ctx context.Context, tx *wire.MsgTx,
	label string) error {

	if m.lnd.PublishHandler != nil {
		if err := m.lnd.PublishHandler(ctx, tx, label); err != nil {
			return err
		}
	}

	m.lnd.AddTx(tx)
	m.lnd.TxPublishChannel <- tx
	return nil
}

func (m *mockWalletKit) SendOutputs(ctx context.Context, outputs []*wire.TxOut,
	feeRate chainfee.SatPerKWeight, _ string) (*wire.MsgTx, error) {

	var inputTxHash chainhash.Hash

	tx := wire.MsgTx{}
	tx.AddTxIn(&wire.TxIn{
		PreviousOutPoint: wire.OutPoint{
			Hash:  inputTxHash,
			Index: 0,
		},
	})

	for _, out := range outputs {
		tx.AddTxOut(&wire.TxOut{
			PkScript: out.PkScript,
			Value:    out.Value,
		})
	}

	m.lnd.AddTx(&tx)
	m.lnd.SendOutputsChannel <- tx

	return &tx, nil
}

func (m *mockWalletKit) setFeeEstimate(confTarget int32, fee chainfee.SatPerKWeight) {
	m.feeEstimateLock.Lock()
	defer m.feeEstimateLock.Unlock()

	m.feeEstimates[confTarget] = fee
}

func (m *mockWalletKit) EstimateFeeRate(ctx context.Context,
	confTarget int32) (chainfee.SatPerKWeight, error) {

	m.feeEstimateLock.Lock()
	defer m.feeEstimateLock.Unlock()

	if confTarget <= 1 {
		return 0, errors.New("conf target must be greater than 1")
	}

	feeEstimate, ok := m.feeEstimates[confTarget]
	if !ok {
		return DefaultMockFee, nil
	}

	return feeEstimate, nil
}

func (m *mockWalletKit) setMinRelayFee(fee chainfee.SatPerKWeight) {
	m.feeEstimateLock.Lock()
	defer m.feeEstimateLock.Unlock()

	m.minRelayFee = fee
}

// setListUnspent sets the list of UTXOs returned by ListUnspent.
func (m *mockWalletKit) setListUnspent(utxos []*lnwallet.Utxo) {
	m.listUnspent = utxos
}

// MinRelayFee returns the current minimum relay fee based on our chain backend
// in sat/kw. It can be set with setMinRelayFee.
func (m *mockWalletKit) MinRelayFee(
	ctx context.Context) (chainfee.SatPerKWeight, error) {

	m.feeEstimateLock.Lock()
	defer m.feeEstimateLock.Unlock()

	return m.minRelayFee, nil
}

// ListSweeps returns a list of the sweep transaction ids known to our node.
func (m *mockWalletKit) ListSweeps(_ context.Context, _ int32) (
	[]string, error) {

	return m.lnd.Sweeps, nil
}

// ListSweepsVerbose returns a list of sweep transactions known to our node
// with verbose information about each sweep.
func (m *mockWalletKit) ListSweepsVerbose(ctx context.Context, _ int32) (
	[]lnwallet.TransactionDetail, error) {

	return m.lnd.SweepsVerbose, nil
}

// BumpFee attempts to bump the fee of a transaction by spending one of
// its outputs at the given fee rate. This essentially results in a
// child-pays-for-parent (CPFP) scenario. If the given output has been
// used in a previous BumpFee call, then a transaction replacing the
// previous is broadcast, resulting in a replace-by-fee (RBF) scenario.
func (m *mockWalletKit) BumpFee(context.Context, wire.OutPoint,
	chainfee.SatPerKWeight, ...lndclient.BumpFeeOption) error {

	return nil
}

// ListAccounts retrieves all accounts belonging to the wallet by default.
// Optional name and addressType can be provided to filter through all of the
// wallet accounts and return only those matching.
func (m *mockWalletKit) ListAccounts(context.Context, string,
	walletrpc.AddressType) ([]*walletrpc.Account, error) {

	return nil, nil
}

// FundPsbt creates a fully populated PSBT that contains enough inputs
// to fund the outputs specified in the template. There are two ways of
// specifying a template: Either by passing in a PSBT with at least one
// output declared or by passing in a raw TxTemplate message. If there
// are no inputs specified in the template, coin selection is performed
// automatically. If the template does contain any inputs, it is assumed
// that full coin selection happened externally and no additional inputs
// are added. If the specified inputs aren't enough to fund the outputs
// with the given fee rate, an error is returned.
// After either selecting or verifying the inputs, all input UTXOs are
// locked with an internal app ID.
//
// NOTE: If this method returns without an error, it is the caller's
// responsibility to either spend the locked UTXOs (by finalizing and
// then publishing the transaction) or to unlock/release the locked
// UTXOs in case of an error on the caller's side.
func (m *mockWalletKit) FundPsbt(_ context.Context,
	_ *walletrpc.FundPsbtRequest) (*psbt.Packet, int32,
	[]*walletrpc.UtxoLease, error) {

	return nil, 0, nil, nil
}

// finalScriptWitness is a sample signature suitable to put into PSBT.
var finalScriptWitness = func() []byte {
	const pver = 0
	var buf bytes.Buffer

	// Write the number of witness elements.
	if err := wire.WriteVarInt(&buf, pver, 1); err != nil {
		panic(err)
	}

	// Write a single witness element with a signature.
	signature := make([]byte, 64)
	if err := wire.WriteVarBytes(&buf, pver, signature); err != nil {
		panic(err)
	}

	return buf.Bytes()
}()

// SignPsbt expects a partial transaction with all inputs and outputs
// fully declared and tries to sign all unsigned inputs that have all
// required fields (UTXO information, BIP32 derivation information,
// witness or sig scripts) set.
// If no error is returned, the PSBT is ready to be given to the next
// signer or to be finalized if lnd was the last signer.
//
// NOTE: This RPC only signs inputs (and only those it can sign), it
// does not perform any other tasks (such as coin selection, UTXO
// locking or input/output/fee value validation, PSBT finalization). Any
// input that is incomplete will be skipped.
func (m *mockWalletKit) SignPsbt(_ context.Context,
	packet *psbt.Packet) (*psbt.Packet, error) {

	inputs := make([]psbt.PInput, len(packet.Inputs))
	copy(inputs, packet.Inputs)

	for i := range inputs {
		inputs[i].FinalScriptWitness = finalScriptWitness
	}

	signedPacket := *packet
	signedPacket.Inputs = inputs

	return &signedPacket, nil
}

// FinalizePsbt expects a partial transaction with all inputs and
// outputs fully declared and tries to sign all inputs that belong to
// the wallet. Lnd must be the last signer of the transaction. That
// means, if there are any unsigned non-witness inputs or inputs without
// UTXO information attached or inputs without witness data that do not
// belong to lnd's wallet, this method will fail. If no error is
// returned, the PSBT is ready to be extracted and the final TX within
// to be broadcast.
//
// NOTE: This method does NOT publish the transaction once finalized. It
// is the caller's responsibility to either publish the transaction on
// success or unlock/release any locked UTXOs in case of an error in
// this method.
func (m *mockWalletKit) FinalizePsbt(_ context.Context, _ *psbt.Packet,
	_ string) (*psbt.Packet, *wire.MsgTx, error) {

	return nil, nil, nil
}

// ImportPublicKey imports a public key as watch-only into the wallet.
func (m *mockWalletKit) ImportPublicKey(ctx context.Context,
	pubkey *btcec.PublicKey, addrType lnwallet.AddressType) error {

	return fmt.Errorf("unimplemented")
}

// ImportTaprootScript imports a user-provided taproot script into the
// wallet. The imported script will act as a pay-to-taproot address.
func (m *mockWalletKit) ImportTaprootScript(ctx context.Context,
	tapscript *waddrmgr.Tapscript) (btcutil.Address, error) {

	return nil, nil
}
