package reservation

import (
	"bytes"
	"context"
	"errors"
	"fmt"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/btcutil"
	"github.com/btcsuite/btcd/wire"
	"github.com/lightninglabs/lndclient"
	"github.com/lightninglabs/loop/fsm"
	reservation_script "github.com/lightninglabs/loop/instantout/reservation/script"
	"github.com/lightningnetwork/lnd/input"
	"github.com/lightningnetwork/lnd/keychain"
)

// ID is a unique identifier for a reservation.
type ID [IdLength]byte

// FromByteSlice creates a reservation id from a byte slice.
func (r *ID) FromByteSlice(b []byte) error {
	if len(b) != IdLength {
		return fmt.Errorf("reservation id must be 32 bytes, got %d, %x",
			len(b), b)
	}

	copy(r[:], b)

	return nil
}

// Reservation holds all the necessary information for the 2-of-2 multisig
// reservation utxo.
type Reservation struct {
	// ID is the unique identifier of the reservation.
	ID ID

	// ProtocolVersion is the version of the protocol used for the
	// reservation.
	ProtocolVersion ProtocolVersion

	// State is the current state of the reservation.
	State fsm.StateType

	// ClientPubkey is the client's pubkey.
	ClientPubkey *btcec.PublicKey

	// ServerPubkey is the server's pubkey.
	ServerPubkey *btcec.PublicKey

	// Value is the amount of the reservation.
	Value btcutil.Amount

	// Expiry is the absolute block height at which the reservation expires.
	Expiry uint32

	// KeyLocator is the key locator of the client's key.
	KeyLocator keychain.KeyLocator

	// Outpoint is the outpoint of the reservation.
	Outpoint *wire.OutPoint

	// InitiationHeight is the height at which the reservation was
	// initiated.
	InitiationHeight int32

	// ConfirmationHeight is the height at which the reservation was
	// confirmed.
	ConfirmationHeight uint32
}

func NewReservation(id ID, serverPubkey, clientPubkey *btcec.PublicKey,
	value btcutil.Amount, expiry, heightHint uint32,
	keyLocator keychain.KeyLocator, protocolVersion ProtocolVersion) (
	*Reservation, error) {

	if id == [32]byte{} {
		return nil, errors.New("id is empty")
	}

	if clientPubkey == nil {
		return nil, errors.New("client pubkey is nil")
	}

	if serverPubkey == nil {
		return nil, errors.New("server pubkey is nil")
	}

	if expiry == 0 {
		return nil, errors.New("expiry is 0")
	}

	if value == 0 {
		return nil, errors.New("value is 0")
	}

	if keyLocator.Family == 0 {
		return nil, errors.New("key locator family is 0")
	}
	return &Reservation{
		ID:               id,
		Value:            value,
		ClientPubkey:     clientPubkey,
		ServerPubkey:     serverPubkey,
		KeyLocator:       keyLocator,
		Expiry:           expiry,
		InitiationHeight: int32(heightHint),
		ProtocolVersion:  protocolVersion,
	}, nil
}

// GetPkScript returns the pk script of the reservation.
func (r *Reservation) GetPkScript() ([]byte, error) {
	// Now that we have all the required data, we can create the pk script.
	pkScript, err := reservation_script.ReservationScript(
		r.Expiry, r.ServerPubkey, r.ClientPubkey,
	)
	if err != nil {
		return nil, err
	}

	return pkScript, nil
}

// Output returns the reservation output.
func (r *Reservation) Output() (*wire.TxOut, error) {
	pkscript, err := r.GetPkScript()
	if err != nil {
		return nil, err
	}

	return wire.NewTxOut(int64(r.Value), pkscript), nil
}

func (r *Reservation) findReservationOutput(tx *wire.MsgTx) (*wire.OutPoint,
	error) {

	pkScript, err := r.GetPkScript()
	if err != nil {
		return nil, err
	}

	for i, txOut := range tx.TxOut {
		if bytes.Equal(txOut.PkScript, pkScript) {
			return &wire.OutPoint{
				Hash:  tx.TxHash(),
				Index: uint32(i),
			}, nil
		}
	}

	return nil, errors.New("reservation output not found")
}

// Musig2CreateSession creates a musig2 session for the reservation.
func (r *Reservation) Musig2CreateSession(ctx context.Context,
	signer lndclient.SignerClient) (*input.MuSig2SessionInfo, error) {

	signers := [][]byte{
		r.ClientPubkey.SerializeCompressed(),
		r.ServerPubkey.SerializeCompressed(),
	}

	expiryLeaf, err := reservation_script.TaprootExpiryScript(
		r.Expiry, r.ServerPubkey,
	)
	if err != nil {
		return nil, err
	}

	rootHash := expiryLeaf.TapHash()

	musig2SessionInfo, err := signer.MuSig2CreateSession(
		ctx, input.MuSig2Version100RC2, &r.KeyLocator, signers,
		lndclient.MuSig2TaprootTweakOpt(rootHash[:], false),
	)
	if err != nil {
		return nil, err
	}

	return musig2SessionInfo, nil
}
