package main

import (
	"encoding/hex"
	"errors"
	"fmt"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/console/prompt"
	tronaddr "github.com/fbsobreira/gotron-sdk/pkg/address"
	logging "github.com/ipfs/go-log/v2"
	"github.com/urfave/cli/v2"
	"google.golang.org/grpc"
	"gorm.io/gorm"
	"io/ioutil"
	"key-manager/conf"
	"key-manager/crypto"
	"key-manager/dao"
	"key-manager/service"
	"net"
	"os"
	"strings"
)

var log = logging.Logger("main")

func main() {
	_ = logging.SetLogLevel("*", "INFO")

	app := cli.App{
		Name:  "key-manager",
		Usage: "eth and tron key manager service",
		Commands: []*cli.Command{
			initCmd,
			runCmd,
			whitelistCmd,
		},
		EnableBashCompletion: true,
	}

	if err := app.Run(os.Args); err != nil {
		fmt.Fprintf(os.Stderr, "ERROR: %s\n\n", err)
		os.Exit(1)
	}
}

var initCmd = &cli.Command{
	Name:  "init",
	Usage: "init key-manager service",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:  "config",
			Usage: "config file path",
		},
	},
	Action: func(cctx *cli.Context) error {
		err := conf.InitConfig(cctx.String("config"))
		if err != nil {
			return err
		}
		db, err := dao.InitMysqlDB()
		if err != nil {
			return err
		}

		_, err = db.GetPassword()
		if err == nil {
			return errors.New("password already set")
		}

		if !errors.Is(err, gorm.ErrRecordNotFound) {
			return err
		}

		log.Info("Please set a password for the encrypted mnemonic")

		password, err := stdinPassword(true)
		if err != nil {
			return err
		}
		scrypt := crypto.Scrypt(password)
		scryptKey := hex.EncodeToString(scrypt)
		err = db.CreatePassword(&dao.Password{
			Password: scryptKey,
		})

		if err != nil {
			return err
		}

		return nil
	},
}

var runCmd = &cli.Command{
	Name:  "run",
	Usage: "run key-manager process",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:  "listen",
			Usage: "The host address and port on which the key manager will listen",
			Value: "127.0.0.1:5556",
		},
		&cli.StringFlag{
			Name:  "config",
			Usage: "config file path",
		},
		&cli.StringFlag{
			Name:  "password",
			Usage: "password file path",
		},
	},
	Action: func(cctx *cli.Context) error {
		err := conf.InitConfig(cctx.String("config"))
		if err != nil {
			return err
		}
		db, err := dao.InitMysqlDB()
		if err != nil {
			return err
		}

		scrypt, err := db.GetPassword()
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return errors.New("please set a password first. run: key-manager init")
		} else if err != nil {
			return err
		}

		scryptKey, err := hex.DecodeString(scrypt)
		if err != nil {
			return err
		}

		var password string

		if cctx.IsSet("password") {
			p := cctx.String("password")
			data, err := ioutil.ReadFile(p)
			if err != nil {
				return err
			}
			password = strings.TrimSpace(string(data))
		} else {
			var err error
			password, err = stdinPassword(false)
			if err != nil {
				return err
			}
		}

		ok, err := crypto.VerifyScrypt(password, scryptKey)
		if err != nil {
			return err
		}
		if !ok {
			return errors.New("password verification failed")
		}

		listen := cctx.String("listen")
		listener, err := net.Listen("tcp", listen)
		if err != nil {
			return err
		}

		log.Infof("grpc server Listing on: %s", listen)

		grpcServer := grpc.NewServer()
		server, err := service.NewKeyManager(db, password)
		if err != nil {
			return err
		}

		service.RegisterKeyManagerServer(grpcServer, server)
		if err = grpcServer.Serve(listener); err != nil {
			log.Error(err)
			return err
		}

		return nil
	},
}

var whitelistCmd = &cli.Command{
	Name:  "whitelist",
	Usage: "whitelist tools",
	Subcommands: []*cli.Command{
		listCmd,
		addCmd,
		deleteCmd,
	},
}

var addCmd = &cli.Command{
	Name:      "add",
	Usage:     "add whitelist address",
	ArgsUsage: "[name] [address]",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:  "config",
			Usage: "config file path",
		},
	},
	Action: func(cctx *cli.Context) error {
		err := conf.InitConfig(cctx.String("config"))
		if err != nil {
			return err
		}
		db, err := dao.InitMysqlDB()
		if err != nil {
			return err
		}
		if cctx.NArg() != 2 {
			return errors.New("params need [name] [address]")
		}

		name := cctx.Args().Get(0)
		address := cctx.Args().Get(1)
		if address[:2] == "0x" {
			address = common.HexToAddress(address).String()
		} else {
			addr, err := tronaddr.Base58ToAddress(address)
			if err != nil {
				return err
			}
			address = addr.String()
		}

		err = db.CreateWhitelist(&dao.Whitelist{
			Name:         name,
			WhiteAddress: address,
		})
		if err != nil {
			return err
		}

		fmt.Printf("add whitelist address success, name: %s, addr: %s \n", name, address)

		return nil
	},
}

var deleteCmd = &cli.Command{
	Name:      "delete",
	Usage:     "delete whitelist address",
	ArgsUsage: "[name]",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:  "config",
			Usage: "config file path",
		},
	},
	Action: func(cctx *cli.Context) error {
		err := conf.InitConfig(cctx.String("config"))
		if err != nil {
			return err
		}
		db, err := dao.InitMysqlDB()
		if err != nil {
			return err
		}
		if cctx.NArg() != 1 {
			return errors.New("params need [name]")
		}

		name := cctx.Args().Get(0)

		err = db.DeleteWhitelist(name)
		if err != nil {
			return err
		}

		fmt.Printf("delete whitelist address success, name: %s \n", name)

		return nil
	},
}

var listCmd = &cli.Command{
	Name:  "list",
	Usage: "list whitelist address",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:  "config",
			Usage: "config file path",
		},
	},
	Action: func(cctx *cli.Context) error {
		err := conf.InitConfig(cctx.String("config"))
		if err != nil {
			return err
		}
		db, err := dao.InitMysqlDB()
		if err != nil {
			return err
		}

		list, err := db.GetAllWhitelists()
		if err != nil {
			return err
		}

		for _, l := range list {
			fmt.Printf("whitelist address, name: %s, addr: %s \n", l.Name, l.WhiteAddress)
		}

		return nil
	},
}

func stdinPassword(isConfirm bool) (string, error) {
	password, err := prompt.Stdin.PromptPassword("Password: ")
	if err != nil {
		return "", err
	}

	if isConfirm {
		confirm, err := prompt.Stdin.PromptPassword("Confirm Password: ")
		if err != nil {
			return "", fmt.Errorf("failed to read password confirmation: %v", err)
		}
		if password != confirm {
			return "", errors.New("passwords do not match")
		}
	}

	return password, nil
}
