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

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"os"
	"runtime"
	"slices"
	"strings"
	"sync"
	"time"

	"github.com/c2h5oh/datasize"
	"github.com/erigontech/mdbx-go/mdbx"
	"github.com/erigontech/secp256k1"
	"github.com/spf13/cobra"
	"golang.org/x/sync/errgroup"
	"golang.org/x/sync/semaphore"

	"github.com/erigontech/erigon/cl/clparams"
	"github.com/erigontech/erigon/cmd/hack/tool/fromdb"
	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/dbg"
	"github.com/erigontech/erigon/common/dir"
	"github.com/erigontech/erigon/common/estimate"
	"github.com/erigontech/erigon/common/log/v3"
	"github.com/erigontech/erigon/db/datadir"
	"github.com/erigontech/erigon/db/integrity"
	"github.com/erigontech/erigon/db/kv"
	"github.com/erigontech/erigon/db/kv/dbcfg"
	"github.com/erigontech/erigon/db/kv/prune"
	"github.com/erigontech/erigon/db/migrations"
	"github.com/erigontech/erigon/db/rawdb"
	"github.com/erigontech/erigon/db/rawdb/blockio"
	"github.com/erigontech/erigon/db/rawdb/rawtemporaldb"
	"github.com/erigontech/erigon/db/services"
	"github.com/erigontech/erigon/db/snapshotsync/freezeblocks"
	dbstate "github.com/erigontech/erigon/db/state"
	"github.com/erigontech/erigon/db/state/execctx"
	"github.com/erigontech/erigon/db/state/stats"
	"github.com/erigontech/erigon/execution/builder/buildercfg"
	chain2 "github.com/erigontech/erigon/execution/chain"
	chainspec "github.com/erigontech/erigon/execution/chain/spec"
	"github.com/erigontech/erigon/execution/protocol/rules"
	"github.com/erigontech/erigon/execution/stagedsync"
	"github.com/erigontech/erigon/execution/stagedsync/rawdbreset"
	"github.com/erigontech/erigon/execution/stagedsync/stageloop"
	"github.com/erigontech/erigon/execution/stagedsync/stages"
	"github.com/erigontech/erigon/execution/state/genesiswrite"
	"github.com/erigontech/erigon/execution/tracing"
	"github.com/erigontech/erigon/execution/types"
	"github.com/erigontech/erigon/execution/vm"
	"github.com/erigontech/erigon/node/debug"
	"github.com/erigontech/erigon/node/eth"
	"github.com/erigontech/erigon/node/ethconfig"
	"github.com/erigontech/erigon/node/ethconfig/features"
	"github.com/erigontech/erigon/node/logging"
	"github.com/erigontech/erigon/node/nodecfg"
	"github.com/erigontech/erigon/node/rulesconfig"
	"github.com/erigontech/erigon/node/shards"
	"github.com/erigontech/erigon/p2p"
	"github.com/erigontech/erigon/p2p/sentry"
	"github.com/erigontech/erigon/p2p/sentry/sentry_multi_client"
	"github.com/erigontech/erigon/polygon/bor/borcfg"
	"github.com/erigontech/erigon/polygon/bridge"
	"github.com/erigontech/erigon/polygon/heimdall"
	"github.com/erigontech/erigon/polygon/heimdall/poshttp"

	_ "github.com/erigontech/erigon/polygon/chain" // Register Polygon chains
)

// makeStageCmd creates a cobra command that opens the DB, runs stageFn, and handles errors.
// Set applyMigrations=false for read-only commands. Set timeit=true to log elapsed time.
// Set debugVerbosity=true to force log verbosity to "debug" before SetupCobra.
func makeStageCmd(use string, stageFn func(kv.TemporalRwDB, context.Context, log.Logger) error, applyMigrations, timeit, debugVerbosity bool) *cobra.Command {
	return &cobra.Command{
		Use:          use,
		Short:        "",
		SilenceUsage: true,
		RunE: func(cmd *cobra.Command, args []string) error {
			if debugVerbosity {
				cmd.Flags().Set(logging.LogConsoleVerbosityFlag.Name, "debug")
			}
			logger := debug.SetupCobra(cmd, "integration")
			db, err := openDB(dbCfg(dbcfg.ChainDB, chaindata), applyMigrations, chain, logger)
			if err != nil {
				return fmt.Errorf("opening DB: %w", err)
			}
			defer db.Close()
			if timeit {
				defer func(t time.Time) { logger.Info("total", "took", time.Since(t)) }(time.Now())
			}
			ctx, cancel := context.WithCancel(cmd.Context())
			defer cancel()
			if err := stageFn(db, ctx, logger); err != nil {
				if errors.Is(err, context.Canceled) {
					return nil // graceful shutdown
				}
				return err
			}
			return nil
		},
	}
}

var (
	cmdStageSnapshots   = makeStageCmd("stage_snapshots", stageSnapshots, true, false, false)
	cmdStageHeaders     = makeStageCmd("stage_headers", stageHeaders, true, false, false)
	cmdStageBodies      = makeStageCmd("stage_bodies", stageBodies, true, false, false)
	cmdStageSenders     = makeStageCmd("stage_senders", stageSenders, true, false, false)
	cmdStageExec        = makeStageCmd("stage_exec", stageExec, true, true, false)
	cmdStageCustomTrace = makeStageCmd("stage_custom_trace", stageCustomTrace, true, true, false)
	cmdStageTxLookup    = makeStageCmd("stage_tx_lookup", stageTxLookup, true, false, false)
	cmdPrintStages      = makeStageCmd("print_stages", printAllStages, false, false, true)
)

var cmdAlloc = &cobra.Command{
	Use:     "alloc",
	Example: "integration allocates and holds 1Gb (or given size)",
	Run: func(cmd *cobra.Command, args []string) {
		cmd.Flags().Set(logging.LogConsoleVerbosityFlag.Name, "debug")
		v, err := datasize.ParseString(args[0])
		if err != nil {
			panic(err)
		}
		n := make([]byte, v.Bytes())
		common.Sleep(cmd.Context(), 265*24*time.Hour)
		_ = n
	},
}

var cmdPrintTableSizes = &cobra.Command{
	Use:   "print_table_sizes",
	Short: "",
	Run: func(cmd *cobra.Command, args []string) {
		logger := debug.SetupCobra(cmd, "integration")
		db, err := openDB(dbCfg(dbcfg.ChainDB, chaindata), false, chain, logger)
		if err != nil {
			logger.Error("Opening DB", "error", err)
			return
		}
		defer db.Close()

		tableSizes, err := kv.CollectTableSizes(cmd.Context(), db)
		if err != nil {
			logger.Error("error while collecting table sizes", "err", err)
			return
		}

		var sb strings.Builder
		sb.WriteString("Table")
		sb.WriteRune(',')
		sb.WriteString("Size")
		sb.WriteRune('\n')
		for _, t := range tableSizes {
			sb.WriteString(t.Name)
			sb.WriteRune(',')
			sb.WriteString(common.ByteCount(t.Size))
			sb.WriteRune('\n')
		}

		if outputCsvFile == "" {
			logger.Info("table sizes", "csv", sb.String())
			return
		}

		f, err := os.Create(outputCsvFile)
		if err != nil {
			logger.Error("issue creating file", "file", outputCsvFile, "err", err)
			return
		}

		_, err = f.WriteString(sb.String())
		if err != nil {
			logger.Error("issue writing output to file", "file", outputCsvFile, "err", err)
			return
		}

		logger.Info("wrote table sizes to csv output file", "file", outputCsvFile)
	},
}

var cmdPrintMigrations = &cobra.Command{
	Use:   "print_migrations",
	Short: "",
	Run: func(cmd *cobra.Command, args []string) {
		logger := debug.SetupCobra(cmd, "integration")
		dirs := datadir.New(datadirCli)
		migrationsDB, err := migrations.OpenMigrationsDB(dirs.Migrations, logger)
		if err != nil {
			logger.Error("Opening migrations DB", "error", err)
			return
		}
		defer migrationsDB.Close()
		if err := printAppliedMigrations(migrationsDB, cmd.Context(), logger); err != nil {
			if !errors.Is(err, context.Canceled) {
				logger.Error(err.Error())
			}
			return
		}
	},
}

var cmdRemoveMigration = &cobra.Command{
	Use:   "remove_migration",
	Short: "",
	Run: func(cmd *cobra.Command, args []string) {
		logger := debug.SetupCobra(cmd, "integration")
		dirs := datadir.New(datadirCli)
		migrationsDB, err := migrations.OpenMigrationsDB(dirs.Migrations, logger)
		if err != nil {
			logger.Error("Opening migrations DB", "error", err)
			return
		}
		defer migrationsDB.Close()
		if err := removeMigration(migrationsDB, cmd.Context()); err != nil {
			if !errors.Is(err, context.Canceled) {
				logger.Error(err.Error())
			}
			return
		}
	},
}

var cmdRunMigrations = &cobra.Command{
	Use:   "run_migrations",
	Short: "",
	Run: func(cmd *cobra.Command, args []string) {
		logger := debug.SetupCobra(cmd, "integration")
		migrateDB := func(label kv.Label, path string) {
			logger.Info("Opening DB", "label", label, "path", path)
			// Non-accede and exclusive mode - to apply creation of new tables if needed.
			cfg := dbCfg(label, path).RemoveFlags(mdbx.Accede).Exclusive(true)
			db, err := openDB(cfg, true, chain, logger)
			if err != nil {
				logger.Error("Opening DB", "error", err)
				return
			}
			defer db.Close()
			// Nothing to do, migrations will be applied automatically
		}

		// Chaindata DB *must* be the first one because guaranteed to contain data in Config table
		// (see openSnapshotOnce in allSnapshots below).
		migrateDB(dbcfg.ChainDB, chaindata)

		// Migrations must be applied also to the consensus DB because ConsensusTables contain also ChaindataTables
		// (see kv/tables.go).
		consensus := strings.Replace(chaindata, "chaindata", "aura", 1)
		if exists, err := dir.Exist(consensus); err == nil && exists {
			migrateDB(dbcfg.ConsensusDB, consensus)
		} else {
			consensus = strings.Replace(chaindata, "chaindata", "clique", 1)
			if exists, err := dir.Exist(consensus); err == nil && exists {
				migrateDB(dbcfg.ConsensusDB, consensus)
			}
		}
		// Migrations must be applied also to the Bor heimdall and polygon-bridge DBs.
		heimdall := strings.Replace(chaindata, "chaindata", "heimdall", 1)
		if exists, err := dir.Exist(heimdall); err == nil && exists {
			migrateDB(dbcfg.HeimdallDB, heimdall)
		}
		polygonBridge := strings.Replace(chaindata, "chaindata", "polygon-bridge", 1)
		if exists, err := dir.Exist(polygonBridge); err == nil && exists {
			migrateDB(dbcfg.PolygonBridgeDB, polygonBridge)
		}
	},
}

func init() {
	withConfig(cmdPrintStages)
	withDataDir(cmdPrintStages)
	rootCmd.AddCommand(cmdPrintStages)

	rootCmd.AddCommand(cmdAlloc)

	withDataDir(cmdPrintTableSizes)
	withOutputCsvFile(cmdPrintTableSizes)
	rootCmd.AddCommand(cmdPrintTableSizes)

	// Snapshots: no Heimdall/Unwind, so withStageBase doesn't fit
	withConfig(cmdStageSnapshots)
	withDataDir(cmdStageSnapshots)
	withChain(cmdStageSnapshots)
	withChaosMonkey(cmdStageSnapshots)
	withReset(cmdStageSnapshots)
	rootCmd.AddCommand(cmdStageSnapshots)

	withStageBase(cmdStageHeaders)
	withIntegrityChecks(cmdStageHeaders)
	withReset(cmdStageHeaders)
	rootCmd.AddCommand(cmdStageHeaders)

	withStageBase(cmdStageBodies)
	rootCmd.AddCommand(cmdStageBodies)

	withStageBase(cmdStageSenders)
	withIntegrityChecks(cmdStageSenders)
	withReset(cmdStageSenders)
	withBlock(cmdStageSenders)
	rootCmd.AddCommand(cmdStageSenders)

	withStageBase(cmdStageExec)
	withReset(cmdStageExec)
	withBlock(cmdStageExec)
	withPruneTo(cmdStageExec)
	withTraceFlags(cmdStageExec)
	withChainTipMode(cmdStageExec)
	rootCmd.AddCommand(cmdStageExec)

	withStageBase(cmdStageCustomTrace)
	withReset(cmdStageCustomTrace)
	withBlock(cmdStageCustomTrace)
	withPruneTo(cmdStageCustomTrace)
	withTraceFlags(cmdStageCustomTrace)
	withDomain(cmdStageCustomTrace)
	rootCmd.AddCommand(cmdStageCustomTrace)

	withStageBase(cmdStageTxLookup)
	withReset(cmdStageTxLookup)
	withBlock(cmdStageTxLookup)
	withPruneTo(cmdStageTxLookup)
	rootCmd.AddCommand(cmdStageTxLookup)

	withConfig(cmdPrintMigrations)
	withDataDir(cmdPrintMigrations)
	rootCmd.AddCommand(cmdPrintMigrations)

	withConfig(cmdRemoveMigration)
	withDataDir(cmdRemoveMigration)
	withMigration(cmdRemoveMigration)
	withChain(cmdRemoveMigration)
	withHeimdall(cmdRemoveMigration)
	rootCmd.AddCommand(cmdRemoveMigration)

	withConfig(cmdRunMigrations)
	withDataDir(cmdRunMigrations)
	withChain(cmdRunMigrations)
	withHeimdall(cmdRunMigrations)
	rootCmd.AddCommand(cmdRunMigrations)
}

func stageSnapshots(db kv.TemporalRwDB, ctx context.Context, logger log.Logger) error {
	br, bw := blocksIO(db, logger)

	tx, err := db.BeginTemporalRw(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	if reset {
		if err := stages.SaveStageProgress(tx, stages.Snapshots, 0); err != nil {
			return fmt.Errorf("saving Snapshots progress failed: %w", err)
		}
	}
	dirs := datadir.New(datadirCli)
	if err := rawdbreset.ResetBlocks(tx, db, br, bw, dirs, logger); err != nil {
		return fmt.Errorf("resetting blocks: %w", err)
	}
	domains, err := execctx.NewSharedDomains(ctx, tx, logger)
	if err != nil {
		return err
	}
	defer domains.Close()
	_, blockNum, err := domains.SeekCommitment(ctx, tx)
	if err != nil {
		return err
	}

	// stagedsync.SpawnStageSnapshots(s, ctx, rwTx, logger)
	progress, err := stages.GetStageProgress(tx, stages.Snapshots)
	if err != nil {
		return fmt.Errorf("re-read Snapshots progress: %w", err)
	}

	if blockNum > progress {
		if err := stages.SaveStageProgress(tx, stages.Execution, blockNum); err != nil {
			return fmt.Errorf("saving Snapshots progress failed: %w", err)
		}
		progress, err = stages.GetStageProgress(tx, stages.Snapshots)
		if err != nil {
			return fmt.Errorf("re-read Snapshots progress: %w", err)
		}
	}
	logger.Info("Progress", "snapshots", progress)
	return tx.Commit()
}

func stageHeaders(db kv.TemporalRwDB, ctx context.Context, logger log.Logger) error {
	dirs := datadir.New(datadirCli)
	if err := datadir.ApplyMigrations(dirs); err != nil {
		return err
	}

	br, bw := blocksIO(db, logger)

	if integritySlow {
		log.Info("[integrity] no gaps in canonical headers")
		if err := integrity.NoGapsInCanonicalHeaders(ctx, db, br, true); err != nil {
			return err
		}
		return nil
	}

	if !(unwind > 0 || reset) {
		logger.Error("This command only works with --unwind or --reset options")
		return nil
	}

	return db.Update(ctx, func(tx kv.RwTx) error {
		if reset {
			if err := rawdbreset.ResetBlocks(tx, db, br, bw, dirs, logger); err != nil {
				return err
			}
			return nil
		}

		progress, err := stages.GetStageProgress(tx, stages.Headers)
		if err != nil {
			return fmt.Errorf("read Bodies progress: %w", err)
		}
		var unwindTo uint64
		if unwind > progress {
			unwindTo = 1 // keep genesis
		} else {
			unwindTo = uint64(max(1, int(progress)-int(unwind)))
		}

		if err = stages.SaveStageProgress(tx, stages.Headers, unwindTo); err != nil {
			return fmt.Errorf("saving Headers progress failed: %w", err)
		}
		progress, err = stages.GetStageProgress(tx, stages.Headers)
		if err != nil {
			return fmt.Errorf("re-read Headers progress: %w", err)
		}
		{ // hard-unwind stage_body also
			if err := rawdb.TruncateBlocks(ctx, tx, progress+1); err != nil {
				return err
			}
			progressBodies, err := stages.GetStageProgress(tx, stages.Bodies)
			if err != nil {
				return fmt.Errorf("read Bodies progress: %w", err)
			}
			if progress < progressBodies {
				if err = stages.SaveStageProgress(tx, stages.Bodies, progress); err != nil {
					return fmt.Errorf("saving Bodies progress failed: %w", err)
				}
			}
		}
		// remove all canonical markers from this point
		if err = rawdb.TruncateCanonicalHash(tx, progress+1, false /* markChainAsBad */); err != nil {
			return err
		}
		if err = rawdb.TruncateTd(tx, progress+1); err != nil {
			return err
		}
		hash, ok, err := br.CanonicalHash(ctx, tx, progress-1)
		if err != nil {
			return err
		}
		if !ok {
			return fmt.Errorf("canonical hash not found: %d", progress-1)
		}
		if err = rawdb.WriteHeadHeaderHash(tx, hash); err != nil {
			return err
		}

		logger.Info("Progress", "headers", progress)
		return nil
	})
}

func stageBodies(db kv.TemporalRwDB, ctx context.Context, logger log.Logger) error {
	chainConfig := fromdb.ChainConfig(db)
	_, engine, _, sync := newSync(ctx, db, nil /* miningConfig */, logger)
	defer engine.Close()
	br, bw := blocksIO(db, logger)

	if err := db.Update(ctx, func(tx kv.RwTx) error {
		s := stage(sync, tx, stages.Bodies)

		if unwind > 0 {
			if unwind > s.BlockNumber {
				return errors.New("cannot unwind past 0")
			}

			u := sync.NewUnwindState(stages.Bodies, s.BlockNumber-unwind, s.BlockNumber, true, false)
			cfg := stagedsync.StageBodiesCfg(nil, nil, nil, nil, 0, chainConfig, br, bw)
			if err := stagedsync.UnwindBodiesStage(u, tx, cfg); err != nil {
				return err
			}

			progress, err := stages.GetStageProgress(tx, stages.Bodies)
			if err != nil {
				return fmt.Errorf("re-read Bodies progress: %w", err)
			}
			logger.Info("Progress", "bodies", progress)
			return nil
		}
		logger.Info("This command only works with --unwind option")
		return nil
	}); err != nil {
		return err
	}
	return nil
}

func stageSenders(db kv.TemporalRwDB, ctx context.Context, logger log.Logger) error {
	tmpdir := datadir.New(datadirCli).Tmp
	chainConfig := fromdb.ChainConfig(db)
	_, engine, _, sync := newSync(ctx, db, nil /* miningConfig */, logger)
	defer engine.Close()

	must(sync.SetCurrentStage(stages.Senders))

	br, _ := blocksIO(db, logger)
	if reset {
		return db.Update(ctx, func(tx kv.RwTx) error { return rawdbreset.ResetSenders(ctx, tx) })
	}

	tx, err := db.BeginRw(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	if integritySlow {
		secp256k1.ContextForThread(1)
		for i := block; ; i++ {
			if err := common.Stopped(ctx.Done()); err != nil {
				return err
			}
			h, _ := br.HeaderByNumber(ctx, tx, i)
			if h == nil {
				break
			}
			withoutSenders, senders, err := br.BlockWithSenders(ctx, tx, h.Hash(), h.Number.Uint64())
			if err != nil {
				return err
			}
			withoutSenders.Body().SendersFromTxs() //remove senders info from txs
			txs := withoutSenders.Transactions()
			if txs.Len() != len(senders) {
				logger.Error("not equal amount of senders", "block", i, "db", len(senders), "expect", txs.Len())
				return nil
			}
			if txs.Len() == 0 {
				continue
			}
			signer := types.MakeSigner(chainConfig, i, h.Time)
			for j := 0; j < txs.Len(); j++ {
				from, err := signer.Sender(txs[j])
				if err != nil {
					return err
				}
				fromValue := from.Value()
				if !bytes.Equal(fromValue[:], senders[j][:]) {
					logger.Error("wrong sender", "block", i, "tx", j, "db", fmt.Sprintf("%x", senders[j]), "expect", fmt.Sprintf("%x", from))
				}
			}
			if i%10 == 0 {
				logger.Info("checked", "block", i)
			}
		}
		return nil
	}

	s := stage(sync, tx, stages.Senders)
	logger.Info("Stage", "name", s.ID, "progress", s.BlockNumber)

	pm, err := prune.Get(tx)
	if err != nil {
		return err
	}

	cfg := stagedsync.StageSendersCfg(chainConfig, sync.Cfg(), false /* badBlockHalt */, tmpdir, pm, br, nil /* hd */)
	if unwind > 0 {
		u := sync.NewUnwindState(stages.Senders, s.BlockNumber-unwind, s.BlockNumber, true, false)
		if err = stagedsync.UnwindSendersStage(u, tx, cfg, ctx); err != nil {
			return err
		}
	} else if pruneTo > 0 {
		//noop
		return nil
	} else {
		if err = stagedsync.SpawnRecoverSendersStage(cfg, s, sync, tx, block, ctx, logger); err != nil {
			return err
		}
	}
	return tx.Commit()
}

func stageExec(db kv.TemporalRwDB, ctx context.Context, logger log.Logger) error {
	if chainTipMode && noCommit {
		return errors.New("--sync.mode.chaintip cannot work with --no-commit to be false")
	}
	dirs := datadir.New(datadirCli)
	if err := datadir.ApplyMigrations(dirs); err != nil {
		return err
	}

	_, engine, vmConfig, sync := newSync(ctx, db, nil /* miningConfig */, logger)
	defer engine.Close()
	must(sync.SetCurrentStage(stages.Execution))
	if reset {
		if err := rawdbreset.ResetExec(ctx, db); err != nil {
			return err
		}
		return nil
	}

	if txtrace {
		// Activate tracing and writing into json files for each transaction
		vmConfig.Tracer = &tracing.Hooks{}
	}

	var batchSize datasize.ByteSize
	must(batchSize.UnmarshalText([]byte(batchSizeStr)))

	var s *stagedsync.StageState
	_ = db.ViewTemporal(ctx, func(tx kv.TemporalTx) error {
		s = stage(sync, tx, stages.Execution)
		return nil
	})
	if chainTipMode {
		s.CurrentSyncCycle.IsFirstCycle = false
		s.CurrentSyncCycle.IsInitialCycle = false
	}

	logger.Info("Stage", "name", s.ID, "progress", s.BlockNumber)
	chainConfig, pm := fromdb.ChainConfig(db), fromdb.PruneMode(db)
	if pruneTo > 0 {
		pm.History = prune.Distance(s.BlockNumber - pruneTo)
	}

	genesis := readGenesis(chain)
	br, _ := blocksIO(db, logger)

	notifications := shards.NewNotifications(nil)
	cfg := stagedsync.StageExecuteBlocksCfg(db, pm, batchSize, chainConfig, engine, vmConfig, notifications,
		/*stateStream=*/ false,
		/*badBlockHalt=*/ true,
		dirs, br, nil, genesis, syncCfg, nil /*experimentalBAL=*/, false)

	if unwind > 0 {
		if err := db.ViewTemporal(ctx, func(tx kv.TemporalTx) error {
			minUnwindableBlockNum, _, err := rawtemporaldb.CanUnwindBeforeBlockNum(s.BlockNumber-unwind, tx)
			if err != nil {
				return err
			}
			unwind = s.BlockNumber - minUnwindableBlockNum
			return nil
		}); err != nil {
			return err
		}

		if unwind < pruneTo {
			return fmt.Errorf("can't prune beyond unwind: unwind=%d, prune=%d", unwind, pruneTo)
		}
	}

	tx, err := db.BeginTemporalRw(ctx) //nolint
	if err != nil {
		return err
	}

	defer func() { tx.Rollback() }()

	if pruneTo > 0 {
		p, err := sync.PruneStageState(stages.Execution, s.BlockNumber, tx, true)
		if err != nil {
			return err
		}
		err = stagedsync.PruneExecutionStage(ctx, p, tx, cfg, 0, logger)
		if err != nil {
			return err
		}
		if noCommit {
			return nil
		}
		return tx.Commit()
	}

	var sendersProgress, execProgress uint64

	if execProgress, err = stages.GetStageProgress(tx, stages.Execution); err != nil {
		return err
	}
	if execProgress == 0 { // then fallback to how much data we have in stat_snapshots
		doms, err := execctx.NewSharedDomains(ctx, tx, log.New())
		if err != nil {
			panic(err)
		}
		_, execProgress, err = doms.SeekCommitment(ctx, tx)
		if err != nil {
			panic(err)
		}
		doms.Close()
	}
	if sendersProgress, err = stages.GetStageProgress(tx, stages.Senders); err != nil {
		return err
	}

	if block == 0 {
		block = sendersProgress
	}

	doms, err := execctx.NewSharedDomains(ctx, tx, logger)
	if err != nil {
		return err
	}
	doms.SetInMemHistoryReads(false)

	if chainTipMode {
		//if chainTip = true, forced noCommit = false
		for bn := execProgress; bn < block; bn++ {
			if err := stagedsync.SpawnExecuteBlocksStage(s, sync, doms, tx, bn, ctx, cfg, logger); err != nil {
				if !errors.Is(err, &stagedsync.ErrLoopExhausted{}) {
					return err
				}
			}
			if err := doms.Flush(ctx, tx); err != nil {
				return err
			}
			doms.ClearRam(true)
			if err := tx.Commit(); err != nil {
				return err
			}
			if tx, err = db.BeginTemporalRw(ctx); err != nil {
				return err
			}

			pruneStage, err := sync.PruneStageState(stages.Execution, s.BlockNumber, tx, s.CurrentSyncCycle.IsInitialCycle)
			if err != nil {
				return err
			}
			if err := stagedsync.PruneExecutionStage(ctx, pruneStage, tx, cfg, 0, logger); err != nil {
				return err
			}

			if err := tx.Commit(); err != nil {
				return err
			}
			if tx, err = db.BeginTemporalRw(ctx); err != nil {
				return err
			}
		}
		if err := doms.Flush(ctx, tx); err != nil {
			return err
		}
		doms.ClearRam(true)
		return tx.Commit()
	}
	agg := (db.(dbstate.HasAgg).Agg()).(*dbstate.Aggregator)
	blockSnapBuildSema := semaphore.NewWeighted(int64(runtime.NumCPU()))
	agg.SetSnapshotBuildSema(blockSnapBuildSema)
	agg.PresetOfflineExecution()
	agg.PeriodicalyPrintProcessSet(ctx)
	agg.LockWorkersEditing()

	for {
		if err := stagedsync.SpawnExecuteBlocksStage(s, sync, doms, tx, block, ctx, cfg, logger); err != nil {
			if !errors.Is(err, &stagedsync.ErrLoopExhausted{}) {
				return err
			}
		}

		if err := doms.Flush(ctx, tx); err != nil {
			return err
		}
		doms.ClearRam(true)

		pruneStage, err := sync.PruneStageState(stages.Execution, s.BlockNumber, tx, s.CurrentSyncCycle.IsInitialCycle)
		if err != nil {
			return err
		}
		if err := stagedsync.PruneExecutionStage(ctx, pruneStage, tx, cfg, 0, logger); err != nil {
			return err
		}

		if !noCommit {
			if err := tx.Commit(); err != nil {
				return err
			}
			if tx, err = db.BeginTemporalRw(ctx); err != nil {
				return err
			}
		}

		if execProgress, err = stages.GetStageProgress(tx, stages.Execution); err != nil {
			return err
		}
		if execProgress >= block {
			break
		}
	}

	return nil
}

func stageCustomTrace(db kv.TemporalRwDB, ctx context.Context, logger log.Logger) error {
	dirs := datadir.New(datadirCli)
	if err := datadir.ApplyMigrations(dirs); err != nil {
		return err
	}

	br, engine, vmConfig, sync := newSync(ctx, db, nil /* miningConfig */, logger)
	defer engine.Close()
	must(sync.SetCurrentStage(stages.Execution))

	chainConfig := fromdb.ChainConfig(db)
	genesis := readGenesis(chain)
	blockReader, _ := blocksIO(db, logger)

	cfg := stagedsync.StageCustomTraceCfg(strings.Split(domain, ","), db, dirs, blockReader, chainConfig, engine, genesis, syncCfg)
	if reset {
		if err := stagedsync.StageCustomTraceReset(ctx, db, cfg.Produce); err != nil {
			return err
		}
		//if err := rawdbreset.Reset(ctx, db, stages.CustomTrace); err != nil {
		//	return err
		//}
		return nil
	}

	if txtrace {
		// Activate tracing and writing into json files for each transaction
		vmConfig.Tracer = &tracing.Hooks{}
	}

	var batchSize datasize.ByteSize
	must(batchSize.UnmarshalText([]byte(batchSizeStr)))

	agg := db.(dbstate.HasAgg).Agg().(*dbstate.Aggregator)
	defer br.(*freezeblocks.BlockRetire).MadvNormal().DisableReadAhead()
	//defer agg.MadvNormal().DisableReadAhead()

	blockSnapBuildSema := semaphore.NewWeighted(int64(runtime.NumCPU()))
	agg.SetSnapshotBuildSema(blockSnapBuildSema)
	agg.PresetOfflineExecution()
	agg.PeriodicalyPrintProcessSet(ctx)

	err := stagedsync.SpawnCustomTrace(cfg, ctx, logger)
	if err != nil {
		return err
	}

	return nil
}

func stageTxLookup(db kv.TemporalRwDB, ctx context.Context, logger log.Logger) error {
	dirs, pm := datadir.New(datadirCli), fromdb.PruneMode(db)
	_, engine, _, sync := newSync(ctx, db, nil /* miningConfig */, logger)
	defer engine.Close()

	must(sync.SetCurrentStage(stages.TxLookup))
	if reset {
		return db.Update(ctx, rawdbreset.ResetTxLookup)
	}
	tx, err := db.BeginRw(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	s := stage(sync, tx, stages.TxLookup)
	if pruneTo > 0 {
		pm.History = prune.Distance(s.BlockNumber - pruneTo)
	}
	logger.Info("Stage", "name", s.ID, "progress", s.BlockNumber)

	br, _ := blocksIO(db, logger)
	cfg := stagedsync.StageTxLookupCfg(pm, dirs.Tmp, br)
	if unwind > 0 {
		u := sync.NewUnwindState(stages.TxLookup, s.BlockNumber-unwind, s.BlockNumber, true, false)
		err = stagedsync.UnwindTxLookup(u, s, tx, cfg, ctx, logger)
		if err != nil {
			return err
		}
	} else if pruneTo > 0 {
		p, err := sync.PruneStageState(stages.TxLookup, s.BlockNumber, tx, true)
		if err != nil {
			return err
		}
		err = stagedsync.PruneTxLookup(p, tx, cfg, ctx, logger)
		if err != nil {
			return err
		}
	} else {
		err = stagedsync.SpawnTxLookup(s, tx, block, cfg, ctx, logger)
		if err != nil {
			return err
		}
	}
	return tx.Commit()
}

func printAllStages(db kv.TemporalRwDB, ctx context.Context, logger log.Logger) error {
	sn, borSn, _, _, _, _, _ := allSnapshots(ctx, db, logger) // ignore error here to get some stat.
	defer sn.Close()
	defer borSn.Close()
	return db.ViewTemporal(ctx, func(tx kv.TemporalTx) error { return printStages(tx, sn, borSn) })
}

func printAppliedMigrations(migrationsDB kv.RwDB, ctx context.Context, logger log.Logger) error {
	return migrationsDB.View(ctx, func(tx kv.Tx) error {
		applied, err := migrations.AppliedMigrations(tx, false /* withPayload */)
		if err != nil {
			return err
		}
		var appliedStrs = make([]string, len(applied))
		i := 0
		for k := range applied {
			appliedStrs[i] = k
			i++
		}
		slices.Sort(appliedStrs)
		logger.Info("Applied", "migrations", strings.Join(appliedStrs, " "))
		return nil
	})
}

func removeMigration(migrationsDB kv.RwDB, ctx context.Context) error {
	return migrationsDB.Update(ctx, func(tx kv.RwTx) error {
		return tx.Delete(kv.Migrations, []byte(migration))
	})
}

var openSnapshotOnce sync.Once
var _allSnapshotsSingleton *freezeblocks.RoSnapshots
var _allBorSnapshotsSingleton *heimdall.RoSnapshots
var _allCaplinSnapshotsSingleton *freezeblocks.CaplinSnapshots
var _aggSingleton *dbstate.Aggregator
var _bridgeStoreSingleton bridge.Store
var _heimdallStoreSingleton heimdall.Store

func allSnapshots(ctx context.Context, db kv.RoDB, logger log.Logger) (*freezeblocks.RoSnapshots, *heimdall.RoSnapshots, *dbstate.Aggregator, *freezeblocks.CaplinSnapshots, bridge.Store, heimdall.Store, error) {
	var err error

	openSnapshotOnce.Do(func() {
		if syncCfg, err = features.EnableSyncCfg(db, syncCfg); err != nil {
			return
		}

		dirs := datadir.New(datadirCli)

		chainConfig := fromdb.ChainConfig(db)
		snapCfg := ethconfig.NewSnapCfg(true, true, true, chainConfig.ChainName)

		_allSnapshotsSingleton = freezeblocks.NewRoSnapshots(snapCfg, dirs.Snap, logger)
		_allBorSnapshotsSingleton = heimdall.NewRoSnapshots(snapCfg, dirs.Snap, logger)
		_bridgeStoreSingleton = bridge.NewSnapshotStore(bridge.NewDbStore(db), _allBorSnapshotsSingleton, chainConfig.Bor)
		_heimdallStoreSingleton = heimdall.NewSnapshotStore(heimdall.NewDbStore(db), _allBorSnapshotsSingleton)
		blockReader := freezeblocks.NewBlockReader(_allSnapshotsSingleton, _allBorSnapshotsSingleton)
		txNums := blockReader.TxnumReader()

		var erigonDBSettings *dbstate.ErigonDBSettings
		if erigonDBSettings, err = dbstate.ResolveErigonDBSettings(dirs, logger, false); err != nil {
			return
		}
		_aggSingleton = dbstate.New(dirs).Logger(logger).WithErigonDBSettings(erigonDBSettings).MustOpen(ctx, db)

		_aggSingleton.SetProduceMod(snapCfg.ProduceE3)

		g := &errgroup.Group{}
		g.Go(func() error {
			_allSnapshotsSingleton.OptimisticalyOpenFolder()
			return nil
		})
		g.Go(func() error {
			_allBorSnapshotsSingleton.OptimisticalyOpenFolder()
			return nil
		})
		g.Go(func() error {
			err := _aggSingleton.OpenFolder()
			if err != nil {
				return fmt.Errorf("aggregator opening: %w", err)
			}
			return nil
		})
		g.Go(func() error {
			chainConfig := fromdb.ChainConfig(db)
			var beaconConfig *clparams.BeaconChainConfig
			_, beaconConfig, _, err = clparams.GetConfigsByNetworkName(chainConfig.ChainName)
			if err == nil {
				_allCaplinSnapshotsSingleton = freezeblocks.NewCaplinSnapshots(snapCfg, beaconConfig, dirs, logger)
				if err = _allCaplinSnapshotsSingleton.OpenFolder(); err != nil {
					return fmt.Errorf("caplin snapshots: %w", err)
				}
				_allCaplinSnapshotsSingleton.LogStat("caplin")
			}
			return nil
		})

		if err = g.Wait(); err != nil {
			return
		}

		_allSnapshotsSingleton.LogStat("blocks")
		//_allBorSnapshotsSingleton.LogStat("bor")
		_ = db.View(context.Background(), func(tx kv.Tx) error {
			ac := _aggSingleton.BeginFilesRo()
			defer ac.Close()
			stats.LogStats(ac, tx, logger, func(endTxNumMinimax uint64) (uint64, error) {
				histBlockNumProgress, _, err := txNums.FindBlockNum(ctx, tx, endTxNumMinimax)
				if err != nil {
					return histBlockNumProgress, fmt.Errorf("findBlockNum(%d) fails: %w", endTxNumMinimax, err)
				}
				return histBlockNumProgress, nil
			})
			return nil
		})
	})

	if err != nil {
		log.Error("[snapshots] failed to open", "err", err)
		return nil, nil, nil, nil, nil, nil, err
	}
	return _allSnapshotsSingleton, _allBorSnapshotsSingleton, _aggSingleton, _allCaplinSnapshotsSingleton, _bridgeStoreSingleton, _heimdallStoreSingleton, nil
}

var openBlockReaderOnce sync.Once
var _blockReaderSingleton services.FullBlockReader
var _blockWriterSingleton *blockio.BlockWriter

func blocksIO(db kv.RoDB, logger log.Logger) (services.FullBlockReader, *blockio.BlockWriter) {
	openBlockReaderOnce.Do(func() {
		sn, borSn, _, _, _, _, err := allSnapshots(context.Background(), db, logger)
		if err != nil {
			panic(err)
		}
		_blockReaderSingleton = freezeblocks.NewBlockReader(sn, borSn)
		_blockWriterSingleton = blockio.NewBlockWriter()
	})
	return _blockReaderSingleton, _blockWriterSingleton
}

const blockBufferSize = 128

func newSync(ctx context.Context, db kv.TemporalRwDB, builderConfig *buildercfg.BuilderConfig, logger log.Logger) (
	services.BlockRetire, rules.Engine, *vm.Config, *stagedsync.Sync,
) {
	dirs, pm := datadir.New(datadirCli), fromdb.PruneMode(db)

	vmConfig := &vm.Config{}

	genesis := readGenesis(chain)
	chainConfig, genesisBlock, genesisErr := genesiswrite.CommitGenesisBlock(db, genesis, chain, dirs, logger)
	if _, ok := genesisErr.(*chain2.ConfigCompatError); genesisErr != nil && !ok {
		panic(genesisErr)
	}
	//logger.Info("Initialised chain configuration", "config", chainConfig)

	var batchSize datasize.ByteSize
	must(batchSize.UnmarshalText([]byte(batchSizeStr)))

	cfg := ethconfig.Defaults
	if chainTipMode {
		syncCfg.LoopBlockLimit = 1
		syncCfg.AlwaysGenerateChangesets = true
		noCommit = false
	}

	if !dbg.BatchCommitments {
		syncCfg.AlwaysGenerateChangesets = true
	}

	cfg.Sync = syncCfg

	cfg.Prune = pm
	cfg.BatchSize = batchSize
	cfg.TxPool.Disable = true
	cfg.Genesis = genesis
	if builderConfig != nil {
		cfg.Builder = *builderConfig
	}
	cfg.Dirs = dirs
	dbReadConcurrency := runtime.GOMAXPROCS(-1) * 16
	blockSnapBuildSema := semaphore.NewWeighted(int64(dbg.BuildSnapshotAllowance))
	blockReader, blockWriter, allSn, borSn, bridgeStore, heimdallStore, _, err := eth.SetUpBlockReader(ctx, db, dirs, &cfg, chainConfig, dbReadConcurrency, logger, blockSnapBuildSema)
	if err != nil {
		panic(err)
	}
	cfg.Snapshot = allSn.Cfg()
	if borSn != nil {
		borSn.DownloadComplete() // mark as ready
	}
	engine := initRulesEngine(ctx, chainConfig, cfg.Dirs.DataDir, db, blockReader, bridgeStore, heimdallStore, logger)

	statusDataProvider := sentry.NewStatusDataProvider(
		db,
		chainConfig,
		genesisBlock,
		chainConfig.ChainID.Uint64(),
		logger,
		blockReader,
	)

	maxBlockBroadcastPeers := func(header *types.Header) uint { return 0 }

	sentryControlServer, err := sentry_multi_client.NewMultiClient(
		dirs,
		db,
		chainConfig,
		engine,
		nil,
		ethconfig.Defaults.Sync,
		blockReader,
		blockBufferSize,
		statusDataProvider,
		false,
		maxBlockBroadcastPeers,
		false, /* disableBlockDownload */
		false, /* enableWitProtocol */
		logger,
	)
	if err != nil {
		panic(err)
	}
	notifications := shards.NewNotifications(nil)
	blockRetire := freezeblocks.NewBlockRetire(estimate.CompressSnapshot.Workers(), dirs, blockReader, blockWriter, db, heimdallStore, bridgeStore, chainConfig, &cfg, notifications.Events, blockSnapBuildSema, logger)
	stageList := stageloop.NewDefaultStages(context.Background(), db, p2p.Config{}, &cfg, sentryControlServer, notifications, nil, blockReader, blockRetire, nil, nil, nil)
	sync := stagedsync.New(cfg.Sync, stageList, stagedsync.DefaultUnwindOrder, stagedsync.DefaultPruneOrder, logger, stages.ModeApplyingBlocks)
	return blockRetire, engine, vmConfig, sync
}

func progress(tx kv.Getter, stage stages.SyncStage) uint64 {
	res, err := stages.GetStageProgress(tx, stage)
	if err != nil {
		panic(err)
	}
	return res
}

func stage(st *stagedsync.Sync, tx kv.Tx, stage stages.SyncStage) *stagedsync.StageState {
	res, err := st.StageState(stage, tx, true, false)
	if err != nil {
		panic(err)
	}
	return res
}

func initRulesEngine(ctx context.Context, cc *chain2.Config, dir string, db kv.RwDB, blockReader services.FullBlockReader, bridgeStore bridge.Store, heimdallStore heimdall.Store, logger log.Logger) rules.Engine {
	config := ethconfig.Defaults
	var polygonBridge *bridge.Service
	var heimdallService *heimdall.Service
	var heimdallClient heimdall.Client
	var bridgeClient bridge.Client
	var rulesConfig any
	if cc.Clique != nil {
		rulesConfig = chainspec.CliqueSnapshot
	} else if cc.Aura != nil {
		rulesConfig = &config.Aura
	} else if cc.Bor != nil {
		rulesConfig = cc.Bor
		config.HeimdallURL = HeimdallURL
		if !config.WithoutHeimdall {
			heimdallClient = heimdall.NewHttpClient(config.HeimdallURL, logger, poshttp.WithApiVersioner(ctx))
			bridgeClient = bridge.NewHttpClient(config.HeimdallURL, logger, poshttp.WithApiVersioner(ctx))
		} else {
			heimdallClient = heimdall.NewIdleClient(config.Builder)
			bridgeClient = bridge.NewIdleClient()
		}
		borConfig := rulesConfig.(*borcfg.BorConfig)

		polygonBridge = bridge.NewService(bridge.ServiceConfig{
			Store:        bridgeStore,
			Logger:       logger,
			BorConfig:    borConfig,
			EventFetcher: bridgeClient,
		})

		if err := heimdallStore.Prepare(ctx); err != nil {
			panic(err)
		}

		if err := bridgeStore.Prepare(ctx); err != nil {
			panic(err)
		}

		heimdallService = heimdall.NewService(heimdall.ServiceConfig{
			Store:     heimdallStore,
			BorConfig: borConfig,
			Client:    heimdallClient,
			Logger:    logger,
		})

	} else {
		rulesConfig = &config.Ethash
	}
	return rulesconfig.CreateRulesEngine(ctx, &nodecfg.Config{Dirs: datadir.New(dir)}, cc, rulesConfig,
		false /* noVerify */, config.WithoutHeimdall, blockReader, db.ReadOnly(), logger, polygonBridge, heimdallService)
}

func readGenesis(chain string) *types.Genesis {
	spec, err := chainspec.ChainSpecByName(chain)
	if err != nil || spec.Genesis == nil {
		panic(fmt.Errorf("genesis is nil. probably you passed wrong --chain: %w", err))
	}
	_ = spec.Genesis.Alloc // nil check
	return spec.Genesis
}
