// VulcanizeDB
// Copyright © 2019 Vulcanize

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.

// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package eth

import (
	"errors"
	"fmt"
	"math/big"
	"strconv"

	"github.com/cerc-io/ipld-eth-server/v5/pkg/log"
	"github.com/cerc-io/plugeth-statediff/indexer/models"
	sdtypes "github.com/cerc-io/plugeth-statediff/types"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/ethereum/go-ethereum/common/math"
	"github.com/ethereum/go-ethereum/core"
	"github.com/ethereum/go-ethereum/core/types"
)

// RPCTransaction represents a transaction that will serialize to the RPC representation of a transaction
// Note: copied from go-ethereum/internal/ethapi
type RPCTransaction struct {
	BlockHash           *common.Hash      `json:"blockHash"`
	BlockNumber         *hexutil.Big      `json:"blockNumber"`
	From                common.Address    `json:"from"`
	Gas                 hexutil.Uint64    `json:"gas"`
	GasPrice            *hexutil.Big      `json:"gasPrice"`
	GasFeeCap           *hexutil.Big      `json:"maxFeePerGas,omitempty"`
	GasTipCap           *hexutil.Big      `json:"maxPriorityFeePerGas,omitempty"`
	MaxFeePerBlobGas    *hexutil.Big      `json:"maxFeePerBlobGas,omitempty"`
	Hash                common.Hash       `json:"hash"`
	Input               hexutil.Bytes     `json:"input"`
	Nonce               hexutil.Uint64    `json:"nonce"`
	To                  *common.Address   `json:"to"`
	TransactionIndex    *hexutil.Uint64   `json:"transactionIndex"`
	Value               *hexutil.Big      `json:"value"`
	Type                hexutil.Uint64    `json:"type"`
	Accesses            *types.AccessList `json:"accessList,omitempty"`
	ChainID             *hexutil.Big      `json:"chainId,omitempty"`
	BlobVersionedHashes []common.Hash     `json:"blobVersionedHashes,omitempty"`
	V                   *hexutil.Big      `json:"v"`
	R                   *hexutil.Big      `json:"r"`
	S                   *hexutil.Big      `json:"s"`
	YParity             *hexutil.Uint64   `json:"yParity,omitempty"`
}

// RPCReceipt represents a receipt that will serialize to the RPC representation of a receipt
type RPCReceipt struct {
	BlockHash        *common.Hash    `json:"blockHash"`
	BlockNumber      *hexutil.Big    `json:"blockNumber"`
	TransactionHash  *common.Hash    `json:"transactionHash"`
	TransactionIndex *hexutil.Uint64 `json:"transactionIndex"`
	From             common.Address  `json:"from"`
	To               *common.Address `json:"to"`
	GasUsed          hexutil.Uint64  `json:"gasUsed"`
	CumulativeGsUsed hexutil.Uint64  `json:"cumulativeGasUsed"`
	ContractAddress  *common.Address `json:"contractAddress"`
	Logs             []*types.Log    `json:"logs"`
	Bloom            types.Bloom     `json:"logsBloom"`
	Root             []byte          `json:"root"`
	Status           uint64          `json:"status"`
}

// AccountResult struct for GetProof
type AccountResult struct {
	Address      common.Address  `json:"address"`
	AccountProof []string        `json:"accountProof"`
	Balance      *hexutil.Big    `json:"balance"`
	CodeHash     common.Hash     `json:"codeHash"`
	Nonce        hexutil.Uint64  `json:"nonce"`
	StorageHash  common.Hash     `json:"storageHash"`
	StorageProof []StorageResult `json:"storageProof"`
}

// StorageResult for GetProof
type StorageResult struct {
	Key   string       `json:"key"`
	Value *hexutil.Big `json:"value"`
	Proof []string     `json:"proof"`
}

// CallArgs represents the arguments for a call.
type CallArgs struct {
	From                 *common.Address   `json:"from"`
	To                   *common.Address   `json:"to"`
	Gas                  *hexutil.Uint64   `json:"gas"`
	GasPrice             *hexutil.Big      `json:"gasPrice"`
	MaxFeePerGas         *hexutil.Big      `json:"maxFeePerGas"`
	MaxPriorityFeePerGas *hexutil.Big      `json:"maxPriorityFeePerGas"`
	Value                *hexutil.Big      `json:"value"`
	Data                 *hexutil.Bytes    `json:"data"`
	AccessList           *types.AccessList `json:"accessList,omitempty"`
	Input                *hexutil.Bytes    `json:"input"`
}

// from retrieves the transaction sender address.
func (arg *CallArgs) from() common.Address {
	if arg.From == nil {
		return common.Address{}
	}
	return *arg.From
}

// data retrieves the transaction calldata. Input field is preferred.
func (arg *CallArgs) data() []byte {
	if arg.Input != nil {
		return *arg.Input
	}
	if arg.Data != nil {
		return *arg.Data
	}
	return nil
}

// ToMessage converts the transaction arguments to the Message type used by the
// core evm. This method is used in calls and traces that do not require a real
// live transaction.
func (arg *CallArgs) ToMessage(globalGasCap uint64, baseFee *big.Int) (*core.Message, error) {
	// Reject invalid combinations of pre- and post-1559 fee styles
	if arg.GasPrice != nil && (arg.MaxFeePerGas != nil || arg.MaxPriorityFeePerGas != nil) {
		return nil, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified")
	}
	// Set sender address or use zero address if none specified.
	addr := arg.from()

	// Set default gas & gas price if none were set
	gas := globalGasCap
	if gas == 0 {
		gas = uint64(math.MaxUint64 / 2)
	}
	if arg.Gas != nil {
		gas = uint64(*arg.Gas)
	}
	if globalGasCap != 0 && globalGasCap < gas {
		log.Warn("Caller gas above allowance, capping", "requested", gas, "cap", globalGasCap)
		gas = globalGasCap
	}
	var (
		gasPrice  *big.Int
		gasFeeCap *big.Int
		gasTipCap *big.Int
	)
	if baseFee == nil {
		// If there's no basefee, then it must be a non-1559 execution
		gasPrice = new(big.Int)
		if arg.GasPrice != nil {
			gasPrice = arg.GasPrice.ToInt()
		}
		gasFeeCap, gasTipCap = gasPrice, gasPrice
	} else {
		// A basefee is provided, necessitating 1559-type execution
		if arg.GasPrice != nil {
			// User specified the legacy gas field, convert to 1559 gas typing
			gasPrice = arg.GasPrice.ToInt()
			gasFeeCap, gasTipCap = gasPrice, gasPrice
		} else {
			// User specified 1559 gas feilds (or none), use those
			gasFeeCap = new(big.Int)
			if arg.MaxFeePerGas != nil {
				gasFeeCap = arg.MaxFeePerGas.ToInt()
			}
			gasTipCap = new(big.Int)
			if arg.MaxPriorityFeePerGas != nil {
				gasTipCap = arg.MaxPriorityFeePerGas.ToInt()
			}
			// Backfill the legacy gasPrice for EVM execution, unless we're all zeroes
			gasPrice = new(big.Int)
			if gasFeeCap.BitLen() > 0 || gasTipCap.BitLen() > 0 {
				gasPrice = math.BigMin(new(big.Int).Add(gasTipCap, baseFee), gasFeeCap)
			}
		}
	}
	value := new(big.Int)
	if arg.Value != nil {
		value = arg.Value.ToInt()
	}
	data := arg.data()
	var accessList types.AccessList
	if arg.AccessList != nil {
		accessList = *arg.AccessList
	}
	msg := &core.Message{
		Nonce:             0,
		GasLimit:          gas,
		GasPrice:          gasPrice,
		GasFeeCap:         gasFeeCap,
		GasTipCap:         gasTipCap,
		To:                arg.To,
		Value:             value,
		Data:              data,
		AccessList:        accessList,
		SkipAccountChecks: true,
		From:              addr,
	}
	return msg, nil
}

// ConvertedPayload is a custom type which packages raw ETH data for publishing to IPFS and filtering to subscribers
// Returned by PayloadConverter
// Passed to IPLDPublisher and ResponseFilterer
type ConvertedPayload struct {
	TotalDifficulty *big.Int
	Block           *types.Block
	TxMetaData      []models.TxModel
	Receipts        types.Receipts
	ReceiptMetaData []models.ReceiptModel
	StateNodes      []sdtypes.StateLeafNode
	StorageNodes    map[string][]sdtypes.StorageLeafNode
}

// LogResult represent a log.
type LogResult struct {
	LeafCID     string `db:"cid"`
	ReceiptID   string `db:"rct_id"`
	Address     string `db:"address"`
	Index       int64  `db:"index"`
	Data        []byte `db:"log_data"`
	Topic0      string `db:"topic0"`
	Topic1      string `db:"topic1"`
	Topic2      string `db:"topic2"`
	Topic3      string `db:"topic3"`
	LogLeafData []byte `db:"data"`
	RctCID      string `db:"rct_cid"`
	RctStatus   uint64 `db:"post_status"`
	BlockNumber string `db:"block_number"`
	BlockHash   string `db:"block_hash"`
	TxnIndex    int64  `db:"txn_index"`
	TxHash      string `db:"tx_hash"`
}

// GetSliceResponse holds response for the eth_getSlice method
type GetSliceResponse struct {
	SliceID   string                             `json:"sliceId"`
	MetaData  GetSliceResponseMetadata           `json:"metadata"`
	TrieNodes GetSliceResponseTrieNodes          `json:"trieNodes"`
	Leaves    map[string]GetSliceResponseAccount `json:"leaves"` // key: Keccak256Hash(address) in hex (leafKey)
}

func (sr *GetSliceResponse) init(path string, depth int, root common.Hash) {
	sr.SliceID = fmt.Sprintf("%s-%d-%s", path, depth, root.String())
	sr.MetaData = GetSliceResponseMetadata{
		NodeStats: make(map[string]string, 0),
		TimeStats: make(map[string]string, 0),
	}
	sr.Leaves = make(map[string]GetSliceResponseAccount)
	sr.TrieNodes = GetSliceResponseTrieNodes{
		Stem:  make(map[string]string),
		Head:  make(map[string]string),
		Slice: make(map[string]string),
	}
}

func (sr *GetSliceResponse) populateMetaData(metaData metaDataFields) {
	sr.MetaData.NodeStats["00-stem-and-head-nodes"] = strconv.Itoa(len(sr.TrieNodes.Stem) + len(sr.TrieNodes.Head))
	sr.MetaData.NodeStats["01-max-depth"] = strconv.Itoa(metaData.maxDepth)
	sr.MetaData.NodeStats["02-total-trie-nodes"] = strconv.Itoa(len(sr.TrieNodes.Stem) + len(sr.TrieNodes.Head) + len(sr.TrieNodes.Slice))
	sr.MetaData.NodeStats["03-leaves"] = strconv.Itoa(metaData.leafCount)
	sr.MetaData.NodeStats["04-smart-contracts"] = strconv.Itoa(len(sr.Leaves))

	sr.MetaData.TimeStats["00-trie-loading"] = strconv.FormatInt(metaData.trieLoadingTime, 10)
	sr.MetaData.TimeStats["01-fetch-stem-keys"] = strconv.FormatInt(metaData.stemNodesFetchTime, 10)
	sr.MetaData.TimeStats["02-fetch-slice-keys"] = strconv.FormatInt(metaData.sliceNodesFetchTime, 10)
	sr.MetaData.TimeStats["03-fetch-leaves-info"] = strconv.FormatInt(metaData.leavesFetchTime, 10)
}

type GetSliceResponseMetadata struct {
	TimeStats map[string]string `json:"timeStats"` // stem, state, storage (one by one)
	NodeStats map[string]string `json:"nodeStats"` // total, leaves, smart contracts
}

type GetSliceResponseTrieNodes struct {
	Stem  map[string]string `json:"stem"` // key: Keccak256Hash(data) in hex, value: trie node data in hex
	Head  map[string]string `json:"head"`
	Slice map[string]string `json:"sliceNodes"`
}

type GetSliceResponseAccount struct {
	StorageRoot string `json:"storageRoot"`
	EVMCode     string `json:"evmCode"`
}

type metaDataFields struct {
	maxDepth            int
	leafCount           int
	trieLoadingTime     int64
	stemNodesFetchTime  int64
	sliceNodesFetchTime int64
	leavesFetchTime     int64
}