package stagedsync

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"os"
	"sync"
	"time"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/dbg"
	"github.com/erigontech/erigon/common/log/v3"
	"github.com/erigontech/erigon/db/consensuschain"
	"github.com/erigontech/erigon/db/kv"
	"github.com/erigontech/erigon/db/rawdb"
	"github.com/erigontech/erigon/db/rawdb/rawtemporaldb"
	"github.com/erigontech/erigon/db/state/changeset"
	"github.com/erigontech/erigon/execution/commitment"
	"github.com/erigontech/erigon/execution/exec"
	"github.com/erigontech/erigon/execution/protocol"
	"github.com/erigontech/erigon/execution/protocol/rules"
	"github.com/erigontech/erigon/execution/receipts"
	"github.com/erigontech/erigon/execution/state"
	"github.com/erigontech/erigon/execution/tests/chaos_monkey"
	"github.com/erigontech/erigon/execution/types"
	"github.com/erigontech/erigon/execution/types/accounts"
	"github.com/erigontech/erigon/node/shards"
)

type serialExecutor struct {
	txExecutor
	// outputs
	txCount           uint64
	blockGasUsed      uint64 // accumulated regular gas (pre-Amsterdam: same as block gas)
	blockStateGasUsed uint64 // EIP-8037: accumulated state gas
	blobGasUsed       uint64
	lastBlockResult   *blockResult
	worker            *exec.Worker

	// accumulator for the current block; set at StartChange and used by the
	// block-end stateWriter so that AuRa system-call nonce changes are
	// included in the txpool state-diff batch.
	accumulator *shards.Accumulator
}

func warmTxsHashes(block *types.Block) {
	for _, t := range block.Transactions() {
		_ = t.Hash()
	}
	_ = block.Hash()
}

func (se *serialExecutor) exec(ctx context.Context, execStage *StageState, u Unwinder,
	startBlockNum uint64, offsetFromBlockBeginning uint64, maxBlockNum uint64, blockLimit uint64,
	initialTxNum uint64, inputTxNum uint64, initialCycle bool, rwTx kv.TemporalRwTx,
	accumulator *shards.Accumulator, readAhead chan uint64, logEvery *time.Ticker) (*types.Header, kv.TemporalRwTx, error) {

	se.resetWorkers(ctx, se.rs, se.applyTx)

	havePartialBlock := false
	blockNum := startBlockNum

	var b *types.Block

	lastFrozenStep := se.applyTx.StepsInFiles(kv.CommitmentDomain)

	var lastFrozenTxNum uint64
	if lastFrozenStep > 0 {
		lastFrozenTxNum = uint64((lastFrozenStep+1)*kv.Step(se.doms.StepSize())) - 1
	}

	toBlockNum := maxBlockNum
	if blockLimit > 0 {
		toBlockNum = min(maxBlockNum, blockNum+blockLimit-1)
	}
	if maxBlockNum > blockNum+16 {
		log.Info(fmt.Sprintf("[%s] serial starting", execStage.LogPrefix()),
			"from", blockNum, "to", toBlockNum, "initialTxNum", initialTxNum,
			"initialBlockTxOffset", offsetFromBlockBeginning, "lastFrozenStep", lastFrozenStep,
			"initialCycle", initialCycle, "isForkValidation", se.isForkValidation)
	}

	for ; blockNum <= maxBlockNum; blockNum++ {
		shouldGenerateChangesets := shouldGenerateChangeSets(se.cfg, blockNum, maxBlockNum)
		changeSet := &changeset.StateChangeSet{}
		if shouldGenerateChangesets && blockNum > 0 {
			se.doms.SetChangesetAccumulator(changeSet)
		}

		select {
		case readAhead <- blockNum:
		default:
		}

		canonicalHash, err := rawdb.ReadCanonicalHash(se.applyTx, blockNum)
		if err != nil {
			return nil, rwTx, err
		}
		var ok bool
		b, ok = se.cfg.readAheader.ReadBlockWithSenders(canonicalHash)
		if b == nil || !ok {
			b, err = exec.BlockWithSenders(ctx, se.cfg.db, se.applyTx, se.cfg.blockReader, blockNum)
			if err != nil {
				return nil, rwTx, err
			}
		}
		if b == nil {
			// TODO: panic here and see that overall process deadlock
			return nil, rwTx, fmt.Errorf("nil block %d", blockNum)
		}
		go warmTxsHashes(b)

		txs := b.Transactions()
		header := b.HeaderNoCopy()
		getHashFnMutex := sync.Mutex{}

		if se.cfg.chainConfig.AmsterdamTime != nil && *se.cfg.chainConfig.AmsterdamTime > 0 && se.cfg.chainConfig.IsAmsterdam(header.Time) {
			se.logger.Error(fmt.Sprintf("[%s] BLOCK PROCESSING FAILED: Amsterdam processing is not supported by serial exec", se.logPrefix), "fork-block", blockNum)
			return nil, rwTx, fmt.Errorf("amsterdam processing is not supported by serial exec from block: %d", blockNum)
		}

		blockContext := protocol.NewEVMBlockContext(header, protocol.GetHashFn(header, func(hash common.Hash, number uint64) (*types.Header, error) {
			getHashFnMutex.Lock()
			defer getHashFnMutex.Unlock()
			return se.getHeader(ctx, hash, number)
		}), se.cfg.engine, se.cfg.author, se.cfg.chainConfig)

		se.accumulator = accumulator // keep in sync for executeBlock's stateWriter
		if accumulator != nil {
			txs, err := se.cfg.blockReader.RawTransactions(context.Background(), se.applyTx, b.NumberU64(), b.NumberU64())
			if err != nil {
				return nil, rwTx, err
			}
			accumulator.StartChange(header, txs, false)
		}

		var txTasks []exec.Task

		for txIndex := -1; txIndex <= len(txs); txIndex++ {
			// Do not oversend, wait for the result heap to go under certain size
			txTask := &exec.TxTask{
				TxNum:           inputTxNum,
				TxIndex:         txIndex,
				Header:          header,
				Uncles:          b.Uncles(),
				Txs:             txs,
				EvmBlockContext: blockContext,
				Withdrawals:     b.Withdrawals(),
				// use history reader instead of state reader to catch up to the tx where we left off
				HistoryExecution: lastFrozenTxNum > 0 && inputTxNum <= lastFrozenTxNum,
				Trace:            dbg.TraceTx(blockNum, txIndex),
				Hooks:            se.hooks,
				Logger:           se.logger,
			}

			if txTask.TxNum > 0 && txTask.TxNum <= initialTxNum {
				havePartialBlock = true
				inputTxNum++
				continue
			}

			txTasks = append(txTasks, txTask)
			inputTxNum++
		}

		start := time.Now()
		continueLoop, err := se.executeBlock(ctx, txTasks, execStage.CurrentSyncCycle.IsInitialCycle, false)

		if took := time.Since(start); took > 50*time.Millisecond { // prevent logs spamming
			log.Debug(fmt.Sprintf("[%s] executed block %d in %s", se.logPrefix, blockNum, took))
		}
		if err != nil {
			return nil, rwTx, err
		}

		if !continueLoop {
			return b.HeaderNoCopy(), rwTx, nil
		}

		if !dbg.BatchCommitments || shouldGenerateChangesets || se.cfg.syncCfg.KeepExecutionProofs {
			traceBlk := dbg.TraceBlock(blockNum)
			if traceBlk {
				fmt.Println(blockNum, "Commitment")
				se.doms.GetCommitmentCtx().SetTraceWriter(os.Stderr)
			}
			// Warmup is enabled via EnableTrieWarmup at executor init
			rh, err := se.doms.ComputeCommitment(ctx, se.applyTx, true, blockNum, inputTxNum-1, se.logPrefix, nil)
			if traceBlk {
				se.doms.GetCommitmentCtx().SetTraceWriter(nil)
			}

			if err != nil {
				return nil, rwTx, err
			}

			if shouldGenerateChangesets && blockNum > 0 {
				se.doms.SavePastChangesetAccumulator(b.Hash(), blockNum, changeSet)
			}
			se.doms.SetChangesetAccumulator(nil)

			if !bytes.Equal(rh, header.Root[:]) {
				se.logger.Error(fmt.Sprintf("[%s] Wrong trie root of block %d: %x, expected (from header): %x. Block hash: %x", se.logPrefix, header.Number.Uint64(), rh, header.Root[:], header.Hash()))
				return b.HeaderNoCopy(), rwTx, fmt.Errorf("%w, block=%d", ErrWrongTrieRoot, blockNum)
			}
		}

		if dbg.StopAfterBlock > 0 && blockNum == dbg.StopAfterBlock {
			se.logger.Warn(fmt.Sprintf("[%s] STOP_AFTER_BLOCK reached, exiting without commit (debug mode)", se.logPrefix), "block", blockNum)
			// Intentional os.Exit: STOP_AFTER_BLOCK is a debug switch used to
			// capture state at exactly N blocks executed. The DB is left as it
			// was *before* this block was applied so the next run reproduces
			// the stop point with the same input. panic() (the prior implementation)
			// unwinds defers and fires the very commit we're trying to skip.
			// Mirrors the design documented in PR #19803 — debug only, never set
			// in production.
			os.Exit(0)
		}

		if offsetFromBlockBeginning > 0 {
			// after history execution no offset will be required
			offsetFromBlockBeginning = 0
		}

		select {
		case <-logEvery.C:
			if se.isApplyingBlocks {
				se.LogExecution()
			}
		default:
		}

		isBatchFull := se.isApplyingBlocks && se.readState().SizeEstimateBeforeCommitment() >= se.cfg.batchSize.Bytes()
		// havePartialBlock: partial first block isn't validated, compute root ASAP for fail-fast.
		needCalcRoot := se.isApplyingBlocks && (isBatchFull || havePartialBlock)
		havePartialBlock = false

		if needCalcRoot {
			resetExecGauges(ctx)
			ok, times, err := computeAndCheckCommitmentV3(ctx, b.HeaderNoCopy(), rwTx, se.doms, se.cfg, execStage, false, se.logger, u)
			if err != nil {
				return nil, rwTx, err
			}
			if !ok {
				return b.HeaderNoCopy(), rwTx, nil
			}
			resetCommitmentGauges(ctx)
			se.txExecutor.lastCommittedBlockNum.Store(b.NumberU64())
			se.txExecutor.lastCommittedTxNum.Store(inputTxNum)
			se.logger.Info(
				"periodic commit check",
				"block", b.NumberU64(),
				"txNum", inputTxNum,
				"commitment", times.ComputeCommitment,
			)
			se.doms.PrintCacheStats()
			if isBatchFull && blockNum != maxBlockNum {
				return b.HeaderNoCopy(), rwTx, &ErrLoopExhausted{From: startBlockNum, To: blockNum, Reason: "block batch is full"}
			}
		}

		select {
		case <-ctx.Done():
			return b.HeaderNoCopy(), rwTx, ctx.Err()
		default:
		}

		lastExecutedStep := kv.Step(uint64(se.lastExecutedTxNum.Load()) / se.doms.StepSize())

		if shouldMarkExhaustedAtBlock(initialCycle, lastExecutedStep, lastFrozenStep, dbg.DiscardCommitment(), blockLimit, blockNum, startBlockNum, maxBlockNum) {
			return b.HeaderNoCopy(), rwTx, &ErrLoopExhausted{From: startBlockNum, To: blockNum, Reason: "block limit reached"}
		}
	}
	se.doms.PrintCacheStats()

	return b.HeaderNoCopy(), rwTx, nil
}

func (se *serialExecutor) LogExecution() {
	se.progress.LogExecution(se.rs.StateV3, se)
}

func (se *serialExecutor) LogCommitments(committedTransactions uint64, stepsInDb float64, lastProgress commitment.CommitProgress) {
	se.txExecutor.lastCommittedTxNum.Add(committedTransactions)
	se.progress.LogCommitments(se.rs.StateV3, se, stepsInDb, lastProgress)
}

func (se *serialExecutor) LogComplete(stepsInDb float64) {
	se.progress.LogComplete(se.rs.StateV3, se, stepsInDb)
}

func (se *serialExecutor) resetWorkers(ctx context.Context, rs *state.StateV3Buffered, applyTx kv.TemporalTx) (err error) {

	if se.worker == nil {
		se.taskExecMetrics = exec.NewWorkerMetrics()
		se.worker = exec.NewWorker(context.Background(), false, se.taskExecMetrics,
			se.cfg.db.(kv.TemporalRoDB), nil, se.cfg.blockReader, se.cfg.chainConfig, se.cfg.genesis, nil, se.cfg.engine, se.cfg.dirs, se.logger)
	}

	if se.applyTx != applyTx {
		if se.applyTx != nil {
			se.applyTx.Rollback()
		}

		if applyTx != nil {
			se.applyTx = applyTx
		} else {
			applyTx, err := se.cfg.db.BeginRo(ctx) //nolint
			if err != nil {
				applyTx.Rollback()
				return err
			}
			se.applyTx = applyTx.(kv.TemporalTx)
		}
	}

	se.worker.ResetState(rs, se.applyTx, nil, nil, nil)

	return nil
}

func (se *serialExecutor) executeBlock(ctx context.Context, tasks []exec.Task, isInitialCycle bool, profile bool) (cont bool, err error) {
	blockReceipts := make([]*types.Receipt, 0, len(tasks))
	var startTxIndex int

	if se.blockExecMetrics == nil {
		se.blockExecMetrics = newBlockExecMetrics()
	}

	defer func(t time.Time) {
		se.blockExecMetrics.BlockCount.Add(1)
		se.blockExecMetrics.Duration.Add(time.Since(t))
	}(time.Now())

	if len(tasks) > 0 {
		// During the first block execution, we may have half-block data in the snapshots.
		// Thus, we need to skip the first txs in the block, however, this causes the GasUsed to be incorrect.
		// So we need to skip that check for the first block, if we find half-executed data (startTxIndex>0).
		startTxIndex = max(tasks[0].(*exec.TxTask).TxIndex, 0)
	}

	var gasPool *protocol.GasPool
	for _, task := range tasks {
		txTask := task.(*exec.TxTask)

		if gasPool == nil {
			gasPool = protocol.NewGasPool(task.BlockGasLimit(), se.cfg.chainConfig.GetMaxBlobGasPerBlock(tasks[0].BlockTime()))
		}

		txTask.ResetGasPool(gasPool)
		txTask.Config = se.cfg.chainConfig
		txTask.Engine = se.cfg.engine

		result := se.worker.RunTxTask(txTask)

		if err := func() error {
			if errors.Is(result.Err, context.Canceled) {
				return result.Err
			}
			if result.Err != nil {
				return fmt.Errorf("%w, txnIdx=%d, %v", rules.ErrInvalidBlock, txTask.TxIndex, result.Err) //same as in stage_exec.go
			}

			se.txCount++
			se.blockGasUsed += result.ExecutionResult.BlockRegularGasUsed
			se.blockStateGasUsed += result.ExecutionResult.BlockStateGasUsed
			mxExecTransactions.Add(1)
			if txTask.Tx() != nil {
				se.blobGasUsed += txTask.Tx().GetBlobGas()
			}
			if txTask.IsBlockEnd() && txTask.BlockNumber() > 0 {
				//fmt.Printf("txNum=%d, blockNum=%d, finalisation of the block\n", txTask.TxNum, txTask.BlockNum)
				// End of block transaction in a block
				ibs := state.New(state.NewReaderV3(se.rs.Domains().AsGetter(se.applyTx)))
				defer ibs.Release(true)
				ibs.SetTxContext(txTask.BlockNumber(), txTask.TxIndex)
				syscall := func(contract accounts.Address, data []byte) ([]byte, error) {
					return protocol.SysCallContract(contract, data, se.cfg.chainConfig, ibs, txTask.Header, se.cfg.engine, false /* constCall */, *se.cfg.vmConfig)
				}

				chainReader := consensuschain.NewReader(se.cfg.chainConfig, se.applyTx, se.cfg.blockReader, se.logger)

				finalizeReceipts := blockReceipts
				priorComplete := startTxIndex == 0
				if startTxIndex > 0 && len(txTask.Txs) > 0 {
					firstTask := tasks[0].(*exec.TxTask)
					blockStartTxNum := firstTask.TxNum - uint64(firstTask.TxIndex)
					priorReceipts, priorErr := se.reconstructPriorReceipts(ctx, se.applyTx, txTask.Header, txTask.Txs, startTxIndex, blockStartTxNum)
					if priorErr != nil {
						se.logger.Warn(fmt.Sprintf("[%s] failed to reconstruct prior receipts for partial block", se.logPrefix),
							"block", txTask.BlockNumber(), "startTxIndex", startTxIndex, "err", priorErr)
					} else {
						finalizeReceipts = append(priorReceipts, blockReceipts...)
						priorComplete = true
					}
					// The post-exec validator, which fills receipt blooms for full
					// blocks, skips partial ones — do it here, even when prior
					// receipts couldn't be reconstructed (suffix still needs blooms).
					receipts.DeriveFields(finalizeReceipts, txTask.BlockHash())
				}

				_, err = se.cfg.engine.Finalize(
					se.cfg.chainConfig, types.CopyHeader(txTask.Header), ibs, txTask.Uncles,
					finalizeReceipts, txTask.Withdrawals, chainReader, syscall, false, se.logger)

				if err != nil {
					return fmt.Errorf("%w, txnIdx=%d, %w", rules.ErrInvalidBlock, txTask.TxIndex, err)
				}

				result.Logs = append(result.Logs, ibs.GetRawLogs(txTask.TxIndex)...)

				if priorComplete && !isInitialCycle {
					se.cfg.notifications.RecentReceipts.Add(finalizeReceipts, txTask.Txs, txTask.Header)
				}
				checkBloom := !se.cfg.vmConfig.StatelessExec && !se.cfg.vmConfig.NoReceipts
				checkReceipts := checkBloom && se.cfg.chainConfig.IsByzantium(txTask.BlockNumber())

				if txTask.BlockNumber() > 0 && startTxIndex == 0 {
					//Disable check for genesis. Maybe need somehow improve it in future - to satisfy TestExecutionSpec
					// Block gas = max(regular, state). Pre-Amsterdam: blockStateGasUsed is 0.
					blockGasUsed := max(se.blockGasUsed, se.blockStateGasUsed)
					if err := validateBlockPostExecution(se.cfg.engine, se.cfg.chainConfig, txTask.Header, blockGasUsed, se.blobGasUsed, checkReceipts, checkBloom, blockReceipts, txTask.Txs, se.logger); err != nil {
						return fmt.Errorf("%w, txnIdx=%d, %w", rules.ErrInvalidBlock, txTask.TxIndex, err) //same as in stage_exec.go
					}
				}

				// Pass se.accumulator so that AuRa / system-call nonce changes
				// are included in the txpool state-diff batch (fixes empty block
				// production on Gnosis Chain caused by stale pending txns).
				stateWriter := state.NewWriter(se.doms.AsPutDel(se.applyTx), se.accumulator, txTask.TxNum)

				if err = ibs.MakeWriteSet(txTask.Rules(), stateWriter); err != nil {
					panic(err)
				}
			} else if txTask.TxIndex >= 0 {
				var prev, receipt *types.Receipt
				if txTask.TxIndex > 0 && txTask.TxIndex-startTxIndex > 0 {
					prev = blockReceipts[txTask.TxIndex-startTxIndex-1]
					receipt, err = result.CreateNextReceipt(prev)
					if err != nil {
						return err
					}
				} else if txTask.TxIndex > 0 {
					// reconstruct receipt from previous receipt values
					cumGasUsed, cumBlobGasUsed, logIndexAfterTx, err := rawtemporaldb.ReceiptAsOf(se.applyTx, txTask.TxNum)
					if err != nil {
						return err
					}
					// This tx's own blob gas was accumulated above; fold in the
					// pre-resume cumulative so ApplyTxIndexes persists correct values.
					se.blobGasUsed += cumBlobGasUsed
					receipt, err = result.CreateReceipt(txTask.TxIndex, cumGasUsed+result.ExecutionResult.ReceiptGasUsed, logIndexAfterTx)
					if err != nil {
						return err
					}
				} else {
					receipt, err = result.CreateNextReceipt(nil)
					if err != nil {
						return err
					}
				}

				blockReceipts = append(blockReceipts, receipt)
				if hooks := result.TracingHooks(); hooks != nil && hooks.OnTxEnd != nil {
					hooks.OnTxEnd(receipt, result.Err)
				}
			} else {
				se.onBlockStart(ctx, txTask.BlockNumber(), txTask.BlockHash())
			}

			if se.cfg.syncCfg.ChaosMonkey && se.enableChaosMonkey {
				chaosErr := chaos_monkey.ThrowRandomConsensusError(false, txTask.TxIndex, se.cfg.badBlockHalt, result.Err)
				if chaosErr != nil {
					log.Warn("Monkey in a consensus")
					return chaosErr
				}
			}
			return nil
		}(); err != nil {
			if errors.Is(err, context.Canceled) {
				return false, err
			}
			se.logger.Warn(fmt.Sprintf("[%s] Execution failed", se.logPrefix),
				"block", txTask.BlockNumber(), "txNum", txTask.TxNum, "header-hash", txTask.Header.Hash().String(), "err", err, "isForkValidation", se.isForkValidation)
			if se.cfg.badBlockHalt {
				return false, err
			}

			if se.u != nil {
				unwindReason := ExecUnwind
				if errors.Is(err, rules.ErrInvalidBlock) {
					unwindReason = BadBlock(txTask.Header.Hash(), err)
				}
				if err := se.u.UnwindTo(txTask.BlockNumber()-1, unwindReason, se.applyTx); err != nil {
					return false, err
				}
			}

			return false, err
		}

		var applyReceipt *types.Receipt
		if txTask.TxIndex >= 0 && txTask.TxIndex-startTxIndex < len(blockReceipts) {
			applyReceipt = blockReceipts[txTask.TxIndex-startTxIndex]
		}

		if txTask.IsBlockEnd() {
			if se.cfg.chainConfig.Bor != nil && txTask.TxIndex >= 1 {
				// get last receipt and store the last log index + 1
				if len(blockReceipts) >= txTask.TxIndex-startTxIndex {
					applyReceipt = blockReceipts[txTask.TxIndex-startTxIndex-1]
				}

				if applyReceipt == nil {
					if startTxIndex > 0 {
						// if we're in the startup block and the last tx has been skipped we'll
						// need to run it as a historic tx to recover its logs
						prevTask := *txTask
						prevTask.HistoryExecution = true
						prevTask.ResetTx(txTask.TxNum-1, txTask.TxIndex-1)
						result := se.worker.RunTxTaskNoLock(&prevTask)
						if result.Err != nil {
							return false, fmt.Errorf("error while finding last receipt: %w", result.Err)
						}
						var cumulativeGasUsed uint64
						var logIndexAfterTx uint32
						if txTask.TxIndex > 1 {
							cumulativeGasUsed, _, logIndexAfterTx, err = rawtemporaldb.ReceiptAsOf(se.applyTx, txTask.TxNum-2)
							if err != nil {
								return false, err
							}
						}
						applyReceipt, err = result.CreateReceipt(txTask.TxIndex-1,
							cumulativeGasUsed+result.ExecutionResult.ReceiptGasUsed, logIndexAfterTx)
						if err != nil {
							return false, err
						}
					} else {
						return false, fmt.Errorf("receipt is nil but should be populated, txIndex=%d, block=%d", txTask.TxIndex-1, txTask.BlockNumber())
					}
				}
			}
		}

		if !txTask.HistoryExecution {
			if err := se.rs.ApplyStateWrites(ctx, se.applyTx, txTask.BlockNumber(), txTask.TxNum, nil,
				txTask.BalanceIncreaseSet, txTask.Rules(), nil); err != nil {
				return false, err
			}
			if err := se.rs.ApplyTxIndexes(se.applyTx, txTask.TxNum, applyReceipt, se.blobGasUsed,
				result.Logs, result.TraceFroms, result.TraceTos); err != nil {
				return false, err
			}
			if err := se.rs.CommitStepBoundary(ctx, se.applyTx, txTask.BlockNumber(), txTask.TxNum); err != nil {
				return false, err
			}
		}

		se.doms.SetTxNum(txTask.TxNum)
		se.lastBlockResult = &blockResult{
			BlockNum:  txTask.BlockNumber(),
			lastTxNum: txTask.TxNum,
		}
		se.lastExecutedTxNum.Store(int64(txTask.TxNum))
		se.lastExecutedBlockNum.Store(int64(txTask.BlockNumber()))

		if task.IsBlockEnd() {
			se.executedGas.Add(int64(max(se.blockGasUsed, se.blockStateGasUsed)))
			se.blockGasUsed = 0
			se.blockStateGasUsed = 0
			se.blobGasUsed = 0
			gasPool = nil
		}
	}

	return true, nil
}
