package libwallet

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"

	"github.com/btcsuite/btcd/txscript"
	"github.com/btcsuite/btcd/wire"
	"github.com/btcsuite/btcutil"
	"github.com/pkg/errors"
)

type SubmarineSwap interface {
	SwapUuid() string
	Invoice() string
	Receiver() SubmarineSwapReceiver
	FundingOutput() SubmarineSwapFundingOutput

	PreimageInHex() string
}

type SubmarineSwapReceiver interface {
	Alias() string
	PublicKey() string
}

type SubmarineSwapFundingOutput interface {
	OutputAddress() string
	OutputAmount() int64
	ConfirmationsNeeded() int
	UserLockTime() int64
	UserRefundAddress() MuunAddress
	ServerPaymentHashInHex() string
	ServerPublicKeyInHex() string
}

func CreateAddressSubmarineSwap(publicKey *HDPublicKey) (MuunAddress, error) {
	pubkey, err := btcutil.NewAddressPubKey(publicKey.Raw(), publicKey.Network.network)
	if err != nil {
		return nil, err
	}

	pubkeyHash := pubkey.AddressPubKeyHash()
	address := pubkeyHash.String()

	return &muunAddress{address: address, version: addressSubmarineSwap, derivationPath: publicKey.Path}, nil
}

func addUserSignatureInputSubmarineSwap(input Input, index int, tx *wire.MsgTx, privateKey *HDPrivateKey,
	muunKey *HDPublicKey) (*wire.TxIn, error) {

	submarineSwap := input.SubmarineSwap()
	if submarineSwap == nil {
		return nil, errors.Errorf("submarine swap data is nil for ss input")
	}

	witnessScript, err := createWitnessScriptSubmarineSwap(submarineSwap.RefundAddress(),
		submarineSwap.PaymentHash256(),
		submarineSwap.ServerPublicKey(),
		submarineSwap.LockTime(),
		privateKey.Network)
	if err != nil {
		return nil, err
	}

	redeemScript, err := createNonNativeSegwitRedeemScript(witnessScript)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to build reedem script for signing")
	}

	sig, err := signNonNativeSegwitInput(input, index, tx, privateKey, redeemScript, witnessScript)
	if err != nil {
		return nil, err
	}

	txInput := tx.TxIn[index]
	txInput.Witness = wire.TxWitness{sig, privateKey.PublicKey().Raw(), witnessScript}

	return txInput, nil
}

func createRedeemScriptSubmarineSwapForUser(publicKey *HDPublicKey) {

}

func createWitnessScriptSubmarineSwap(refundAddress string, paymentHash []byte, swapServerPubKey []byte, lockTime int64, network *Network) ([]byte, error) {

	// It turns out that the payment hash present in an invoice is just the SHA256 of the
	// payment preimage, so we still have to do a pass of RIPEMD160 before pushing it to the
	// script
	paymentHash160 := ripemd160(paymentHash)
	decodedRefundAddress, err := btcutil.DecodeAddress(refundAddress, network.network)
	if err != nil {
		return nil, errors.Wrapf(err, "refund address is invalid")
	}

	refundAddressHash := decodedRefundAddress.ScriptAddress()

	builder := txscript.NewScriptBuilder()
	builder.AddOp(txscript.OP_DUP)

	// Condition to decide which branch to follow:
	builder.AddOp(txscript.OP_HASH160).
		AddData(paymentHash160).
		AddOp(txscript.OP_EQUAL)

	// SubmarineSwap service spending script, for successful LN payments:
	builder.AddOp(txscript.OP_IF).
		AddOp(txscript.OP_DROP).
		AddData(swapServerPubKey)

	// User spending script, for failed LN payments:
	builder.AddOp(txscript.OP_ELSE).
		AddInt64(lockTime).
		AddOp(txscript.OP_CHECKLOCKTIMEVERIFY).
		AddOp(txscript.OP_DROP).
		AddOp(txscript.OP_DUP).
		AddOp(txscript.OP_HASH160).
		AddData(refundAddressHash).
		AddOp(txscript.OP_EQUALVERIFY)

	// Final verification for both branches:
	builder.AddOp(txscript.OP_ENDIF).
		AddOp(txscript.OP_CHECKSIG)

	return builder.Script()
}

func ValidateSubmarineSwap(rawInvoice string, userPublicKey *HDPublicKey, muunPublicKey *HDPublicKey, swap SubmarineSwap, network *Network) error {

	invoice, err := ParseInvoice(rawInvoice, network)
	if err != nil {
		return errors.Wrapf(err, "failed to decode invoice")
	}

	// Check the payment hash matches

	serverPaymentHash, err := hex.DecodeString(swap.FundingOutput().ServerPaymentHashInHex())
	if err != nil {
		return errors.Wrapf(err, "server payment hash is not valid hex")
	}

	if !bytes.Equal(invoice.PaymentHash[:], serverPaymentHash) {
		return errors.Errorf("payment hash doesn't match %v != %v", invoice.PaymentHash, swap.FundingOutput().ServerPaymentHashInHex())
	}

	// TODO: check that timelock is acceptable

	// Validate that the refund address is one we can derive

	swapRefundAddress := swap.FundingOutput().UserRefundAddress()
	derivedUserKey, err := userPublicKey.DeriveTo(swapRefundAddress.DerivationPath())
	if err != nil {
		return errors.Wrapf(err, "failed to derive user key")
	}
	derivedMuunKey, err := muunPublicKey.DeriveTo(swapRefundAddress.DerivationPath())
	if err != nil {
		return errors.Wrapf(err, "failed to derive muun key")
	}

	refundAddress, err := newMuunAddress(AddressVersion(swapRefundAddress.Version()), derivedUserKey, derivedMuunKey)
	if err != nil {
		return errors.Wrapf(err, "failed to generate refund address")
	}

	if refundAddress.Address() != swapRefundAddress.Address() {
		return errors.Errorf("refund address doesn't match generated (%v != %v)", swapRefundAddress.Address(), refundAddress.Address())
	}

	// Check the swap's witness script is a valid swap script

	serverPubKey, err := hex.DecodeString(swap.FundingOutput().ServerPublicKeyInHex())
	if err != nil {
		return errors.Wrapf(err, "server pub key is not hex")
	}

	witnessScript, err := createWitnessScriptSubmarineSwap(
		swapRefundAddress.Address(),
		serverPaymentHash,
		serverPubKey,
		swap.FundingOutput().UserLockTime(),
		network)
	if err != nil {
		return errors.Wrapf(err, "failed to compute witness script")
	}

	redeemScript, err := createNonNativeSegwitRedeemScript(witnessScript)
	if err != nil {
		return errors.Wrapf(err, "failed to build redeem script")
	}

	address, err := btcutil.NewAddressScriptHash(redeemScript, network.network)
	if err != nil {
		return errors.Wrapf(err, "failed to build address for swap script")
	}

	if address.EncodeAddress() != swap.FundingOutput().OutputAddress() {
		return errors.Errorf("address for swap script mismatch (%v != %v)", address.EncodeAddress(), swap.FundingOutput().OutputAddress())
	}

	if len(swap.PreimageInHex()) > 0 {
		preimage, err := hex.DecodeString(swap.PreimageInHex())
		if err != nil {
			return errors.Wrapf(err, "preimagehex is not actually hex 🤔")
		}

		calculatedPaymentHash := sha256.Sum256(preimage)
		if !bytes.Equal(invoice.PaymentHash[:], calculatedPaymentHash[:]) {
			return errors.Errorf("payment hash doesn't match preimage (%v != hash(%v)", invoice.PaymentHash, swap.PreimageInHex())
		}
	}

	return nil
}
