package evm

import (
	"context"
	"errors"
	"math/big"
	"strings"
	"time"

	"github.com/ethereum/go-ethereum"
	"github.com/ethereum/go-ethereum/accounts/abi"
	"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/rpc"
	"github.com/shopspring/decimal"
	"github.com/tech/sendico/gateway/tron/internal/service/gateway/driver"
	"github.com/tech/sendico/gateway/tron/shared"
	"github.com/tech/sendico/gateway/tron/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	"go.uber.org/zap"
)

var (
	erc20ABI abi.ABI
)

func init() {
	var err error
	erc20ABI, err = abi.JSON(strings.NewReader(erc20ABIJSON))
	if err != nil {
		panic("evm driver: failed to parse erc20 abi: " + err.Error())
	}
}

const erc20ABIJSON = `
[
	{
		"constant": false,
		"inputs": [
			{ "name": "_to", "type": "address" },
			{ "name": "_value", "type": "uint256" }
		],
		"name": "transfer",
		"outputs": [{ "name": "", "type": "bool" }],
		"payable": false,
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"constant": true,
		"inputs": [],
		"name": "decimals",
		"outputs": [{ "name": "", "type": "uint8" }],
		"payable": false,
		"stateMutability": "view",
        "type": "function"
	}
]`

// NormalizeAddress validates and normalizes EVM hex addresses.
func NormalizeAddress(address string) (string, error) {
	trimmed := strings.TrimSpace(address)
	if trimmed == "" {
		return "", merrors.InvalidArgument("address is required")
	}
	if !common.IsHexAddress(trimmed) {
		return "", merrors.InvalidArgument("invalid hex address")
	}
	return strings.ToLower(common.HexToAddress(trimmed).Hex()), nil
}

func nativeCurrency(network shared.Network) string {
	currency := strings.ToUpper(strings.TrimSpace(network.NativeToken))
	if currency == "" {
		currency = strings.ToUpper(network.Name.String())
	}
	return currency
}

func parseBaseUnitAmount(amount string) (*big.Int, error) {
	trimmed := strings.TrimSpace(amount)
	if trimmed == "" {
		return nil, merrors.InvalidArgument("amount is required")
	}
	value, ok := new(big.Int).SetString(trimmed, 10)
	if !ok {
		return nil, merrors.InvalidArgument("invalid amount")
	}
	if value.Sign() < 0 {
		return nil, merrors.InvalidArgument("amount must be non-negative")
	}
	return value, nil
}

// Balance fetches ERC20 token balance for the provided address.
func Balance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet, address string) (*moneyv1.Money, error) {
	logger := deps.Logger.Named("evm")
	registry := deps.Registry

	if registry == nil {
		return nil, merrors.Internal("rpc clients not initialised")
	}
	if wallet == nil {
		return nil, merrors.InvalidArgument("wallet is required")
	}

	normalizedAddress, err := NormalizeAddress(address)
	if err != nil {
		return nil, err
	}

	rpcURL := strings.TrimSpace(network.RPCURL)
	logFields := []zap.Field{
		zap.String("wallet_ref", wallet.WalletRef),
		zap.String("network", network.Name.String()),
		zap.String("token_symbol", strings.ToUpper(strings.TrimSpace(wallet.TokenSymbol))),
		zap.String("contract", strings.TrimSpace(wallet.ContractAddress)),
		zap.String("wallet_address", normalizedAddress),
	}
	if rpcURL == "" {
		logger.Warn("Network rpc url is not configured", logFields...)
		return nil, merrors.Internal("network rpc url is not configured")
	}

	contract, err := shared.TronBase58ToHex(strings.TrimSpace(wallet.ContractAddress))
	if err != nil {
		logger.Warn("Failed to convert contract address", zap.String("contract_address", wallet.ContractAddress))
		return nil, err
	}
	if contract == "" {
		logger.Debug("Native balance requested", logFields...)
		return NativeBalance(ctx, deps, network, wallet, normalizedAddress)
	}
	if !common.IsHexAddress(contract) {
		logger.Warn("Invalid contract address for balance fetch", logFields...)
		return nil, merrors.InvalidArgument("invalid contract address")
	}

	logger.Info("Fetching on-chain wallet balance", logFields...)

	rpcClient, err := registry.RPCClient(network.Name.String())
	if err != nil {
		logger.Warn("Failed to fetch rpc client", append(logFields, zap.Error(err))...)
		return nil, err
	}

	timeout := deps.RPCTimeout
	if timeout <= 0 {
		timeout = 10 * time.Second
	}
	timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	logger.Debug("Calling token decimals", logFields...)
	decimals, err := readDecimals(timeoutCtx, rpcClient, contract)
	if err != nil {
		logger.Warn("Token decimals call failed", append(logFields, zap.Error(err))...)
		return nil, err
	}

	logger.Debug("Calling token balanceOf", append(logFields, zap.Uint8("decimals", decimals))...)
	bal, err := readBalanceOf(timeoutCtx, rpcClient, contract, normalizedAddress)
	if err != nil {
		logger.Warn("Token balanceOf call failed", append(logFields, zap.Uint8("decimals", decimals), zap.Error(err))...)
		return nil, err
	}

	dec := decimal.NewFromBigInt(bal, 0).Shift(-int32(decimals))
	logger.Info("On-chain wallet balance fetched",
		append(logFields,
			zap.Uint8("decimals", decimals),
			zap.String("balance_raw", bal.String()),
			zap.String("balance", dec.String()),
		)...,
	)
	return &moneyv1.Money{Currency: wallet.TokenSymbol, Amount: dec.String()}, nil
}

// NativeBalance fetches native token balance for the provided address.
func NativeBalance(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet, address string) (*moneyv1.Money, error) {
	logger := deps.Logger.Named("evm")
	registry := deps.Registry

	if registry == nil {
		return nil, merrors.Internal("rpc clients not initialised")
	}
	if wallet == nil {
		return nil, merrors.InvalidArgument("wallet is required")
	}

	normalizedAddress, err := NormalizeAddress(address)
	if err != nil {
		return nil, err
	}

	rpcURL := strings.TrimSpace(network.RPCURL)
	logFields := []zap.Field{
		zap.String("wallet_ref", wallet.WalletRef),
		zap.String("network", network.Name.String()),
		zap.String("wallet_address", normalizedAddress),
	}
	if rpcURL == "" {
		logger.Warn("Network rpc url is not configured", logFields...)
		return nil, merrors.Internal("network rpc url is not configured")
	}

	client, err := registry.Client(network.Name.String())
	if err != nil {
		logger.Warn("Failed to fetch rpc client", append(logFields, zap.Error(err))...)
		return nil, err
	}

	timeout := deps.RPCTimeout
	if timeout <= 0 {
		timeout = 10 * time.Second
	}
	timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	bal, err := client.BalanceAt(timeoutCtx, common.HexToAddress(normalizedAddress), nil)
	if err != nil {
		logger.Warn("Native balance call failed", append(logFields, zap.Error(err))...)
		return nil, err
	}

	logger.Debug("On-chain native balance fetched",
		append(logFields,
			zap.String("balance_raw", bal.String()),
		)...,
	)
	return &moneyv1.Money{
		Currency: nativeCurrency(network),
		Amount:   bal.String(),
	}, nil
}

// EstimateFee estimates ERC20 transfer fees for the given parameters.
func EstimateFee(ctx context.Context, deps driver.Deps, network shared.Network, wallet *model.ManagedWallet, fromAddress, destination string, amount *moneyv1.Money) (*moneyv1.Money, error) {
	logger := deps.Logger.Named("evm")
	registry := deps.Registry

	if registry == nil {
		return nil, merrors.Internal("rpc clients not initialised")
	}
	if wallet == nil {
		return nil, merrors.InvalidArgument("wallet is required")
	}
	if amount == nil {
		return nil, merrors.InvalidArgument("amount is required")
	}

	rpcURL := strings.TrimSpace(network.RPCURL)
	if rpcURL == "" {
		return nil, merrors.InvalidArgument("network rpc url not configured")
	}
	if _, err := NormalizeAddress(fromAddress); err != nil {
		return nil, merrors.InvalidArgument("invalid source wallet address")
	}
	if _, err := NormalizeAddress(destination); err != nil {
		return nil, merrors.InvalidArgument("invalid destination address")
	}

	client, err := registry.Client(network.Name.String())
	if err != nil {
		logger.Warn("Failed to resolve client", zap.Error(err), zap.String("network_name", network.Name.String()))
		return nil, err
	}
	rpcClient, err := registry.RPCClient(network.Name.String())
	if err != nil {
		logger.Warn("Failed to resolve RPC client", zap.Error(err), zap.String("network_name", network.Name.String()))
		return nil, err
	}

	timeout := deps.RPCTimeout
	if timeout <= 0 {
		timeout = 15 * time.Second
	}
	timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	contract := strings.TrimSpace(wallet.ContractAddress)
	toAddr := common.HexToAddress(destination)
	fromAddr := common.HexToAddress(fromAddress)

	if contract == "" {
		amountBase, err := parseBaseUnitAmount(amount.GetAmount())
		if err != nil {
			logger.Warn("Failed to parse base unit amount", zap.Error(err), zap.String("amount", amount.GetAmount()))
			return nil, err
		}
		gasPrice, err := client.SuggestGasPrice(timeoutCtx)
		if err != nil {
			logger.Warn("Failed to suggest gas price", zap.Error(err))
			return nil, merrors.Internal("failed to suggest gas price: " + err.Error())
		}
		callMsg := ethereum.CallMsg{
			From:     fromAddr,
			To:       &toAddr,
			GasPrice: gasPrice,
			Value:    amountBase,
		}
		gasLimit, err := estimateGas(timeoutCtx, network, client, rpcClient, callMsg)
		if err != nil {
			logger.Warn("Failed to estimate gas", zap.Error(err), zap.Any("call_mesasge", callMsg))
			return nil, merrors.Internal("failed to estimate gas: " + err.Error())
		}
		fee := new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(gasLimit))
		feeDec := decimal.NewFromBigInt(fee, 0)
		return &moneyv1.Money{
			Currency: nativeCurrency(network),
			Amount:   feeDec.String(),
		}, nil
	}
	if !common.IsHexAddress(contract) {
		logger.Warn("Failed to validate contract", zap.String("contract", contract))
		return nil, merrors.InvalidArgument("invalid token contract address")
	}

	tokenAddr := common.HexToAddress(contract)

	decimals, err := erc20Decimals(timeoutCtx, rpcClient, tokenAddr)
	if err != nil {
		logger.Warn("Failed to read token decimals", zap.Error(err))
		return nil, err
	}

	amountBase, err := toBaseUnits(strings.TrimSpace(amount.GetAmount()), decimals)
	if err != nil {
		return nil, err
	}

	input, err := erc20ABI.Pack("transfer", toAddr, amountBase)
	if err != nil {
		logger.Warn("Failed to encode transfer call", zap.Error(err))
		return nil, merrors.Internal("failed to encode transfer call: " + err.Error())
	}

	gasPrice, err := client.SuggestGasPrice(timeoutCtx)
	if err != nil {
		logger.Warn("Failed to suggest gas price", zap.Error(err))
		return nil, merrors.Internal("failed to suggest gas price: " + err.Error())
	}

	callMsg := ethereum.CallMsg{
		From:     fromAddr,
		To:       &tokenAddr,
		GasPrice: gasPrice,
		Data:     input,
	}
	gasLimit, err := estimateGas(timeoutCtx, network, client, rpcClient, callMsg)
	if err != nil {
		logger.Warn("Failed to estimate gas", zap.Error(err), zap.Any("call_message", callMsg))
		return nil, merrors.Internal("failed to estimate gas: " + err.Error())
	}

	fee := new(big.Int).Mul(gasPrice, new(big.Int).SetUint64(gasLimit))
	feeDec := decimal.NewFromBigInt(fee, 0)

	return &moneyv1.Money{
		Currency: nativeCurrency(network),
		Amount:   feeDec.String(),
	}, nil
}

// SubmitTransfer submits an ERC20 transfer on an EVM-compatible chain.
func SubmitTransfer(ctx context.Context, deps driver.Deps, network shared.Network, transfer *model.Transfer, source *model.ManagedWallet, fromAddress, destination string) (string, error) {
	logger := deps.Logger.Named("evm")
	registry := deps.Registry

	if deps.KeyManager == nil {
		logger.Warn("Key manager not configured")
		return "", executorInternal("key manager is not configured", nil)
	}
	if registry == nil {
		return "", executorInternal("rpc clients not initialised", nil)
	}
	rpcURL := strings.TrimSpace(network.RPCURL)
	if rpcURL == "" {
		logger.Warn("Network rpc url missing", zap.String("network", network.Name.String()))
		return "", executorInvalid("network rpc url is not configured")
	}
	if source == nil || transfer == nil {
		logger.Warn("Transfer context missing")
		return "", executorInvalid("transfer context missing")
	}
	if strings.TrimSpace(source.KeyReference) == "" {
		logger.Warn("Source wallet missing key reference", zap.String("wallet_ref", source.WalletRef))
		return "", executorInvalid("source wallet missing key reference")
	}
	if _, err := NormalizeAddress(fromAddress); err != nil {
		logger.Warn("Invalid source wallet address", zap.String("wallet_ref", source.WalletRef))
		return "", executorInvalid("invalid source wallet address")
	}
	if _, err := NormalizeAddress(destination); err != nil {
		logger.Warn("Invalid destination address", zap.String("transfer_ref", transfer.TransferRef), zap.String("address", destination))
		return "", executorInvalid("invalid destination address " + destination)
	}

	logger.Info("Submitting transfer",
		zap.String("transfer_ref", transfer.TransferRef),
		zap.String("source_wallet_ref", source.WalletRef),
		zap.String("network", network.Name.String()),
		zap.String("destination", strings.ToLower(destination)),
	)

	client, err := registry.Client(network.Name.String())
	if err != nil {
		logger.Warn("Failed to initialise RPC client", zap.Error(err), zap.String("network", network.Name.String()))
		return "", err
	}
	rpcClient, err := registry.RPCClient(network.Name.String())
	if err != nil {
		logger.Warn("Failed to initialise RPC client", zap.String("network", network.Name.String()))
		return "", err
	}

	sourceAddress := common.HexToAddress(fromAddress)
	destinationAddr := common.HexToAddress(destination)

	ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
	defer cancel()

	nonce, err := client.PendingNonceAt(ctx, sourceAddress)
	if err != nil {
		logger.Warn("Failed to fetch nonce", zap.Error(err),
			zap.String("transfer_ref", transfer.TransferRef), zap.String("wallet_ref", source.WalletRef),
		)
		return "", executorInternal("failed to fetch nonce", err)
	}

	gasPrice, err := client.SuggestGasPrice(ctx)
	if err != nil {
		logger.Warn("Failed to suggest gas price", zap.Error(err),
			zap.String("transfer_ref", transfer.TransferRef), zap.String("network", network.Name.String()),
		)
		return "", executorInternal("failed to suggest gas price", err)
	}

	chainID := new(big.Int).SetUint64(network.ChainID)

	contract, err := shared.TronBase58ToHex(strings.TrimSpace(transfer.ContractAddress))
	if err != nil {
		logger.Warn("Failed to convert contract address", zap.String("contract_address", transfer.ContractAddress))
		return "", err
	}
	amount := transfer.NetAmount
	if amount == nil || strings.TrimSpace(amount.Amount) == "" {
		logger.Warn("Transfer missing net amount", zap.String("transfer_ref", transfer.TransferRef))
		return "", executorInvalid("transfer missing net amount")
	}

	var tx *types.Transaction
	if contract == "" {
		amountInt, err := parseBaseUnitAmount(amount.Amount)
		if err != nil {
			logger.Warn("Invalid native amount", zap.Error(err), zap.String("transfer_ref", transfer.TransferRef))
			return "", err
		}
		callMsg := ethereum.CallMsg{
			From:     sourceAddress,
			To:       &destinationAddr,
			GasPrice: gasPrice,
			Value:    amountInt,
		}
		gasLimit, err := estimateGas(ctx, network, client, rpcClient, callMsg)
		if err != nil {
			logger.Warn("Failed to estimate gas", zap.Error(err),
				zap.String("transfer_ref", transfer.TransferRef),
			)
			return "", executorInternal("failed to estimate gas", err)
		}
		tx = types.NewTransaction(nonce, destinationAddr, amountInt, gasLimit, gasPrice, nil)
	} else {
		if !common.IsHexAddress(contract) {
			logger.Warn("Invalid token contract address",
				zap.String("transfer_ref", transfer.TransferRef),
				zap.String("contract", contract),
			)
			return "", executorInvalid("invalid token contract address " + contract)
		}
		tokenAddress := common.HexToAddress(contract)

		decimals, err := erc20Decimals(ctx, rpcClient, tokenAddress)
		if err != nil {
			logger.Warn("Failed to read token decimals", zap.Error(err),
				zap.String("transfer_ref", transfer.TransferRef), zap.String("contract", contract),
			)
			return "", err
		}

		amountInt, err := toBaseUnits(amount.Amount, decimals)
		if err != nil {
			logger.Warn("Failed to convert amount to base units", zap.Error(err),
				zap.String("transfer_ref", transfer.TransferRef), zap.String("amount", amount.Amount),
			)
			return "", err
		}

		input, err := erc20ABI.Pack("transfer", destinationAddr, amountInt)
		if err != nil {
			logger.Warn("Failed to encode transfer call", zap.Error(err),
				zap.String("transfer_ref", transfer.TransferRef),
			)
			return "", executorInternal("failed to encode transfer call", err)
		}

		callMsg := ethereum.CallMsg{
			From:     sourceAddress,
			To:       &tokenAddress,
			GasPrice: gasPrice,
			Data:     input,
		}
		gasLimit, err := estimateGas(ctx, network, client, rpcClient, callMsg)
		if err != nil {
			logger.Warn("Failed to estimate gas", zap.Error(err),
				zap.String("transfer_ref", transfer.TransferRef),
			)
			return "", executorInternal("failed to estimate gas", err)
		}

		tx = types.NewTransaction(nonce, tokenAddress, big.NewInt(0), gasLimit, gasPrice, input)
	}

	signedTx, err := deps.KeyManager.SignTransaction(ctx, source.KeyReference, tx, chainID)
	if err != nil {
		logger.Warn("Failed to sign transaction", zap.Error(err),
			zap.String("transfer_ref", transfer.TransferRef), zap.String("wallet_ref", source.WalletRef),
		)
		return "", err
	}

	if err := client.SendTransaction(ctx, signedTx); err != nil {
		logger.Warn("Failed to send transaction", zap.Error(err),
			zap.String("transfer_ref", transfer.TransferRef),
		)
		return "", executorInternal("failed to send transaction", err)
	}

	txHash := signedTx.Hash().Hex()
	logger.Info("Transaction submitted", zap.String("transfer_ref", transfer.TransferRef),
		zap.String("tx_hash", txHash), zap.String("network", network.Name.String()),
	)

	return txHash, nil
}

// AwaitConfirmation waits for the transaction receipt.
func AwaitConfirmation(ctx context.Context, deps driver.Deps, network shared.Network, txHash string) (*types.Receipt, error) {
	logger := deps.Logger.Named("evm")
	registry := deps.Registry

	if strings.TrimSpace(txHash) == "" {
		logger.Warn("Missing transaction hash for confirmation", zap.String("network", network.Name.String()))
		return nil, executorInvalid("tx hash is required")
	}
	rpcURL := strings.TrimSpace(network.RPCURL)
	if rpcURL == "" {
		logger.Warn("Network rpc url missing while awaiting confirmation", zap.String("tx_hash", txHash))
		return nil, executorInvalid("network rpc url is not configured")
	}
	if registry == nil {
		return nil, executorInternal("rpc clients not initialised", nil)
	}

	client, err := registry.Client(network.Name.String())
	if err != nil {
		return nil, err
	}

	hash := common.HexToHash(txHash)
	ticker := time.NewTicker(3 * time.Second)
	defer ticker.Stop()

	for {
		receipt, err := client.TransactionReceipt(ctx, hash)
		if err != nil {
			if errors.Is(err, ethereum.NotFound) {
				select {
				case <-ticker.C:
					logger.Debug("Transaction not yet mined", zap.String("tx_hash", txHash),
						zap.String("network", network.Name.String()),
					)
					continue
				case <-ctx.Done():
					logger.Warn("Context cancelled while awaiting confirmation", zap.String("tx_hash", txHash),
						zap.String("network", network.Name.String()),
					)
					return nil, ctx.Err()
				}
			}
			logger.Warn("Failed to fetch transaction receipt", zap.Error(err),
				zap.String("tx_hash", txHash), zap.String("network", network.Name.String()),
			)
			return nil, executorInternal("failed to fetch transaction receipt", err)
		}
		logger.Info("Transaction confirmed", zap.String("tx_hash", txHash),
			zap.String("network", network.Name.String()), zap.Uint64("status", receipt.Status),
			zap.Uint64("block_number", receipt.BlockNumber.Uint64()),
		)
		return receipt, nil
	}
}

func readDecimals(ctx context.Context, client *rpc.Client, token string) (uint8, error) {
	call := map[string]string{
		"to":   strings.ToLower(common.HexToAddress(token).Hex()),
		"data": "0x313ce567",
	}
	var hexResp string
	if err := client.CallContext(ctx, &hexResp, "eth_call", call, "latest"); err != nil {
		return 0, merrors.Internal("decimals call failed: " + err.Error())
	}
	val, err := shared.DecodeHexUint8(hexResp)
	if err != nil {
		return 0, merrors.Internal("decimals decode failed: " + err.Error())
	}
	return val, nil
}

func readBalanceOf(ctx context.Context, client *rpc.Client, token string, wallet string) (*big.Int, error) {
	tokenAddr := common.HexToAddress(token)
	walletAddr := common.HexToAddress(wallet)
	addr := strings.TrimPrefix(walletAddr.Hex(), "0x")
	if len(addr) < 64 {
		addr = strings.Repeat("0", 64-len(addr)) + addr
	}
	call := map[string]string{
		"to":   strings.ToLower(tokenAddr.Hex()),
		"data": "0x70a08231" + addr,
	}
	var hexResp string
	if err := client.CallContext(ctx, &hexResp, "eth_call", call, "latest"); err != nil {
		return nil, merrors.Internal("balanceOf call failed: " + err.Error())
	}
	bigVal, err := shared.DecodeHexBig(hexResp)
	if err != nil {
		return nil, merrors.Internal("balanceOf decode failed: " + err.Error())
	}
	return bigVal, nil
}

func erc20Decimals(ctx context.Context, client *rpc.Client, token common.Address) (uint8, error) {
	call := map[string]string{
		"to":   strings.ToLower(token.Hex()),
		"data": "0x313ce567",
	}
	var hexResp string
	if err := client.CallContext(ctx, &hexResp, "eth_call", call, "latest"); err != nil {
		return 0, executorInternal("decimals call failed", err)
	}
	val, err := shared.DecodeHexUint8(hexResp)
	if err != nil {
		return 0, executorInternal("decimals decode failed", err)
	}
	return val, nil
}

type gasEstimator interface {
	EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error)
}

func estimateGas(ctx context.Context, network shared.Network, client gasEstimator, rpcClient *rpc.Client, callMsg ethereum.CallMsg) (uint64, error) {
	if isTronNetwork(network) {
		if rpcClient == nil {
			return 0, merrors.Internal("rpc client not initialised")
		}
		return estimateGasTron(ctx, rpcClient, callMsg)
	}
	return client.EstimateGas(ctx, callMsg)
}

func estimateGasTron(ctx context.Context, rpcClient *rpc.Client, callMsg ethereum.CallMsg) (uint64, error) {
	call := tronEstimateCall(callMsg)
	var hexResp string
	if err := rpcClient.CallContext(ctx, &hexResp, "eth_estimateGas", call); err != nil {
		return 0, err
	}
	val, err := shared.DecodeHexBig(hexResp)
	if err != nil {
		return 0, err
	}
	if val == nil {
		return 0, merrors.Internal("failed to decode gas estimate")
	}
	return val.Uint64(), nil
}

func tronEstimateCall(callMsg ethereum.CallMsg) map[string]string {
	call := make(map[string]string)
	if callMsg.From != (common.Address{}) {
		call["from"] = strings.ToLower(callMsg.From.Hex())
	}
	if callMsg.To != nil {
		call["to"] = strings.ToLower(callMsg.To.Hex())
	}
	if callMsg.Gas > 0 {
		call["gas"] = hexutil.EncodeUint64(callMsg.Gas)
	}
	if callMsg.GasPrice != nil {
		call["gasPrice"] = hexutil.EncodeBig(callMsg.GasPrice)
	}
	if callMsg.Value != nil {
		call["value"] = hexutil.EncodeBig(callMsg.Value)
	}
	if len(callMsg.Data) > 0 {
		call["data"] = hexutil.Encode(callMsg.Data)
	}
	return call
}

func isTronNetwork(network shared.Network) bool {
	return network.Name.IsTron()
}

func toBaseUnits(amount string, decimals uint8) (*big.Int, error) {
	value, err := decimal.NewFromString(strings.TrimSpace(amount))
	if err != nil {
		return nil, merrors.InvalidArgument("invalid amount " + amount + ": " + err.Error())
	}
	if value.IsNegative() {
		return nil, merrors.InvalidArgument("amount must be positive")
	}
	multiplier := decimal.NewFromInt(1).Shift(int32(decimals))
	scaled := value.Mul(multiplier)
	if !scaled.Equal(scaled.Truncate(0)) {
		return nil, merrors.InvalidArgument("amount " + amount + " exceeds token precision")
	}
	return scaled.BigInt(), nil
}

func executorInvalid(msg string) error {
	return merrors.InvalidArgument("executor: " + msg)
}

func executorInternal(msg string, err error) error {
	if err != nil {
		msg = msg + ": " + err.Error()
	}
	return merrors.Internal("executor: " + msg)
}
