package eth

import (
	"context"
	"crypto/ecdsa"
	"encoding/json"
	"fmt"
	"log"
	"math/big"
	"strings"
	"syncscan-go/erc20"
	"syncscan-go/model"

	"github.com/ethereum/go-ethereum"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethclient"
	"github.com/pkg/errors"
	"github.com/ybbus/jsonrpc/v2"
	"golang.org/x/crypto/sha3"
)

type Eth struct {
	host      string
	client    *ethclient.Client
	RpcClient jsonrpc.RPCClient
}

var LogTransferSignHash = crypto.Keccak256Hash([]byte("Transfer(address,address,uint256)"))

func New(rawurl string) (*Eth, error) {
	client, err := ethclient.Dial(rawurl)
	if err != nil {
		fmt.Printf("Failed to connect to eth: %v", err)
		return nil, err
	}
	rpcClient := jsonrpc.NewClient(rawurl)
	return &Eth{host: rawurl, client: client, RpcClient: rpcClient}, nil
}

func (this *Eth) GetTxByHash(hash string) (tx model.Tx, err error) {
	response, err := this.RpcClient.Call("eth_getTransactionByHash", hash)
	if err != nil {
		return
	}

	log.Printf("%+v\n\n\n", response.Result)

	err = response.GetObject(&tx)
	if err != nil {
		return
	}
	return
}

func (this *Eth) GetLastBlockNumber() (*big.Int, error) {
	header, err := this.client.HeaderByNumber(context.Background(), nil)
	if err != nil {
		return nil, err
	}
	return header.Number, nil
}

func (this *Eth) GetBlockByNumber(number *big.Int) (*types.Block, error) {
	b, err := this.client.BlockByNumber(context.Background(), number)
	if err != nil {
		return nil, err
	}
	return b, nil
}

const ETH = 1_000_000_000_000_000_000

func getFixedReward(blockNo uint64) uint64 {
	if blockNo < 4370000 {
		return ETH * 5
	} else if blockNo < 7280000 {
		return ETH * 3
	} else {
		return ETH * 2
	}
}

// 당근을 위해 커스터마이징 된 부분
func getFixedRewardForDangnn(blockNo uint64) uint64 {
	return ETH * 3
}

func (this *Eth) GetReward(b *types.Block, rts types.Receipts) (blockReward *big.Float, txFee *big.Int, uncleTotalReward *big.Float, err error) {

	fixedReward := getFixedRewardForDangnn(b.Number().Uint64())
	blockReward = big.NewFloat(float64(fixedReward))
	txFee = big.NewInt(0)

	txs := b.Transactions()
	for i, rt := range rts {
		gasUsed := big.NewInt(int64(rt.GasUsed))
		fee := new(big.Int).Mul(gasUsed, txs[i].GasPrice())
		txFee.Add(txFee, fee)
	}

	uncleCount := len(b.Uncles())

	if uncleCount > 2 {
		uncleCount = 2
	}

	uncleTotalReward = big.NewFloat(0)
	if uncleCount > 0 {
		//블럭 마이너에게 돌아가는 리워드
		uncleInclusionFee := (float64(fixedReward) * 0.03125) * float64(uncleCount)
		for i, u := range b.Uncles() {
			if i == 2 {
				break
			}
			//엉클블럭 마이너에게 돌아가는 리워드
			uncleReward := float64(u.Number.Uint64() - b.Number().Uint64() + 8)
			uncleReward *= float64(fixedReward / 8)
			uncleTotalReward.Add(uncleTotalReward, big.NewFloat(uncleReward))
		}
		uncleTotalReward.Add(uncleTotalReward, big.NewFloat(uncleInclusionFee))
	}

	//Fixed_Fee + Tx_Fee + Uncle_Fee
	blockReward.Add(blockReward, big.NewFloat(float64(txFee.Uint64())))
	blockReward.Add(blockReward, uncleTotalReward)

	return blockReward, txFee, uncleTotalReward, nil
}

func (this *Eth) GetTxReceipts(transactions types.Transactions) (ret types.Receipts, err error) {

	for _, tx := range transactions {

		receipt, err := this.client.TransactionReceipt(context.Background(), tx.Hash())
		if err != nil {
			return nil, err
		}
		if receipt != nil {
			ret = append(ret, receipt)
		}
	}
	return
}

func (this *Eth) GetTxReceipt(txhash string) (ret *types.Receipt, err error) {

	receipt, err := this.client.TransactionReceipt(context.Background(), common.HexToHash(txhash))
	if err != nil {
		return
	}
	if receipt != nil {
		ret = receipt
	}
	return
}

func (this *Eth) GetToken(contractAddr common.Address) (token *erc20.Erc20, err error) {
	token, err = erc20.NewErc20(contractAddr, this.client)
	return
}

func (this *Eth) GetUncleBlockByBlockNumberAndIndex(blockNo uint64, idx int) (interface{}, error) {
	b, err := this.RpcClient.Call("eth_getUncleByBlockNumberAndIndex", fmt.Sprintf("0x%x", blockNo), fmt.Sprintf("0x%x", idx))
	if err != nil {
		return "", err
	}

	return b.Result, nil
}

func (this *Eth) CreateWallet() (privateKey, walletAddr string) {
	key, err := crypto.GenerateKey()
	if err != nil {
		log.Fatal(err)
	}

	privateKeyBytes := crypto.FromECDSA(key)
	privateKey = hexutil.Encode(privateKeyBytes)[2:]

	publicKey := key.Public()
	publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
	if !ok {
		log.Fatal("error casting public key to ECDSA")
	}

	publicKeyBytes := crypto.FromECDSAPub(publicKeyECDSA)
	//log.Println(hexutil.Encode(publicKeyBytes)[4:])

	//address := crypto.PubkeyToAddress(*publicKeyECDSA).Hex()
	hash := sha3.NewLegacyKeccak256()
	hash.Write(publicKeyBytes[1:])
	walletAddr = hexutil.Encode(hash.Sum(nil)[12:])
	return
}

func (this *Eth) GetWalletAddressFromPrivateKey(privateKey string) (string, error) {

	if privateKey[:2] == "0x" {
		privateKey = privateKey[2:]
	}

	pkey, err := crypto.HexToECDSA(privateKey)
	if err != nil {
		return "", err
	}
	publicKey := pkey.Public()
	publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
	if !ok {
		return "", errors.New("error casting public key to ECDSA")
	}

	fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
	return fromAddress.Hex(), nil
}

func (this *Eth) Transfer(privateKey, to string, valueInWei, gasPrice *big.Int, gasLimit uint64) (txHash string, err error) {

	if privateKey[:2] == "0x" {
		privateKey = privateKey[2:]
	}
	if to[:2] == "0x" {
		to = to[2:]
	}

	pkey, err := crypto.HexToECDSA(privateKey)
	if err != nil {
		return
	}
	publicKey := pkey.Public()
	publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
	if !ok {
		err = errors.New("error casting public key to ECDSA")
	}

	fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)

	nonce, err := this.getNonce(fromAddress.Hex())
	if err != nil {
		return
	}

	toAddress := common.HexToAddress(to)
	tx := types.NewTransaction(nonce, toAddress, valueInWei, gasLimit, gasPrice, nil)
	chainID, err := this.client.NetworkID(context.Background())
	if err != nil {
		return
	}

	signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), pkey)
	if err != nil {
		log.Fatal(err)
	}

	err = this.client.SendTransaction(context.Background(), signedTx)
	if err != nil {
		return
	}

	txHash = signedTx.Hash().Hex()
	log.Printf("tx sent: %s\n", txHash)
	return
}

func (this *Eth) getNonce(fromAddress string) (uint64, error) {
	resp, err := this.RpcClient.Call("eth_getTransactionCount", fromAddress, "pending")
	if err != nil {
		return 0, err
	}
	m := resp.Result.(string)
	nonce, err := hexutil.DecodeUint64(m)
	return nonce, err
}

func (this *Eth) GetSuggestedGasPrice() (*big.Int, error) {
	gasPrice, err := this.client.SuggestGasPrice(context.Background())
	return gasPrice, err
}

func (this *Eth) GetEstimatedGasLimit(tokenAddr, pkey, toAddr string, tokenAmount *big.Int) ([]byte, uint64, error) {
	if strings.HasPrefix(pkey, "0x") {
		pkey = pkey[2:]
	}

	privateKey, err := crypto.HexToECDSA(pkey)
	if err != nil {
		return nil, 0, err
	}

	publicKey := privateKey.Public()
	publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
	if !ok {
		err = errors.New("error casting public key to ECDSA")
		return nil, 0, err
	}

	fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
	toAddress := common.HexToAddress(toAddr)
	tokenAddress := common.HexToAddress(tokenAddr)

	transferFnSignature := []byte("transfer(address,uint256)")
	hash := sha3.NewLegacyKeccak256()
	hash.Write(transferFnSignature)
	methodID := hash.Sum(nil)[:4]

	paddedAddress := common.LeftPadBytes(toAddress.Bytes(), 32)

	paddedAmount := common.LeftPadBytes(tokenAmount.Bytes(), 32)

	var data []byte
	data = append(data, methodID...)
	data = append(data, paddedAddress...)
	data = append(data, paddedAmount...)

	gasLimit, err := this.client.EstimateGas(context.Background(), ethereum.CallMsg{
		From: fromAddress,
		To:   &tokenAddress,
		Data: data,
	})
	return data, gasLimit, err
}

func (this *Eth) TransferToken(tokenAddr, pkey, toAddr string, tokenAmount *big.Int, gasPrice *big.Int, gasLimit uint64) (txHash string, err error) {

	if strings.HasPrefix(pkey, "0x") {
		pkey = pkey[2:]
	}

	privateKey, err := crypto.HexToECDSA(pkey)
	if err != nil {
		return "", err
	}

	publicKey := privateKey.Public()
	publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
	if !ok {
		err = errors.New("error casting public key to ECDSA")
		return "", err
	}

	fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
	nonce, err := this.getNonce(fromAddress.Hex())
	if err != nil {
		return "", err
	}

	value := big.NewInt(0) // in wei (0 eth)
	tokenAddress := common.HexToAddress(tokenAddr)

	data, _, err := this.GetEstimatedGasLimit(tokenAddr, pkey, toAddr, tokenAmount)
	if err != nil {
		return "", err
	}

	v := &transferValue{
		nonce:    nonce,
		gasLimit: gasLimit,
		gasPrice: gasPrice,
	}

	log.Printf("nonce: %d, gas price: %d, gas limit: %d\n", nonce, v.gasPrice, v.gasLimit) // 25624
	//gasPrice = gasPrice.Add(gasPrice, big.NewInt(100_000_000))
	//gasPrice = big.NewInt(5320000000)

	tx := types.NewTransaction(v.nonce, tokenAddress, value, v.gasLimit, v.gasPrice, data)

	chainID, err := this.client.NetworkID(context.Background())
	if err != nil {
		return "", err
	}

	signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
	if err != nil {
		return "", err
	}

	err = this.client.SendTransaction(context.Background(), signedTx)
	if err != nil {
		return "", err
	}

	return signedTx.Hash().Hex(), nil
}

func (this *Eth) GetTokenBalance(tokenAddress, address string) (balance *big.Int, err error) {
	token, err := this.GetToken(common.HexToAddress(tokenAddress))
	b, err := token.BalanceOf(nil, common.HexToAddress(address))

	//token.Transfer(&bind.TransactOpts{
	//	From:      common.Address{},
	//	Nonce:     nil,
	//	Signer:    nil,
	//	Value:     nil,
	//	GasPrice:  nil,
	//	GasFeeCap: nil,
	//	GasTipCap: nil,
	//	GasLimit:  0,
	//	Context:   nil,
	//	NoSend:    false,
	//}, common.HexToAddress(address), big.NewInt(100))

	return b, err
}

func (this *Eth) GetBalance(address string) (balance *big.Int, err error) {
	resp, err := this.RpcClient.Call("eth_getBalance", address, "latest")
	if err != nil {
		return
	}
	b, _ := new(big.Int).SetString(resp.Result.(string)[2:], 16)
	return b, nil
}

func (this *Eth) GetInternalTxsByHash(blockNo uint64, txHash string) (calls []model.Call, err error) {

	lastestBlockNo, err := this.GetLastBlockNumber()
	if err != nil {
		return
	}

	args := make([]interface{}, 0)
	args = append(args, txHash)
	args = append(args, map[string]interface {
	}{
		"tracer": "callTracer",
		"reexec": lastestBlockNo.Uint64() - blockNo + 20,
	})

	resp, err := this.RpcClient.Call("debug_traceTransaction", args)
	if err != nil {
		return nil, err
	}

	data, _ := json.Marshal(resp.Result)
	log.Println(string(data))

	m, ok := resp.Result.(map[string]interface{})
	if !ok {
		return
	}

	data, err = json.Marshal(m["calls"])
	if err != nil {
		return
	}
	err = json.Unmarshal(data, &calls)
	if err != nil {
		return
	}
	return
}
