package blob

import (
	"bytes"
	"io"

	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/btcsuite/btcd/btcec/v2/schnorr"
	"github.com/btcsuite/btcd/chaincfg/chainhash"
	"github.com/btcsuite/btcd/txscript"
	"github.com/btcsuite/btcd/wire"
	secp "github.com/decred/dcrd/dcrec/secp256k1/v4"
	"github.com/lightningnetwork/lnd/input"
	"github.com/lightningnetwork/lnd/lnwallet"
	"github.com/lightningnetwork/lnd/lnwire"
)

// JusticeKit is an interface that describes lé Blob of Justice. An
// implementation of the JusticeKit contains information required to construct
// a justice transaction, that sweeps a remote party's revoked commitment
// transaction. It supports encryption and decryption using chacha20poly1305,
// allowing the client to encrypt the contents of the blob, and for a
// watchtower to later decrypt if action must be taken.
type JusticeKit interface {
	// ToLocalOutputSpendInfo returns the info required to send the to-local
	// output. It returns the output pub key script and the witness required
	// to spend the output.
	ToLocalOutputSpendInfo() (*txscript.PkScript, wire.TxWitness, error)

	// ToRemoteOutputSpendInfo returns the info required to send the
	// to-remote output. It returns the output pub key script, the witness
	// required to spend the output and the sequence to apply.
	ToRemoteOutputSpendInfo() (*txscript.PkScript, wire.TxWitness, uint32,
		error)

	// HasCommitToRemoteOutput returns true if the kit does include the
	// information required to sweep the to-remote output.
	HasCommitToRemoteOutput() bool

	// AddToLocalSig adds the to-local signature to the kit.
	AddToLocalSig(sig lnwire.Sig)

	// AddToRemoteSig adds the to-remote signature to the kit.
	AddToRemoteSig(sig lnwire.Sig)

	// SweepAddress returns the sweep address to be used on the justice tx
	// output.
	SweepAddress() []byte

	// PlainTextSize is the size of the encoded-but-unencrypted blob in
	// bytes.
	PlainTextSize() int

	encode(w io.Writer) error
	decode(r io.Reader) error
}

// legacyJusticeKit is an implementation of the JusticeKit interface which can
// be used for backing up commitments of legacy (pre-anchor) channels.
type legacyJusticeKit struct {
	justiceKitPacketV0
}

// A compile-time check to ensure that legacyJusticeKit implements the
// JusticeKit interface.
var _ JusticeKit = (*legacyJusticeKit)(nil)

// newLegacyJusticeKit constructs a new legacyJusticeKit.
func newLegacyJusticeKit(sweepScript []byte,
	breachInfo *lnwallet.BreachRetribution,
	withToRemote bool) *legacyJusticeKit {

	keyRing := breachInfo.KeyRing

	packet := justiceKitPacketV0{
		sweepAddress:     sweepScript,
		revocationPubKey: toBlobPubKey(keyRing.RevocationKey),
		localDelayPubKey: toBlobPubKey(keyRing.ToLocalKey),
		csvDelay:         breachInfo.RemoteDelay,
	}

	if withToRemote {
		packet.commitToRemotePubKey = toBlobPubKey(
			keyRing.ToRemoteKey,
		)
	}

	return &legacyJusticeKit{packet}
}

// ToLocalOutputSpendInfo returns the info required to send the to-local output.
// It returns the output pub key script and the witness required to spend the
// output.
//
// NOTE: This is part of the JusticeKit interface.
func (l *legacyJusticeKit) ToLocalOutputSpendInfo() (*txscript.PkScript,
	wire.TxWitness, error) {

	revocationPubKey, err := btcec.ParsePubKey(l.revocationPubKey[:])
	if err != nil {
		return nil, nil, err
	}

	localDelayedPubKey, err := btcec.ParsePubKey(l.localDelayPubKey[:])
	if err != nil {
		return nil, nil, err
	}

	script, err := input.CommitScriptToSelf(
		l.csvDelay, localDelayedPubKey, revocationPubKey,
	)
	if err != nil {
		return nil, nil, err
	}

	scriptPubKey, err := input.WitnessScriptHash(script)
	if err != nil {
		return nil, nil, err
	}

	toLocalSig, err := l.commitToLocalSig.ToSignature()
	if err != nil {
		return nil, nil, err
	}

	witness := make(wire.TxWitness, 3)
	witness[0] = append(toLocalSig.Serialize(), byte(txscript.SigHashAll))
	witness[1] = []byte{1}
	witness[2] = script

	pkScript, err := txscript.ParsePkScript(scriptPubKey)
	if err != nil {
		return nil, nil, err
	}

	return &pkScript, witness, nil
}

// ToRemoteOutputSpendInfo returns the info required to spend the to-remote
// output. It returns the output pub key script, the witness required to spend
// the output and the sequence to apply.
//
// NOTE: This is part of the JusticeKit interface.
func (l *legacyJusticeKit) ToRemoteOutputSpendInfo() (*txscript.PkScript,
	wire.TxWitness, uint32, error) {

	if !btcec.IsCompressedPubKey(l.commitToRemotePubKey[:]) {
		return nil, nil, 0, ErrNoCommitToRemoteOutput
	}

	toRemoteScript := l.commitToRemotePubKey[:]

	// Since the to-remote witness script should just be a regular p2wkh
	// output, we'll parse it to retrieve the public key.
	toRemotePubKey, err := btcec.ParsePubKey(toRemoteScript)
	if err != nil {
		return nil, nil, 0, err
	}

	// Compute the witness script hash from the to-remote pubkey, which will
	// be used to locate the output on the breach commitment transaction.
	toRemoteScriptHash, err := input.CommitScriptUnencumbered(
		toRemotePubKey,
	)
	if err != nil {
		return nil, nil, 0, err
	}

	toRemoteSig, err := l.commitToRemoteSig.ToSignature()
	if err != nil {
		return nil, nil, 0, err
	}

	witness := make(wire.TxWitness, 2)
	witness[0] = append(toRemoteSig.Serialize(), byte(txscript.SigHashAll))
	witness[1] = toRemoteScript

	pkScript, err := txscript.ParsePkScript(toRemoteScriptHash)
	if err != nil {
		return nil, nil, 0, err
	}

	return &pkScript, witness, 0, nil
}

// HasCommitToRemoteOutput returns true if the blob contains a to-remote p2wkh
// pubkey.
//
// NOTE: This is part of the JusticeKit interface.
func (l *legacyJusticeKit) HasCommitToRemoteOutput() bool {
	return btcec.IsCompressedPubKey(l.commitToRemotePubKey[:])
}

// SweepAddress returns the sweep address to be used on the justice tx
// output.
//
// NOTE: This is part of the JusticeKit interface.
func (l *legacyJusticeKit) SweepAddress() []byte {
	return l.sweepAddress
}

// AddToLocalSig adds the to-local signature to the kit.
//
// NOTE: This is part of the JusticeKit interface.
func (l *legacyJusticeKit) AddToLocalSig(sig lnwire.Sig) {
	l.commitToLocalSig = sig
}

// AddToRemoteSig adds the to-remote signature to the kit.
//
// NOTE: This is part of the JusticeKit interface.
func (l *legacyJusticeKit) AddToRemoteSig(sig lnwire.Sig) {
	l.commitToRemoteSig = sig
}

// PlainTextSize is the size of the encoded-but-unencrypted blob in
// bytes.
//
// NOTE: This is part of the JusticeKit interface.
func (l *legacyJusticeKit) PlainTextSize() int {
	return V0PlaintextSize
}

// anchorJusticeKit is an implementation of the JusticeKit interface which can
// be used for backing up commitments of anchor channels. It inherits most of
// the methods from the legacyJusticeKit and overrides the
// ToRemoteOutputSpendInfo method since the to-remote output of an anchor
// output is a P2WSH instead of the P2WPKH used by the legacy channels.
type anchorJusticeKit struct {
	legacyJusticeKit
}

// A compile-time check to ensure that legacyJusticeKit implements the
// JusticeKit interface.
var _ JusticeKit = (*anchorJusticeKit)(nil)

// newAnchorJusticeKit constructs a new anchorJusticeKit.
func newAnchorJusticeKit(sweepScript []byte,
	breachInfo *lnwallet.BreachRetribution,
	withToRemote bool) *anchorJusticeKit {

	legacyKit := newLegacyJusticeKit(sweepScript, breachInfo, withToRemote)

	return &anchorJusticeKit{
		legacyJusticeKit: *legacyKit,
	}
}

// ToRemoteOutputSpendInfo returns the info required to send the to-remote
// output. It returns the output pub key script, the witness required to spend
// the output and the sequence to apply.
//
// NOTE: This is part of the JusticeKit interface.
func (a *anchorJusticeKit) ToRemoteOutputSpendInfo() (*txscript.PkScript,
	wire.TxWitness, uint32, error) {

	if !btcec.IsCompressedPubKey(a.commitToRemotePubKey[:]) {
		return nil, nil, 0, ErrNoCommitToRemoteOutput
	}

	pk, err := btcec.ParsePubKey(a.commitToRemotePubKey[:])
	if err != nil {
		return nil, nil, 0, err
	}

	toRemoteScript, err := input.CommitScriptToRemoteConfirmed(pk)
	if err != nil {
		return nil, nil, 0, err
	}

	toRemoteScriptHash, err := input.WitnessScriptHash(toRemoteScript)
	if err != nil {
		return nil, nil, 0, err
	}

	toRemoteSig, err := a.commitToRemoteSig.ToSignature()
	if err != nil {
		return nil, nil, 0, err
	}

	witness := make([][]byte, 2)
	witness[0] = append(toRemoteSig.Serialize(), byte(txscript.SigHashAll))
	witness[1] = toRemoteScript

	pkScript, err := txscript.ParsePkScript(toRemoteScriptHash)
	if err != nil {
		return nil, nil, 0, err
	}

	return &pkScript, witness, 1, nil
}

// taprootJusticeKit is an implementation of the JusticeKit interface which can
// be used for backing up commitments of taproot channels.
type taprootJusticeKit struct {
	justiceKitPacketV1
}

// A compile-time check to ensure that taprootJusticeKit implements the
// JusticeKit interface.
var _ JusticeKit = (*taprootJusticeKit)(nil)

// newTaprootJusticeKit constructs a new taprootJusticeKit.
func newTaprootJusticeKit(sweepScript []byte,
	breachInfo *lnwallet.BreachRetribution,
	withToRemote bool) (*taprootJusticeKit, error) {

	keyRing := breachInfo.KeyRing

	tree, err := input.NewLocalCommitScriptTree(
		breachInfo.RemoteDelay, keyRing.ToLocalKey,
		keyRing.RevocationKey,
	)
	if err != nil {
		return nil, err
	}

	packet := justiceKitPacketV1{
		sweepAddress: sweepScript,
		revocationPubKey: toBlobSchnorrPubKey(
			keyRing.RevocationKey,
		),
		localDelayPubKey: toBlobSchnorrPubKey(keyRing.ToLocalKey),
		delayScriptHash:  tree.SettleLeaf.TapHash(),
	}

	if withToRemote {
		packet.commitToRemotePubKey = toBlobPubKey(keyRing.ToRemoteKey)
	}

	return &taprootJusticeKit{packet}, nil
}

// ToLocalOutputSpendInfo returns the info required to send the to-local
// output. It returns the output pubkey script and the witness required
// to spend the output.
//
// NOTE: This is part of the JusticeKit interface.
func (t *taprootJusticeKit) ToLocalOutputSpendInfo() (*txscript.PkScript,
	wire.TxWitness, error) {

	revocationPubKey, err := schnorr.ParsePubKey(t.revocationPubKey[:])
	if err != nil {
		return nil, nil, err
	}

	localDelayedPubKey, err := schnorr.ParsePubKey(t.localDelayPubKey[:])
	if err != nil {
		return nil, nil, err
	}

	revokeScript, err := input.TaprootLocalCommitRevokeScript(
		localDelayedPubKey, revocationPubKey,
	)
	if err != nil {
		return nil, nil, err
	}

	revokeLeaf := txscript.NewBaseTapLeaf(revokeScript)
	revokeLeafHash := revokeLeaf.TapHash()
	rootHash := tapBranchHash(revokeLeafHash[:], t.delayScriptHash[:])

	outputKey := txscript.ComputeTaprootOutputKey(
		&input.TaprootNUMSKey, rootHash[:],
	)

	scriptPk, err := input.PayToTaprootScript(outputKey)
	if err != nil {
		return nil, nil, err
	}

	ctrlBlock := txscript.ControlBlock{
		InternalKey:     &input.TaprootNUMSKey,
		OutputKeyYIsOdd: isOddPub(outputKey),
		LeafVersion:     revokeLeaf.LeafVersion,
		InclusionProof:  t.delayScriptHash[:],
	}

	ctrlBytes, err := ctrlBlock.ToBytes()
	if err != nil {
		return nil, nil, err
	}

	toLocalSig, err := t.commitToLocalSig.ToSignature()
	if err != nil {
		return nil, nil, err
	}

	witness := make([][]byte, 3)
	witness[0] = toLocalSig.Serialize()
	witness[1] = revokeScript
	witness[2] = ctrlBytes

	pkScript, err := txscript.ParsePkScript(scriptPk)
	if err != nil {
		return nil, nil, err
	}

	return &pkScript, witness, nil
}

// ToRemoteOutputSpendInfo returns the info required to send the to-remote
// output. It returns the output pubkey script, the witness required to spend
// the output and the sequence to apply.
//
// NOTE: This is part of the JusticeKit interface.
func (t *taprootJusticeKit) ToRemoteOutputSpendInfo() (*txscript.PkScript,
	wire.TxWitness, uint32, error) {

	if len(t.commitToRemotePubKey[:]) == 0 {
		return nil, nil, 0, ErrNoCommitToRemoteOutput
	}

	toRemotePk, err := btcec.ParsePubKey(t.commitToRemotePubKey[:])
	if err != nil {
		return nil, nil, 0, err
	}

	scriptTree, err := input.NewRemoteCommitScriptTree(toRemotePk)
	if err != nil {
		return nil, nil, 0, err
	}

	script, err := input.PayToTaprootScript(scriptTree.TaprootKey)
	if err != nil {
		return nil, nil, 0, err
	}

	settleControlBlock := input.MakeTaprootCtrlBlock(
		scriptTree.SettleLeaf.Script, &input.TaprootNUMSKey,
		scriptTree.TapscriptTree,
	)

	ctrl, err := settleControlBlock.ToBytes()
	if err != nil {
		return nil, nil, 0, err
	}

	toRemoteSig, err := t.commitToRemoteSig.ToSignature()
	if err != nil {
		return nil, nil, 0, err
	}

	witness := make([][]byte, 3)
	witness[0] = toRemoteSig.Serialize()
	witness[1] = scriptTree.SettleLeaf.Script
	witness[2] = ctrl

	pkScript, err := txscript.ParsePkScript(script)
	if err != nil {
		return nil, nil, 0, err
	}

	return &pkScript, witness, 1, nil
}

// HasCommitToRemoteOutput returns true if the blob contains a to-remote pubkey.
//
// NOTE: This is part of the JusticeKit interface.
func (t *taprootJusticeKit) HasCommitToRemoteOutput() bool {
	return btcec.IsCompressedPubKey(t.commitToRemotePubKey[:])
}

// AddToLocalSig adds the to-local signature to the kit.
//
// NOTE: This is part of the JusticeKit interface.
func (t *taprootJusticeKit) AddToLocalSig(sig lnwire.Sig) {
	t.commitToLocalSig = sig
}

// AddToRemoteSig adds the to-remote signature to the kit.
//
// NOTE: This is part of the JusticeKit interface.
func (t *taprootJusticeKit) AddToRemoteSig(sig lnwire.Sig) {
	t.commitToRemoteSig = sig
}

// SweepAddress returns the sweep address to be used on the justice tx output.
//
// NOTE: This is part of the JusticeKit interface.
func (t *taprootJusticeKit) SweepAddress() []byte {
	return t.sweepAddress
}

// PlainTextSize is the size of the encoded-but-unencrypted blob in bytes.
//
// NOTE: This is part of the JusticeKit interface.
func (t *taprootJusticeKit) PlainTextSize() int {
	return V1PlaintextSize
}

func tapBranchHash(l, r []byte) chainhash.Hash {
	if bytes.Compare(l, r) > 0 {
		l, r = r, l
	}

	return *chainhash.TaggedHash(chainhash.TagTapBranch, l, r)
}

func isOddPub(key *btcec.PublicKey) bool {
	return key.SerializeCompressed()[0] == secp.PubKeyFormatCompressedOdd
}
