package service

import (
	"context"
	"encoding/hex"
	"encoding/json"
	"errors"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	tronaddr "github.com/fbsobreira/gotron-sdk/pkg/address"
	troncommon "github.com/fbsobreira/gotron-sdk/pkg/common"
	"github.com/fbsobreira/gotron-sdk/pkg/proto/api"
	troncore "github.com/fbsobreira/gotron-sdk/pkg/proto/core"
	"google.golang.org/grpc"
	"google.golang.org/protobuf/proto"
	"key-manager/service"
	"math/big"
	"sync"
	"wallet-server/conf"
	"wallet-server/dao"
)

const (
	ethNetwork  = "eth"
	tronNetwork = "tron"
	ethCoin     = "eth"
	trxCoin     = "trx"
	usdtCoin    = "usdt"
	usdcCoin    = "usdc"
)

type WalletServer struct {
	UnimplementedWalletServerServer
	lk         sync.Mutex
	node       *Node
	dao        *dao.Dao
	keyManager service.KeyManagerClient
}

func NewWalletServer(dao *dao.Dao) (*WalletServer, error) {
	config := conf.GetConfig()
	node, err := NewNode(config.EthRpcAddrs, config.TronRpcAddrs)
	if err != nil {
		return nil, err
	}

	keyManagerClient, _, err := newKeyManagerClient(config.KeyManagerAddr)
	if err != nil {
		return nil, err
	}

	return &WalletServer{
		node:       node,
		dao:        dao,
		keyManager: keyManagerClient,
	}, nil
}

func newKeyManagerClient(keyManagerAddr string) (service.KeyManagerClient, *grpc.ClientConn, error) {
	conn, err := grpc.Dial(keyManagerAddr, grpc.WithInsecure())
	if err != nil {
		return nil, nil, err
	}
	client := service.NewKeyManagerClient(conn)
	return client, conn, nil
}

func (w *WalletServer) Transfer(ctx context.Context, req *TransferRequest) (*TransferResponse, error) {
	network := req.Network
	if network != ethNetwork && network != tronNetwork {
		return &TransferResponse{
			Req:    req,
			Code:   networkErrCode,
			Msg:    networkErrMsg,
			TxHash: "",
		}, nil
	}

	coin := req.Coin
	if coin != ethCoin && coin != trxCoin && coin != usdcCoin && coin != usdtCoin {
		return &TransferResponse{
			Req:    req,
			Code:   coinErrCode,
			Msg:    coinErrMsg,
			TxHash: "",
		}, nil
	}

	from, err1 := format(network, req.From)
	to, err2 := format(network, req.To)
	if err1 != nil || err2 != nil {
		return &TransferResponse{
			Req:    req,
			Code:   addressErrCode,
			Msg:    addressErrMsg,
			TxHash: "",
		}, nil
	}

	req.From = from
	req.To = to

	amount, ok := big.NewInt(0).SetString(req.Amount, 10)
	if !ok || amount.Cmp(big.NewInt(0)) <= 0 {
		return &TransferResponse{
			Req:    req,
			Code:   amountErrCode,
			Msg:    amountErrMsg,
			TxHash: "",
		}, nil
	}

	res, err := w.keyManager.GetIndex(ctx, &service.GetIndexRequest{
		Network: network,
		Address: from,
	})
	if err != nil {
		return &TransferResponse{
			Req:    req,
			Code:   keyManagerErrCode,
			Msg:    err.Error(),
			TxHash: "",
		}, nil
	}
	if res.Code != okCode {
		return &TransferResponse{
			Req:    req,
			Code:   res.Code,
			Msg:    res.Msg,
			TxHash: "",
		}, nil
	}

	var txStr string
	var txHash string
	if network == ethNetwork {
		var ethTx *types.Transaction
		code := serverErrCode
		if coin == ethCoin {
			ethTx, err = buildTransferEth(w.node.ethClient, req.From, req.To, amount)
		} else if coin == usdcCoin {
			ethTx, err = buildTransferUsdcOfEth(w.node.ethClient, req.From, req.To, amount)
		} else if coin == usdtCoin {
			ethTx, err = buildTransferUsdtOfEth(w.node.ethClient, req.From, req.To, amount)
		} else {
			code = coinErrCode
			err = errors.New("coin and network do not match")
		}
		if err != nil {
			return &TransferResponse{
				Req:    req,
				Code:   code,
				Msg:    err.Error(),
				TxHash: "",
			}, nil
		}

		txStr, err = marshalJEthTx(ethTx)
		if err != nil {
			return &TransferResponse{
				Req:    req,
				Code:   serverErrCode,
				Msg:    err.Error(),
				TxHash: "",
			}, nil
		}
	} else {
		var tronTx *api.TransactionExtention
		code := serverErrCode
		if coin == trxCoin {
			tronTx, err = buildTransferTrx(w.node.tronClient, req.From, req.To, amount.Int64())
		} else if coin == usdcCoin {
			tronTx, err = buildTransferUsdcOfTron(w.node.tronClient, req.From, req.To, amount)
		} else if coin == usdtCoin {
			tronTx, err = buildTransferUsdtOfTron(w.node.tronClient, req.From, req.To, amount)
		} else {
			code = coinErrCode
			err = errors.New("coin and network do not match")
		}
		if err != nil {
			return &TransferResponse{
				Req:    req,
				Code:   code,
				Msg:    err.Error(),
				TxHash: "",
			}, nil
		}
		txStr, err = marshalJTronTx(tronTx.Transaction)
		if err != nil {
			return &TransferResponse{
				Req:    req,
				Code:   serverErrCode,
				Msg:    err.Error(),
				TxHash: "",
			}, nil
		}
		txHash = troncommon.BytesToHexString(tronTx.GetTxid())
	}

	signRes, err := w.keyManager.Sign(ctx, &service.SignRequest{
		Network: network,
		Sender:  from,
		Tx:      txStr,
	})
	if err != nil {
		return &TransferResponse{
			Req:    req,
			Code:   keyManagerErrCode,
			Msg:    err.Error(),
			TxHash: "",
		}, nil
	}

	if signRes.Code != okCode {
		return &TransferResponse{
			Req:    req,
			Code:   signRes.Code,
			Msg:    signRes.Msg,
			TxHash: "",
		}, nil
	}

	if network == ethNetwork {
		tx, err := unmarshalJEthTx(signRes.SignedTx)
		if err != nil {
			return &TransferResponse{
				Req:    req,
				Code:   serverErrCode,
				Msg:    err.Error(),
				TxHash: "",
			}, nil
		}

		err = w.node.ethClient.SendTransaction(ctx, tx)
		if err != nil {
			return &TransferResponse{
				Req:    req,
				Code:   serverErrCode,
				Msg:    err.Error(),
				TxHash: "",
			}, nil
		}

		txHash = tx.Hash().String()
	} else {
		tx, err := unmarshalJTronTx(signRes.SignedTx)
		if err != nil {
			return &TransferResponse{
				Req:    req,
				Code:   serverErrCode,
				Msg:    err.Error(),
				TxHash: "",
			}, nil
		}

		result, err := w.node.tronClient.Broadcast(tx)
		if result.Code != 0 || !result.Result {
			d, _ := json.Marshal(result)
			return &TransferResponse{
				Req:    req,
				Code:   serverErrCode,
				Msg:    string(d),
				TxHash: "",
			}, nil
		}
	}

	err = w.dao.CreateTransaction(&dao.Transaction{
		TxId:    req.Id,
		Network: req.Network,
		Coin:    req.Coin,
		From:    req.From,
		To:      req.To,
		Amount:  req.Amount,
		TxHash:  txHash,
	})
	if err != nil {
		log.Warnw("save transaction to mysql failed", "id", req.Id, "network", req.Network, "coin", req.Coin, "from", req.From, "to", req.To, "amount", req.Amount, "txhash", txHash, "err", err)
	}

	log.Infow("send transaction to chain", "id", req.Id, "network", req.Network, "coin", req.Coin, "from", req.From, "to", req.To, "amount", req.Amount, "txhash", txHash)

	return &TransferResponse{
		Req:    req,
		Code:   okCode,
		Msg:    okMsg,
		TxHash: txHash,
	}, nil
}

func format(network, address string) (string, error) {
	if network == ethNetwork {
		if address[:2] != "0x" {
			return "", errors.New("invalid address")
		}

		address = common.HexToAddress(address).String()
	} else {
		if address[:1] != "T" {
			return "", errors.New("invalid address")
		}
		addr, err := tronaddr.Base58ToAddress(address)
		if err != nil {
			return "", err
		}
		address = addr.String()
	}
	return address, nil
}

func marshalJEthTx(transaction *types.Transaction) (string, error) {
	b, err := transaction.MarshalJSON()
	if err != nil {
		return "", err
	}

	return hex.EncodeToString(b), nil
}

func unmarshalJEthTx(tx string) (*types.Transaction, error) {
	b, err := hex.DecodeString(tx)
	if err != nil {
		return nil, err
	}

	var transaction = &types.Transaction{}
	err = transaction.UnmarshalJSON(b)
	if err != nil {
		return nil, err
	}

	return transaction, err
}

func marshalJTronTx(transaction *troncore.Transaction) (string, error) {
	b, err := proto.Marshal(transaction)
	if err != nil {
		return "", err
	}

	return hex.EncodeToString(b), nil
}

func unmarshalJTronTx(tx string) (*troncore.Transaction, error) {
	b, err := hex.DecodeString(tx)
	if err != nil {
		return nil, err
	}

	var transaction troncore.Transaction
	err = proto.Unmarshal(b, &transaction)
	if err != nil {
		return nil, err
	}

	return &transaction, nil
}
