package service

import (
	"context"
	"crypto/ecdsa"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
	tronaddr "github.com/fbsobreira/gotron-sdk/pkg/address"
	troncore "github.com/fbsobreira/gotron-sdk/pkg/proto/core"
	"google.golang.org/protobuf/proto"
	"key-manager/dao"
	"math/big"
)

func format(network, address string) (string, error) {
	if network == eth {
		address = common.HexToAddress(address).String()
	} else {

		addr, err := tronaddr.Base58ToAddress(address)
		if err != nil {
			return "", err
		}
		address = addr.String()
	}
	return address, nil
}

func (km *KeyManager) loadSigner(network, signer string) error {
	var err error
	signer, err = format(network, signer)
	if err != nil {
		return err
	}

	if _, ok := km.signers[signer]; !ok {
		var info dao.AccountInfo
		if network == eth {
			info, err = km.dao.GetEthFromAddress(signer)
			if err != nil {
				return err
			}
		} else {
			info, err = km.dao.GetTronFromAddress(signer)
			if err != nil {
				return err
			}
		}

		if _, exist := km.mnemonics[info.GetName()]; !exist {
			return errors.New("user mnemonic lost")
		}

		account, err := createKey(km.mnemonics[info.GetName()], network, info.GetIndex())
		if err != nil {
			return err
		}
		km.signers[signer] = account.GetPriKey()
	}

	return nil
}

func (km *KeyManager) Sign(ctx context.Context, req *SignRequest) (*SignResponse, error) {
	km.lk.Lock()
	defer km.lk.Unlock()

	if err := checkNetwork(req.Network); err != nil {
		return &SignResponse{
			Code:     networkErrCode,
			Msg:      networkErrMsg,
			Network:  req.Network,
			Sender:   req.Sender,
			SignedTx: "",
		}, nil
	}

	if err := km.loadSigner(req.Network, req.Sender); err != nil {
		return &SignResponse{
			Code:     loadSignerErrCode,
			Msg:      err.Error(),
			Network:  req.Network,
			Sender:   req.Sender,
			SignedTx: "",
		}, nil
	}

	var marshalTx string
	var coin string
	var to string
	var amount string

	if req.Network == eth {
		tx, err := unmarshalJEthTx(req.Tx)
		if err != nil {
			return &SignResponse{
				Code:     unmarshalJEthTxErrCode,
				Msg:      err.Error(),
				Network:  req.Network,
				Sender:   req.Sender,
				SignedTx: "",
			}, nil
		}

		coin, to, amount, err = checkErc(tx, km.whitelists)
		if err != nil {
			return &SignResponse{
				Code:     checkErcTxErrCode,
				Msg:      err.Error(),
				Network:  req.Network,
				Sender:   req.Sender,
				SignedTx: "",
			}, nil
		}

		signedTx, err := signEthTransaction(tx, km.signers[req.Sender])
		if err != nil {
			return &SignResponse{
				Code:     signEthTxErrCode,
				Msg:      err.Error(),
				Network:  req.Network,
				Sender:   req.Sender,
				SignedTx: "",
			}, nil
		}

		marshalTx, err = marshalJEthTx(signedTx)
		if err != nil {
			return &SignResponse{
				Code:     marshalJEthTxErrCode,
				Msg:      err.Error(),
				Network:  req.Network,
				Sender:   req.Sender,
				SignedTx: "",
			}, nil
		}
	} else {
		tx, err := unmarshalJTronTx(req.Tx)
		if err != nil {
			return &SignResponse{
				Code:     unmarshalJTronTxErrCode,
				Msg:      err.Error(),
				Network:  req.Network,
				Sender:   req.Sender,
				SignedTx: "",
			}, nil
		}

		coin, to, amount, err = checkTron(tx, km.whitelists)
		if err != nil {
			return &SignResponse{
				Code:     checkTronTxErrCode,
				Msg:      err.Error(),
				Network:  req.Network,
				Sender:   req.Sender,
				SignedTx: "",
			}, nil
		}

		signedTx, err := signTronTransaction(tx, km.signers[req.Sender])
		if err != nil {
			return &SignResponse{
				Code:     signTronTxErrCode,
				Msg:      err.Error(),
				Network:  req.Network,
				Sender:   req.Sender,
				SignedTx: "",
			}, nil
		}

		marshalTx, err = marshalJTronTx(signedTx)
		if err != nil {
			return &SignResponse{
				Code:     marshalJTronTxErrCode,
				Msg:      err.Error(),
				Network:  req.Network,
				Sender:   req.Sender,
				SignedTx: "",
			}, nil
		}
	}

	err := km.dao.CreateSignTx(&dao.SignTx{
		Network: req.Network,
		Coin:    coin,
		From:    req.Sender,
		To:      to,
		Amount:  amount,
	})

	if err != nil {
		log.Warnw("save Sign Tx to mysql failed", "network", req.Network, "coin", coin, "from", req.Sender, "to", to, "amount", amount, "err", err)
	}

	return &SignResponse{
		Code:     okCode,
		Msg:      okMsg,
		Network:  req.Network,
		Sender:   req.Sender,
		SignedTx: marshalTx,
	}, 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
}

func signEthTransaction(transaction *types.Transaction, priv *ecdsa.PrivateKey) (*types.Transaction, error) {
	signer := types.NewLondonSigner(big.NewInt(1))
	var err error
	transaction, err = types.SignTx(transaction, signer, priv)
	if err != nil {
		return nil, err
	}

	return transaction, nil
}

func signTronTransaction(transaction *troncore.Transaction, priv *ecdsa.PrivateKey) (*troncore.Transaction, error) {
	rawData, err := proto.Marshal(transaction.GetRawData())
	if err != nil {
		return nil, fmt.Errorf("proto marshal tx raw data error: %v", err)
	}
	h256h := sha256.New()
	h256h.Write(rawData)
	hash := h256h.Sum(nil)
	signature, err := crypto.Sign(hash, priv)
	if err != nil {
		return nil, fmt.Errorf("sign error: %v", err)
	}
	transaction.Signature = append(transaction.Signature, signature)
	return transaction, nil
}
