/**
* @Author: XJX
* @Description: blockchain tool
* @File: wallet.go
* @Date: 2022/1/20 18:06
 */

package blockchain

import (
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/33cn/chain33/common"
	"github.com/33cn/chain33/common/address"
	"github.com/33cn/chain33/common/crypto"
	"github.com/33cn/chain33/types"
	chainUtil "github.com/33cn/chain33/util"
	"github.com/33cn/chain33/wallet/bipwallet"
	"github.com/go-resty/resty/v2"
	"math/rand"
)

const (
	LangEnglish int = iota
	LangChinese

	CoinTypeBty uint32 = bipwallet.TypeBty
	CoinTypeYcc uint32 = bipwallet.TypeYcc

	DEATypeSECP256K1 uint32 = types.SECP256K1
	DEATypeED25519   uint32 = types.ED25519
	DEATypeSM2       uint32 = types.SM2

	btyUnit int64 = 1e8
)

type RpcClient struct {
	address string
	client  *resty.Client
	body    interface{}
	reply   *Reply
}

type WalletInfo struct {
	PublicKey  string
	PrivateKey string
	Address    string
}

type BalanceResult struct {
	Currency int    `json:"currency"`
	Balance  int    `json:"balance"`
	Frozen   int    `json:"frozen"`
	Addr     string `json:"addr"`
}

type QueryBalanceReply struct {
	Id     int             `json:"id"`
	Result []BalanceResult `json:"result"`
	Error  interface{}     `json:"error"`
}

type BalanceParams struct {
	Addresses   []string `json:"addresses"`
	Execer      string   `json:"execer"`
	AssetExec   string   `json:"asset_exec"`
	AssetSymbol string   `json:"asset_symbol"`
}

type QueryBalanceArgs struct {
	Jsonrpc string          `json:"jsonrpc"`
	Id      int             `json:"id"`
	Method  string          `json:"method"`
	Params  []BalanceParams `json:"params"`
}

type ParacrossParams struct {
	To         string `json:"to"`
	Amount     int    `json:"amount"`
	ExecName   string `json:"execName"`
	IsWithdraw bool   `json:"isWithdraw"`
}

type GetTxParacrossArgs struct {
	Jsonrpc string            `json:"jsonrpc"`
	Id      int               `json:"id"`
	Method  string            `json:"method"`
	Params  []ParacrossParams `json:"params"`
}

type GetTxReply struct {
	Id     int         `json:"id"`
	Result string      `json:"result"`
	Error  interface{} `json:"error"`
}

type CrossAssetTransferPayload struct {
	AssetExec   string `json:"assetExec"`
	AssetSymbol string `json:"assetSymbol"`
	Amount      int    `json:"amount"`
	ToAddr      string `json:"toAddr"`
	Note        string `json:"note"`
}

type CrossAssetTransferParams struct {
	Execer     string                    `json:"execer"`
	ActionName string                    `json:"actionName"`
	Payload    CrossAssetTransferPayload `json:"payload"`
}

type GetTxCrossAssetTransferArgs struct {
	Jsonrpc string                     `json:"jsonrpc"`
	Id      int                        `json:"id"`
	Method  string                     `json:"method"`
	Params  []CrossAssetTransferParams `json:"params"`
}

type TransferToExecPayload struct {
	To        string `json:"to"`
	ExecName  string `json:"execName"`
	Amount    int    `json:"amount"`
	Cointoken string `json:"cointoken"`
}

type TransferToExecParams struct {
	Execer     string                `json:"execer"`
	ActionName string                `json:"actionName"`
	Payload    TransferToExecPayload `json:"payload"`
}

type GetTransferToExecTx struct {
	Method  string                 `json:"method"`
	Jsonrpc string                 `json:"jsonrpc"`
	Id      int                    `json:"id"`
	Params  []TransferToExecParams `json:"params"`
}

type TxParams struct {
	Data  string `json:"data"`
	Token string `json:"token"`
}

type SendTxArgs struct {
	Jsonrpc string      `json:"jsonrpc"`
	Id      interface{} `json:"id"`
	Method  string      `json:"method"`
	Params  []TxParams  `json:"params"`
}

type ConvertExectoAddrParams struct {
	Execname string `json:"execname"`
}

type ConvertExectoAddrArgs struct {
	Jsonrpc string                    `json:"jsonrpc"`
	Id      int                       `json:"id"`
	Method  string                    `json:"method"`
	Params  []ConvertExectoAddrParams `json:"params"`
}

type QueryTxArgs struct {
	Jsonrpc string `json:"jsonrpc"`
	Id      int32  `json:"id"`
	Method  string `json:"method"`
	Params  []struct {
		Hash string `json:"hash"`
	} `json:"params"`
}

type QueryTxReply struct {
	Id     int32 `json:"id"`
	Result struct {
		Tx      interface{} `json:"tx"`
		Receipt struct {
			Ty   int32       `json:"ty"`
			Logs interface{} `json:"logs"`
		} `json:"receipt"`
		Proofs     []string `json:"proofs"`
		Height     int64    `json:"height"`
		Index      int32    `json:"index"`
		BlockTime  int64    `json:"blockTime"`
		Amount     int64    `json:"amount"`
		FromAddr   string   `json:"fromAddr"`
		ActionName string   `json:"actionName"`
	} `json:"result"`
	Error interface{} `json:"error"`
}

type SendRedPacketArgs struct {
	Jsonrpc string                `json:"jsonrpc"`
	Method  string                `json:"method"`
	Params  []SendRedPacketParams `json:"params"`
}

type SendRedPacketParams struct {
	AssetExec     string   `json:"assetExec"`
	AssetSymbol   string   `json:"assetSymbol"`
	Amount        int64    `json:"amount"`
	Size          int      `json:"size"`
	Type          int      `json:"type"`
	Remark        string   `json:"remark"`
	ToAddr        []string `json:"toAddr"`
	SignPubkey    string   `json:"signPubkey"`
	ExpiresTime   int64    `json:"expiresTime"`
	DecimalPlaces int      `json:"decimalPlaces"`
}

type SendRedPacketReply struct {
	Id     interface{} `json:"id"`
	Result string      `json:"result"`
	Error  interface{} `json:"error"`
}

type BackRedPacketArgs struct {
	Jsonrpc string                `json:"jsonrpc"`
	Method  string                `json:"method"`
	Params  []BackRedPacketParams `json:"params"`
}

type BackRedPacketParams struct {
	PacketId string `json:"packetId"`
}

type BackRedPacketReply struct {
	Id     interface{} `json:"id"`
	Result string      `json:"result"`
	Error  interface{} `json:"error"`
}

func GenerateMnemonic(lang, bitsize int) (string, error) {
	mnemonic, err := bipwallet.NewMnemonicString(lang, bitsize)
	return mnemonic, err
}

func NewWalletByMnemonic(coinType, DEAType uint32, mnemonic string) (*WalletInfo, error) {
	result := &WalletInfo{}
	wallet, err := bipwallet.NewWalletFromMnemonic(coinType, DEAType, mnemonic)
	if err == nil {
		priv, pub, err := wallet.NewKeyPair(0)
		if err == nil {
			result.PublicKey = hex.EncodeToString(pub)
			result.PrivateKey = hex.EncodeToString(priv)
			addr, err := bipwallet.PubToAddress(pub)
			if err == nil {
				result.Address = addr
			}
		}
	}
	return result, err
}

func PrivateKeyToPublicKey(coinType, DEAType uint32, privateKey string) (string, error) {
	depriv, err := hex.DecodeString(privateKey)
	if err == nil {
		pub, err := bipwallet.PrivkeyToPub(coinType, DEAType, depriv)
		if err == nil {
			return hex.EncodeToString(pub), nil
		}
	}
	return "", err
}

func PublicKeyToAddress(publicKey string) (string, error) {
	depub, err := hex.DecodeString(publicKey)
	if err == nil {
		addr, err := bipwallet.PubToAddress(depub)
		if err == nil {
			return addr, nil
		}
	}
	return "", err
}

func NewRpcClient(address string) (*RpcClient, error) {
	client := resty.New()
	rpcClient := &RpcClient{
		address: address,
		client:  client,
		reply:   &Reply{},
	}
	return rpcClient, nil
}

func (rpcClient *RpcClient) QueryBalance(args QueryBalanceArgs) ([]BalanceResult, error) {
	url := rpcClient.address
	resp, err := rpcClient.client.R().
		SetHeader("Content-Type", "application/json").
		SetBody(args).
		SetResult(&QueryBalanceReply{}).
		Post(url)
	reply := &QueryBalanceReply{}
	result := []BalanceResult{}
	if err == nil {
		json.Unmarshal([]byte(resp.String()), reply)
		if reply.Error == nil {
			result = reply.Result
			return result, nil
		} else {
			err = errors.New(reply.Error.(string))
		}
	}
	return result, err
}

func (rpcClient *RpcClient) GetParacrossTx(args GetTxParacrossArgs) (string, error) {
	tx := ""
	url := rpcClient.address
	resp, err := rpcClient.client.R().
		SetHeader("Content-Type", "application/json").
		SetBody(args).
		SetResult(&GetTxReply{}).
		Post(url)
	reply := &GetTxReply{}
	if err == nil {
		json.Unmarshal([]byte(resp.String()), reply)
		if reply.Error == nil {
			tx = reply.Result
			return tx, nil
		} else {
			err = errors.New(reply.Error.(string))
		}
	}
	return tx, err
}

func (rpcClient *RpcClient) GetCrossAssetTransferTx(args GetTxCrossAssetTransferArgs) (string, error) {
	tx := ""
	url := rpcClient.address
	resp, err := rpcClient.client.R().
		SetHeader("Content-Type", "application/json").
		SetBody(args).
		SetResult(&GetTxReply{}).
		Post(url)
	reply := &GetTxReply{}
	if err == nil {
		json.Unmarshal([]byte(resp.String()), reply)
		if reply.Error == nil {
			tx = reply.Result
			return tx, nil
		} else {
			err = errors.New(reply.Error.(string))
		}
	}
	return tx, err
}

func (rpcClient *RpcClient) GetTransferToExecTx(args GetTransferToExecTx) (string, error) {
	tx := ""
	url := rpcClient.address
	resp, err := rpcClient.client.R().
		SetHeader("Content-Type", "application/json").
		SetBody(args).
		SetResult(&GetTxReply{}).
		Post(url)
	reply := &GetTxReply{}
	if err == nil {
		json.Unmarshal([]byte(resp.String()), reply)
		if reply.Error == nil {
			tx = reply.Result
			return tx, nil
		} else {
			err = errors.New(reply.Error.(string))
		}
	}
	return tx, err
}

func (rpcClient *RpcClient) ConvertExectoAddr(args ConvertExectoAddrArgs) (string, error) {
	url := rpcClient.address
	resp, err := rpcClient.client.R().
		SetHeader("Content-Type", "application/json").
		SetBody(args).
		SetResult(&GetTxReply{}).
		Post(url)
	reply := &GetTxReply{}
	if err == nil {
		json.Unmarshal([]byte(resp.String()), reply)
		if reply.Error == nil {
			return reply.Result, nil
		} else {
			err = errors.New(reply.Error.(string))
		}
	}
	return "", err
}

func (rpcClient *RpcClient) SendTx(signStr string) (string, error) {
	args := SendTxArgs{
		Jsonrpc: "2.0",
		Method:  "Chain33.SendTransaction",
		Id:      0,
		Params: []TxParams{
			{
				Data: signStr,
			},
		},
	}
	tx := ""
	url := rpcClient.address
	resp, err := rpcClient.client.R().
		SetHeader("Content-Type", "application/json").
		SetBody(args).
		SetResult(&GetTxReply{}).
		Post(url)
	reply := &GetTxReply{}
	if err == nil {
		json.Unmarshal([]byte(resp.String()), reply)
		if reply.Error == nil {
			return reply.Result, nil
		} else {
			err = errors.New(reply.Error.(string))
		}
	}
	return tx, err
}

func (rpcClient *RpcClient) createNoneTx(execName string, fee int64) *types.Transaction {
	tx := &types.Transaction{
		Execer: []byte(execName),
		Nonce:  rand.Int63(),
		To:     address.ExecAddress(execName),
		Fee:    fee,
	}
	return tx
}

func (rpcClient *RpcClient) CreateGroupTx(txs []string, execName string, fee int64) (string, error) {
	n := len(txs)
	if n > 20 {
		return "", errors.New("txs: The maximum length cannot exceed 20")
	}
	var err error
	var minFeeRate int64 = 1e5
	var txGrous []*types.Transaction
	txGrous = append(txGrous, rpcClient.createNoneTx(execName, fee*int64(n)))
	for _, tx := range txs {
		var tmpTx types.Transaction
		t, err := common.FromHex(tx)
		if err != nil {
			break
		}
		types.Decode(t, &tmpTx)
		txGrous = append(txGrous, &tmpTx)
	}
	if err == nil {
		txgrop, err := types.CreateTxGroup(txGrous, minFeeRate)
		if err == nil {
			groupTx := common.ToHex(types.Encode(txgrop.Tx()))
			return groupTx, nil
		}
	}
	return "", err
}

func (rpcClient *RpcClient) QueryTransaction(hash string) (bool, error) {
	url := rpcClient.address

	args := QueryTxArgs{
		Jsonrpc: "2.0",
		Method:  "Chain33.QueryTransaction",
		Id:      1,
		Params: []struct {
			Hash string `json:"hash"`
		}{{Hash: hash}},
	}

	resp, err := rpcClient.client.R().
		SetHeader("Content-Type", "application/json").
		SetBody(args).
		SetResult(&QueryTxReply{}).
		Post(url)
	reply := &QueryTxReply{}
	if err == nil {
		json.Unmarshal([]byte(resp.String()), &reply)
		if reply.Error == nil {
			if reply.Result.Receipt.Ty == 2 {
				return true, nil
			} else {
				return false, errors.New("Tx is fail")
			}
		} else {
			err = errors.New(reply.Error.(string))
		}
	}
	return false, err
}

func (rpcClient *RpcClient) SignTx(rawTx, privkey string) (string, error) {
	tx := new(types.Transaction)
	txBytes, err := common.FromHex(rawTx)
	if err == nil {
		err = types.Decode(txBytes, tx)
		if err == nil {
			c, err := crypto.Load("secp256k1", -1)
			if err == nil {
				key, err := common.FromHex(privkey)
				if err == nil {
					signKey, err := c.PrivKeyFromBytes(key)
					if err == nil {
						tx.Sign(types.SECP256K1, signKey)
						return common.ToHex(types.Encode(tx)), nil
					}
				}
			}
		}
	}
	return "", err
}

func (rpcClient *RpcClient) SendRedPacket(args SendRedPacketArgs) (string, error) {
	url := rpcClient.address
	hash := ""
	resp, err := rpcClient.client.R().
		SetHeader("Content-Type", "application/json").
		SetBody(args).
		SetResult(&SendRedPacketReply{}).
		Post(url)
	reply := &SendRedPacketReply{}
	if err == nil {
		json.Unmarshal([]byte(resp.String()), reply)
		if reply.Error == nil {
			return reply.Result, nil
		} else {
			err = errors.New(reply.Error.(string))
		}
	}
	return hash, err
}

func (rpcClient *RpcClient) BackRedPacket(args BackRedPacketArgs) (string, error) {
	url := rpcClient.address
	hash := ""
	resp, err := rpcClient.client.R().
		SetHeader("Content-Type", "application/json").
		SetBody(args).
		SetResult(&BackRedPacketReply{}).
		Post(url)
	reply := &BackRedPacketReply{}
	if err == nil {
		json.Unmarshal([]byte(resp.String()), reply)
		if reply.Error == nil {
			return reply.Result, nil
		} else {
			err = errors.New(reply.Error.(string))
		}
	}
	return hash, err
}

func (rpcClient *RpcClient) SignTxGroup(rawGroupTx, privkey string, feePrivkey string) (string, []string, error) {
	txbytes, err := common.FromHex(rawGroupTx)
	hashs := []string{}
	if err == nil {
		var tx types.Transaction
		err = types.Decode(txbytes, &tx)

		if err == nil {
			txs, err := tx.GetTxGroup()
			if err == nil {
				priv := chainUtil.HexToPrivkey(privkey)
				feePriv := chainUtil.HexToPrivkey(feePrivkey)
				for i := range txs.GetTxs() {
					if i == 0 {
						err = txs.SignN(i, types.SECP256K1, feePriv)
					} else {
						err = txs.SignN(i, types.SECP256K1, priv)
					}
					if err != nil {
						break
					}
				}

				for i := 0; i < len(txs.GetTxs())-1; i++ {
					hashs = append(hashs, common.ToHex(txs.Txs[i].Next))
				}
				return common.ToHex(types.Encode(txs.Tx())), hashs, nil
			}
		}
	}
	return "", hashs, err
}

type Method string

const (
	MethodRawTx   = "Chain33.CreateRawTransaction"
	MethodTx      = "Chain33.CreateTransaction"
	MethodBalance = "Chain33.GetBalance"
)

func (rpcClient *RpcClient) CreateTx(method Method, params interface{}) (string, error) {
	rpcClient.body = Body{
		JsonRpc: "2.0",
		Id:      0,
		Method:  method,
		Params:  params,
	}
	res, err := rpcClient.sendRequest()
	if err != nil {
		return "", err
	}
	tx, ok := res.(string)
	if !ok {
		return "", nil
	}
	return tx, nil
}

func (rpcClient *RpcClient) GetParaCrossBalance(addr string) (*Balance, error) {
	rpcClient.body = Body{
		JsonRpc: "2.0",
		Id:      0,
		Method:  MethodBalance,
		Params: []map[string]interface{}{
			{
				"addresses":    []string{addr},
				"execer":       "paracross",
				"asset_exec":   "coins",
				"asset_symbol": "bty",
			},
		},
	}
	resp, err := rpcClient.client.R().
		SetHeader("Content-Type", "application/json").
		SetBody(rpcClient.body).
		SetResult(&BalanceReply{}).
		Post(rpcClient.address)
	reply := &BalanceReply{}
	if err == nil {
		json.Unmarshal([]byte(resp.String()), &reply)
		if reply.Error == nil {
			return reply.Result[0], nil
		} else {
			err = errors.New(reply.Error.(string))
		}
	}
	return nil, err
}

func (rpcClient *RpcClient) sendRequest() (interface{}, error) {
	url := rpcClient.address
	resp, err := rpcClient.client.R().
		SetHeader("Content-Type", "application/json").
		SetBody(rpcClient.body).
		Post(url)
	var res interface{}
	if err == nil {
		err := json.Unmarshal([]byte(resp.String()), rpcClient.reply)

		if err != nil {
			fmt.Println(err)
		}
		if rpcClient.reply.Error == nil {
			res = rpcClient.reply.Result
			return res, nil
		} else {
			err = errors.New(rpcClient.reply.Error.(string))
		}

	}
	return res, err
}

type Reply struct {
	Id     int         `json:"id"`
	Result interface{} `json:"result"`
	Error  interface{} `json:"error"`
}

type Body struct {
	JsonRpc string      `json:"jsonrpc"`
	Id      int         `json:"id"`
	Method  Method      `json:"method"`
	Params  interface{} `json:"params"`
}

type BalanceReply struct {
	Id     int         `json:"id"`
	Result []*Balance  `json:"result"`
	Error  interface{} `json:"error"`
}

type Balance struct {
	Currency int64  `json:"currency"`
	Balance  int64  `json:"balance"`
	Frozen   int64  `json:"frozen"`
	Addr     string `json:"addr"`
}
