package eth

import (
	"blockbook/bchain"
	"encoding/hex"
	"math/big"
	"strconv"

	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/golang/protobuf/proto"
	"github.com/juju/errors"
)

// EthereumTypeAddressDescriptorLen - in case of EthereumType, the AddressDescriptor has fixed length
const EthereumTypeAddressDescriptorLen = 20

// EtherAmountDecimalPoint defines number of decimal points in Ether amounts
const EtherAmountDecimalPoint = 18

// EthereumParser handle
type EthereumParser struct {
	*bchain.BaseParser
}

// NewEthereumParser returns new EthereumParser instance
func NewEthereumParser(b int) *EthereumParser {
	return &EthereumParser{&bchain.BaseParser{
		BlockAddressesToKeep: b,
		AmountDecimalPoint:   EtherAmountDecimalPoint,
	}}
}

type rpcHeader struct {
	Hash       string `json:"hash"`
	ParentHash string `json:"parentHash"`
	Difficulty string `json:"difficulty"`
	Number     string `json:"number"`
	Time       string `json:"timestamp"`
	Size       string `json:"size"`
	Nonce      string `json:"nonce"`
}

type rpcTransaction struct {
	AccountNonce     string `json:"nonce"`
	GasPrice         string `json:"gasPrice"`
	GasLimit         string `json:"gas"`
	To               string `json:"to"` // nil means contract creation
	Value            string `json:"value"`
	Payload          string `json:"input"`
	Hash             string `json:"hash"`
	BlockNumber      string `json:"blockNumber"`
	BlockHash        string `json:"blockHash,omitempty"`
	From             string `json:"from"`
	TransactionIndex string `json:"transactionIndex"`
	// Signature values - ignored
	// V string `json:"v"`
	// R string `json:"r"`
	// S string `json:"s"`
}

type rpcLog struct {
	Address string   `json:"address"`
	Topics  []string `json:"topics"`
	Data    string   `json:"data"`
}

type rpcLogWithTxHash struct {
	rpcLog
	Hash string `json:"transactionHash"`
}

type rpcReceipt struct {
	GasUsed string    `json:"gasUsed"`
	Status  string    `json:"status"`
	Logs    []*rpcLog `json:"logs"`
}

type rpcEtcReceipt struct {
	GasUsed string    `json:"gasUsed"`
	Status  int       `json:"status"`
	Logs    []*rpcLog `json:"logs"`
}

type completeTransaction struct {
	Tx      *rpcTransaction `json:"tx"`
	Receipt *rpcReceipt     `json:"receipt,omitempty"`
}

type rpcBlockTransactions struct {
	Transactions []rpcTransaction `json:"transactions"`
}

type rpcBlockTxids struct {
	Transactions []string `json:"transactions"`
}

func ethNumber(n string) (int64, error) {
	if len(n) > 2 {
		return strconv.ParseInt(n[2:], 16, 64)
	}
	return 0, errors.Errorf("Not a number: '%v'", n)
}

func (p *EthereumParser) ethTxToTx(tx *rpcTransaction, receipt *rpcReceipt, blocktime int64, confirmations uint32) (*bchain.Tx, error) {
	txid := tx.Hash
	var (
		fa, ta []string
		err    error
	)
	if len(tx.From) > 2 {
		fa = []string{tx.From}
	}
	if len(tx.To) > 2 {
		ta = []string{tx.To}
	}
	ct := completeTransaction{
		Tx:      tx,
		Receipt: receipt,
	}
	vs, err := hexutil.DecodeBig(tx.Value)
	if err != nil {
		return nil, err
	}
	return &bchain.Tx{
		Blocktime:     blocktime,
		Confirmations: confirmations,
		// Hex
		// LockTime
		Time: blocktime,
		Txid: txid,
		Vin: []bchain.Vin{
			{
				Addresses: fa,
				// Coinbase
				// ScriptSig
				// Sequence
				// Txid
				// Vout
			},
		},
		Vout: []bchain.Vout{
			{
				N:        0, // there is always up to one To address
				ValueSat: *vs,
				ScriptPubKey: bchain.ScriptPubKey{
					// Hex
					Addresses: ta,
				},
			},
		},
		CoinSpecificData: ct,
	}, nil
}

// GetAddrDescFromVout returns internal address representation of given transaction output
func (p *EthereumParser) GetAddrDescFromVout(output *bchain.Vout) (bchain.AddressDescriptor, error) {
	if len(output.ScriptPubKey.Addresses) != 1 {
		return nil, bchain.ErrAddressMissing
	}
	return p.GetAddrDescFromAddress(output.ScriptPubKey.Addresses[0])
}

func has0xPrefix(s string) bool {
	return len(s) >= 2 && s[0] == '0' && (s[1]|32) == 'x'
}

// GetAddrDescFromAddress returns internal address representation of given address
func (p *EthereumParser) GetAddrDescFromAddress(address string) (bchain.AddressDescriptor, error) {
	// github.com/ethereum/go-ethereum/common.HexToAddress does not handle address errors, using own decoding
	if has0xPrefix(address) {
		address = address[2:]
	}
	if len(address) != EthereumTypeAddressDescriptorLen*2 {
		return nil, bchain.ErrAddressMissing
	}
	return hex.DecodeString(address)
}

// GetAddressesFromAddrDesc returns addresses for given address descriptor with flag if the addresses are searchable
func (p *EthereumParser) GetAddressesFromAddrDesc(addrDesc bchain.AddressDescriptor) ([]string, bool, error) {
	return []string{hexutil.Encode(addrDesc)}, true, nil
}

// GetScriptFromAddrDesc returns output script for given address descriptor
func (p *EthereumParser) GetScriptFromAddrDesc(addrDesc bchain.AddressDescriptor) ([]byte, error) {
	return addrDesc, nil
}

func hexDecode(s string) ([]byte, error) {
	b, err := hexutil.Decode(s)
	if err != nil && err != hexutil.ErrEmptyString {
		return nil, err
	}
	return b, nil
}

func hexDecodeBig(s string) ([]byte, error) {
	b, err := hexutil.DecodeBig(s)
	if err != nil {
		return nil, err
	}
	return b.Bytes(), nil
}

func hexEncodeBig(b []byte) string {
	var i big.Int
	i.SetBytes(b)
	return hexutil.EncodeBig(&i)
}

// PackTx packs transaction to byte array
func (p *EthereumParser) PackTx(tx *bchain.Tx, height uint32, blockTime int64) ([]byte, error) {
	var err error
	var n uint64
	r, ok := tx.CoinSpecificData.(completeTransaction)
	if !ok {
		return nil, errors.New("Missing CoinSpecificData")
	}
	pt := &ProtoCompleteTransaction{}
	pt.Tx = &ProtoCompleteTransaction_TxType{}
	if pt.Tx.AccountNonce, err = hexutil.DecodeUint64(r.Tx.AccountNonce); err != nil {
		return nil, errors.Annotatef(err, "AccountNonce %v", r.Tx.AccountNonce)
	}
	// pt.BlockNumber = height
	if n, err = hexutil.DecodeUint64(r.Tx.BlockNumber); err != nil {
		return nil, errors.Annotatef(err, "BlockNumber %v", r.Tx.BlockNumber)
	}
	pt.BlockNumber = uint32(n)
	pt.BlockTime = uint64(blockTime)
	if pt.Tx.From, err = hexDecode(r.Tx.From); err != nil {
		return nil, errors.Annotatef(err, "From %v", r.Tx.From)
	}
	if pt.Tx.GasLimit, err = hexutil.DecodeUint64(r.Tx.GasLimit); err != nil {
		return nil, errors.Annotatef(err, "GasLimit %v", r.Tx.GasLimit)
	}
	if pt.Tx.Hash, err = hexDecode(r.Tx.Hash); err != nil {
		return nil, errors.Annotatef(err, "Hash %v", r.Tx.Hash)
	}
	if pt.Tx.Payload, err = hexDecode(r.Tx.Payload); err != nil {
		return nil, errors.Annotatef(err, "Payload %v", r.Tx.Payload)
	}
	if pt.Tx.GasPrice, err = hexDecodeBig(r.Tx.GasPrice); err != nil {
		return nil, errors.Annotatef(err, "Price %v", r.Tx.GasPrice)
	}
	// if pt.R, err = hexDecodeBig(r.R); err != nil {
	// 	return nil, errors.Annotatef(err, "R %v", r.R)
	// }
	// if pt.S, err = hexDecodeBig(r.S); err != nil {
	// 	return nil, errors.Annotatef(err, "S %v", r.S)
	// }
	// if pt.V, err = hexDecodeBig(r.V); err != nil {
	// 	return nil, errors.Annotatef(err, "V %v", r.V)
	// }
	if pt.Tx.To, err = hexDecode(r.Tx.To); err != nil {
		return nil, errors.Annotatef(err, "To %v", r.Tx.To)
	}
	if n, err = hexutil.DecodeUint64(r.Tx.TransactionIndex); err != nil {
		return nil, errors.Annotatef(err, "TransactionIndex %v", r.Tx.TransactionIndex)
	}
	pt.Tx.TransactionIndex = uint32(n)
	if pt.Tx.Value, err = hexDecodeBig(r.Tx.Value); err != nil {
		return nil, errors.Annotatef(err, "Value %v", r.Tx.Value)
	}
	if r.Receipt != nil {
		pt.Receipt = &ProtoCompleteTransaction_ReceiptType{}
		if pt.Receipt.GasUsed, err = hexDecodeBig(r.Receipt.GasUsed); err != nil {
			return nil, errors.Annotatef(err, "GasUsed %v", r.Receipt.GasUsed)
		}
		if pt.Receipt.Status, err = hexDecodeBig(r.Receipt.Status); err != nil {
			return nil, errors.Annotatef(err, "Status %v", r.Receipt.Status)
		}
		ptLogs := make([]*ProtoCompleteTransaction_ReceiptType_LogType, len(r.Receipt.Logs))
		for i, l := range r.Receipt.Logs {
			a, err := hexutil.Decode(l.Address)
			if err != nil {
				return nil, errors.Annotatef(err, "Address cannot be decoded %v", l)
			}
			d, err := hexutil.Decode(l.Data)
			if err != nil {
				return nil, errors.Annotatef(err, "Data cannot be decoded %v", l)
			}
			t := make([][]byte, len(l.Topics))
			for j, s := range l.Topics {
				t[j], err = hexutil.Decode(s)
				if err != nil {
					return nil, errors.Annotatef(err, "Topic cannot be decoded %v", l)
				}
			}
			ptLogs[i] = &ProtoCompleteTransaction_ReceiptType_LogType{
				Address: a,
				Data:    d,
				Topics:  t,
			}

		}
		pt.Receipt.Log = ptLogs
	}
	return proto.Marshal(pt)
}

// UnpackTx unpacks transaction from byte array
func (p *EthereumParser) UnpackTx(buf []byte) (*bchain.Tx, uint32, error) {
	var pt ProtoCompleteTransaction
	err := proto.Unmarshal(buf, &pt)
	if err != nil {
		return nil, 0, err
	}
	rt := rpcTransaction{
		AccountNonce: hexutil.EncodeUint64(pt.Tx.AccountNonce),
		BlockNumber:  hexutil.EncodeUint64(uint64(pt.BlockNumber)),
		From:         hexutil.Encode(pt.Tx.From),
		GasLimit:     hexutil.EncodeUint64(pt.Tx.GasLimit),
		Hash:         hexutil.Encode(pt.Tx.Hash),
		Payload:      hexutil.Encode(pt.Tx.Payload),
		GasPrice:     hexEncodeBig(pt.Tx.GasPrice),
		// R:                hexEncodeBig(pt.R),
		// S:                hexEncodeBig(pt.S),
		// V:                hexEncodeBig(pt.V),
		To:               hexutil.Encode(pt.Tx.To),
		TransactionIndex: hexutil.EncodeUint64(uint64(pt.Tx.TransactionIndex)),
		Value:            hexEncodeBig(pt.Tx.Value),
	}
	var rr *rpcReceipt
	if pt.Receipt != nil {
		logs := make([]*rpcLog, len(pt.Receipt.Log))
		for i, l := range pt.Receipt.Log {
			topics := make([]string, len(l.Topics))
			for j, t := range l.Topics {
				topics[j] = hexutil.Encode(t)
			}
			logs[i] = &rpcLog{
				Address: hexutil.Encode(l.Address),
				Data:    hexutil.Encode(l.Data),
				Topics:  topics,
			}
		}
		rr = &rpcReceipt{
			GasUsed: hexEncodeBig(pt.Receipt.GasUsed),
			Status:  hexEncodeBig(pt.Receipt.Status),
			Logs:    logs,
		}
	}
	tx, err := p.ethTxToTx(&rt, rr, int64(pt.BlockTime), 0)
	if err != nil {
		return nil, 0, err
	}
	return tx, pt.BlockNumber, nil
}

// PackedTxidLen returns length in bytes of packed txid
func (p *EthereumParser) PackedTxidLen() int {
	return 32
}

// PackTxid packs txid to byte array
func (p *EthereumParser) PackTxid(txid string) ([]byte, error) {
	if has0xPrefix(txid) {
		txid = txid[2:]
	}
	return hex.DecodeString(txid)
}

// UnpackTxid unpacks byte array to txid
func (p *EthereumParser) UnpackTxid(buf []byte) (string, error) {
	return hexutil.Encode(buf), nil
}

// PackBlockHash packs block hash to byte array
func (p *EthereumParser) PackBlockHash(hash string) ([]byte, error) {
	if has0xPrefix(hash) {
		hash = hash[2:]
	}
	return hex.DecodeString(hash)
}

// UnpackBlockHash unpacks byte array to block hash
func (p *EthereumParser) UnpackBlockHash(buf []byte) (string, error) {
	return hexutil.Encode(buf), nil
}

// GetChainType returns EthereumType
func (p *EthereumParser) GetChainType() bchain.ChainType {
	return bchain.ChainEthereumType
}

// GetHeightFromTx returns ethereum specific data from bchain.Tx
func GetHeightFromTx(tx *bchain.Tx) (uint32, error) {
	var bn string
	csd, ok := tx.CoinSpecificData.(completeTransaction)
	if !ok {
		return 0, errors.New("Missing CoinSpecificData")
	}
	bn = csd.Tx.BlockNumber
	n, err := hexutil.DecodeUint64(bn)
	if err != nil {
		return 0, errors.Annotatef(err, "BlockNumber %v", bn)
	}
	return uint32(n), nil
}

// EthereumTypeGetErc20FromTx returns Erc20 data from bchain.Tx
func (p *EthereumParser) EthereumTypeGetErc20FromTx(tx *bchain.Tx) ([]bchain.Erc20Transfer, error) {
	var r []bchain.Erc20Transfer
	var err error
	csd, ok := tx.CoinSpecificData.(completeTransaction)
	if ok {
		if csd.Receipt != nil {
			r, err = erc20GetTransfersFromLog(csd.Receipt.Logs)
		} else {
			r, err = erc20GetTransfersFromTx(csd.Tx)
		}
		if err != nil {
			return nil, err
		}
	}
	return r, nil
}

const (
	txStatusUnknown = iota - 2
	txStatusPending
	txStatusFailure
	txStatusOK
)

// EthereumTxData contains ethereum specific transaction data
type EthereumTxData struct {
	Status   int      `json:"status"` // 1 OK, 0 Fail, -1 pending, -2 unknown
	Nonce    uint64   `json:"nonce"`
	GasLimit *big.Int `json:"gaslimit"`
	GasUsed  *big.Int `json:"gasused"`
	GasPrice *big.Int `json:"gasprice"`
}

// GetEthereumTxData returns EthereumTxData from bchain.Tx
func GetEthereumTxData(tx *bchain.Tx) *EthereumTxData {
	etd := EthereumTxData{Status: txStatusPending}
	csd, ok := tx.CoinSpecificData.(completeTransaction)
	if ok {
		if csd.Tx != nil {
			etd.Nonce, _ = hexutil.DecodeUint64(csd.Tx.AccountNonce)
			etd.GasLimit, _ = hexutil.DecodeBig(csd.Tx.GasLimit)
			etd.GasPrice, _ = hexutil.DecodeBig(csd.Tx.GasPrice)
		}
		if csd.Receipt != nil {
			switch csd.Receipt.Status {
			case "0x1":
				etd.Status = txStatusOK
			case "": // old transactions did not set status
				etd.Status = txStatusUnknown
			default:
				etd.Status = txStatusFailure
			}
			etd.GasUsed, _ = hexutil.DecodeBig(csd.Receipt.GasUsed)
		}
	}
	return &etd
}
