package types

import (
	"bytes"
	"crypto/ecdsa"
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"math/big"
	"sync/atomic"

	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/cosmos/cosmos-sdk/wire"
	"github.com/cosmos/cosmos-sdk/x/auth"
	ethcmn "github.com/ethereum/go-ethereum/common"
	ethtypes "github.com/ethereum/go-ethereum/core/types"
	ethcrypto "github.com/ethereum/go-ethereum/crypto"
	ethsha "github.com/ethereum/go-ethereum/crypto/sha3"
	"github.com/ethereum/go-ethereum/rlp"

	"github.com/pkg/errors"
)

const (
	// TypeTxEthereum reflects an Ethereum Transaction type.
	TypeTxEthereum = "Ethereum"
)

// ----------------------------------------------------------------------------
// Ethereum transaction
// ----------------------------------------------------------------------------

type (
	// Transaction implements the Ethereum transaction structure as an exact
	// copy. It implements the Cosmos sdk.Tx interface. Due to the private
	// fields, it must be replicated here and cannot be embedded or used
	// directly.
	//
	// Note: The transaction also implements the sdk.Msg interface to perform
	// basic validation that is done in the BaseApp.
	Transaction struct {
		Data TxData

		// caches
		hash atomic.Value
		size atomic.Value
		from atomic.Value
	}

	// TxData implements the Ethereum transaction data structure as an exact
	// copy. It is used solely as intended in Ethereum abiding by the protocol
	// except for the payload field which may embed a Cosmos SDK transaction.
	TxData struct {
		AccountNonce uint64          `json:"nonce"`
		Price        sdk.Int         `json:"gasPrice"`
		GasLimit     uint64          `json:"gas"`
		Recipient    *ethcmn.Address `json:"to"` // nil means contract creation
		Amount       sdk.Int         `json:"value"`
		Payload      []byte          `json:"input"`
		Signature    *EthSignature   `json:"signature"`

		// hash is only used when marshaling to JSON
		Hash *ethcmn.Hash `json:"hash"`
	}

	// EthSignature reflects an Ethereum signature. We wrap this in a structure
	// to support Amino serialization of transactions.
	EthSignature struct {
		v, r, s *big.Int
	}
)

// NewEthSignature returns a new instantiated Ethereum signature.
func NewEthSignature(v, r, s *big.Int) *EthSignature {
	return &EthSignature{v, r, s}
}

func (es *EthSignature) sanitize() {
	if es.v == nil {
		es.v = new(big.Int)
	}
	if es.r == nil {
		es.r = new(big.Int)
	}
	if es.s == nil {
		es.s = new(big.Int)
	}
}

// MarshalAmino defines a custom encoding scheme for a EthSignature.
func (es EthSignature) MarshalAmino() ([3]string, error) {
	es.sanitize()
	return ethSigMarshalAmino(es)
}

// UnmarshalAmino defines a custom decoding scheme for a EthSignature.
func (es *EthSignature) UnmarshalAmino(raw [3]string) error {
	es.sanitize()
	return ethSigUnmarshalAmino(es, raw)
}

// NewTransaction mimics ethereum's NewTransaction function. It returns a
// reference to a new Ethereum Transaction.
func NewTransaction(
	nonce uint64, to ethcmn.Address, amount sdk.Int,
	gasLimit uint64, gasPrice sdk.Int, payload []byte,
) Transaction {

	if len(payload) > 0 {
		payload = ethcmn.CopyBytes(payload)
	}

	txData := TxData{
		Recipient:    &to,
		AccountNonce: nonce,
		Payload:      payload,
		GasLimit:     gasLimit,
		Amount:       amount,
		Price:        gasPrice,
		Signature:    NewEthSignature(new(big.Int), new(big.Int), new(big.Int)),
	}

	return Transaction{Data: txData}
}

// Sign calculates a secp256k1 ECDSA signature and signs the transaction. It
// takes a private key and chainID to sign an Ethereum transaction according to
// EIP155 standard. It mutates the transaction as it populates the V, R, S
// fields of the Transaction's Signature.
func (tx *Transaction) Sign(chainID sdk.Int, priv *ecdsa.PrivateKey) {
	h := rlpHash([]interface{}{
		tx.Data.AccountNonce,
		tx.Data.Price.BigInt(),
		tx.Data.GasLimit,
		tx.Data.Recipient,
		tx.Data.Amount.BigInt(),
		tx.Data.Payload,
		chainID.BigInt(), uint(0), uint(0),
	})

	sig, err := ethcrypto.Sign(h[:], priv)
	if err != nil {
		panic(err)
	}

	if len(sig) != 65 {
		panic(fmt.Sprintf("wrong size for signature: got %d, want 65", len(sig)))
	}

	r := new(big.Int).SetBytes(sig[:32])
	s := new(big.Int).SetBytes(sig[32:64])

	var v *big.Int
	if chainID.Sign() == 0 {
		v = new(big.Int).SetBytes([]byte{sig[64] + 27})
	} else {
		v = big.NewInt(int64(sig[64] + 35))
		chainIDMul := new(big.Int).Mul(chainID.BigInt(), big.NewInt(2))
		v.Add(v, chainIDMul)
	}

	tx.Data.Signature.v = v
	tx.Data.Signature.r = r
	tx.Data.Signature.s = s
}

// Type implements the sdk.Msg interface. It returns the type of the
// Transaction.
func (tx Transaction) Type() string {
	return TypeTxEthereum
}

// ValidateBasic implements the sdk.Msg interface. It performs basic validation
// checks of a Transaction. If returns an sdk.Error if validation fails.
func (tx Transaction) ValidateBasic() sdk.Error {
	if tx.Data.Price.Sign() != 1 {
		return ErrInvalidValue(DefaultCodespace, "price must be positive")
	}

	if tx.Data.Amount.Sign() != 1 {
		return ErrInvalidValue(DefaultCodespace, "amount must be positive")
	}

	return nil
}

// GetSignBytes performs a no-op and should not be used. It implements the
// sdk.Msg Interface
func (tx Transaction) GetSignBytes() (sigBytes []byte) { return }

// GetSigners performs a no-op and should not be used. It implements the
// sdk.Msg Interface
//
// CONTRACT: The transaction must already be signed.
func (tx Transaction) GetSigners() (signers []sdk.AccAddress) { return }

// GetMsgs returns a single message containing the Transaction itself. It
// implements the Cosmos sdk.Tx interface.
func (tx Transaction) GetMsgs() []sdk.Msg {
	return []sdk.Msg{tx}
}

// ConvertTx attempts to converts a Transaction to a new Ethereum transaction
// with the signature set. The signature if first recovered and then a new
// Transaction is created with that signature. If setting the signature fails,
// a panic will be triggered.
func (tx Transaction) ConvertTx(chainID *big.Int) ethtypes.Transaction {
	gethTx := ethtypes.NewTransaction(
		tx.Data.AccountNonce, *tx.Data.Recipient, tx.Data.Amount.BigInt(),
		tx.Data.GasLimit, tx.Data.Price.BigInt(), tx.Data.Payload,
	)

	sig := recoverEthSig(tx.Data.Signature, chainID)
	signer := ethtypes.NewEIP155Signer(chainID)

	gethTx, err := gethTx.WithSignature(signer, sig)
	if err != nil {
		panic(errors.Wrap(err, "failed to convert transaction with a given signature"))
	}

	return *gethTx
}

// HasEmbeddedTx returns a boolean reflecting if the transaction contains an
// SDK transaction or not based on the recipient address.
func (tx Transaction) HasEmbeddedTx(addr ethcmn.Address) bool {
	return bytes.Equal(tx.Data.Recipient.Bytes(), addr.Bytes())
}

// GetEmbeddedTx returns the embedded SDK transaction from an Ethereum
// transaction. It returns an error if decoding the inner transaction fails.
//
// CONTRACT: The payload field of an Ethereum transaction must contain a valid
// encoded SDK transaction.
func (tx Transaction) GetEmbeddedTx(codec *wire.Codec) (EmbeddedTx, sdk.Error) {
	etx := EmbeddedTx{}

	err := codec.UnmarshalBinary(tx.Data.Payload, &etx)
	if err != nil {
		return EmbeddedTx{}, sdk.ErrTxDecode("failed to encode embedded tx")
	}

	return etx, nil
}

// ----------------------------------------------------------------------------
// embedded SDK transaction
// ----------------------------------------------------------------------------

type (
	// EmbeddedTx implements an SDK transaction. It is to be encoded into the
	// payload field of an Ethereum transaction in order to route and handle SDK
	// transactions.
	EmbeddedTx struct {
		Messages   []sdk.Msg   `json:"messages"`
		Fee        auth.StdFee `json:"fee"`
		Signatures [][]byte    `json:"signatures"`
	}

	// embeddedSignDoc implements a simple SignDoc for a EmbeddedTx signer to
	// sign over.
	embeddedSignDoc struct {
		ChainID       string            `json:"chainID"`
		AccountNumber int64             `json:"accountNumber"`
		Sequence      int64             `json:"sequence"`
		Messages      []json.RawMessage `json:"messages"`
		Fee           json.RawMessage   `json:"fee"`
	}

	// EmbeddedTxSign implements a structure for containing the information
	// necessary for building and signing an EmbeddedTx.
	EmbeddedTxSign struct {
		ChainID       string
		AccountNumber int64
		Sequence      int64
		Messages      []sdk.Msg
		Fee           auth.StdFee
	}
)

// GetMsgs implements the sdk.Tx interface. It returns all the SDK transaction
// messages.
func (etx EmbeddedTx) GetMsgs() []sdk.Msg {
	return etx.Messages
}

// GetRequiredSigners returns all the required signers of an SDK transaction
// accumulated from messages. It returns them in a deterministic fashion given
// a list of messages.
func (etx EmbeddedTx) GetRequiredSigners() []sdk.AccAddress {
	seen := map[string]bool{}

	var signers []sdk.AccAddress
	for _, msg := range etx.GetMsgs() {
		for _, addr := range msg.GetSigners() {
			if !seen[addr.String()] {
				signers = append(signers, sdk.AccAddress(addr))
				seen[addr.String()] = true
			}
		}
	}

	return signers
}

// Bytes returns the EmbeddedTxSign signature bytes for a signer to sign over.
func (ets EmbeddedTxSign) Bytes() ([]byte, error) {
	sigBytes, err := EmbeddedSignBytes(ets.ChainID, ets.AccountNumber, ets.Sequence, ets.Messages, ets.Fee)
	if err != nil {
		return nil, err
	}

	hash := sha256.Sum256(sigBytes)
	return hash[:], nil
}

// EmbeddedSignBytes creates signature bytes for a signer to sign an embedded
// transaction. The signature bytes require a chainID and an account number.
// The signature bytes are JSON encoded.
func EmbeddedSignBytes(chainID string, accnum, sequence int64, msgs []sdk.Msg, fee auth.StdFee) ([]byte, error) {
	var msgsBytes []json.RawMessage
	for _, msg := range msgs {
		msgsBytes = append(msgsBytes, json.RawMessage(msg.GetSignBytes()))
	}

	signDoc := embeddedSignDoc{
		ChainID:       chainID,
		AccountNumber: accnum,
		Sequence:      sequence,
		Messages:      msgsBytes,
		Fee:           json.RawMessage(fee.Bytes()),
	}

	bz, err := typesCodec.MarshalJSON(signDoc)
	if err != nil {
		errors.Wrap(err, "failed to JSON encode EmbeddedSignDoc")
	}

	return bz, nil
}

// ----------------------------------------------------------------------------
// Utilities
// ----------------------------------------------------------------------------

// TxDecoder returns an sdk.TxDecoder that given raw transaction bytes,
// attempts to decode them into a Transaction or an EmbeddedTx or returning an
// error if decoding fails.
func TxDecoder(codec *wire.Codec, sdkAddress ethcmn.Address) sdk.TxDecoder {
	return func(txBytes []byte) (sdk.Tx, sdk.Error) {
		var tx = Transaction{}

		if len(txBytes) == 0 {
			return nil, sdk.ErrTxDecode("txBytes are empty")
		}

		// The given codec should have all the appropriate message types
		// registered.
		err := codec.UnmarshalBinary(txBytes, &tx)
		if err != nil {
			return nil, sdk.ErrTxDecode("failed to decode tx").TraceSDK(err.Error())
		}

		// If the transaction is routed as an SDK transaction, decode and
		// return the embedded transaction.
		if tx.HasEmbeddedTx(sdkAddress) {
			etx, err := tx.GetEmbeddedTx(codec)
			if err != nil {
				return nil, err
			}

			return etx, nil
		}

		return tx, nil
	}
}

// recoverEthSig recovers a signature according to the Ethereum specification.
func recoverEthSig(es *EthSignature, chainID *big.Int) []byte {
	var v byte

	r, s := es.r.Bytes(), es.s.Bytes()
	sig := make([]byte, 65)

	copy(sig[32-len(r):32], r)
	copy(sig[64-len(s):64], s)

	if chainID.Sign() == 0 {
		v = byte(es.v.Uint64() - 27)
	} else {
		chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2))
		V := new(big.Int).Sub(es.v, chainIDMul)

		v = byte(V.Uint64() - 35)
	}

	sig[64] = v
	return sig
}

func rlpHash(x interface{}) (h ethcmn.Hash) {
	hasher := ethsha.NewKeccak256()

	rlp.Encode(hasher, x)
	hasher.Sum(h[:0])

	return h
}

func ethSigMarshalAmino(es EthSignature) (raw [3]string, err error) {
	vb, err := es.v.MarshalText()
	if err != nil {
		return raw, err
	}
	rb, err := es.r.MarshalText()
	if err != nil {
		return raw, err
	}
	sb, err := es.s.MarshalText()
	if err != nil {
		return raw, err
	}

	raw[0], raw[1], raw[2] = string(vb), string(rb), string(sb)
	return raw, err
}

func ethSigUnmarshalAmino(es *EthSignature, raw [3]string) (err error) {
	if err = es.v.UnmarshalText([]byte(raw[0])); err != nil {
		return
	}
	if err = es.r.UnmarshalText([]byte(raw[1])); err != nil {
		return
	}
	if err = es.s.UnmarshalText([]byte(raw[2])); err != nil {
		return
	}

	return
}