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

import (
	"cmp"
	"context"
	"errors"
	"fmt"
	"io/fs"
	"net/url"
	"os"
	"path/filepath"
	"runtime"
	"slices"
	"strings"

	g "github.com/anacrolix/generics"
	"github.com/anacrolix/missinggo/v2/panicif"
	"github.com/anacrolix/torrent/metainfo"
	"github.com/go-viper/mapstructure/v2"
	"github.com/pelletier/go-toml/v2"
	"github.com/spf13/cobra"
	"github.com/urfave/cli/v3"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"

	"github.com/erigontech/erigon/cmd/utils/app"
	"github.com/erigontech/erigon/db/downloader/webseeds"

	"github.com/erigontech/erigon/cmd/downloader/downloadernat"
	"github.com/erigontech/erigon/cmd/utils"
	"github.com/erigontech/erigon/cmd/utils/flags"
	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/dbg"
	"github.com/erigontech/erigon/common/dir"
	"github.com/erigontech/erigon/common/log/v3"
	"github.com/erigontech/erigon/db/datadir"
	"github.com/erigontech/erigon/db/downloader"
	"github.com/erigontech/erigon/db/downloader/downloadercfg"
	"github.com/erigontech/erigon/db/downloader/downloadergrpc"
	"github.com/erigontech/erigon/db/fromdb"
	"github.com/erigontech/erigon/db/kv/dbcfg"
	"github.com/erigontech/erigon/db/kv/mdbx"
	"github.com/erigontech/erigon/db/snapcfg"
	"github.com/erigontech/erigon/db/version"
	chainspec "github.com/erigontech/erigon/execution/chain/spec"
	"github.com/erigontech/erigon/node/debug"
	"github.com/erigontech/erigon/node/gointerfaces/downloaderproto"
	"github.com/erigontech/erigon/node/gointerfaces/grpcutil"
	"github.com/erigontech/erigon/node/logging"
	"github.com/erigontech/erigon/node/paths"
	"github.com/erigontech/erigon/p2p/nat"

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

	_ "github.com/erigontech/erigon/db/snaptype2"     //hack
	_ "github.com/erigontech/erigon/polygon/heimdall" //hack
)

func main() {
	var err error
	common.WithProfilersMain(func() {
		ctx, cancel := common.RootContext()
		defer cancel()
		err = rootCmd.ExecuteContext(ctx)
	})
	if err != nil {
		os.Exit(1)
	}
}

var (
	chain                          string
	filePath                       string
	forceRebuild                   bool
	verify                         bool
	verifyFailfast                 bool
	_verifyFiles                   string
	verifyFiles                    []string
	downloaderApiAddr              string
	natSetting                     string
	torrentVerbosity               int
	downloadRateStr, uploadRateStr string
	// Deprecated (v3.0): Captured for backward compatibility but intentionally unused.
	// Cobra doesn't have built-in deprecation, so we handle it via runtime warning (see Downloader() function).
	torrentDownloadSlots int
	staticPeersStr       string
	torrentPort          int
	torrentMaxPeers      int
	torrentConnsPerFile  int
	targetFile           string
	disableIPV6          bool
	disableIPV4          bool
	seedbox              bool
	dbWritemap           bool
	all                  bool
	diffMode             bool
)

var cobraFlagValues struct {
	webseeds string
	//preverifiedSource string
	datadir      string
	chainTomlURL string
}

func init() {
	utils.CobraFlags(rootCmd, debug.Flags, utils.MetricFlags, logging.Flags)

	withDataDir(rootCmd)
	withChainFlag(rootCmd)

	rootCmd.Flags().StringVar(&cobraFlagValues.webseeds, utils.WebSeedsFlag.Name, utils.WebSeedsFlag.Value, utils.WebSeedsFlag.Usage)
	rootCmd.Flags().StringVar(&cobraFlagValues.chainTomlURL, utils.SnapChainTomlURLFlag.Name, utils.SnapChainTomlURLFlag.Value, utils.SnapChainTomlURLFlag.Usage)
	rootCmd.Flags().StringVar(&natSetting, "nat", utils.NATFlag.Value, utils.NATFlag.Usage)
	rootCmd.Flags().StringVar(&downloaderApiAddr, "downloader.api.addr", "127.0.0.1:9093", "external downloader api network address, for example: 127.0.0.1:9093 serves remote downloader interface")
	rootCmd.Flags().StringVar(&downloadRateStr, "torrent.download.rate", utils.TorrentDownloadRateFlag.Value, utils.TorrentDownloadRateFlag.Usage)
	rootCmd.Flags().StringVar(&uploadRateStr, "torrent.upload.rate", utils.TorrentUploadRateFlag.Value, utils.TorrentUploadRateFlag.Usage)
	rootCmd.Flags().IntVar(&torrentVerbosity, "torrent.verbosity", utils.TorrentVerbosityFlag.Value, utils.TorrentVerbosityFlag.Usage)
	rootCmd.Flags().IntVar(&torrentPort, "torrent.port", utils.TorrentPortFlag.Value, utils.TorrentPortFlag.Usage)
	rootCmd.Flags().IntVar(&torrentMaxPeers, "torrent.maxpeers", utils.TorrentMaxPeersFlag.Value, utils.TorrentMaxPeersFlag.Usage)
	rootCmd.Flags().IntVar(&torrentConnsPerFile, "torrent.conns.perfile", utils.TorrentConnsPerFileFlag.Value, utils.TorrentConnsPerFileFlag.Usage)
	// Deprecated (v3.0): This flag is kept for backward compatibility but has no effect.
	// The downloader automatically manages concurrent downloads. Will be removed in future release.
	rootCmd.Flags().IntVar(&torrentDownloadSlots, "torrent.download.slots", utils.TorrentDownloadSlotsFlag.Value, utils.TorrentDownloadSlotsFlag.Usage)
	rootCmd.Flags().StringVar(&staticPeersStr, utils.TorrentStaticPeersFlag.Name, utils.TorrentStaticPeersFlag.Value, utils.TorrentStaticPeersFlag.Usage)
	rootCmd.Flags().BoolVar(&disableIPV6, "downloader.disable.ipv6", utils.DisableIPV6.Value, utils.DisableIPV6.Usage)
	rootCmd.Flags().BoolVar(&disableIPV4, "downloader.disable.ipv4", utils.DisableIPV4.Value, utils.DisableIPV4.Usage)
	rootCmd.Flags().BoolVar(&seedbox, "seedbox", false, "Turns downloader into independent (doesn't need Erigon) software which discover/download/seed new files - useful for Erigon network, and can work on very cheap hardware. It will: 1) download .torrent from webseed 2) download new files after upgrade 3) we planing add discovery of new files soon")
	rootCmd.Flags().BoolVar(&dbWritemap, utils.DbWriteMapFlag.Name, utils.DbWriteMapFlag.Value, utils.DbWriteMapFlag.Usage)
	rootCmd.PersistentFlags().BoolVar(&verify, "verify", false, utils.DownloaderVerifyFlag.Usage)
	rootCmd.PersistentFlags().StringVar(&_verifyFiles, "verify.files", "", "Limit list of files to verify")
	rootCmd.PersistentFlags().BoolVar(&verifyFailfast, "verify.failfast", false, "Stop on first found error. Report it and exit")

	withDataDir(createTorrent)
	withFile(createTorrent)
	withChainFlag(createTorrent)
	rootCmd.AddCommand(createTorrent)
	createTorrent.Flags().BoolVar(&all, "all", true, "Produce all possible .torrent files")

	rootCmd.AddCommand(torrentCat)
	rootCmd.AddCommand(torrentMagnet)

	withDataDir(torrentClean)
	rootCmd.AddCommand(torrentClean)

	withDataDir(manifestCmd)
	withChainFlag(manifestCmd)
	rootCmd.AddCommand(manifestCmd)
	manifestCmd.Flags().BoolVar(&all, "all", true, "Produce all possible .torrent files")

	manifestVerifyCmd.Flags().StringVar(&cobraFlagValues.webseeds, utils.WebSeedsFlag.Name, utils.WebSeedsFlag.Value, utils.WebSeedsFlag.Usage)
	manifestVerifyCmd.PersistentFlags().BoolVar(&verifyFailfast, "verify.failfast", false, "Stop on first found error. Report it and exit")
	withChainFlag(manifestVerifyCmd)
	rootCmd.AddCommand(manifestVerifyCmd)

	withDataDir(printTorrentHashes)
	withChainFlag(printTorrentHashes)
	printTorrentHashes.Flags().BoolVar(&all, "all", true, "Produce all possible .torrent files")
	printTorrentHashes.PersistentFlags().BoolVar(&forceRebuild, "rebuild", false, "Force re-create .torrent files")
	printTorrentHashes.Flags().StringVar(&targetFile, "targetfile", "", "write output to file")
	if err := printTorrentHashes.MarkFlagFilename("targetfile"); err != nil {
		panic(err)
	}
	printTorrentHashes.Flags().BoolVar(&diffMode, "diff", false, "show diff against the currently released .toml from erigon-snapshot (GitHub)")
	rootCmd.AddCommand(printTorrentHashes)

	withChainFlag(&verifyWebseedsCmd)
	rootCmd.AddCommand(&verifyWebseedsCmd)
	utils.CobraFlags(&verifyWebseedsCmd, []cli.Flag{&app.PreverifiedFlag})
}

func withDataDir(cmd *cobra.Command) {
	flags.DirVar(cmd.Flags(), &cobraFlagValues.datadir, utils.DataDirFlag.Name, paths.DefaultDataDir(), utils.DataDirFlag.Usage)
	panicif.Err(cmd.MarkFlagRequired(utils.DataDirFlag.Name))
	panicif.Err(cmd.MarkFlagDirname(utils.DataDirFlag.Name))
}

func withChainFlag(cmd *cobra.Command) {
	cmd.Flags().StringVar(&chain, utils.ChainFlag.Name, utils.ChainFlag.Value, utils.ChainFlag.Usage)
	panicif.Err(cmd.MarkFlagRequired(utils.ChainFlag.Name))
}

func withFile(cmd *cobra.Command) {
	cmd.Flags().StringVar(&filePath, "file", "", "")
	if err := cmd.MarkFlagFilename("file"); err != nil {
		panic(err)
	}
}

var logger log.Logger

var rootCmd = &cobra.Command{
	Use:          "",
	Short:        "snapshot downloader",
	Example:      "go run ./cmd/downloader --datadir <your_datadir> --downloader.api.addr 127.0.0.1:9093",
	SilenceUsage: true,
	PersistentPostRun: func(cmd *cobra.Command, args []string) {
		debug.Exit()
	},
	PersistentPreRun: func(cmd *cobra.Command, args []string) {
		if cmd.Name() != "torrent_cat" {
			logger = debug.SetupCobra(cmd, "downloader")
			logger.Info("Build info", "git_branch", version.GitBranch, "git_tag", version.GitTag, "git_commit", version.GitCommit)
		}
	},
	RunE: func(cmd *cobra.Command, args []string) error {
		return Downloader(cmd, logger)
	},
}

func Downloader(cmd *cobra.Command, logger log.Logger) error {
	ctx := cmd.Context()
	dirs := datadir.New(cobraFlagValues.datadir)
	if err := datadir.ApplyMigrations(dirs); err != nil {
		return err
	}
	if err := checkChainName(ctx, dirs, chain); err != nil {
		return err
	}
	torrentLogLevel, err := downloadercfg.Int2LogLevel(torrentVerbosity)
	if err != nil {
		return err
	}

	downloadRate, err := utils.GetStringFlagRateLimit(downloadRateStr)
	if err != nil {
		return err
	}
	uploadRate, err := utils.GetStringFlagRateLimit(uploadRateStr)
	if err != nil {
		return err
	}

	logger.Info(
		"[snapshots] cli flags",
		"chain", chain,
		"addr", downloaderApiAddr,
		"datadir", dirs.DataDir,
		"ipv6-enabled", !disableIPV6,
		"ipv4-enabled", !disableIPV4,
		"download.rate", downloadRateStr,
		"upload.rate", uploadRateStr,
		"webseed", cobraFlagValues.webseeds,
	)

	// Warn if deprecated flag was explicitly set by user
	if cmd.Flags().Changed("torrent.download.slots") {
		logger.Warn(
			"[DEPRECATED] --torrent.download.slots flag is deprecated and has no effect",
			"flag", "torrent.download.slots",
			"provided_value", torrentDownloadSlots,
			"action", "This flag will be removed in a future release. The downloader now manages concurrent downloads automatically.",
		)
	}

	version := "erigon: " + version.VersionWithCommit(version.GitCommit)

	webseedsList := common.CliString2Array(cobraFlagValues.webseeds)
	if known, ok := snapcfg.GetEmbeddedWebseeds(chain); ok {
		webseedsList = append(webseedsList, known...)
	}
	if seedbox {
		err = downloadercfg.LoadSnapshotsHashes(ctx, dirs, chain, strings.TrimSpace(cobraFlagValues.chainTomlURL))
		if err != nil {
			return err
		}
	}
	cfg, err := downloadercfg.New(
		ctx,
		dirs,
		version,
		torrentLogLevel,
		torrentPort,
		torrentConnsPerFile,
		webseedsList,
		chain,
		dbWritemap,
		downloadercfg.NewCfgOpts{
			DownloadRateLimit: downloadRate.TorrentRateLimit(),
			UploadRateLimit:   uploadRate.TorrentRateLimit(),
		},
	)
	if err != nil {
		return err
	}

	cfg.ClientConfig.PieceHashersPerTorrent = dbg.EnvInt("DL_HASHERS", runtime.NumCPU())
	cfg.ClientConfig.DisableIPv6 = disableIPV6
	cfg.ClientConfig.DisableIPv4 = disableIPV4

	natif, err := nat.Parse(natSetting)
	if err != nil {
		return fmt.Errorf("invalid nat option %s: %w", natSetting, err)
	}
	downloadernat.DoNat(natif, cfg.ClientConfig, logger)

	manualDataVerification := verify || verifyFailfast || len(verifyFiles) > 0
	cfg.ManualDataVerification = manualDataVerification

	cfg.LogPrefix = "[snapshots] "

	d, err := downloader.New(ctx, cfg, logger)
	if err != nil {
		return err
	}
	defer d.Close()
	logger.Info("[snapshots] Start bittorrent server", "my_peer_id", fmt.Sprintf("%x", d.TorrentClient().PeerID()))

	d.HandleTorrentClientStatus(nil)

	incomplete, err := d.AddTorrentsFromDisk(ctx)
	if err != nil {
		return fmt.Errorf("adding torrents from disk: %w", err)
	}

	if incomplete != 0 && manualDataVerification {
		return fmt.Errorf("%v torrents are incomplete", incomplete)
	}

	// I think we could use DisableInitialPieceVerification to get the behaviour we want here: One
	// hash, and fail if it's in the verify files list, or we have fail fast on.

	if len(_verifyFiles) > 0 {
		verifyFiles = strings.Split(_verifyFiles, ",")
	}
	if manualDataVerification { // remove and create .torrent files (will re-read all snapshots)
		if err = d.VerifyData(ctx, verifyFiles, verifyFailfast); err != nil {
			return err
		}
		if verifyFailfast {
			return nil
		}
	}

	// Turn automatic data verification back on (verify incomplete). This only works if
	// Cfg.ManualDataVerification is held by reference by the Downloader. The alternative is to pass
	// the value through AddTorrentsFromDisk, do it per Torrent ourselves, or defer all hashing to
	// the torrent Client in the Downloader and wait for it to complete.
	cfg.ManualDataVerification = false

	bittorrentServer, err := downloader.NewGrpcServer(d)
	if err != nil {
		return fmt.Errorf("new server: %w", err)
	}

	// I'm kinda curious... but it was false before.
	d.InitBackgroundLogger(true)
	if seedbox {
		var downloadItems []*downloaderproto.DownloadItem
		snapCfg := snapcfg.KnownCfgOrDevnet(chain)
		for _, it := range snapCfg.Preverified.Items {
			downloadItems = append(downloadItems, &downloaderproto.DownloadItem{
				Path:        it.Name,
				TorrentHash: downloadergrpc.String2Proto(it.Hash),
			})
		}
		if _, err := bittorrentServer.Download(ctx, &downloaderproto.DownloadRequest{Items: downloadItems}); err != nil {
			return err
		}
	}

	grpcServer, err := StartGrpc(bittorrentServer, downloaderApiAddr, nil /* transportCredentials */, logger)
	if err != nil {
		return err
	}
	defer grpcServer.GracefulStop()

	<-ctx.Done()
	return nil
}

var createTorrent = &cobra.Command{
	Use:     "torrent_create",
	Example: "go run ./cmd/downloader torrent_create --datadir=<your_datadir> --file=<relative_file_path> ",
	RunE: func(cmd *cobra.Command, args []string) error {
		dirs := datadir.New(cobraFlagValues.datadir)
		if err := checkChainName(cmd.Context(), dirs, chain); err != nil {
			return err
		}
		createdAmount, err := downloader.BuildTorrentFilesIfNeed(cmd.Context(), dirs, downloader.NewAtomicTorrentFS(dirs.Snap), chain, nil, all)
		if err != nil {
			return err
		}
		log.Info("created .torrent files", "amount", createdAmount)
		return nil
	},
}

var printTorrentHashes = &cobra.Command{
	Use:     "torrent_hashes",
	Example: "go run ./cmd/downloader torrent_hashes --datadir <your_datadir>",
	RunE: func(cmd *cobra.Command, args []string) error {
		logger := debug.SetupCobra(cmd, "downloader")
		if err := doPrintTorrentHashes(cmd.Context(), logger); err != nil {
			log.Error(err.Error())
		}
		return nil
	},
}

var manifestCmd = &cobra.Command{
	Use:     "manifest",
	Example: "go run ./cmd/downloader manifest --datadir <your_datadir>",
	RunE: func(cmd *cobra.Command, args []string) error {
		logger := debug.SetupCobra(cmd, "downloader")
		if err := manifest(cmd.Context(), logger); err != nil {
			log.Error(err.Error())
		}
		return nil
	},
}

var manifestVerifyCmd = &cobra.Command{
	Use:     "manifest_verify",
	Aliases: []string{"manifest-verify"},
	Example: "go run ./cmd/downloader manifest_verify --chain <chain> [--webseed 'a','b','c']",
	RunE: func(cmd *cobra.Command, args []string) error {
		logger := debug.SetupCobra(cmd, "downloader")
		if err := manifestVerify(cmd.Context(), logger); err != nil {
			log.Error(err.Error())
			os.Exit(1) // to mark CI as failed
		}
		return nil
	},
}

var verifyWebseedsCmd = cobra.Command{
	Use:   "verify_webseeds [--chain chain] [--preverified { local | remote | embedded }]",
	Short: "verify webseed metainfos and data match preverified",
	Long:  "Loads preverified for the specified chain or all known chains, and checks the metainfohashes matches, then the data matches the metainfo hashes. Outputs it all to JSON.",
	RunE: func(cmd *cobra.Command, args []string) error {
		return webseeds.Verify(
			cmd.Context(),
			app.PreverifiedFlag.Value,
			cobraFlagValues.datadir,
			app.ConcurrencyFlag.Value,
			g.Some(chain),
		)
	},
}

var torrentCat = &cobra.Command{
	Use:     "torrent_cat",
	Example: "go run ./cmd/downloader torrent_cat <path_to_torrent_file>",
	RunE: func(cmd *cobra.Command, args []string) error {
		if len(args) == 0 {
			return errors.New("please pass .torrent file path by first argument")
		}
		fPath := args[0]
		mi, err := metainfo.LoadFromFile(fPath)
		if err != nil {
			return fmt.Errorf("LoadFromFile: %w, file=%s", err, fPath)
		}
		var ms map[string]any
		err = mapstructure.Decode(mi, &ms)
		if err != nil {
			return fmt.Errorf("decoding metainfo into map: %w", err)
		}
		fmt.Printf("InfoHash = '%x'\n", mi.HashInfoBytes())
		delete(ms, "InfoBytes")
		bytes, err := toml.Marshal(ms)
		if err != nil {
			return err
		}
		_, err = os.Stdout.Write(bytes)
		return err
	},
}
var torrentClean = &cobra.Command{
	Use:     "torrent_clean",
	Short:   "RemoveFile all .torrent files from datadir directory",
	Example: "go run ./cmd/downloader torrent_clean --datadir=<datadir>",
	RunE: func(cmd *cobra.Command, args []string) error {
		dirs := datadir.New(cobraFlagValues.datadir)

		logger.Info("[snapshots.webseed] processing local file etags")
		removedTorrents := 0
		walker := func(path string, de fs.DirEntry, err error) error {
			if err != nil || de.IsDir() {
				if err != nil {
					logger.Warn("[snapshots.torrent] walk and cleanup", "err", err, "path", path)
				}
				return nil //nolint
			}

			if !strings.HasSuffix(de.Name(), ".torrent") || strings.HasPrefix(de.Name(), ".") {
				return nil
			}
			err = dir.RemoveFile(filepath.Join(dirs.Snap, path))
			if err != nil {
				logger.Warn("[snapshots.torrent] remove", "err", err, "path", path)
				return err
			}
			removedTorrents++
			return nil
		}

		sfs := os.DirFS(dirs.Snap)
		if err := fs.WalkDir(sfs, ".", walker); err != nil {
			return err
		}
		logger.Info("[snapshots.torrent] cleanup finished", "count", removedTorrents)
		return nil
	},
}

var torrentMagnet = &cobra.Command{
	Use:     "torrent_magnet",
	Example: "go run ./cmd/downloader torrent_magnet <path_to_torrent_file>",
	RunE: func(cmd *cobra.Command, args []string) error {
		if len(args) == 0 {
			return errors.New("please pass .torrent file path by first argument")
		}
		fPath := args[0]
		mi, err := metainfo.LoadFromFile(fPath)
		if err != nil {
			return fmt.Errorf("LoadFromFile: %w, file=%s", err, fPath)
		}
		fmt.Printf("%s\n", mi.Magnet(nil, nil).String())
		return nil
	},
}

func manifestVerify(ctx context.Context, logger log.Logger) error {
	webseedsList := common.CliString2Array(cobraFlagValues.webseeds)
	if len(webseedsList) == 0 { // fallback to default if exact list not passed
		if known, ok := snapcfg.GetEmbeddedWebseeds(chain); ok {
			for _, s := range known {
				//TODO: enable validation of this buckets also. skipping to make CI useful.
				if strings.Contains(s, "erigon2-v2") {
					continue
				}
				webseedsList = append(webseedsList, s)
			}
		}
	}

	webseedUrlsOrFiles := webseedsList
	webseedHttpProviders := make([]*url.URL, 0, len(webseedUrlsOrFiles))
	webseedFileProviders := make([]string, 0, len(webseedUrlsOrFiles))
	for _, webseed := range webseedUrlsOrFiles {
		if !strings.HasPrefix(webseed, "v") { // has marker v1/v2/...
			uri, err := url.ParseRequestURI(webseed)
			if err != nil {
				exists, existsErr := dir.FileExist(webseed)
				if existsErr != nil {
					log.Warn("[webseed] FileExist error", "err", err)
					continue
				}
				if strings.HasSuffix(webseed, ".toml") && exists {
					webseedFileProviders = append(webseedFileProviders, webseed)
				}
				continue
			}
			webseedHttpProviders = append(webseedHttpProviders, uri)
			continue
		}

		withoutVersionPrefix, err := snapcfg.WebseedToUrl(webseed)
		if err != nil {
			log.Warn("[webseed] skipping webseed", "webseed", webseed, "err", err)
			continue
		}
		uri, err := url.ParseRequestURI(withoutVersionPrefix)
		if err != nil {
			log.Warn("[webseed] can't parse url", "err", err, "url", withoutVersionPrefix)
			continue
		}
		webseedHttpProviders = append(webseedHttpProviders, uri)
	}
	if len(webseedFileProviders) > 0 {
		logger.Warn("file providers are not supported yet", "fileProviders", webseedFileProviders)
	}

	wseed := downloader.NewWebSeeds(webseedHttpProviders, log.LvlDebug, logger)
	return wseed.VerifyManifestedBuckets(ctx, verifyFailfast)
}

func manifest(ctx context.Context, logger log.Logger) error {
	dirs := datadir.New(cobraFlagValues.datadir)

	files, err := downloader.SeedableFiles(dirs, chain, true)
	if err != nil {
		return err
	}

	extList := []string{
		".torrent",
		//".seg", ".idx", // e2
		//".kv", ".kvi", ".bt", ".kvei", // e3 domain
		//".v", ".vi", //e3 hist
		//".ef", ".efi", //e3 idx
		".txt", //salt-state.txt, salt-blocks.txt, manifest.txt
	}
	l, _ := dir.ListFiles(dirs.Snap, extList...)
	for _, fPath := range l {
		_, fName := filepath.Split(fPath)
		files = append(files, fName)
	}
	l, _ = dir.ListFiles(dirs.SnapDomain, extList...)
	for _, fPath := range l {
		_, fName := filepath.Split(fPath)
		files = append(files, "domain/"+fName)
	}
	l, _ = dir.ListFiles(dirs.SnapHistory, extList...)
	for _, fPath := range l {
		_, fName := filepath.Split(fPath)
		files = append(files, "history/"+fName)
	}
	l, _ = dir.ListFiles(dirs.SnapIdx, extList...)
	for _, fPath := range l {
		_, fName := filepath.Split(fPath)
		files = append(files, "idx/"+fName)
	}

	slices.Sort(files)
	for _, f := range files {
		fmt.Printf("%s\n", f)
	}
	return nil
}

func doPrintTorrentHashes(ctx context.Context, logger log.Logger) error {
	dirs := datadir.New(cobraFlagValues.datadir)
	if err := datadir.ApplyMigrations(dirs); err != nil {
		return err
	}

	tf := downloader.NewAtomicTorrentFS(dirs.Snap)

	if forceRebuild { // remove and create .torrent files (will re-read all snapshots)
		//removePieceCompletionStorage(snapDir)
		files, err := downloader.AllTorrentPaths(dirs)
		if err != nil {
			return err
		}
		for _, filePath := range files {
			if err := dir.RemoveFile(filePath); err != nil {
				return err
			}
		}
		createdAmount, err := downloader.BuildTorrentFilesIfNeed(ctx, dirs, tf, chain, nil, all)
		if err != nil {
			return fmt.Errorf("BuildTorrentFilesIfNeed: %w", err)
		}
		log.Info("created .torrent files", "amount", createdAmount)
	}

	res := map[string]string{}
	torrents, err := downloader.AllTorrentSpecs(dirs, tf)
	if err != nil {
		return err
	}

	for _, t := range torrents {
		res[t.DisplayName] = t.InfoHash.String()
	}
	if diffMode {
		return doDiffTorrentHashes(ctx, res)
	}

	serialized, err := toml.Marshal(res)
	if err != nil {
		return err
	}

	if targetFile == "" {
		fmt.Printf("%s\n", serialized)
		return nil
	}

	oldContent, err := os.ReadFile(targetFile)
	if err != nil {
		return err
	}
	oldLines := map[string]string{}
	if err := toml.Unmarshal(oldContent, &oldLines); err != nil {
		return fmt.Errorf("unmarshal: %w", err)
	}
	if len(oldLines) >= len(res) {
		logger.Info("amount of lines in target file is equal or greater than amount of lines in snapshot dir", "old", len(oldLines), "new", len(res))
		return nil
	}
	if err := os.WriteFile(targetFile, serialized, 0644); err != nil { // nolint
		return err
	}
	return nil
}

type hashChange struct{ name, released, local string }

func doDiffTorrentHashes(ctx context.Context, local map[string]string) error {
	branch := version.DefaultSnapshotGitBranch
	url := snapcfg.ChainTomlGitHubURL(branch, chain)
	body, err := snapcfg.FetchChainToml(ctx, snapcfg.Github, branch, chain)
	if err != nil {
		return fmt.Errorf("diff: %w", err)
	}

	released := map[string]string{}
	if err := toml.Unmarshal(body, &released); err != nil {
		return fmt.Errorf("diff: unmarshal released toml: %w", err)
	}

	var added, removed []string
	var changed []hashChange
	for name, localHash := range local {
		if relHash, ok := released[name]; !ok {
			added = append(added, name)
		} else if relHash != localHash {
			changed = append(changed, hashChange{name, relHash, localHash})
		}
	}
	for name := range released {
		if _, ok := local[name]; !ok {
			removed = append(removed, name)
		}
	}

	slices.Sort(added)
	slices.Sort(removed)
	slices.SortFunc(changed, func(a, b hashChange) int { return cmp.Compare(a.name, b.name) })

	fmt.Printf("released branch: %s  url: %s\n", branch, url)
	fmt.Printf("released: %d entries, local: %d entries, changed: %d, added: %d, removed: %d\n", len(released), len(local), len(changed), len(added), len(removed))

	if len(added) == 0 && len(removed) == 0 && len(changed) == 0 {
		fmt.Println("no diff")
		return nil
	}
	for _, c := range changed {
		fmt.Printf("CHANGED %s released=%s local=%s\n", c.name, c.released, c.local)
	}
	for _, name := range added {
		fmt.Printf("ADDED   %s %s\n", name, local[name])
	}
	for _, name := range removed {
		fmt.Printf("REMOVED %s %s\n", name, released[name])
	}
	return nil
}

func StartGrpc(snServer *downloader.GrpcServer, addr string, creds credentials.TransportCredentials, logger log.Logger) (*grpc.Server, error) {
	grpcServer := grpcutil.NewServerWithOpts(creds)
	if snServer != nil {
		downloaderproto.RegisterDownloaderServer(grpcServer, snServer)
	}

	if err := grpcutil.StartServer(grpcServer, addr, true, logger, "gRPC server stop"); err != nil {
		return nil, err
	}
	logger.Info("Started gRPC server", "on", addr)
	return grpcServer, nil
}

func checkChainName(ctx context.Context, dirs datadir.Dirs, chainName string) error {
	exists, err := dir.FileExist(filepath.Join(dirs.Chaindata, "mdbx.dat"))
	if err != nil {
		return err
	}
	if !exists {
		return nil
	}
	db, err := mdbx.New(dbcfg.ChainDB, log.New()).
		Path(dirs.Chaindata).
		Accede(true).
		Open(ctx)
	if err != nil {
		return err
	}
	defer db.Close()

	if cc := fromdb.ChainConfig(db); cc != nil {
		spc, err := chainspec.ChainSpecByName(chainName)
		if err != nil {
			return fmt.Errorf("unknown chain: %s", chainName)
		}
		if spc.Config.ChainID.Uint64() != cc.ChainID.Uint64() {
			advice := fmt.Sprintf("\nTo change to '%s', remove %s %s\nAnd then start over with --chain=%s", chainName, dirs.Chaindata, filepath.Join(dirs.Snap, "preverified.toml"), chainName)
			return fmt.Errorf("datadir already was configured with --chain=%s"+advice, cc.ChainName)
		}
	}
	return nil
}
