// 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 (
	"context"
	"fmt"
	"path/filepath"

	"github.com/spf13/cobra"
	"golang.org/x/sync/semaphore"

	"github.com/erigontech/erigon/cmd/utils"
	"github.com/erigontech/erigon/common/log/v3"
	"github.com/erigontech/erigon/db/datadir"
	"github.com/erigontech/erigon/db/kv"
	"github.com/erigontech/erigon/db/kv/dbcfg"
	kv2 "github.com/erigontech/erigon/db/kv/mdbx"
	"github.com/erigontech/erigon/db/kv/temporal"
	"github.com/erigontech/erigon/db/migrations"
	"github.com/erigontech/erigon/node/debug"
	"github.com/erigontech/erigon/node/logging"
)

var rootCmd = &cobra.Command{
	Use:   "integration",
	Short: "long and heavy integration tests for Erigon",
	PersistentPreRun: func(cmd *cobra.Command, args []string) {
		if chaindata == "" {
			chaindata = filepath.Join(datadirCli, "chaindata")
		}
	},
	PersistentPostRun: func(cmd *cobra.Command, args []string) {
		defer debug.Exit()
	},
}

func RootCommand() *cobra.Command {
	utils.CobraFlags(rootCmd, debug.Flags, utils.MetricFlags, logging.Flags)
	return rootCmd
}

func dbCfg(label kv.Label, path string) kv2.MdbxOpts {
	const ThreadsLimit = 9_000
	limiterB := semaphore.NewWeighted(ThreadsLimit)
	opts := kv2.New(label, log.New()).
		Path(path).
		RoTxsLimiter(limiterB).
		WriteMap(dbWriteMap).
		Accede(true) // integration tool: must not create db. must open db without stoping erigon.

	if databaseVerbosity != -1 {
		opts = opts.DBVerbosity(kv.DBVerbosityLvl(databaseVerbosity))
	}
	return opts
}

func openDB(ctx context.Context, opts kv2.MdbxOpts, applyMigrations bool, chain string, logger log.Logger) (tdb kv.TemporalRwDB, err error) {
	migrationDBs := map[kv.Label]bool{
		dbcfg.ChainDB:         true,
		dbcfg.ConsensusDB:     true,
		dbcfg.HeimdallDB:      true,
		dbcfg.PolygonBridgeDB: true,
	}
	if _, ok := migrationDBs[opts.GetLabel()]; !ok {
		panic(opts.GetLabel())
	}

	// Apply migrations BEFORE the accede-mode open. In accede mode MDBX cannot
	// create new tables, so if a table was added to the schema after the DB was
	// originally created (e.g. BlockAccessList) the open would panic. The
	// exclusive-mode open used during migration creates any missing tables as a
	// side-effect, preventing the subsequent accede-mode open from panicking.
	if applyMigrations {
		dirs := datadir.New(datadirCli)
		migrationsDB, err := migrations.OpenMigrationsDB(dirs.Migrations, logger)
		if err != nil {
			return nil, fmt.Errorf("open migrations db: %w", err)
		}
		defer migrationsDB.Close()

		migrator := migrations.NewMigrator(opts.GetLabel())
		has, err := migrator.HasPendingMigrations(migrationsDB)
		if err != nil {
			return nil, err
		}
		if has {
			logger.Info("Re-Opening DB in exclusive mode to apply DB migrations")
			rawDBExcl := opts.Exclusive(true).MustOpen()
			if err := migrator.Apply(rawDBExcl, migrationsDB, datadirCli, "", logger); err != nil {
				rawDBExcl.Close()
				return nil, err
			}
			rawDBExcl.Close()
		}
	}

	rawDB := opts.MustOpen()

	dirs := datadir.New(datadirCli)
	if err := CheckSaltFilesExist(dirs); err != nil {
		return nil, err
	}

	blockSnaps, _, agg, _, _, _, err := allSnapshots(ctx, rawDB, logger)
	if err != nil {
		return nil, err
	}

	db, err := temporal.New(rawDB, agg, blockSnaps)
	if err != nil {
		return nil, err
	}
	return db, nil
}
