// Copyright Fuzamei Corp. 2018 All Rights Reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package commands

import (
	"fmt"
	"os"
	"strconv"
	"time"

	"errors"

	"github.com/33cn/chain33/rpc/jsonclient"
	rpctypes "github.com/33cn/chain33/rpc/types"
	. "github.com/33cn/chain33/system/dapp/commands/types"
	"github.com/33cn/chain33/types"
	"github.com/spf13/cobra"
)

// WalletCmd wallet command
func WalletCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "wallet",
		Short: "Wallet management",
		Args:  cobra.MinimumNArgs(1),
	}

	cmd.AddCommand(
		LockCmd(),
		UnlockCmd(),
		WalletStatusCmd(),
		SetPwdCmd(),
		WalletListTxsCmd(),
		MergeBalanceCmd(),
		AutoMineCmd(),
		SignRawTxCmd(),
		NoBalanceCmd(),
		SetFeeCmd(),
		SendTxCmd(),
	)

	return cmd
}

// LockCmd lock the wallet
func LockCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "lock",
		Short: "Lock wallet",
		Run:   lock,
	}
	return cmd
}

func lock(cmd *cobra.Command, args []string) {
	rpcLaddr, _ := cmd.Flags().GetString("rpc_laddr")
	var res rpctypes.Reply
	ctx := jsonclient.NewRpcCtx(rpcLaddr, "Chain33.Lock", nil, &res)
	ctx.Run()
}

// UnlockCmd unlock the wallet
func UnlockCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "unlock",
		Short: "Unlock wallet",
		Run:   unLock,
	}
	addUnlockFlags(cmd)
	return cmd
}

func addUnlockFlags(cmd *cobra.Command) {
	cmd.Flags().StringP("pwd", "p", "", "password needed to unlock")
	cmd.MarkFlagRequired("pwd")

	cmd.Flags().Int64P("time_out", "t", 0, "time out for unlock operation(0 for unlimited)")
	cmd.Flags().StringP("scope", "s", "wallet", "unlock scope(wallet/ticket)")
}

func unLock(cmd *cobra.Command, args []string) {
	rpcLaddr, _ := cmd.Flags().GetString("rpc_laddr")
	pwd, _ := cmd.Flags().GetString("pwd")
	timeOut, _ := cmd.Flags().GetInt64("time_out")
	scope, _ := cmd.Flags().GetString("scope")
	var walletOrTicket bool
	switch scope {
	case "wallet":
		walletOrTicket = false
	case "ticket":
		walletOrTicket = true
	default:
		fmt.Println("unlock scope code wrong: wallet or ticket")
		return
	}

	params := types.WalletUnLock{
		Passwd:         pwd,
		Timeout:        timeOut,
		WalletOrTicket: walletOrTicket,
	}
	var res rpctypes.Reply
	ctx := jsonclient.NewRpcCtx(rpcLaddr, "Chain33.UnLock", params, &res)
	ctx.Run()
}

// WalletStatusCmd get wallet status
func WalletStatusCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "status",
		Short: "Get wallet status",
		Run:   walletStatus,
	}
	return cmd
}

func walletStatus(cmd *cobra.Command, args []string) {
	rpcLaddr, _ := cmd.Flags().GetString("rpc_laddr")
	var res rpctypes.WalletStatus
	ctx := jsonclient.NewRpcCtx(rpcLaddr, "Chain33.GetWalletStatus", nil, &res)
	ctx.Run()
}

// SetPwdCmd set password
func SetPwdCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "set_pwd",
		Short: "Set password",
		Run:   setPwd,
	}
	addSetPwdFlags(cmd)
	return cmd
}

func addSetPwdFlags(cmd *cobra.Command) {
	cmd.Flags().StringP("old", "o", "", "old password")
	cmd.MarkFlagRequired("old")

	cmd.Flags().StringP("new", "n", "", "new password")
	cmd.MarkFlagRequired("new")
}

func setPwd(cmd *cobra.Command, args []string) {
	rpcLaddr, _ := cmd.Flags().GetString("rpc_laddr")
	oldPwd, _ := cmd.Flags().GetString("old")
	newPwd, _ := cmd.Flags().GetString("new")
	params := types.ReqWalletSetPasswd{
		OldPass: oldPwd,
		NewPass: newPwd,
	}
	var res rpctypes.Reply
	ctx := jsonclient.NewRpcCtx(rpcLaddr, "Chain33.SetPasswd", params, &res)
	ctx.Run()
}

// WalletListTxsCmd  get wallet transactions
func WalletListTxsCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "list_txs",
		Short: "List transactions in wallet",
		Run:   walletListTxs,
	}
	addWalletListTxsFlags(cmd)
	return cmd
}

func addWalletListTxsFlags(cmd *cobra.Command) {
	cmd.Flags().StringP("from", "f", "", "from which transaction begin")
	cmd.MarkFlagRequired("from")

	cmd.Flags().Int32P("count", "c", 0, "number of transactions")
	cmd.MarkFlagRequired("count")

	cmd.Flags().Int32P("direction", "d", 1, "query direction (0: pre page, 1: next page)")
}

func walletListTxs(cmd *cobra.Command, args []string) {
	rpcLaddr, _ := cmd.Flags().GetString("rpc_laddr")
	txHash, _ := cmd.Flags().GetString("from")
	count, _ := cmd.Flags().GetInt32("count")
	direction, _ := cmd.Flags().GetInt32("dir")
	params := rpctypes.ReqWalletTransactionList{
		FromTx:    txHash,
		Count:     count,
		Direction: direction,
	}
	var res rpctypes.WalletTxDetails
	ctx := jsonclient.NewRpcCtx(rpcLaddr, "Chain33.WalletTxList", params, &res)
	ctx.SetResultCb(parseWalletTxListRes)
	ctx.Run()
}

func parseWalletTxListRes(arg interface{}) (interface{}, error) {
	res := arg.(*rpctypes.WalletTxDetails)
	var result WalletTxDetailsResult
	for _, v := range res.TxDetails {
		amountResult := strconv.FormatFloat(float64(v.Amount)/float64(types.Coin), 'f', 4, 64)
		wtxd := &WalletTxDetailResult{
			Tx:         DecodeTransaction(v.Tx),
			Receipt:    v.Receipt,
			Height:     v.Height,
			Index:      v.Index,
			Blocktime:  v.BlockTime,
			Amount:     amountResult,
			Fromaddr:   v.FromAddr,
			Txhash:     v.TxHash,
			ActionName: v.ActionName,
		}
		result.TxDetails = append(result.TxDetails, wtxd)
	}
	return result, nil
}

// MergeBalanceCmd  merge all balance to an account
func MergeBalanceCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "merge",
		Short: "Merge accounts' balance into address",
		Run:   mergeBalance,
	}
	addMergeBalanceFlags(cmd)
	return cmd
}

func addMergeBalanceFlags(cmd *cobra.Command) {
	cmd.Flags().StringP("to", "t", "", "destination account address")
	cmd.MarkFlagRequired("to")
}

func mergeBalance(cmd *cobra.Command, args []string) {
	rpcLaddr, _ := cmd.Flags().GetString("rpc_laddr")
	toAddr, _ := cmd.Flags().GetString("to")
	params := types.ReqWalletMergeBalance{
		To: toAddr,
	}
	var res rpctypes.ReplyHashes
	ctx := jsonclient.NewRpcCtx(rpcLaddr, "Chain33.MergeBalance", params, &res)
	ctx.Run()
}

// AutoMineCmd  set auto mining: 为了兼容现在的命令行, 这个命令就放到wallet，实际上依赖 ticket
func AutoMineCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "auto_mine",
		Short: "Set auto mine on/off",
		Run:   autoMine,
	}
	addAutoMineFlags(cmd)
	return cmd
}

func addAutoMineFlags(cmd *cobra.Command) {
	cmd.Flags().Int32P("flag", "f", 0, `auto mine(0: off, 1: on)`)
	cmd.MarkFlagRequired("flag")
}

func autoMine(cmd *cobra.Command, args []string) {
	rpcLaddr, _ := cmd.Flags().GetString("rpc_laddr")
	flag, _ := cmd.Flags().GetInt32("flag")
	if flag != 0 && flag != 1 {
		cmd.UsageFunc()(cmd)
		return
	}
	params := struct {
		Flag int32
	}{
		Flag: flag,
	}
	var res rpctypes.Reply
	ctx := jsonclient.NewRpcCtx(rpcLaddr, "ticket.SetAutoMining", params, &res)
	ctx.Run()
}

// NoBalanceCmd sign raw tx
func NoBalanceCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "nobalance",
		Short: "Create nobalance transaction",
		Run:   noBalanceTx,
	}
	addNoBalanceFlags(cmd)
	return cmd
}

func addNoBalanceFlags(cmd *cobra.Command) {
	cmd.Flags().StringP("data", "d", "", "raw transaction data")
	cmd.MarkFlagRequired("data")
	cmd.Flags().StringP("key", "k", "", "private key of pay fee account (optional)")
	cmd.Flags().StringP("payaddr", "a", "", "account address of pay fee (optional)")
	cmd.Flags().StringP("expire", "e", "120s", "transaction expire time")
}

// SignRawTxCmd sign raw tx
func SignRawTxCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "sign",
		Short: "Sign transaction",
		Run:   signRawTx,
	}
	addSignRawTxFlags(cmd)
	return cmd
}

func addSignRawTxFlags(cmd *cobra.Command) {
	cmd.Flags().StringP("data", "d", "", "raw transaction data")
	cmd.MarkFlagRequired("data")

	cmd.Flags().Int32P("index", "i", 0, "transaction index to be signed")
	cmd.Flags().StringP("key", "k", "", "private key (optional)")
	cmd.Flags().StringP("addr", "a", "", "account address (optional)")
	cmd.Flags().StringP("expire", "e", "120s", "transaction expire time")
	// A duration string is a possibly signed sequence of
	// decimal numbers, each with optional fraction and a unit suffix,
	// such as "300ms", "-1.5h" or "2h45m".
	// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
}

func noBalanceTx(cmd *cobra.Command, args []string) {
	rpcLaddr, _ := cmd.Flags().GetString("rpc_laddr")
	data, _ := cmd.Flags().GetString("data")
	addr, _ := cmd.Flags().GetString("payaddr")
	privkey, _ := cmd.Flags().GetString("key")
	expire, _ := cmd.Flags().GetString("expire")
	expireTime, err := time.ParseDuration(expire)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		return
	}
	if expireTime < time.Minute*2 && expireTime != time.Second*0 {
		expire = "120s"
		fmt.Println("expire time must longer than 2 minutes, changed expire time into 2 minutes")
	}
	params := types.NoBalanceTx{
		PayAddr: addr,
		TxHex:   data,
		Expire:  expire,
		Privkey: privkey,
	}
	ctx := jsonclient.NewRpcCtx(rpcLaddr, "Chain33.CreateNoBalanceTransaction", params, nil)
	ctx.RunWithoutMarshal()
}

func parseTxHeight(expire string) error {
	if len(expire) == 0 {
		return errors.New("expire string should not be empty")
	}

	if expire[0] == 'H' && expire[1] == ':' {
		txHeight, err := strconv.Atoi(expire[2:])
		if err != nil {
			return err
		}
		if txHeight <= 0 {
			fmt.Printf("txHeight should be grate to 0")
			return errors.New("txHeight should be grate to 0")
		}

		return nil
	}

	return errors.New("Invalid expire format. Should be one of {time:\"3600s/1min/1h\" block:\"123\" txHeight:\"H:123\"}")
}

func parseExpireOpt(expire string) (string, error) {
	//时间格式123s/1m/1h
	expireTime, err := time.ParseDuration(expire)
	if err == nil {
		if expireTime < time.Minute*2 && expireTime != time.Second*0 {
			expire = "120s"
			fmt.Println("expire time must longer than 2 minutes, changed expire time into 2 minutes")
		}

		return expire, nil
	}

	//区块高度格式，123
	blockInt, err := strconv.Atoi(expire)
	if err == nil {
		if blockInt <= 0 {
			fmt.Printf("block height should be grate to 0")
			return "", errors.New("block height should be grate to 0")
		}
		return expire, nil
	}

	//Txheight格式，H:123
	err = parseTxHeight(expire)
	if err != nil {
		return "", err
	}

	return expire, err
}

func signRawTx(cmd *cobra.Command, args []string) {
	rpcLaddr, _ := cmd.Flags().GetString("rpc_laddr")
	data, _ := cmd.Flags().GetString("data")
	key, _ := cmd.Flags().GetString("key")
	addr, _ := cmd.Flags().GetString("addr")
	index, _ := cmd.Flags().GetInt32("index")
	expire, _ := cmd.Flags().GetString("expire")
	expire, err := parseExpireOpt(expire)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		return
	}

	params := types.ReqSignRawTx{
		Addr:    addr,
		Privkey: key,
		TxHex:   data,
		Expire:  expire,
		Index:   index,
	}
	ctx := jsonclient.NewRpcCtx(rpcLaddr, "Chain33.SignRawTx", params, nil)
	ctx.RunWithoutMarshal()
}

// SetFeeCmd set tx fee
func SetFeeCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "set_fee",
		Short: "Set transaction fee",
		Run:   setFee,
	}
	addSetFeeFlags(cmd)
	return cmd
}

func addSetFeeFlags(cmd *cobra.Command) {
	cmd.Flags().Float64P("amount", "a", 0, "tx fee amount")
	cmd.MarkFlagRequired("amount")
}

func setFee(cmd *cobra.Command, args []string) {
	rpcLaddr, _ := cmd.Flags().GetString("rpc_laddr")
	amount, _ := cmd.Flags().GetFloat64("amount")
	amountInt64 := int64(amount * 1e4)
	params := types.ReqWalletSetFee{
		Amount: amountInt64 * 1e4,
	}
	var res rpctypes.Reply
	ctx := jsonclient.NewRpcCtx(rpcLaddr, "Chain33.SetTxFee", params, &res)
	ctx.Run()
}

// SendTxCmd  send raw tx
func SendTxCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "send",
		Short: "Send a transaction",
		Run:   sendTx,
	}
	addSendTxFlags(cmd)
	return cmd
}

func addSendTxFlags(cmd *cobra.Command) {
	cmd.Flags().StringP("data", "d", "", "transaction content")
	cmd.MarkFlagRequired("data")

	cmd.Flags().StringP("token", "t", types.BTY, "token name. (BTY supported)")
}

func sendTx(cmd *cobra.Command, args []string) {
	rpcLaddr, _ := cmd.Flags().GetString("rpc_laddr")
	data, _ := cmd.Flags().GetString("data")
	token, _ := cmd.Flags().GetString("token")
	params := rpctypes.RawParm{
		Token: token,
		Data:  data,
	}

	ctx := jsonclient.NewRpcCtx(rpcLaddr, "Chain33.SendTransaction", params, nil)
	ctx.RunWithoutMarshal()
}
