// Copyright 2024 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

package jsonrpc

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/holiman/uint256"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/dbg"
	"github.com/erigontech/erigon/common/hexutil"
	"github.com/erigontech/erigon/common/log/v3"
	"github.com/erigontech/erigon/execution/protocol"
	"github.com/erigontech/erigon/execution/state"
	tracersConfig "github.com/erigontech/erigon/execution/tracing/tracers/config"
	"github.com/erigontech/erigon/execution/types"
	"github.com/erigontech/erigon/execution/types/accounts"
	"github.com/erigontech/erigon/execution/vm"
	"github.com/erigontech/erigon/execution/vm/evmtypes"
	bortypes "github.com/erigontech/erigon/polygon/bor/types"
	polygontracer "github.com/erigontech/erigon/polygon/tracer"
	"github.com/erigontech/erigon/rpc"
	"github.com/erigontech/erigon/rpc/ethapi"
	"github.com/erigontech/erigon/rpc/jsonstream"
	"github.com/erigontech/erigon/rpc/rpchelper"
	"github.com/erigontech/erigon/rpc/transactions"
)

// TraceBlockByNumber implements debug_traceBlockByNumber. Returns Geth style block traces.
func (api *DebugAPIImpl) TraceBlockByNumber(ctx context.Context, blockNum rpc.BlockNumber, config *tracersConfig.TraceConfig, stream jsonstream.Stream) error {
	return api.traceBlock(ctx, rpc.BlockNumberOrHashWithNumber(blockNum), config, stream)
}

// TraceBlockByHash implements debug_traceBlockByHash. Returns Geth style block traces.
func (api *DebugAPIImpl) TraceBlockByHash(ctx context.Context, hash common.Hash, config *tracersConfig.TraceConfig, stream jsonstream.Stream) error {
	return api.traceBlock(ctx, rpc.BlockNumberOrHashWithHash(hash, true), config, stream)
}

func (api *DebugAPIImpl) traceBlock(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash, config *tracersConfig.TraceConfig, stream jsonstream.Stream) error {
	tx, err := api.db.BeginTemporalRo(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	blockNumber, hash, _, err := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, tx, api._blockReader, api.filters)
	if err != nil {
		return err
	}

	if (blockNrOrHash.BlockHash == nil && hash == common.Hash{}) {
		stream.WriteNil()
		return fmt.Errorf("block #%d not found", *blockNrOrHash.BlockNumber)
	}

	if blockNumber == 0 {
		stream.WriteNil()
		return fmt.Errorf("genesis is not traceable")
	}

	// if we've pruned this history away for this block then just return early
	// to save any red herring errors
	err = api.BaseAPI.checkPruneHistory(ctx, tx, blockNumber)
	if err != nil {
		return err
	}

	block, err := api.blockWithSenders(ctx, tx, hash, blockNumber)
	if err != nil {
		return err
	}
	if block == nil {
		stream.WriteNil()
		return fmt.Errorf("invalid arguments; block with hash %x not found", hash)
	}

	if config == nil {
		config = &tracersConfig.TraceConfig{}
	}

	if config.BorTraceEnabled == nil {
		var disabled bool
		config.BorTraceEnabled = &disabled
	}

	chainConfig, err := api.chainConfig(ctx, tx)
	if err != nil {
		return err
	}
	engine := api.engine()

	err = rpchelper.CheckBlockExecuted(tx, blockNumber)
	if err != nil {
		return err
	}

	ibs, blockCtx, _, rules, signer, err := transactions.ComputeBlockContext(ctx, engine, block.HeaderNoCopy(), chainConfig, api._blockReader, api.stateCache, api._txNumReader, tx, 0)
	if err != nil {
		return err
	}

	var precompiles vm.PrecompiledContracts
	if config.BlockOverrides != nil {
		if err := config.BlockOverrides.Override(&blockCtx); err != nil {
			return err
		}
	}
	if config.StateOverrides != nil {
		rules = blockCtx.Rules(chainConfig)
		precompiles = vm.ActivePrecompiledContracts(rules)
		if err := config.StateOverrides.Override(ibs, precompiles, rules); err != nil {
			return err
		}
	}

	stream.WriteArrayStart()

	txns := block.Transactions()

	var borStateSyncTxn types.Transaction

	if *config.BorTraceEnabled {
		borStateSyncTxHash := bortypes.ComputeBorTxHash(block.NumberU64(), block.Hash())

		_, ok, err := api.bridgeReader.EventTxnLookup(ctx, borStateSyncTxHash)

		if err != nil {
			return err
		}
		if ok {
			borStateSyncTxn = bortypes.NewBorTransaction()
			txns = append(txns, borStateSyncTxn)
		}
	}

	var gasUsed uint64
	for txnIndex, txn := range txns {
		isBorStateSyncTxn := borStateSyncTxn == txn
		var txnHash common.Hash
		if isBorStateSyncTxn {
			txnHash = bortypes.ComputeBorTxHash(block.NumberU64(), block.Hash())
		} else {
			txnHash = txn.Hash()
		}

		stream.WriteObjectStart()
		stream.WriteObjectField("txHash")
		stream.WriteString(txnHash.Hex())
		stream.WriteMore()
		stream.WriteObjectField("result")
		select {
		default:
		case <-ctx.Done():
			return ctx.Err()
		}
		ibs.SetTxContext(blockCtx.BlockNumber, txnIndex)
		msg, _ := txn.AsMessage(*signer, block.BaseFee(), rules)

		txCtx := evmtypes.TxContext{
			TxHash:     txnHash,
			Origin:     msg.From(),
			GasPrice:   *msg.GasPrice(),
			BlobHashes: msg.BlobHashes(),
		}

		if isBorStateSyncTxn {
			var stateSyncEvents []*types.Message
			stateSyncEvents, err = api.bridgeReader.Events(ctx, block.Hash(), blockNumber)
			if err != nil {
				return err
			}

			var _gasUsed uint64
			_gasUsed, err = polygontracer.TraceBorStateSyncTxnDebugAPI(
				ctx,
				chainConfig,
				config,
				ibs,
				block.Hash(),
				block.NumberU64(),
				block.Time(),
				blockCtx,
				stream,
				api.evmCallTimeout,
				stateSyncEvents,
				txnIndex,
			)
			gasUsed += _gasUsed
		} else {
			var _gasUsed uint64
			_gasUsed, err = transactions.TraceTx(ctx, engine, txn, msg, blockCtx, txCtx, &block.Header().Number, block.Hash(), txnIndex, ibs, config, chainConfig, stream, api.evmCallTimeout, precompiles)
			gasUsed += _gasUsed
		}
		if err == nil {
			err = ibs.FinalizeTx(rules, state.NewNoopWriter())
		}

		// if we have an error we want to output valid json for it before continuing after clearing down potential writes to the stream
		if err != nil {
			stream.WriteMore()
			rpc.HandleError(err, stream)
		}

		stream.WriteObjectEnd()
		if txnIndex != len(txns)-1 {
			stream.WriteMore()
		}

		if err := stream.Flush(); err != nil {
			return err
		}
	}

	if dbg.AssertEnabled {
		var refunds = true
		if config.NoRefunds != nil && *config.NoRefunds {
			refunds = false
		}

		if refunds && block.GasUsed() != gasUsed {
			panic(fmt.Errorf("assert: block.GasUsed() %d != gasUsed %d. blockNum=%d", block.GasUsed(), gasUsed, blockNumber))
		}
	}

	stream.WriteArrayEnd()
	return nil
}

// TraceTransaction implements debug_traceTransaction. Returns Geth style transaction traces.
func (api *DebugAPIImpl) TraceTransaction(ctx context.Context, hash common.Hash, config *tracersConfig.TraceConfig, stream jsonstream.Stream) error {
	tx, err := api.db.BeginTemporalRo(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback()
	chainConfig, err := api.chainConfig(ctx, tx)
	if err != nil {
		return err
	}
	// Retrieve the transaction and assemble its EVM context
	var isBorStateSyncTxn bool
	blockNum, _, ok, err := api.txnLookup(ctx, tx, hash)
	if err != nil {
		return err
	}

	if !ok {
		if chainConfig.Bor == nil {
			stream.WriteNil()
			return nil
		}

		// otherwise this may be a bor state sync transaction - check
		blockNum, ok, err = api.bridgeReader.EventTxnLookup(ctx, hash)
		if err != nil {
			stream.WriteNil()
			return err
		}
		if !ok {
			stream.WriteNil()
			return nil
		}
		if config == nil || config.BorTraceEnabled == nil || !*config.BorTraceEnabled {
			stream.WriteEmptyArray() // matches maticnetwork/bor API behaviour for consistency
			return nil
		}

		isBorStateSyncTxn = true
	}

	if blockNum == 0 {
		stream.WriteNil()
		return fmt.Errorf("genesis is not traceable")
	}

	// check pruning to ensure we have history at this block level
	err = api.BaseAPI.checkPruneHistory(ctx, tx, blockNum)
	if err != nil {
		return err
	}

	block, err := api.blockByNumberWithSenders(ctx, tx, blockNum)
	if err != nil {
		return err
	}
	if block == nil {
		stream.WriteNil()
		return nil
	}
	var txnIndex int
	var txn types.Transaction
	for i := 0; i < block.Transactions().Len() && !isBorStateSyncTxn; i++ {
		transaction := block.Transactions()[i]
		if transaction.Hash() == hash {
			txnIndex = i
			txn = transaction
			break
		}
	}
	if txn == nil {
		if isBorStateSyncTxn {
			// bor state sync txn is appended at the end of the block
			txnIndex = block.Transactions().Len()
		} else {
			stream.WriteNil()
			return fmt.Errorf("transaction %#x not found", hash)
		}
	}
	engine := api.engine()

	ibs, blockCtx, _, rules, signer, err := transactions.ComputeBlockContext(ctx, engine, block.HeaderNoCopy(), chainConfig, api._blockReader, api.stateCache, api._txNumReader, tx, txnIndex)
	if err != nil {
		return err
	}

	var precompiles vm.PrecompiledContracts
	if config != nil {
		if config.BlockOverrides != nil {
			if err := config.BlockOverrides.Override(&blockCtx); err != nil {
				return err
			}
		}
		if config.StateOverrides != nil {
			rules = blockCtx.Rules(chainConfig)
			precompiles = vm.ActivePrecompiledContracts(rules)
			if err := config.StateOverrides.Override(ibs, precompiles, rules); err != nil {
				return err
			}
		}
	}

	if isBorStateSyncTxn {
		stateSyncEvents, err := api.bridgeReader.Events(ctx, block.Hash(), blockNum)
		if err != nil {
			return err
		}

		_, err = polygontracer.TraceBorStateSyncTxnDebugAPI(
			ctx,
			chainConfig,
			config,
			ibs,
			block.Hash(),
			blockNum,
			block.Time(),
			blockCtx,
			stream,
			api.evmCallTimeout,
			stateSyncEvents,
			txnIndex,
		)
		return err
	}

	msg, txCtx, err := transactions.ComputeTxContext(ibs, engine, rules, signer, block, chainConfig, txnIndex)
	if err != nil {
		return err
	}
	txCtx.TxHash = txn.Hash()

	// Trace the transaction and return
	_, err = transactions.TraceTx(ctx, engine, txn, msg, blockCtx, txCtx, &block.Header().Number, block.Hash(), txnIndex, ibs, config, chainConfig, stream, api.evmCallTimeout, precompiles)
	return err
}

// TraceCall implements debug_traceCall. Returns Geth style call traces.
func (api *DebugAPIImpl) TraceCall(ctx context.Context, args ethapi.CallArgs, blockNrOrHash rpc.BlockNumberOrHash, config *tracersConfig.TraceConfig, stream jsonstream.Stream) error {
	dbtx, err := api.db.BeginTemporalRo(ctx)
	if err != nil {
		return fmt.Errorf("create ro transaction: %v", err)
	}
	defer dbtx.Rollback()

	chainConfig, err := api.chainConfig(ctx, dbtx)
	if err != nil {
		return fmt.Errorf("read chain config: %v", err)
	}
	engine := api.engine()

	blockNumber, hash, isLatest, err := rpchelper.GetCanonicalBlockNumber(ctx, blockNrOrHash, dbtx, api._blockReader, api.filters)
	if err != nil {
		return fmt.Errorf("get block number: %v", err)
	}

	err = api.BaseAPI.checkPruneHistory(ctx, dbtx, blockNumber)
	if err != nil {
		return err
	}

	err = rpchelper.CheckBlockExecuted(dbtx, blockNumber)
	if err != nil {
		return err
	}

	var stateReader state.StateReader
	if config == nil || config.TxIndex == nil || isLatest {
		stateReader, err = rpchelper.CreateStateReaderFromBlockNumber(ctx, dbtx, blockNumber, isLatest, 0, api.stateCache, api._txNumReader)
	} else {
		stateReader, err = rpchelper.CreateHistoryStateReader(ctx, dbtx, blockNumber, int(*config.TxIndex), api._txNumReader)
	}
	if err != nil {
		return fmt.Errorf("create state reader: %v", err)
	}
	header, err := api.headerByNumber(ctx, rpc.BlockNumber(blockNumber), dbtx)
	if err != nil {
		return fmt.Errorf("could not fetch header %d(%x): %v", blockNumber, hash, err)
	}
	if header == nil {
		return fmt.Errorf("block %d(%x) not found", blockNumber, hash)
	}
	ibs := state.New(stateReader)

	var baseFee *uint256.Int
	if header.BaseFee != nil {
		baseFee = new(uint256.Int).Set(header.BaseFee)
	}

	if config != nil && config.BlockOverrides != nil {
		if config.BlockOverrides.BaseFeePerGas != nil && baseFee != nil {
			overflow := baseFee.SetFromBig(config.BlockOverrides.BaseFeePerGas.ToInt())
			if overflow {
				return errors.New("BlockOverrides.BaseFee uint256 overflow")
			}
		}

		if config.BlockOverrides.BlobBaseFee != nil {
			args.MaxFeePerBlobGas = config.BlockOverrides.BlobBaseFee
		}
	}

	msg, err := args.ToMessage(api.GasCap, baseFee)
	if err != nil {
		return fmt.Errorf("convert args to msg: %v", err)
	}
	transaction, err := args.ToTransaction(api.GasCap, baseFee)
	if err != nil {
		return fmt.Errorf("convert args to msg: %v", err)
	}

	var precompiles vm.PrecompiledContracts

	blockCtx := transactions.NewEVMBlockContext(engine, header, blockNrOrHash.RequireCanonical, dbtx, api._blockReader, chainConfig)
	if config != nil {
		if config.BlockOverrides != nil {
			err := config.BlockOverrides.Override(&blockCtx)
			if err != nil {
				return err
			}
		}

		// Override the state before block execution.
		if config.StateOverrides != nil {
			rules := blockCtx.Rules(chainConfig)
			precompiles = vm.ActivePrecompiledContracts(rules)
			if err := config.StateOverrides.Override(ibs, precompiles, rules); err != nil {
				return err
			}
		}
	}

	txCtx := protocol.NewEVMTxContext(msg)
	// Trace the transaction and return
	_, err = transactions.TraceTx(ctx, engine, transaction, msg, blockCtx, txCtx, nil, common.Hash{}, 0, ibs, config, chainConfig, stream, api.evmCallTimeout, precompiles)
	return err
}

// TraceCall implements debug_traceCallMany. Returns Geth style call traces.
func (api *DebugAPIImpl) TraceCallMany(ctx context.Context, bundles []Bundle, simulateContext StateContext, config *tracersConfig.TraceConfig, stream jsonstream.Stream) error {
	var (
		hash              common.Hash
		evm               *vm.EVM
		blockCtx          evmtypes.BlockContext
		txCtx             evmtypes.TxContext
		overrideBlockHash map[uint64]common.Hash
	)

	if config == nil {
		config = &tracersConfig.TraceConfig{}
	}

	overrideBlockHash = make(map[uint64]common.Hash)
	tx, err := api.db.BeginTemporalRo(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback()
	chainConfig, err := api.chainConfig(ctx, tx)
	if err != nil {
		return err
	}
	if len(bundles) == 0 {
		stream.WriteNil()
		return errors.New("empty bundles")
	}
	empty := true
	for _, bundle := range bundles {
		if len(bundle.Transactions) != 0 {
			empty = false
		}
	}

	if empty {
		stream.WriteNil()
		return errors.New("empty bundles")
	}

	defer func(start time.Time) { log.Trace("Tracing CallMany finished", "runtime", time.Since(start)) }(time.Now())

	blockNum, hash, isLatest, err := rpchelper.GetBlockNumber(ctx, simulateContext.BlockNumber, tx, api._blockReader, api.filters)
	if err != nil {
		return err
	}

	err = api.BaseAPI.checkPruneHistory(ctx, tx, blockNum)
	if err != nil {
		return err
	}

	var header *types.Header
	header, err = api.headerByNumber(ctx, rpc.BlockNumber(blockNum), tx)
	if err != nil {
		return err
	}
	if header == nil {
		stream.WriteNil()
		return fmt.Errorf("block %d(%x) not found", blockNum, hash)
	}

	var stateReader state.StateReader

	err = rpchelper.CheckBlockExecuted(tx, blockNum)
	if err != nil {
		return err
	}

	if simulateContext.TransactionIndex == nil || *simulateContext.TransactionIndex == -1 || isLatest {
		var blockNrOrHash rpc.BlockNumberOrHash

		rpcBlockNumValue := rpc.BlockNumber(blockNum)
		blockNrOrHash.BlockNumber = &rpcBlockNumValue

		stateReader, err = rpchelper.CreateStateReaderFromBlockNumber(ctx, tx, blockNum, isLatest, 0, api.stateCache, api._txNumReader)
	} else {
		stateReader, err = rpchelper.CreateHistoryStateReader(ctx, tx, blockNum, *simulateContext.TransactionIndex, api._txNumReader)
	}

	if err != nil {
		return err
	}

	ibs := state.New(stateReader)

	getHash := func(i uint64) (common.Hash, error) {
		if hash, ok := overrideBlockHash[i]; ok {
			return hash, nil
		}
		hash, ok, err := api._blockReader.CanonicalHash(ctx, tx, i)
		if err != nil || !ok {
			log.Debug("Can't get block hash by number", "number", i, "only-canonical", true, "err", err, "ok", ok)
			return common.Hash{}, err
		}
		return hash, nil
	}

	blockCtx = protocol.NewEVMBlockContext(header, getHash, api.engine(), accounts.NilAddress /* author */, chainConfig)
	// Apply global block overrides as the baseline for all bundles.
	// Use OverrideBlockContext (not Override) so that BlockHash overrides are also merged
	// into overrideBlockHash and honoured by the getHash closure above.
	if config.BlockOverrides != nil {
		config.BlockOverrides.OverrideBlockContext(&blockCtx, overrideBlockHash)
	}
	// Get a new instance of the EVM
	evm = vm.NewEVM(blockCtx, txCtx, ibs, chainConfig, vm.Config{})
	rules := evm.ChainRules()

	var precompiles vm.PrecompiledContracts
	// after replaying the txns, we want to overload the state
	if config.StateOverrides != nil {
		precompiles = vm.ActivePrecompiledContracts(rules)
		err = config.StateOverrides.Override(ibs, precompiles, rules)
		if err != nil {
			return err
		}
	}

	stream.WriteArrayStart()
	for bundleIndex, bundle := range bundles {
		stream.WriteArrayStart()
		// first change block context
		bundle.BlockOverride.OverrideBlockContext(&blockCtx, overrideBlockHash)
		// do not reset ibs, because we want to keep the overrides and state change
		// ibs.Reset()
		for txnIndex, txn := range bundle.Transactions {
			if txn.Gas == nil || *(txn.Gas) == 0 {
				txn.Gas = (*hexutil.Uint64)(&api.GasCap)
			}
			msg, err := txn.ToMessage(api.GasCap, &blockCtx.BaseFee)
			if err != nil {
				return err
			}
			transaction, err := txn.ToTransaction(api.GasCap, &blockCtx.BaseFee)
			if err != nil {
				return err
			}
			txCtx = protocol.NewEVMTxContext(msg)
			ibs := evm.IntraBlockState()
			ibs.SetTxContext(blockCtx.BlockNumber, txnIndex)
			_, err = transactions.TraceTx(ctx, api.engine(), transaction, msg, blockCtx, txCtx, nil, common.Hash{}, txnIndex, evm.IntraBlockState(), config, chainConfig, stream, api.evmCallTimeout, precompiles)
			if err != nil {
				return err
			}

			_ = ibs.FinalizeTx(rules, state.NewNoopWriter())

			if txnIndex < len(bundle.Transactions)-1 {
				stream.WriteMore()
			}
		}
		stream.WriteArrayEnd()

		if bundleIndex < len(bundles)-1 {
			stream.WriteMore()
		}
		blockCtx.BlockNumber++
		blockCtx.Time++
	}
	stream.WriteArrayEnd()
	return nil
}
