package vm

import (
	"encoding/binary"
	"errors"
	"fmt"
	"net/url"
	"strings"

	"github.com/tendermint/iavl"
	"github.com/tendermint/tendermint/crypto/merkle"
	"github.com/tendermint/tendermint/crypto/secp256k1"
	cmn "github.com/tendermint/tendermint/libs/common"

	//nolint:staticcheck
	v1 "github.com/ethereum/go-ethereum/core/vm/lightclient/v1"
	v2 "github.com/ethereum/go-ethereum/core/vm/lightclient/v2"
	"github.com/ethereum/go-ethereum/params"
)

const (
	uint64TypeLength                      uint64 = 8
	precompileContractInputMetaDataLength uint64 = 32
	consensusStateLengthBytesLength       uint64 = 32

	tmHeaderValidateResultMetaDataLength uint64 = 32
	merkleProofValidateResultLength      uint64 = 32
)

// input:
// consensus state length | consensus state | tendermint header |
// 32 bytes               |                 |                   |
func decodeTendermintHeaderValidationInput(input []byte) (*v1.ConsensusState, *v1.Header, error) {
	csLen := binary.BigEndian.Uint64(input[consensusStateLengthBytesLength-uint64TypeLength : consensusStateLengthBytesLength])

	if consensusStateLengthBytesLength+csLen < consensusStateLengthBytesLength {
		return nil, nil, fmt.Errorf("integer overflow, csLen: %d", csLen)
	}

	if uint64(len(input)) <= consensusStateLengthBytesLength+csLen {
		return nil, nil, fmt.Errorf("expected payload size %d, actual size: %d", consensusStateLengthBytesLength+csLen, len(input))
	}

	cs, err := v1.DecodeConsensusState(input[consensusStateLengthBytesLength : consensusStateLengthBytesLength+csLen])
	if err != nil {
		return nil, nil, err
	}
	header, err := v1.DecodeHeader(input[consensusStateLengthBytesLength+csLen:])
	if err != nil {
		return nil, nil, err
	}

	return &cs, header, nil
}

// tmHeaderValidate implemented as a native contract. Used to validate the light
// client's new header for tendermint v0.31.12 and its compatible version.
type tmHeaderValidate struct{}

func (c *tmHeaderValidate) RequiredGas(input []byte) uint64 {
	return params.TendermintHeaderValidateGas
}

func (c *tmHeaderValidate) Run(input []byte) (result []byte, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("internal error: %v\n", r)
		}
	}()

	if uint64(len(input)) <= precompileContractInputMetaDataLength {
		return nil, errors.New("invalid input")
	}

	payloadLength := binary.BigEndian.Uint64(input[precompileContractInputMetaDataLength-uint64TypeLength : precompileContractInputMetaDataLength])
	if uint64(len(input)) != payloadLength+precompileContractInputMetaDataLength {
		return nil, fmt.Errorf("invalid input: input size should be %d, actual the size is %d", payloadLength+precompileContractInputMetaDataLength, len(input))
	}

	cs, header, err := decodeTendermintHeaderValidationInput(input[precompileContractInputMetaDataLength:])
	if err != nil {
		return nil, err
	}

	validatorSetChanged, err := cs.ApplyHeader(header)
	if err != nil {
		return nil, err
	}

	consensusStateBytes, err := cs.EncodeConsensusState()
	if err != nil {
		return nil, err
	}

	// result
	// | validatorSetChanged | empty      | consensusStateBytesLength |  new consensusState |
	// | 1 byte              | 23 bytes   | 8 bytes                   |                     |
	lengthBytes := make([]byte, tmHeaderValidateResultMetaDataLength)
	if validatorSetChanged {
		copy(lengthBytes[:1], []byte{0x01})
	}
	consensusStateBytesLength := uint64(len(consensusStateBytes))
	binary.BigEndian.PutUint64(lengthBytes[tmHeaderValidateResultMetaDataLength-uint64TypeLength:], consensusStateBytesLength)

	result = append(lengthBytes, consensusStateBytes...)

	return result, nil
}

// ------------------------------------------------------------------------------------------------------------------------------------------------

// iavlMerkleProofValidate implemented as a native contract.
type iavlMerkleProofValidate struct {
	basicIavlMerkleProofValidate
}

func (c *iavlMerkleProofValidate) RequiredGas(input []byte) uint64 {
	return params.IAVLMerkleProofValidateGas
}

// input:
// | payload length | payload    |
// | 32 bytes       |            |
func (c *iavlMerkleProofValidate) Run(input []byte) (result []byte, err error) {
	return c.basicIavlMerkleProofValidate.Run(input)
}

// tmHeaderValidateNano implemented as a native contract.
type tmHeaderValidateNano struct{}

func (c *tmHeaderValidateNano) RequiredGas(input []byte) uint64 {
	return params.TendermintHeaderValidateGas
}

func (c *tmHeaderValidateNano) Run(input []byte) (result []byte, err error) {
	return nil, errors.New("suspend")
}

type iavlMerkleProofValidateNano struct{}

func (c *iavlMerkleProofValidateNano) RequiredGas(_ []byte) uint64 {
	return params.IAVLMerkleProofValidateGas
}

func (c *iavlMerkleProofValidateNano) Run(_ []byte) (result []byte, err error) {
	return nil, errors.New("suspend")
}

// ------------------------------------------------------------------------------------------------------------------------------------------------
type iavlMerkleProofValidateMoran struct {
	basicIavlMerkleProofValidate
}

func (c *iavlMerkleProofValidateMoran) RequiredGas(_ []byte) uint64 {
	return params.IAVLMerkleProofValidateGas
}

func (c *iavlMerkleProofValidateMoran) Run(input []byte) (result []byte, err error) {
	c.basicIavlMerkleProofValidate.verifiers = []merkle.ProofOpVerifier{
		forbiddenAbsenceOpVerifier,
		singleValueOpVerifier,
		multiStoreOpVerifier,
		forbiddenSimpleValueOpVerifier,
	}
	return c.basicIavlMerkleProofValidate.Run(input)
}

type iavlMerkleProofValidatePlanck struct {
	basicIavlMerkleProofValidate
}

func (c *iavlMerkleProofValidatePlanck) RequiredGas(_ []byte) uint64 {
	return params.IAVLMerkleProofValidateGas
}

func (c *iavlMerkleProofValidatePlanck) Run(input []byte) (result []byte, err error) {
	c.basicIavlMerkleProofValidate.proofRuntime = v1.Ics23CompatibleProofRuntime()
	c.basicIavlMerkleProofValidate.verifiers = []merkle.ProofOpVerifier{
		forbiddenAbsenceOpVerifier,
		singleValueOpVerifier,
		multiStoreOpVerifier,
		forbiddenSimpleValueOpVerifier,
	}
	c.basicIavlMerkleProofValidate.keyVerifier = keyVerifier
	c.basicIavlMerkleProofValidate.opsVerifier = proofOpsVerifier
	return c.basicIavlMerkleProofValidate.Run(input)
}

type iavlMerkleProofValidatePlato struct {
	basicIavlMerkleProofValidate
}

func (c *iavlMerkleProofValidatePlato) RequiredGas(_ []byte) uint64 {
	return params.IAVLMerkleProofValidateGas
}

func (c *iavlMerkleProofValidatePlato) Run(input []byte) (result []byte, err error) {
	c.basicIavlMerkleProofValidate.proofRuntime = v1.Ics23ProofRuntime()
	c.basicIavlMerkleProofValidate.verifiers = []merkle.ProofOpVerifier{
		forbiddenAbsenceOpVerifier,
		singleValueOpVerifier,
		multiStoreOpVerifier,
		forbiddenSimpleValueOpVerifier,
	}
	c.basicIavlMerkleProofValidate.keyVerifier = keyVerifier
	c.basicIavlMerkleProofValidate.opsVerifier = proofOpsVerifier
	return c.basicIavlMerkleProofValidate.Run(input)
}

func successfulMerkleResult() []byte {
	result := make([]byte, merkleProofValidateResultLength)
	binary.BigEndian.PutUint64(result[merkleProofValidateResultLength-uint64TypeLength:], 0x01)
	return result
}

type basicIavlMerkleProofValidate struct {
	keyVerifier  v1.KeyVerifier
	opsVerifier  merkle.ProofOpsVerifier
	verifiers    []merkle.ProofOpVerifier
	proofRuntime *merkle.ProofRuntime
}

func (c *basicIavlMerkleProofValidate) Run(input []byte) (result []byte, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("internal error: %v\n", r)
		}
	}()

	if uint64(len(input)) <= precompileContractInputMetaDataLength {
		return nil, fmt.Errorf("invalid input: input should include %d bytes payload length and payload", precompileContractInputMetaDataLength)
	}

	payloadLength := binary.BigEndian.Uint64(input[precompileContractInputMetaDataLength-uint64TypeLength : precompileContractInputMetaDataLength])
	if uint64(len(input)) != payloadLength+precompileContractInputMetaDataLength {
		return nil, fmt.Errorf("invalid input: input size should be %d, actual the size is %d", payloadLength+precompileContractInputMetaDataLength, len(input))
	}

	kvmp, err := v1.DecodeKeyValueMerkleProof(input[precompileContractInputMetaDataLength:])
	if err != nil {
		return nil, err
	}
	if c.proofRuntime == nil {
		kvmp.SetProofRuntime(v1.DefaultProofRuntime())
	} else {
		kvmp.SetProofRuntime(c.proofRuntime)
	}
	kvmp.SetVerifiers(c.verifiers)
	kvmp.SetOpsVerifier(c.opsVerifier)
	kvmp.SetKeyVerifier(c.keyVerifier)

	valid := kvmp.Validate()
	if !valid {
		return nil, errors.New("invalid merkle proof")
	}

	return successfulMerkleResult(), nil
}

func forbiddenAbsenceOpVerifier(op merkle.ProofOperator) error {
	if op == nil {
		return nil
	}
	if _, ok := op.(iavl.IAVLAbsenceOp); ok {
		return cmn.NewError("absence proof suspend")
	}
	return nil
}

func forbiddenSimpleValueOpVerifier(op merkle.ProofOperator) error {
	if op == nil {
		return nil
	}
	if _, ok := op.(merkle.SimpleValueOp); ok {
		return cmn.NewError("simple value proof suspend")
	}
	return nil
}

func multiStoreOpVerifier(op merkle.ProofOperator) error {
	if op == nil {
		return nil
	}
	if mop, ok := op.(v1.MultiStoreProofOp); ok {
		storeNames := make(map[string]bool, len(mop.Proof.StoreInfos))
		for _, store := range mop.Proof.StoreInfos {
			if exist := storeNames[store.Name]; exist {
				return cmn.NewError("duplicated store")
			} else {
				storeNames[store.Name] = true
			}
		}
	}
	return nil
}

func singleValueOpVerifier(op merkle.ProofOperator) error {
	if op == nil {
		return nil
	}
	if valueOp, ok := op.(iavl.IAVLValueOp); ok {
		if len(valueOp.Proof.Leaves) != 1 {
			return cmn.NewError("range proof suspended")
		}
		for _, innerNode := range valueOp.Proof.LeftPath {
			if len(innerNode.Right) > 0 && len(innerNode.Left) > 0 {
				return cmn.NewError("both right and left hash exit!")
			}
		}
	}
	return nil
}

func proofOpsVerifier(poz merkle.ProofOperators) error {
	if len(poz) != 2 {
		return cmn.NewError("proof ops should be 2")
	}

	// for legacy proof type
	if _, ok := poz[1].(v1.MultiStoreProofOp); ok {
		if _, ok := poz[0].(iavl.IAVLValueOp); !ok {
			return cmn.NewError("invalid proof op")
		}
		return nil
	}

	// for ics23 proof type
	if op2, ok := poz[1].(v1.CommitmentOp); ok {
		if op2.Type != v1.ProofOpSimpleMerkleCommitment {
			return cmn.NewError("invalid proof op")
		}

		op1, ok := poz[0].(v1.CommitmentOp)
		if !ok {
			return cmn.NewError("invalid proof op")
		}

		if op1.Type != v1.ProofOpIAVLCommitment {
			return cmn.NewError("invalid proof op")
		}
		return nil
	}

	return cmn.NewError("invalid proof type")
}

func keyVerifier(key string) error {
	// https://github.com/bnb-chain/tendermint/blob/72375a6f3d4a72831cc65e73363db89a0073db38/crypto/merkle/proof_key_path.go#L88
	// since the upper function is ambiguous, `x:00` can be decoded to both kind of key type
	// we check the key here to make sure the key will not start from `x:`
	if strings.HasPrefix(url.PathEscape(key), "x:") {
		return cmn.NewError("key should not start with x:")
	}
	return nil
}

// cometBFTLightBlockValidate implemented as a native contract. Used to validate the light  blocks for CometBFT v0.37.0
// and its compatible version. Besides, in order to support the BLS cross-chain infrastructure, the SetRelayerAddress
// and SetBlsKey methods should be implemented for the validator.
type cometBFTLightBlockValidate struct{}

func (c *cometBFTLightBlockValidate) RequiredGas(input []byte) uint64 {
	return params.CometBFTLightBlockValidateGas
}

func (c *cometBFTLightBlockValidate) run(input []byte, isHertz bool) (result []byte, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("internal error: %v\n", r)
		}
	}()

	cs, block, err := v2.DecodeLightBlockValidationInput(input)
	if err != nil {
		return nil, err
	}

	validatorSetChanged, err := cs.ApplyLightBlock(block, isHertz)
	if err != nil {
		return nil, err
	}

	consensusStateBytes, err := cs.EncodeConsensusState()
	if err != nil {
		return nil, err
	}

	result = v2.EncodeLightBlockValidationResult(validatorSetChanged, consensusStateBytes)
	return result, nil
}

func (c *cometBFTLightBlockValidate) Run(input []byte) (result []byte, err error) {
	return c.run(input, false)
}

type cometBFTLightBlockValidateHertz struct {
	cometBFTLightBlockValidate
}

func (c *cometBFTLightBlockValidateHertz) Run(input []byte) (result []byte, err error) {
	return c.run(input, true)
}

// secp256k1SignatureRecover implemented as a native contract.
type secp256k1SignatureRecover struct{}

func (c *secp256k1SignatureRecover) RequiredGas(input []byte) uint64 {
	return params.EcrecoverGas
}

const (
	secp256k1PubKeyLength           uint8 = 33
	secp256k1SignatureLength        uint8 = 64
	secp256k1SignatureMsgHashLength uint8 = 32
)

// input:
// | PubKey | Signature  |  SignatureMsgHash  |
// | 33 bytes |  64 bytes    |       32 bytes       |
func (c *secp256k1SignatureRecover) Run(input []byte) (result []byte, err error) {
	if len(input) != int(secp256k1PubKeyLength)+int(secp256k1SignatureLength)+int(secp256k1SignatureMsgHashLength) {
		return nil, errors.New("invalid input")
	}

	return c.runTMSecp256k1Signature(
		input[:secp256k1PubKeyLength],
		input[secp256k1PubKeyLength:secp256k1PubKeyLength+secp256k1SignatureLength],
		input[secp256k1PubKeyLength+secp256k1SignatureLength:],
	)
}

func (c *secp256k1SignatureRecover) runTMSecp256k1Signature(pubkey, signatureStr, msgHash []byte) (result []byte, err error) {
	tmPubKey := secp256k1.PubKeySecp256k1(pubkey)
	ok := tmPubKey.VerifyBytesWithMsgHash(msgHash, signatureStr)
	if !ok {
		return nil, errors.New("invalid signature")
	}
	return tmPubKey.Address().Bytes(), nil
}
