// 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 exec

import (
	"context"
	"fmt"
	"runtime/pprof"
	"sync"
	"sync/atomic"
	"time"

	"golang.org/x/sync/errgroup"

	"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/datadir"
	"github.com/erigontech/erigon/db/dbservices"
	"github.com/erigontech/erigon/db/kv"
	"github.com/erigontech/erigon/db/state/kvmetrics"
	"github.com/erigontech/erigon/diagnostics/metrics"
	"github.com/erigontech/erigon/execution/chain"
	"github.com/erigontech/erigon/execution/protocol"
	"github.com/erigontech/erigon/execution/protocol/rules"
	"github.com/erigontech/erigon/execution/state"
	"github.com/erigontech/erigon/execution/tracing/calltracer"
	"github.com/erigontech/erigon/execution/types"
	"github.com/erigontech/erigon/execution/vm"
	"github.com/erigontech/erigon/execution/vm/evmtypes"
	"github.com/erigontech/erigon/node/shards"
)

var noop = state.NewNoopWriter()

type WorkerMetrics struct {
	Active              activeCount
	GasUsed             activeCount
	Duration            activeDuration
	ReadCount           atomic.Int64
	ReadDuration        activeDuration
	AccountReadCount    atomic.Int64
	AccountReadDuration activeDuration
	StorageReadCount    atomic.Int64
	StorageReadDuration activeDuration
	CodeReadCount       atomic.Int64
	CodeReadDuration    activeDuration
	WriteDuration       activeDuration
}

func NewWorkerMetrics() *WorkerMetrics {
	return &WorkerMetrics{
		Active:              activeCount{Ema: metrics.NewEmaWithBeta[int64](0, 1, 0.2)},
		GasUsed:             activeCount{Ema: metrics.NewEma[int64](0, 0.3)},
		Duration:            activeDuration{Ema: metrics.NewEma[time.Duration](0, 0.3)},
		ReadDuration:        activeDuration{Ema: metrics.NewEma[time.Duration](0, 0.3)},
		AccountReadDuration: activeDuration{Ema: metrics.NewEma[time.Duration](0, 0.3)},
		StorageReadDuration: activeDuration{Ema: metrics.NewEma[time.Duration](0, 0.3)},
		CodeReadDuration:    activeDuration{Ema: metrics.NewEma[time.Duration](0, 0.3)},
		WriteDuration:       activeDuration{Ema: metrics.NewEma[time.Duration](0, 0.3)},
	}
}

type activeDuration struct {
	atomic.Int64
	Ema *metrics.EMA[time.Duration]
}

func (d *activeDuration) Add(i time.Duration) {
	d.Int64.Add(int64(i))
	d.Ema.Update(i)
}

type activeCount struct {
	atomic.Int64
	Total atomic.Int64
	Ema   *metrics.EMA[int64]
}

func (c *activeCount) Add(i int64) {
	c.Int64.Add(i)
	if i > 0 {
		c.Total.Add(i)
	}
	c.Ema.Update(c.Load())
}

type Worker struct {
	lock     *sync.RWMutex
	notifier *sync.Cond
	runnable atomic.Bool
	logger   log.Logger
	chainDb  kv.TemporalRoDB
	// chainRoTx is the raw MDBX roTx that owns this worker's snapshot — the
	// only handle that may be Rolled back. chainTx is the overlay-aware view
	// (chainRoTx wrapped with the SharedDomains BlockOverlay if one is active);
	// all reads must go through chainTx so any new consumer is overlay-aware
	// by construction.
	chainRoTx   kv.TemporalTx
	chainTx     kv.TemporalTx
	background  bool // if true - worker does manage RoTx (begin/rollback) in .ResetTx()
	blockReader dbservices.FullBlockReader
	in          *QueueWithRetry
	rs          *state.StateV3Buffered
	stateWriter state.StateWriter
	stateReader state.StateReader
	historyMode bool // if true - stateReader is HistoryReaderV3, otherwise it's state reader
	chainConfig *chain.Config

	ctx     context.Context
	engine  rules.Engine
	genesis *types.Genesis
	results *ResultsQueue
	chain   rules.ChainReader

	evm *vm.EVM
	ibs *state.IntraBlockState

	dirs datadir.Dirs

	metrics *WorkerMetrics
	// readMetrics is this worker's private domain read-metrics accumulator
	// (lock-free, single-owner). The worker's state reader records into it; at
	// task end it is folded into the per-batch log aggregate and the collector
	// accumulator, then reset (a lock per task, not per read).
	readMetrics *kvmetrics.DomainMetrics
	// collectorAcc retains this worker's reads destined for the process-level
	// collector. At task end the task's readMetrics is folded in, then a
	// non-blocking TrySend hands it off (and a fresh one is allocated). If the
	// collector buffer is momentarily full the send is skipped and the worker
	// keeps adding to the same accumulator — so the collector path never blocks
	// execution and never drops counts. Flushed (blocking) when Run exits.
	collectorAcc *kvmetrics.DomainMetrics
}

// installWorkerGetHash replaces the EVM's GetHash function with one that
// uses the worker's own chainTx for BLOCKHASH lookups, avoiding any share
// of the executeBlocks goroutine's roTx across worker goroutines (data
// race). chainTx is already overlay-aware (see resetTx) so headers staged
// in the BlockOverlay but not yet flushed to MDBX are visible.
func (rw *Worker) installWorkerGetHash(txTask Task) {
	header := txTask.BlockHeader()
	if header == nil {
		return
	}
	workerTx := rw.chainTx
	br := rw.blockReader
	ctx := rw.ctx
	rw.evm.Context.GetHash = protocol.GetHashFn(header, func(hash common.Hash, number uint64) (*types.Header, error) {
		h, err := br.Header(ctx, workerTx, hash, number)
		if h == nil && err == nil {
			h = &types.Header{}
		}
		return h, err
	})
}

func NewWorker(ctx context.Context, background bool, metrics *WorkerMetrics, chainDb kv.TemporalRoDB, in *QueueWithRetry, blockReader dbservices.FullBlockReader, chainConfig *chain.Config, genesis *types.Genesis, results *ResultsQueue, engine rules.Engine, dirs datadir.Dirs, logger log.Logger) *Worker {
	lock := &sync.RWMutex{}

	w := &Worker{
		lock:     lock,
		notifier: sync.NewCond(lock),
		chainDb:  chainDb,
		in:       in,

		logger: logger,
		ctx:    ctx,

		background:  background,
		blockReader: blockReader,

		chainConfig: chainConfig,
		genesis:     genesis,
		results:     results,
		engine:      engine,

		evm: vm.NewEVM(evmtypes.BlockContext{}, evmtypes.TxContext{}, nil, chainConfig, vm.Config{}),

		dirs:         dirs,
		metrics:      metrics,
		readMetrics:  kvmetrics.NewDomainMetrics(),
		collectorAcc: kvmetrics.NewDomainMetrics(),
	}
	w.runnable.Store(true)
	w.ibs = state.New(w.stateReader)
	return w
}

func (rw *Worker) Pause() {
	rw.runnable.Store(false)
}

func (rw *Worker) Paused() (waiter chan any, paused bool) {
	if rw.runnable.Load() {
		return nil, false
	}

	rw.results.Lock()
	defer rw.results.Unlock()

	canlock := rw.lock.TryLock()

	if canlock {
		rw.lock.Unlock()
	} else {
		waiter = rw.results.AddWaiter(false)
	}

	return waiter, canlock
}

func (rw *Worker) Resume() {
	rw.runnable.Store(true)
	rw.notifier.Signal()
}

func (rw *Worker) LogLRUStats() {}

func (rw *Worker) ResetState(rs *state.StateV3Buffered, chainTx kv.TemporalTx, stateReader state.StateReader, stateWriter state.StateWriter, accumulator *shards.Accumulator) error {
	rw.lock.Lock()
	defer rw.lock.Unlock()

	rw.rs = rs

	if stateReader != nil {
		rw.SetReader(stateReader)
	} else {
		var getter kv.TemporalGetter
		if chainTx != nil {
			getter = rs.Domains().AsGetterMetered(chainTx, rw.readMetrics)
		}
		// Use CachedReaderV3 for parallel workers — caches account data
		// on first read per block, providing a stable pre-block committed
		// view for GetCommittedState. The blockStateCache is set per block
		// via SetBlockStateCache before workers start.
		rw.SetReader(state.NewCachedReaderV3(getter, nil))
	}

	if stateWriter != nil {
		rw.stateWriter = stateWriter
	} else {
		var putdel kv.TemporalPutDel
		if chainTx != nil {
			putdel = rs.Domains().AsPutDel(chainTx)
		}
		rw.stateWriter = state.NewWriter(putdel, accumulator, 0)
	}

	if chainTx != nil {
		if err := rw.resetTx(chainTx); err != nil {
			return err
		}
	}

	return nil
}

func (rw *Worker) ResetTx(chainTx kv.TemporalTx) error {
	rw.lock.Lock()
	defer rw.lock.Unlock()
	return rw.resetTx(chainTx)
}

func (rw *Worker) resetTxNum(txNum uint64) {
	type resettable interface {
		SetTxNum(txNum uint64)
	}

	if resettable, ok := rw.stateReader.(resettable); ok {
		resettable.SetTxNum(txNum)
	}

	if resettable, ok := rw.stateWriter.(resettable); ok {
		resettable.SetTxNum(txNum)
	}
}

func (rw *Worker) resetTx(chainTx kv.TemporalTx) error {
	if rw.background && rw.chainRoTx != nil {
		rw.chainRoTx.Rollback()
	}

	rw.chainRoTx = chainTx
	// Wrap with BlockOverlay so block-metadata reads (headers/bodies/td
	// staged by InsertBlocks at chaintip) are visible. Mirrors the blockTx
	// pattern in executeBlocks (exec3.go:538-543). Single wrap here so every
	// consumer of rw.chainTx is overlay-aware by construction.
	if chainTx != nil && rw.rs != nil {
		if sd := rw.rs.Domains(); sd != nil {
			if overlay := sd.BlockOverlay(); overlay != nil {
				chainTx = overlay.NewReadView(chainTx)
			}
		}
	}
	rw.chainTx = chainTx

	if rw.chainTx != nil {
		type latest interface {
			SetGetter(kv.TemporalGetter)
		}

		type historic interface {
			SetTx(kv.TemporalTx)
		}

		switch typedReader := rw.stateReader.(type) {
		case latest:
			typedReader.SetGetter(rw.rs.Domains().AsGetterMetered(rw.chainTx, rw.readMetrics))
		case historic:
			typedReader.SetTx(rw.chainTx)
		default:
			if rw.stateReader != nil {
				return fmt.Errorf("can't set tx for reader: %T", rw.stateReader)
			}
		}

		type withPutter interface {
			SetPutDel(kv.TemporalPutDel)
		}

		type withTx interface {
			SetTx(kv.TemporalTx)
		}

		switch typedWriter := rw.stateWriter.(type) {
		case withPutter:
			typedWriter.SetPutDel(rw.rs.Domains().AsPutDel(rw.chainTx))
		case withTx:
			typedWriter.SetTx(rw.chainTx)
		default:
			// Writers that don't implement withPutter or withTx (e.g.
			// NoopWriter, LightCollector) don't need a DB transaction —
			// they accumulate writes in memory only. This is not an error.
		}

		rw.chain = consensuschain.NewReader(rw.chainConfig, rw.chainTx, rw.blockReader, rw.logger)
	} else {
		rw.chain = nil
		rw.stateReader = nil
		rw.stateWriter = nil
	}

	return nil
}

func (rw *Worker) Run() (err error) {
	pprof.SetGoroutineLabels(pprof.WithLabels(rw.ctx, pprof.Labels("sub", "exec-worker")))
	defer func() {
		if rec := recover(); rec != nil {
			err = fmt.Errorf("exec.Worker panic: %s, %s", rec, dbg.Stack())
			rw.logger.Warn("Worker failed", "err", err)
		}
	}()
	// Ensure the worker's roTx is closed when Run exits, preventing
	// MDBX reader slot leaks that block GC page reclamation.
	defer func() {
		if rw.background && rw.chainRoTx != nil {
			rw.chainRoTx.Rollback()
			rw.chainRoTx = nil
			rw.chainTx = nil
		}
	}()

	for txTask, ok := rw.in.Next(rw.ctx); ok; txTask, ok = rw.in.Next(rw.ctx) {
		result := func() (result *TxResult) {
			defer func() {
				if rec := recover(); rec != nil {
					result = &TxResult{
						Task: txTask,
						Err:  fmt.Errorf("exec task panic: %s, %s", rec, dbg.Stack()),
					}
				}
			}()
			return rw.RunTxTask(txTask)
		}()
		if err := rw.results.Add(rw.ctx, result); err != nil {
			return err
		}
		// Fold this task's reads into the per-batch log aggregate and the
		// retained collector accumulator, then reset. Off the hot path (the
		// result is already queued). The collector hand-off is a non-blocking
		// TrySend: on a full buffer it is skipped and collectorAcc keeps growing
		// (retried next task), so execution never blocks and no count is lost.
		// Skipped entirely when read metrics are off.
		if dbg.KVReadLevelledMetrics && rw.rs != nil {
			doms := rw.rs.Domains()
			doms.LogMergeMetrics(rw.readMetrics)
			rw.collectorAcc.Merge(rw.readMetrics)
			rw.readMetrics.Reset()
			if c := doms.Collector(); c != nil && c.TrySend(kvmetrics.SourceExec, rw.collectorAcc) {
				rw.collectorAcc = kvmetrics.NewDomainMetrics()
			}
		}
	}
	// Worker is done: flush whatever the collector buffer was too full to take
	// during the run. Blocking is fine here (off the hot path, at teardown), and
	// it must not be lost. Only the collector — the per-task log merges already
	// folded this data into sd.metrics via LogMergeMetrics.
	if dbg.KVReadLevelledMetrics && rw.rs != nil {
		if c := rw.rs.Domains().Collector(); c != nil {
			c.Send(kvmetrics.SourceExec, rw.collectorAcc)
		}
	}
	return nil
}

func (rw *Worker) RunTxTask(txTask Task) (result *TxResult) {
	rw.lock.Lock()
	defer rw.lock.Unlock()

	for !rw.runnable.Load() {
		rw.notifier.Wait()
	}

	if rw.metrics != nil && dbg.KVReadLevelledMetrics {
		rw.metrics.Active.Add(1)
		start := time.Now()
		defer func() {
			rw.metrics.Duration.Add(time.Since(start))
			if readDuration := rw.ibs.ReadDuration(); readDuration > 0 {
				rw.metrics.ReadDuration.Add(rw.ibs.ReadDuration())
				rw.metrics.ReadCount.Add(rw.ibs.ReadCount())
				rw.metrics.AccountReadDuration.Add(rw.ibs.AccountReadDuration())
				rw.metrics.AccountReadCount.Add(rw.ibs.AccountReadCount())
				rw.metrics.StorageReadDuration.Add(rw.ibs.StorageReadDuration())
				rw.metrics.StorageReadCount.Add(rw.ibs.StorageReadCount())
				rw.metrics.CodeReadDuration.Add(rw.ibs.CodeReadDuration())
				rw.metrics.CodeReadCount.Add(rw.ibs.CodeReadCount())
			}
			if result != nil {
				// EIP-8037: per-tx max(regular, state) overestimates vs the true block gas
				// (max of sums, not sum of maxes), but is a safe upper bound for metrics.
				rw.metrics.GasUsed.Add(int64(max(result.ExecutionResult.BlockRegularGasUsed, result.ExecutionResult.BlockStateGasUsed)))
			}
			rw.metrics.Active.Add(-1)
		}()
	}

	result = rw.RunTxTaskNoLock(txTask)
	return result
}

// Needed to set history reader when need to offset few txs from block beginning and does not break processing,
// like compute gas used for block and then to set state reader to continue processing on latest data.
func (rw *Worker) SetReader(reader state.StateReader) {
	rw.stateReader = reader
	type latest interface {
		SetGetter(kv.TemporalGetter)
	}

	type historic interface {
		SetTx(kv.TemporalTx)
	}

	switch typedReader := rw.stateReader.(type) {
	case latest:
		typedReader.SetGetter(rw.rs.Domains().AsGetterMetered(rw.chainTx, rw.readMetrics))
	case historic:
		typedReader.SetTx(rw.chainTx)
	}
	rw.ibs = state.New(rw.stateReader)

	switch reader.(type) {
	case *state.HistoryReaderV3:
		rw.historyMode = true
	default:
		rw.historyMode = false
	}
}

// SetBlockStateCache updates the block-level account cache on the worker's
// CachedReaderV3. Called before each block's workers start execution.
func (rw *Worker) SetBlockStateCache(cache *state.BlockStateCache) {
	if cr, ok := rw.stateReader.(*state.CachedReaderV3); ok {
		cr.SetBlockStateCache(cache)
	}
}

func (rw *Worker) RunTxTaskNoLock(txTask Task) *TxResult {
	if txTask.IsHistoric() && !rw.historyMode {
		// in case if we cancelled execution and commitment happened in the middle of the block, we have to process block
		// from the beginning until committed txNum and only then disable history mode.
		// Needed to correctly evaluate spent gas and other things.
		// Chain sd.mem → chainTx so historic-mode reads see prior-tx writes
		// from the current batch (same class as the record-vs-field fix in
		// the coinbase race investigation).
		rw.SetReader(state.NewHistoryReaderV3WithSharedDomains(rw.chainTx, rw.rs.Domains(), txTask.Version().TxNum))
	} else if !txTask.IsHistoric() && (rw.stateReader == nil || rw.historyMode) {
		rw.SetReader(state.NewCachedReaderV3(rw.rs.Domains().AsGetterMetered(rw.chainTx, rw.readMetrics), nil))
	}

	// Set the per-block committed state cache from the task.
	if cache := txTask.GetBlockStateCache(); cache != nil {
		rw.SetBlockStateCache(cache)
	}

	if rw.background && rw.chainTx == nil {
		chainTx, err := rw.chainDb.BeginTemporalRo(rw.ctx) //nolint

		if err != nil {
			return &TxResult{
				Task: txTask,
				Err:  err,
			}
		}

		if err = rw.resetTx(chainTx); err != nil {
			return &TxResult{
				Task: txTask,
				Err:  err,
			}
		}
	}

	txIndex := txTask.Version().TxIndex

	var callTracer *calltracer.CallTracer

	if txIndex != -1 && !txTask.IsBlockEnd() {
		callTracer = calltracer.NewCallTracer(txTask.TracingHooks())
	}

	rw.resetTxNum(txTask.Version().TxNum)

	if err := txTask.Reset(rw.evm, rw.ibs, callTracer); err != nil {
		return &TxResult{
			Task: txTask,
			Err:  err,
		}
	}

	// Override GetHash with a per-worker function that uses the worker's
	// own chainTx. The shared blockTx from executeBlocks is not thread-safe.
	if rw.background && rw.chainTx != nil && rw.blockReader != nil {
		rw.installWorkerGetHash(txTask)
	}

	result := txTask.Execute(rw.evm, rw.engine, rw.genesis, rw.ibs, rw.stateWriter, rw.chainConfig, rw.chain, rw.dirs, true)

	if result.Task == nil {
		result.Task = txTask
	}

	if callTracer != nil {
		result.TraceFroms = callTracer.Froms()
		result.TraceTos = callTracer.Tos()
	}

	// Capture collector-format writes from LightCollector (parallel workers).
	// MakeWriteSet already wrote to rw.stateWriter; extract the accumulated
	// writes so finalize can use them directly without IBS reconstruction.
	if lc, ok := rw.stateWriter.(*state.LightCollector); ok {
		result.CollectorWrites = lc.TakeWrites()
	}

	return result
}

func NewWorkersPool(ctx context.Context, accumulator *shards.Accumulator, background bool, chainDb kv.TemporalRoDB,
	rs *state.StateV3Buffered, stateReader state.StateReader, stateWriter state.StateWriter, in *QueueWithRetry, blockReader dbservices.FullBlockReader, chainConfig *chain.Config, genesis *types.Genesis,
	engine rules.Engine, workerCount int, metrics *WorkerMetrics, dirs datadir.Dirs, logger log.Logger) (reconWorkers []*Worker, applyWorker *Worker, rws *ResultsQueue, clear func(), wait func(), err error) {
	reconWorkers = make([]*Worker, workerCount)

	resultsSize := workerCount * 8
	rws = NewResultsQueue(resultsSize, workerCount)

	g, gctx := errgroup.WithContext(ctx)
	for i := range workerCount {
		reconWorkers[i] = NewWorker(gctx, background, metrics, chainDb, in, blockReader, chainConfig, genesis, rws, engine, dirs, logger)

		if rs != nil {
			reader := stateReader

			if reader == nil {
				reader = state.NewReaderV3(rs.Domains().AsGetterMetered(nil, reconWorkers[i].readMetrics))
			}

			if err = reconWorkers[i].ResetState(rs, nil, reader, stateWriter, accumulator); err != nil {
				return
			}
		}
	}
	if background {
		for i := range workerCount {
			g.Go(func() error {
				return reconWorkers[i].Run()
			})
		}
		wait = func() { g.Wait() }
	}

	var clearDone bool
	clear = func() {
		if clearDone {
			return
		}
		clearDone = true
		g.Wait()
		for _, w := range reconWorkers {
			if err = w.ResetTx(nil); err != nil {
				return
			}
		}
		//applyWorker.ResetTx(nil)
	}
	applyWorker = NewWorker(ctx, false, nil, chainDb, in, blockReader, chainConfig, genesis, rws, engine, dirs, logger)

	return reconWorkers, applyWorker, rws, clear, wait, err
}
