package keeper

import (
	"math/big"

	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
	authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
	"github.com/palantir/stacktrace"

	evmtypes "github.com/tharsis/ethermint/x/evm/types"

	cmath "github.com/ethereum/go-ethereum/common/math"
	"github.com/ethereum/go-ethereum/core"
	ethtypes "github.com/ethereum/go-ethereum/core/types"
)

// DeductTxCostsFromUserBalance it calculates the tx costs and deducts the fees
func (k Keeper) DeductTxCostsFromUserBalance(
	ctx sdk.Context,
	msgEthTx evmtypes.MsgEthereumTx,
	txData evmtypes.TxData,
	denom string,
	homestead, istanbul, london bool,
) (sdk.Coins, error) {
	isContractCreation := txData.GetTo() == nil

	// fetch sender account from signature
	signerAcc, err := authante.GetSignerAcc(ctx, k.accountKeeper, msgEthTx.GetFrom())
	if err != nil {
		return nil, stacktrace.Propagate(err, "account not found for sender %s", msgEthTx.From)
	}

	gasLimit := txData.GetGas()

	var accessList ethtypes.AccessList
	if txData.GetAccessList() != nil {
		accessList = txData.GetAccessList()
	}

	intrinsicGas, err := core.IntrinsicGas(txData.GetData(), accessList, isContractCreation, homestead, istanbul)
	if err != nil {
		return nil, stacktrace.Propagate(sdkerrors.Wrap(
			err,
			"failed to compute intrinsic gas cost"), "failed to retrieve intrinsic gas, contract creation = %t; homestead = %t, istanbul = %t",
			isContractCreation, homestead, istanbul,
		)
	}

	// intrinsic gas verification during CheckTx
	if ctx.IsCheckTx() && gasLimit < intrinsicGas {
		return nil, sdkerrors.Wrapf(
			sdkerrors.ErrOutOfGas,
			"gas limit too low: %d (gas limit) < %d (intrinsic gas)", gasLimit, intrinsicGas,
		)
	}

	// calculate the fees paid to validators based on the effective tip and price
	effectiveTip := txData.GetGasPrice()

	feeMktParams := k.feeMarketKeeper.GetParams(ctx)

	if london && !feeMktParams.NoBaseFee && txData.TxType() == ethtypes.DynamicFeeTxType {
		baseFee := k.feeMarketKeeper.GetBaseFee(ctx)
		gasFeeGap := new(big.Int).Sub(txData.GetGasFeeCap(), baseFee)
		if gasFeeGap.Sign() == -1 {
			return nil, sdkerrors.Wrapf(sdkerrors.ErrInsufficientFee, "the tx gasfeecap is lower than the tx baseFee: %s (gasfeecap), %s (basefee) ", txData.GetGasFeeCap(), baseFee)
		}

		effectiveTip = cmath.BigMin(txData.GetGasTipCap(), gasFeeGap)
	}

	gasUsed := new(big.Int).SetUint64(txData.GetGas())
	feeAmt := new(big.Int).Mul(gasUsed, effectiveTip)

	fees := sdk.Coins{sdk.NewCoin(denom, sdk.NewIntFromBigInt(feeAmt))}

	// deduct the full gas cost from the user balance
	if err := authante.DeductFees(k.bankKeeper, ctx, signerAcc, fees); err != nil {
		return nil, stacktrace.Propagate(
			err,
			"failed to deduct full gas cost %s from the user %s balance",
			fees, msgEthTx.From,
		)
	}
	return fees, nil
}

// CheckSenderBalance validates that the tx cost value is positive and that the
// sender has enough funds to pay for the fees and value of the transaction.
func CheckSenderBalance(
	ctx sdk.Context,
	bankKeeper evmtypes.BankKeeper,
	sender sdk.AccAddress,
	txData evmtypes.TxData,
	denom string,
) error {
	balance := bankKeeper.GetBalance(ctx, sender, denom)
	cost := txData.Cost()

	if cost.Sign() < 0 {
		return stacktrace.Propagate(
			sdkerrors.Wrapf(
				sdkerrors.ErrInvalidCoins,
				"tx cost (%s%s) is negative and invalid", cost, denom,
			),
			"tx cost amount should never be negative")
	}

	if balance.IsNegative() || balance.Amount.BigInt().Cmp(cost) < 0 {
		return stacktrace.Propagate(
			sdkerrors.Wrapf(
				sdkerrors.ErrInsufficientFunds,
				"sender balance < tx cost (%s < %s%s)", balance, txData.Cost(), denom,
			),
			"sender should have had enough funds to pay for tx cost = fee + amount (%s = %s + %s)",
			cost, txData.Fee(), txData.GetValue(),
		)
	}
	return nil
}
