// Package main defines a validator client, a critical actor in Ethereum which manages
// a keystore of private keys, connects to a beacon node to receive assignments,
// and submits blocks/attestations as needed.
package main

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	runtimeDebug "runtime/debug"

	"github.com/OffchainLabs/prysm/v7/cmd"
	accountcommands "github.com/OffchainLabs/prysm/v7/cmd/validator/accounts"
	dbcommands "github.com/OffchainLabs/prysm/v7/cmd/validator/db"
	"github.com/OffchainLabs/prysm/v7/cmd/validator/flags"
	slashingprotectioncommands "github.com/OffchainLabs/prysm/v7/cmd/validator/slashing-protection"
	walletcommands "github.com/OffchainLabs/prysm/v7/cmd/validator/wallet"
	"github.com/OffchainLabs/prysm/v7/cmd/validator/web"
	"github.com/OffchainLabs/prysm/v7/config/features"
	"github.com/OffchainLabs/prysm/v7/io/file"
	"github.com/OffchainLabs/prysm/v7/io/logs"
	"github.com/OffchainLabs/prysm/v7/monitoring/journald"
	"github.com/OffchainLabs/prysm/v7/runtime/debug"
	prefixed "github.com/OffchainLabs/prysm/v7/runtime/logging/logrus-prefixed-formatter"
	_ "github.com/OffchainLabs/prysm/v7/runtime/maxprocs"
	"github.com/OffchainLabs/prysm/v7/runtime/tos"
	"github.com/OffchainLabs/prysm/v7/runtime/version"
	"github.com/OffchainLabs/prysm/v7/validator/node"
	joonix "github.com/joonix/log"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli/v2"
)

var log = logrus.WithField("prefix", "main")

func startNode(ctx *cli.Context) error {
	// Verify if ToS is accepted.
	if err := tos.VerifyTosAcceptedOrPrompt(ctx); err != nil {
		return err
	}

	validatorClient, err := node.NewValidatorClient(ctx)
	if err != nil {
		return err
	}
	validatorClient.Start()
	return nil
}

var appFlags = []cli.Flag{
	flags.BeaconRPCProviderFlag,
	flags.BeaconRESTApiProviderFlag,
	flags.BeaconRESTApiHeaders,
	flags.CertFlag,
	flags.GraffitiFlag,
	flags.DisablePenaltyRewardLogFlag,
	flags.InteropStartIndex,
	flags.InteropNumValidators,
	flags.EnableRPCFlag,
	flags.RPCHost,
	flags.RPCPort,
	flags.HTTPServerPort,
	flags.HTTPServerHost,
	flags.GRPCRetriesFlag,
	flags.GRPCRetryDelayFlag,
	flags.GRPCHeadersFlag,
	flags.HTTPServerCorsDomain,
	flags.DisableAccountMetricsFlag,
	flags.MonitoringPortFlag,
	flags.SlasherRPCProviderFlag,
	flags.SlasherCertFlag,
	flags.WalletPasswordFileFlag,
	flags.WalletDirFlag,
	flags.GraffitiFileFlag,
	flags.EnableDistributed,
	flags.AuthTokenPathFlag,
	flags.DisableDutiesPolling,
	flags.MaxHealthChecksFlag,
	// Consensys' Web3Signer flags
	flags.Web3SignerURLFlag,
	flags.Web3SignerPublicValidatorKeysFlag,
	flags.Web3SignerKeyFileFlag,
	flags.SuggestedFeeRecipientFlag,
	flags.ProposerSettingsURLFlag,
	flags.ProposerSettingsFlag,
	flags.EnableBuilderFlag,
	flags.BuilderGasLimitFlag,
	flags.ValidatorsRegistrationBatchSizeFlag,
	////////////////////
	cmd.DisableMonitoringFlag,
	cmd.MonitoringHostFlag,
	cmd.EnableBackupWebhookFlag,
	cmd.MinimalConfigFlag,
	cmd.E2EConfigFlag,
	cmd.VerbosityFlag,
	cmd.DataDirFlag,
	cmd.ClearDB,
	cmd.ForceClearDB,
	cmd.EnableTracingFlag,
	cmd.TracingProcessNameFlag,
	cmd.TracingEndpointFlag,
	cmd.TraceSampleFractionFlag,
	cmd.LogFormat,
	cmd.LogFileName,
	cmd.ConfigFileFlag,
	cmd.ChainConfigFileFlag,
	cmd.GrpcMaxCallRecvMsgSizeFlag,
	cmd.ApiTimeoutFlag,
	debug.PProfFlag,
	debug.PProfAddrFlag,
	debug.PProfPortFlag,
	debug.MemProfileRateFlag,
	debug.BlockProfileRateFlag,
	debug.MutexProfileFractionFlag,
	cmd.AcceptTosFlag,
	flags.DisableEphemeralLogFile,
}

func init() {
	appFlags = cmd.WrapFlags(append(appFlags, features.ValidatorFlags...))
}

func main() {
	app := cli.App{
		Name:    "validator",
		Usage:   "Launches an Ethereum validator client that interacts with a beacon chain, starts proposer and attester services, p2p connections, and more.",
		Version: version.Version(),
		Action: func(ctx *cli.Context) error {
			if err := startNode(ctx); err != nil {
				log.Fatal(err.Error())
				return err
			}
			return nil
		},
		Commands: []*cli.Command{
			walletcommands.Commands,
			accountcommands.Commands,
			slashingprotectioncommands.Commands,
			dbcommands.Commands,
			web.Commands,
		},
		Flags: appFlags,
		Before: func(ctx *cli.Context) error {
			// Load flags from config file, if specified.
			if err := cmd.LoadFlagsFromConfig(ctx, appFlags); err != nil {
				return err
			}

			// determine log verbosity
			verbosity := ctx.String(cmd.VerbosityFlag.Name)
			verbosityLevel, err := logrus.ParseLevel(verbosity)
			if err != nil {
				return errors.Wrap(err, "failed to parse log verbosity")
			}
			logs.SetLoggingLevel(verbosityLevel)

			logFileName := ctx.String(cmd.LogFileName.Name)

			format := ctx.String(cmd.LogFormat.Name)
			switch format {
			case "text":
				// disabling logrus default output so we can control it via different hooks
				logrus.SetOutput(io.Discard)

				// create a custom formatter and hook for terminal output
				formatter := new(prefixed.TextFormatter)
				formatter.TimestampFormat = "2006-01-02 15:04:05.00"
				formatter.FullTimestamp = true
				formatter.ForceFormatting = true
				formatter.ForceColors = true

				logrus.AddHook(&logs.WriterHook{
					Formatter:     formatter,
					Writer:        os.Stderr,
					AllowedLevels: logrus.AllLevels[:verbosityLevel+1],
				})
			case "fluentd":
				f := joonix.NewFormatter()
				if err := joonix.DisableTimestampFormat(f); err != nil {
					panic(err) // lint:nopanic -- This shouldn't happen, but crashing immediately at startup is OK.
				}
				logrus.SetFormatter(f)
			case "json":
				logrus.SetFormatter(&logrus.JSONFormatter{
					TimestampFormat: "2006-01-02 15:04:05.00",
				})
			case "journald":
				if err := journald.Enable(); err != nil {
					return err
				}
			default:
				return fmt.Errorf("unknown log format %s", format)
			}

			if logFileName != "" {
				if err := logs.ConfigurePersistentLogging(logFileName, format, verbosityLevel); err != nil {
					log.WithError(err).Error("Failed to configuring logging to disk.")
				}
			}

			if !ctx.Bool(flags.DisableEphemeralLogFile.Name) {
				if err := logs.ConfigureEphemeralLogFile(ctx.String(cmd.DataDirFlag.Name), ctx.App.Name); err != nil {
					log.WithError(err).Error("Failed to configure debug log file")
				}
			}

			// Fix data dir for Windows users.
			outdatedDataDir := filepath.Join(file.HomeDir(), "AppData", "Roaming", "Eth2Validators")
			currentDataDir := flags.DefaultValidatorDir()
			if err := cmd.FixDefaultDataDir(outdatedDataDir, currentDataDir); err != nil {
				log.WithError(err).Error("Cannot update data directory")
			}

			if err := debug.Setup(ctx); err != nil {
				return errors.Wrap(err, "failed to setup debug")
			}

			if err := features.ValidateNetworkFlags(ctx); err != nil {
				return errors.Wrap(err, "provided multiple network flags")
			}

			return cmd.ValidateNoArgs(ctx)
		},
	}

	defer func() {
		if x := recover(); x != nil {
			log.Errorf("Runtime panic: %v\n%v", x, string(runtimeDebug.Stack()))
			panic(x) // lint:nopanic -- This is just resurfacing the original panic.
		}
	}()

	if err := app.Run(os.Args); err != nil {
		log.Error(err.Error())
	}
}
