// 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 app contains framework for building a command-line based Erigon node.
package app

import (
	"context"
	"fmt"
	"strings"

	"github.com/urfave/cli/v2"

	enode "github.com/erigontech/erigon/cmd/erigon/node"
	"github.com/erigontech/erigon/cmd/utils"
	"github.com/erigontech/erigon/common/log/v3"
	"github.com/erigontech/erigon/db/datadir"
	"github.com/erigontech/erigon/db/version"
	"github.com/erigontech/erigon/node"
	cli2 "github.com/erigontech/erigon/node/cli"
	"github.com/erigontech/erigon/node/debug"
	"github.com/erigontech/erigon/node/logging"
	"github.com/erigontech/erigon/node/nodecfg"
	shuttercmd "github.com/erigontech/erigon/txnprovider/shutter/cmd"
)

// MakeApp creates a cli application (based on `github.com/urlfave/cli` package).
// The application exits when `action` returns.
// Parameters:
// * action: the main function for the application. receives `*cli.Context` with parsed command-line flags.
// * cliFlags: the list of flags `cli.Flag` that the app should set and parse. By default, use `DefaultFlags()`. If you want to specify your own flag, use `append(DefaultFlags(), myFlag)` for this parameter.
func MakeApp(name string, action cli.ActionFunc, cliFlags []cli.Flag) *cli.App {
	app := cli2.NewApp("erigon")
	app.Name = name
	app.UsageText = app.Name + ` [command] [flags]`
	app.Action = func(context *cli.Context) error {
		// handle case: unknown sub-command
		if context.Args().Present() {
			goodNames := make([]string, 0, len(app.VisibleCommands()))
			for _, c := range app.VisibleCommands() {
				goodNames = append(goodNames, c.Name)
			}
			log.Error(fmt.Sprintf("Command '%s' not found. Available commands: %s", context.Args().First(), goodNames))
			return cli.Exit("", 1) // Exit with error code but no additional output
		}

		// handle case: config flag
		configFilePath := context.String(utils.ConfigFlag.Name)
		if configFilePath != "" {
			if err := cli2.SetFlagsFromConfigFile(context, configFilePath); err != nil {
				log.Error("failed setting config flags from yaml/toml file", "err", err)
				return err
			}
		}

		// run default action
		return action(context)
	}

	app.Before = func(c *cli.Context) error {
		var cancel context.CancelFunc
		c.Context, cancel = context.WithCancel(c.Context)
		go debug.ListenSignals(cancel, log.Root())
		return nil
	}

	app.Flags = appFlags(cliFlags)

	app.After = func(ctx *cli.Context) error {
		debug.Exit()
		return nil
	}
	app.Commands = []*cli.Command{
		&initCommand,
		&importCommand,
		&snapshotCommand,
		&supportCommand,
		&backtestCommitmentCommand,
		//&backupCommand,
	}
	shuttercmd.RegisterCmds(app)
	return app
}

func appFlags(cliFlags []cli.Flag) []cli.Flag {

	flags := append(cliFlags, debug.Flags...) // debug flags are required
	flags = append(flags, utils.MetricFlags...)
	flags = append(flags, logging.Flags...)
	flags = append(flags, &utils.ConfigFlag)

	// remove exact duplicate flags, keeping only the first one. this will allow easier composition later down the line
	allFlags := flags
	newFlags := make([]cli.Flag, 0, len(allFlags))
	seen := map[string]struct{}{}
	for _, vv := range allFlags {
		v := vv
		if _, ok := seen[v.String()]; ok {
			continue
		}
		newFlags = append(newFlags, v)
	}

	return newFlags
}

// MigrateFlags makes all global flag values available in the
// context. This should be called as early as possible in app.Before.
//
// Example:
//
//	geth account new --keystore /tmp/mykeystore --lightkdf
//
// is equivalent after calling this method with:
//
//	geth --keystore /tmp/mykeystore --lightkdf account new
//
// i.e. in the subcommand Action function of 'account new', ctx.Bool("lightkdf)
// will return true even if --lightkdf is set as a global option.
//
// This function may become unnecessary when https://github.com/urfave/cli/pull/1245 is merged.
func MigrateFlags(action cli.ActionFunc) cli.ActionFunc {
	return func(ctx *cli.Context) error {
		doMigrateFlags(ctx)
		return action(ctx)
	}
}

func doMigrateFlags(ctx *cli.Context) {
	// Figure out if there are any aliases of commands. If there are, we want
	// to ignore them when iterating over the flags.
	var aliases = make(map[string]bool)
	for _, fl := range ctx.Command.Flags {
		for _, alias := range fl.Names()[1:] {
			aliases[alias] = true
		}
	}
	for _, name := range ctx.FlagNames() {
		for _, parent := range ctx.Lineage()[1:] {
			if parent.IsSet(name) {
				// When iterating across the lineage, we will be served both
				// the 'canon' and alias formats of all commands. In most cases,
				// it's fine to set it in the ctx multiple times (one for each
				// name), however, the Slice-flags are not fine.
				// The slice-flags accumulate, so if we set it once as
				// "foo" and once as alias "F", then both will be present in the slice.
				if _, isAlias := aliases[name]; isAlias {
					continue
				}
				// If it is a string-slice, we need to set it as
				// "alfa, beta, gamma" instead of "[alfa beta gamma]", in order
				// for the backing StringSlice to parse it properly.
				if result := parent.StringSlice(name); len(result) > 0 {
					ctx.Set(name, strings.Join(result, ","))
				} else {
					ctx.Set(name, parent.String(name))
				}
				break
			}
		}
	}
}

func NewNodeConfig(ctx *cli.Context, logger log.Logger) (*nodecfg.Config, error) {
	nodeConfig, err := enode.NewNodConfigUrfave(ctx, nil, logger)
	if err != nil {
		return nil, err
	}

	// see similar changes in `cmd/geth/config.go#defaultNodeConfig`
	if commit := version.GitCommit; commit != "" {
		nodeConfig.Version = version.VersionWithCommit(commit)
	} else {
		nodeConfig.Version = version.VersionNoMeta
	}

	nodeConfig.IPCPath = "" // force-disable IPC endpoint
	nodeConfig.Name = "erigon"

	if ctx.IsSet(utils.DataDirFlag.Name) {
		nodeConfig.Dirs = datadir.New(ctx.String(utils.DataDirFlag.Name))
	}

	return nodeConfig, nil
}

func MakeNodeWithDefaultConfig(cliCtx *cli.Context, logger log.Logger) (*node.Node, error) {
	conf, err := NewNodeConfig(cliCtx, logger)
	if err != nil {
		return nil, err
	}
	return makeConfigNode(cliCtx.Context, conf, logger), nil
}

func makeConfigNode(ctx context.Context, config *nodecfg.Config, logger log.Logger) *node.Node {
	stack, err := node.New(ctx, config, logger)
	if err != nil {
		utils.Fatalf("Failed to create Erigon node: %v", err)
	}

	return stack
}
