package main

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/spf13/cobra"
	"github.com/spf13/viper"

	cmd "github.com/tendermint/tendermint/cmd/tendermint/commands"
	"github.com/tendermint/tendermint/cmd/tendermint/commands/debug"
	cfg "github.com/tendermint/tendermint/config"
	"github.com/tendermint/tendermint/libs/cli"
	tmflags "github.com/tendermint/tendermint/libs/cli/flags"
	"github.com/tendermint/tendermint/libs/log"
	tmos "github.com/tendermint/tendermint/libs/os"
	tmrand "github.com/tendermint/tendermint/libs/rand"
	"github.com/tendermint/tendermint/p2p"
	tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
	cs "github.com/tendermint/tendermint/test/maverick/consensus"
	nd "github.com/tendermint/tendermint/test/maverick/node"
	"github.com/tendermint/tendermint/types"
	tmtime "github.com/tendermint/tendermint/types/time"
)

var (
	config          = cfg.DefaultConfig()
	logger          = log.NewTMLogger(log.NewSyncWriter(os.Stdout))
	misbehaviorFlag = ""
)

func init() {
	registerFlagsRootCmd(RootCmd)
}

func registerFlagsRootCmd(command *cobra.Command) {
	command.PersistentFlags().String("log_level", config.LogLevel, "Log level")
}

func ParseConfig() (*cfg.Config, error) {
	conf := cfg.DefaultConfig()
	err := viper.Unmarshal(conf)
	if err != nil {
		return nil, err
	}
	conf.SetRoot(conf.RootDir)
	cfg.EnsureRoot(conf.RootDir)
	if err = conf.ValidateBasic(); err != nil {
		return nil, fmt.Errorf("error in config file: %v", err)
	}
	return conf, err
}

// RootCmd is the root command for Tendermint core.
var RootCmd = &cobra.Command{
	Use:   "maverick",
	Short: "Tendermint Maverick Node",
	Long: "Tendermint Maverick Node for testing with faulty consensus misbehaviors in a testnet. Contains " +
		"all the functionality of a normal node but custom misbehaviors can be injected when running the node " +
		"through a flag. See maverick node --help for how the misbehavior flag is constructured",
	PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
		fmt.Printf("use: %v, args: %v", cmd.Use, cmd.Args)
		config, err = ParseConfig()
		if err != nil {
			return err
		}
		if config.LogFormat == cfg.LogFormatJSON {
			logger = log.NewTMJSONLogger(log.NewSyncWriter(os.Stdout))
		}
		logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel())
		if err != nil {
			return err
		}
		if viper.GetBool(cli.TraceFlag) {
			logger = log.NewTracingLogger(logger)
		}
		logger = logger.With("module", "main")
		return nil
	},
}

func main() {
	rootCmd := RootCmd
	rootCmd.AddCommand(
		ListMisbehaviorCmd,
		cmd.GenValidatorCmd,
		InitFilesCmd,
		cmd.ProbeUpnpCmd,
		cmd.ReplayCmd,
		cmd.ReplayConsoleCmd,
		cmd.ResetAllCmd,
		cmd.ResetPrivValidatorCmd,
		cmd.ShowValidatorCmd,
		cmd.ShowNodeIDCmd,
		cmd.GenNodeKeyCmd,
		cmd.VersionCmd,
		debug.DebugCmd,
		cli.NewCompletionCmd(rootCmd, true),
	)

	nodeCmd := &cobra.Command{
		Use:     "start",
		Aliases: []string{"node", "run"},
		Short:   "Run the maverick node",
		RunE: func(command *cobra.Command, args []string) error {
			return startNode(config, logger, misbehaviorFlag)
		},
	}

	cmd.AddNodeFlags(nodeCmd)

	// Create & start node
	rootCmd.AddCommand(nodeCmd)

	// add special flag for misbehaviors
	nodeCmd.Flags().StringVar(
		&misbehaviorFlag,
		"misbehaviors",
		"",
		"Select the misbehaviors of the node (comma-separated, no spaces in between): \n"+
			"e.g. --misbehaviors double-prevote,3\n"+
			"You can also have multiple misbehaviors: e.g. double-prevote,3,no-vote,5")

	cmd := cli.PrepareBaseCmd(rootCmd, "TM", os.ExpandEnv(filepath.Join("$HOME", cfg.DefaultTendermintDir)))
	if err := cmd.Execute(); err != nil {
		panic(err)
	}
}

func startNode(config *cfg.Config, logger log.Logger, misbehaviorFlag string) error {
	misbehaviors, err := nd.ParseMisbehaviors(misbehaviorFlag)
	if err != nil {
		return err
	}

	node, err := nd.DefaultNewNode(config, logger, misbehaviors)
	if err != nil {
		return fmt.Errorf("failed to create node: %w", err)
	}

	if err := node.Start(); err != nil {
		return fmt.Errorf("failed to start node: %w", err)
	}

	logger.Info("Started node", "nodeInfo", node.Switch().NodeInfo())

	// Stop upon receiving SIGTERM or CTRL-C.
	tmos.TrapSignal(logger, func() {
		if node.IsRunning() {
			if err := node.Stop(); err != nil {
				logger.Error("unable to stop the node", "error", err)
			}
		}
	})

	// Run forever.
	select {}
}

var keyType string

var InitFilesCmd = &cobra.Command{
	Use:   "init",
	Short: "Initialize Tendermint",
	RunE:  initFiles,
}

func init() {
	InitFilesCmd.Flags().StringVar(&keyType, "key", types.ABCIPubKeyTypeEd25519,
		"Key type to generate privval file with. Options: ed25519, secp256k1")
}

func initFiles(cmd *cobra.Command, args []string) error {
	return initFilesWithConfig(config)
}

func initFilesWithConfig(config *cfg.Config) error {
	// private validator
	privValKeyFile := config.PrivValidatorKeyFile()
	privValStateFile := config.PrivValidatorStateFile()
	var pv *nd.FilePV
	if tmos.FileExists(privValKeyFile) {
		pv = nd.LoadFilePV(privValKeyFile, privValStateFile)
		logger.Info("Found private validator", "keyFile", privValKeyFile,
			"stateFile", privValStateFile)
	} else {
		pv = nd.GenFilePV(privValKeyFile, privValStateFile)
		pv.Save()
		logger.Info("Generated private validator", "keyFile", privValKeyFile,
			"stateFile", privValStateFile)
	}

	nodeKeyFile := config.NodeKeyFile()
	if tmos.FileExists(nodeKeyFile) {
		logger.Info("Found node key", "path", nodeKeyFile)
	} else {
		if _, err := p2p.LoadOrGenNodeKey(nodeKeyFile); err != nil {
			return err
		}
		logger.Info("Generated node key", "path", nodeKeyFile)
	}

	// genesis file
	genFile := config.GenesisFile()
	if tmos.FileExists(genFile) {
		logger.Info("Found genesis file", "path", genFile)
	} else {
		genDoc := types.GenesisDoc{
			ChainID:         fmt.Sprintf("test-chain-%v", tmrand.Str(6)),
			GenesisTime:     tmtime.Now(),
			ConsensusParams: types.DefaultConsensusParams(),
		}
		if keyType == "secp256k1" {
			genDoc.ConsensusParams.Validator = tmproto.ValidatorParams{
				PubKeyTypes: []string{types.ABCIPubKeyTypeSecp256k1},
			}
		}
		pubKey, err := pv.GetPubKey()
		if err != nil {
			return fmt.Errorf("can't get pubkey: %w", err)
		}
		genDoc.Validators = []types.GenesisValidator{{
			Address: pubKey.Address(),
			PubKey:  pubKey,
			Power:   10,
		}}

		if err := genDoc.SaveAs(genFile); err != nil {
			return err
		}
		logger.Info("Generated genesis file", "path", genFile)
	}

	return nil
}

var ListMisbehaviorCmd = &cobra.Command{
	Use:   "misbehaviors",
	Short: "Lists possible misbehaviors",
	RunE:  listMisbehaviors,
}

func listMisbehaviors(cmd *cobra.Command, args []string) error {
	str := "Currently registered misbehaviors: \n"
	for key := range cs.MisbehaviorList {
		str += fmt.Sprintf("- %s\n", key)
	}
	fmt.Println(str)
	return nil
}
