package ethapi

import (
	"context"
	"errors"
	"fmt"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/ethereum/go-ethereum/consensus"
	"github.com/ethereum/go-ethereum/core"
	"github.com/ethereum/go-ethereum/core/state"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/core/vm"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/log"
	"github.com/ethereum/go-ethereum/rpc"
	"math/big"
	"time"
)

// PublicBlockChainAPI provides an API to access the Ethereum blockchain.
// It offers only methods that operate on public data that is freely available to anyone.
type PublicBlockChainAPI struct {
	b Backend
}

// NewPublicBlockChainAPI creates a new Ethereum blockchain API.
func NewPublicBlockChainAPI(b Backend) *PublicBlockChainAPI {
	return &PublicBlockChainAPI{b}
}

// Call executes the given transaction on the state for the given block number.
//
// Additionally, the caller can specify a batch of contract for fields overriding.
//
// Note, this function doesn't make and changes in the state/blockchain and is
// useful to execute and retrieve values.
func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride) (hexutil.Bytes, error) {
	result, err := DoCall(ctx, s.b, args, blockNrOrHash, overrides, vm.Config{}, 5*time.Second, s.b.RPCGasCap())
	if err != nil {
		return nil, err
	}
	// If the result contains a revert reason, try to unpack and return it.
	if len(result.Revert()) > 0 {
		return nil, newRevertError(result)
	}
	return result.Return(), result.Err
}

func (s *PublicBlockChainAPI) NewCall(ctx context.Context, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride) (CallResult, error) {
	result, err := DoCall(ctx, s.b, args, blockNrOrHash, overrides, vm.Config{}, 5*time.Second, s.b.RPCGasCap())
	if err != nil {
		return CallResult{Success: false, Msg: err.Error()}, nil
	} else {
		if len(result.Revert()) > 0 {
			revertErr := newRevertError(result)
			return CallResult{Success: false, Return: result.Return(), Msg: revertErr.Error()}, nil
		}

		return CallResult{Success: true, Return: result.Return()}, nil
	}
}

// ChainId is the EIP-155 replay-protection chain id for the current ethereum chain config.
func (api *PublicBlockChainAPI) ChainId() (*hexutil.Big, error) {
	// if current block is at or past the EIP-155 replay-protection fork block, return chainID from config
	if config := api.b.ChainConfig(); config.IsEIP155(api.b.CurrentBlock().Number()) {
		return (*hexutil.Big)(config.ChainID), nil
	}
	return nil, fmt.Errorf("chain not synced beyond EIP-155 replay-protection fork block")
}

// BlockNumber returns the block number of the chain head.
func (s *PublicBlockChainAPI) BlockNumber() hexutil.Uint64 {
	header, _ := s.b.HeaderByNumber(context.Background(), rpc.LatestBlockNumber) // latest header should always be available
	return hexutil.Uint64(header.Number.Uint64())
}

// GetBalance returns the amount of wei for the given address in the state of the
// given block number. The rpc.LatestBlockNumber and rpc.PendingBlockNumber meta
// block numbers are also allowed.
func (s *PublicBlockChainAPI) GetBalance(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (*hexutil.Big, error) {
	state, _, err := s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
	if state == nil || err != nil {
		return nil, err
	}
	return (*hexutil.Big)(state.GetBalance(address)), state.Error()
}

// BatchCall executes a series of transactions on the state of a given block as base.
// The base state can be overridden once before transactions are executed.
//
// Additionally, each call can override block context fields such as number.
//
// Note, this function doesn't make any changes in the state/blockchain and is
// useful to execute and retrieve values.
func (s *PublicBlockChainAPI) BatchCall(ctx context.Context, config BatchCallConfig) ([]CallResult, error) {
	state, header, err := s.b.StateAndHeaderByNumberOrHash(ctx, config.Block)
	if state == nil || err != nil {
		return nil, err
	}
	// State overrides are applied once before all calls
	if err := config.StateOverrides.Apply(state); err != nil {
		return nil, err
	}

	// Setup context so it may be cancelled before the calls completed
	// or, in case of unmetered gas, setup a context with a timeout.
	var (
		cancel context.CancelFunc
		//timeout = s.b.RPCEVMTimeout()
		timeout = time.Duration(5000000000)
	)
	if timeout > 0 {
		ctx, cancel = context.WithTimeout(ctx, timeout)
	} else {
		ctx, cancel = context.WithCancel(ctx)
	}
	// Make sure the context is cancelled when the call has completed
	// this makes sure resources are cleaned up.
	defer cancel()
	var (
		results []CallResult
		// Each tx and all the series of txes shouldn't consume more gas than cap
		globalGasCap = s.b.RPCGasCap()
		gp           = new(core.GasPool).AddGas(globalGasCap)
	)
	for _, call := range config.Calls {
		blockContext := core.NewEVMBlockContext(header, NewChainContext(ctx, s.b), nil)
		if call.BlockOverrides != nil {
			call.BlockOverrides.Apply(&blockContext)
		}
		result, doCallErr := doCall(ctx, s.b, call.CallArgs, state, header, timeout, gp, &blockContext)
		if doCallErr != nil {
			results = append(results, CallResult{Success: false, Msg: doCallErr.Error()})
		} else {
			if len(result.Revert()) > 0 {
				revertErr := newRevertError(result)
				results = append(results, CallResult{Success: false, Return: result.Return(), Msg: revertErr.Error()})
			} else {
				results = append(results, CallResult{Success: true, Return: result.Return()})
			}
		}
	}
	return results, nil
}

// EstimateGas returns an estimate of the amount of gas needed to execute the
// given transaction against the current pending block.
func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args CallArgs, blockNrOrHash *rpc.BlockNumberOrHash) (hexutil.Uint64, error) {
	bNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber)
	if blockNrOrHash != nil {
		bNrOrHash = *blockNrOrHash
	}
	return DoEstimateGas(ctx, s.b, args, bNrOrHash, s.b.RPCGasCap())
}

// GetProof returns the Merkle-proof for a given account and optionally some storage keys.
func (s *PublicBlockChainAPI) GetProof(ctx context.Context, address common.Address, storageKeys []string, blockNrOrHash rpc.BlockNumberOrHash) (*AccountResult, error) {
	state, _, err := s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
	if state == nil || err != nil {
		return nil, err
	}

	storageTrie := state.StorageTrie(address)
	storageHash := types.EmptyRootHash
	codeHash := state.GetCodeHash(address)
	storageProof := make([]StorageResult, len(storageKeys))

	// if we have a storageTrie, (which means the account exists), we can update the storagehash
	if storageTrie != nil {
		storageHash = storageTrie.Hash()
	} else {
		// no storageTrie means the account does not exist, so the codeHash is the hash of an empty bytearray.
		codeHash = crypto.Keccak256Hash(nil)
	}

	// create the proof for the storageKeys
	for i, key := range storageKeys {
		if storageTrie != nil {
			proof, storageError := state.GetStorageProof(address, common.HexToHash(key))
			if storageError != nil {
				return nil, storageError
			}
			storageProof[i] = StorageResult{key, (*hexutil.Big)(state.GetState(address, common.HexToHash(key)).Big()), toHexSlice(proof)}
		} else {
			storageProof[i] = StorageResult{key, &hexutil.Big{}, []string{}}
		}
	}

	// create the accountProof
	accountProof, proofErr := state.GetProof(address)
	if proofErr != nil {
		return nil, proofErr
	}

	return &AccountResult{
		Address:      address,
		AccountProof: toHexSlice(accountProof),
		Balance:      (*hexutil.Big)(state.GetBalance(address)),
		CodeHash:     codeHash,
		Nonce:        hexutil.Uint64(state.GetNonce(address)),
		StorageHash:  storageHash,
		StorageProof: storageProof,
	}, state.Error()
}

// GetHeaderByNumber returns the requested canonical block header.
// * When blockNr is -1 the chain head is returned.
// * When blockNr is -2 the pending chain head is returned.
func (s *PublicBlockChainAPI) GetHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (map[string]interface{}, error) {
	header, err := s.b.HeaderByNumber(ctx, number)
	if header != nil && err == nil {
		response := s.rpcMarshalHeader(ctx, header)
		if number == rpc.PendingBlockNumber {
			// Pending header need to nil out a few fields
			for _, field := range []string{"hash", "nonce", "miner"} {
				response[field] = nil
			}
		}
		return response, err
	}
	return nil, err
}

// GetHeaderByHash returns the requested header by hash.
func (s *PublicBlockChainAPI) GetHeaderByHash(ctx context.Context, hash common.Hash) map[string]interface{} {
	header, _ := s.b.HeaderByHash(ctx, hash)
	if header != nil {
		return s.rpcMarshalHeader(ctx, header)
	}
	return nil
}

// GetBlockByNumber returns the requested canonical block.
//   - When blockNr is -1 the chain head is returned.
//   - When blockNr is -2 the pending chain head is returned.
//   - When fullTx is true all transactions in the block are returned, otherwise
//     only the transaction hash is returned.
func (s *PublicBlockChainAPI) GetBlockByNumber(ctx context.Context, number rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) {
	block, err := s.b.BlockByNumber(ctx, number)
	if block != nil && err == nil {
		response, err := s.rpcMarshalBlock(ctx, block, true, fullTx)
		if err == nil && number == rpc.PendingBlockNumber {
			// Pending blocks need to nil out a few fields
			for _, field := range []string{"hash", "nonce", "miner"} {
				response[field] = nil
			}
		}
		return response, err
	}
	return nil, err
}

// GetBlockByHash returns the requested block. When fullTx is true all transactions in the block are returned in full
// detail, otherwise only the transaction hash is returned.
func (s *PublicBlockChainAPI) GetBlockByHash(ctx context.Context, hash common.Hash, fullTx bool) (map[string]interface{}, error) {
	block, err := s.b.BlockByHash(ctx, hash)
	if block != nil {
		return s.rpcMarshalBlock(ctx, block, true, fullTx)
	}
	return nil, err
}

func (s *PublicBlockChainAPI) Health() bool {
	if rpc.RpcServingTimer != nil {
		return rpc.RpcServingTimer.Percentile(0.75) < float64(UnHealthyTimeout)
	}
	return true
}

// GetUncleByBlockNumberAndIndex returns the uncle block for the given block hash and index. When fullTx is true
// all transactions in the block are returned in full detail, otherwise only the transaction hash is returned.
func (s *PublicBlockChainAPI) GetUncleByBlockNumberAndIndex(ctx context.Context, blockNr rpc.BlockNumber, index hexutil.Uint) (map[string]interface{}, error) {
	block, err := s.b.BlockByNumber(ctx, blockNr)
	if block != nil {
		uncles := block.Uncles()
		if index >= hexutil.Uint(len(uncles)) {
			log.Debug("Requested uncle not found", "number", blockNr, "hash", block.Hash(), "index", index)
			return nil, nil
		}
		block = types.NewBlockWithHeader(uncles[index])
		return s.rpcMarshalBlock(ctx, block, false, false)
	}
	return nil, err
}

// GetUncleByBlockHashAndIndex returns the uncle block for the given block hash and index. When fullTx is true
// all transactions in the block are returned in full detail, otherwise only the transaction hash is returned.
func (s *PublicBlockChainAPI) GetUncleByBlockHashAndIndex(ctx context.Context, blockHash common.Hash, index hexutil.Uint) (map[string]interface{}, error) {
	block, err := s.b.BlockByHash(ctx, blockHash)
	if block != nil {
		uncles := block.Uncles()
		if index >= hexutil.Uint(len(uncles)) {
			log.Debug("Requested uncle not found", "number", block.Number(), "hash", blockHash, "index", index)
			return nil, nil
		}
		block = types.NewBlockWithHeader(uncles[index])
		return s.rpcMarshalBlock(ctx, block, false, false)
	}
	return nil, err
}

// GetUncleCountByBlockNumber returns number of uncles in the block for the given block number
func (s *PublicBlockChainAPI) GetUncleCountByBlockNumber(ctx context.Context, blockNr rpc.BlockNumber) *hexutil.Uint {
	if block, _ := s.b.BlockByNumber(ctx, blockNr); block != nil {
		n := hexutil.Uint(len(block.Uncles()))
		return &n
	}
	return nil
}

// GetUncleCountByBlockHash returns number of uncles in the block for the given block hash
func (s *PublicBlockChainAPI) GetUncleCountByBlockHash(ctx context.Context, blockHash common.Hash) *hexutil.Uint {
	if block, _ := s.b.BlockByHash(ctx, blockHash); block != nil {
		n := hexutil.Uint(len(block.Uncles()))
		return &n
	}
	return nil
}

// GetCode returns the code stored at the given address in the state for the given block number.
func (s *PublicBlockChainAPI) GetCode(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) {
	state, _, err := s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
	if state == nil || err != nil {
		return nil, err
	}
	code := state.GetCode(address)
	return code, state.Error()
}

// GetStorageAt returns the storage from the state at the given address, key and
// block number. The rpc.LatestBlockNumber and rpc.PendingBlockNumber meta block
// numbers are also allowed.
func (s *PublicBlockChainAPI) GetStorageAt(ctx context.Context, address common.Address, key string, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) {
	state, _, err := s.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
	if state == nil || err != nil {
		return nil, err
	}
	res := state.GetState(address, common.HexToHash(key))
	return res[:], state.Error()
}

// GetDiffAccounts returns changed accounts in a specific block number.
func (s *PublicBlockChainAPI) GetDiffAccounts(ctx context.Context, blockNr rpc.BlockNumber) ([]common.Address, error) {
	if s.b.Chain() == nil {
		return nil, fmt.Errorf("blockchain not support get diff accounts")
	}

	header, err := s.b.HeaderByNumber(ctx, blockNr)
	if err != nil {
		return nil, fmt.Errorf("block not found for block number (%d): %v", blockNr, err)
	}

	accounts, err := s.b.Chain().GetDiffAccounts(header.Hash())
	if err == nil || !errors.Is(err, core.ErrDiffLayerNotFound) {
		return accounts, err
	}

	// Replay the block when diff layer not found, it is very slow.
	block, err := s.b.BlockByNumber(ctx, blockNr)
	if err != nil {
		return nil, fmt.Errorf("block not found for block number (%d): %v", blockNr, err)
	}
	_, statedb, err := s.replay(ctx, block, nil)
	if err != nil {
		return nil, err
	}
	return statedb.GetDirtyAccounts(), nil
}

func (s *PublicBlockChainAPI) needToReplay(ctx context.Context, block *types.Block, accounts []common.Address) (bool, error) {
	receipts, err := s.b.GetReceipts(ctx, block.Hash())
	if err != nil || len(receipts) != len(block.Transactions()) {
		return false, fmt.Errorf("receipt incorrect for block number (%d): %v", block.NumberU64(), err)
	}

	accountSet := make(map[common.Address]struct{}, len(accounts))
	for _, account := range accounts {
		accountSet[account] = struct{}{}
	}
	spendValueMap := make(map[common.Address]int64, len(accounts))
	receiveValueMap := make(map[common.Address]int64, len(accounts))

	signer := types.MakeSigner(s.b.ChainConfig(), block.Number())
	for index, tx := range block.Transactions() {
		receipt := receipts[index]
		from, err := types.Sender(signer, tx)
		if err != nil {
			return false, fmt.Errorf("get sender for tx failed: %v", err)
		}

		if _, exists := accountSet[from]; exists {
			spendValueMap[from] += int64(receipt.GasUsed) * tx.GasPrice().Int64()
			if receipt.Status == types.ReceiptStatusSuccessful {
				spendValueMap[from] += tx.Value().Int64()
			}
		}

		if tx.To() == nil {
			continue
		}

		if _, exists := accountSet[*tx.To()]; exists && receipt.Status == types.ReceiptStatusSuccessful {
			receiveValueMap[*tx.To()] += tx.Value().Int64()
		}
	}

	parent, err := s.b.BlockByHash(ctx, block.ParentHash())
	if err != nil {
		return false, fmt.Errorf("block not found for block number (%d): %v", block.NumberU64()-1, err)
	}
	parentState, err := s.b.Chain().StateAt(parent.Root())
	if err != nil {
		return false, fmt.Errorf("statedb not found for block number (%d): %v", block.NumberU64()-1, err)
	}
	currentState, err := s.b.Chain().StateAt(block.Root())
	if err != nil {
		return false, fmt.Errorf("statedb not found for block number (%d): %v", block.NumberU64(), err)
	}
	for _, account := range accounts {
		parentBalance := parentState.GetBalance(account).Int64()
		currentBalance := currentState.GetBalance(account).Int64()
		if receiveValueMap[account]-spendValueMap[account] != currentBalance-parentBalance {
			return true, nil
		}
	}

	return false, nil
}

func (s *PublicBlockChainAPI) replay(ctx context.Context, block *types.Block, accounts []common.Address) (*types.DiffAccountsInBlock, *state.StateDB, error) {
	result := &types.DiffAccountsInBlock{
		Number:       block.NumberU64(),
		BlockHash:    block.Hash(),
		Transactions: make([]types.DiffAccountsInTx, 0),
	}

	parent, err := s.b.BlockByHash(ctx, block.ParentHash())
	if err != nil {
		return nil, nil, fmt.Errorf("block not found for block number (%d): %v", block.NumberU64()-1, err)
	}
	statedb, err := s.b.Chain().StateAt(parent.Root())
	if err != nil {
		return nil, nil, fmt.Errorf("state not found for block number (%d): %v", block.NumberU64()-1, err)
	}

	accountSet := make(map[common.Address]struct{}, len(accounts))
	for _, account := range accounts {
		accountSet[account] = struct{}{}
	}

	// Recompute transactions.
	signer := types.MakeSigner(s.b.ChainConfig(), block.Number())
	for _, tx := range block.Transactions() {
		// Skip data empty tx and to is one of the interested accounts tx.
		skip := false
		if len(tx.Data()) == 0 {
			skip = true
		} else if to := tx.To(); to != nil {
			if _, exists := accountSet[*to]; exists {
				skip = true
			}
		}

		diffTx := types.DiffAccountsInTx{
			TxHash:   tx.Hash(),
			Accounts: make(map[common.Address]*big.Int, len(accounts)),
		}

		if !skip {
			// Record account balance
			for _, account := range accounts {
				diffTx.Accounts[account] = statedb.GetBalance(account)
			}
		}

		// Apply transaction
		msg, _ := tx.AsMessage(signer)
		txContext := core.NewEVMTxContext(msg)
		context := core.NewEVMBlockContext(block.Header(), s.b.Chain(), nil)
		vmenv := vm.NewEVM(context, txContext, statedb, s.b.ChainConfig(), vm.Config{})

		if posa, ok := s.b.Engine().(consensus.PoSA); ok {
			if isSystem, _ := posa.IsSystemTransaction(tx, block.Header()); isSystem {
				balance := statedb.GetBalance(consensus.SystemAddress)
				if balance.Cmp(common.Big0) > 0 {
					statedb.SetBalance(consensus.SystemAddress, big.NewInt(0))
					statedb.AddBalance(block.Header().Coinbase, balance)
				}
			}
		}

		if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(tx.Gas())); err != nil {
			return nil, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err)
		}
		statedb.Finalise(vmenv.ChainConfig().IsEIP158(block.Number()))

		if !skip {
			// Compute account balance diff.
			for _, account := range accounts {
				diffTx.Accounts[account] = new(big.Int).Sub(statedb.GetBalance(account), diffTx.Accounts[account])
				if diffTx.Accounts[account].Cmp(big.NewInt(0)) == 0 {
					delete(diffTx.Accounts, account)
				}
			}

			if len(diffTx.Accounts) != 0 {
				result.Transactions = append(result.Transactions, diffTx)
			}
		}
	}

	return result, statedb, nil
}

// GetDiffAccountsWithScope returns detailed changes of some interested accounts in a specific block number.
func (s *PublicBlockChainAPI) GetDiffAccountsWithScope(ctx context.Context, blockNr rpc.BlockNumber, accounts []common.Address) (*types.DiffAccountsInBlock, error) {
	if s.b.Chain() == nil {
		return nil, fmt.Errorf("blockchain not support get diff accounts")
	}

	block, err := s.b.BlockByNumber(ctx, blockNr)
	if err != nil {
		return nil, fmt.Errorf("block not found for block number (%d): %v", blockNr, err)
	}

	needReplay, err := s.needToReplay(ctx, block, accounts)
	if err != nil {
		return nil, err
	}
	if !needReplay {
		return &types.DiffAccountsInBlock{
			Number:       uint64(blockNr),
			BlockHash:    block.Hash(),
			Transactions: make([]types.DiffAccountsInTx, 0),
		}, nil
	}

	result, _, err := s.replay(ctx, block, accounts)
	return result, err
}

// rpcMarshalHeader uses the generalized output filler, then adds the total difficulty field, which requires
// a `PublicBlockchainAPI`.
func (s *PublicBlockChainAPI) rpcMarshalHeader(ctx context.Context, header *types.Header) map[string]interface{} {
	fields := RPCMarshalHeader(header)
	fields["totalDifficulty"] = (*hexutil.Big)(s.b.GetTd(ctx, header.Hash()))
	return fields
}

// rpcMarshalBlock uses the generalized output filler, then adds the total difficulty field, which requires
// a `PublicBlockchainAPI`.
func (s *PublicBlockChainAPI) rpcMarshalBlock(ctx context.Context, b *types.Block, inclTx bool, fullTx bool) (map[string]interface{}, error) {
	fields, err := RPCMarshalBlock(b, inclTx, fullTx)
	if err != nil {
		return nil, err
	}
	if inclTx {
		fields["totalDifficulty"] = (*hexutil.Big)(s.b.GetTd(ctx, b.Hash()))
	}
	return fields, err
}

// CreateAccessList creates a EIP-2930 type AccessList for the given transaction.
// Reexec and BlockNrOrHash can be specified to create the accessList on top of a certain state.
func (s *PublicBlockChainAPI) CreateAccessList(ctx context.Context, args SendTxArgs, blockNrOrHash *rpc.BlockNumberOrHash) (*accessListResult, error) {
	bNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber)
	if blockNrOrHash != nil {
		bNrOrHash = *blockNrOrHash
	}
	acl, gasUsed, vmerr, err := AccessList(ctx, s.b, bNrOrHash, args)
	if err != nil {
		return nil, err
	}
	result := &accessListResult{Accesslist: &acl, GasUsed: hexutil.Uint64(gasUsed)}
	if vmerr != nil {
		result.Error = vmerr.Error()
	}
	return result, nil
}
