// Package antewrapper provides ante decorators and related utilities for transaction processing.
package antewrapper

import (
	storetypes "cosmossdk.io/store/types"

	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
	"github.com/cosmos/cosmos-sdk/x/auth/ante"
)

// ProvSetUpContextDecorator creates and sets the flat-fee GasMeter in the Context and wraps the
// next AnteHandler with a defer clause to recover from any downstream OutOfGas panics in the
// AnteHandler chain to return an error with information on gas provided and gas used.
// CONTRACT: Must be first decorator in the chain.
// CONTRACT: Tx must implement GasTx interface.
// This is similar to "github.com/cosmos/cosmos-sdk/x/auth/ante".SetUpContextDecorator
// except we set and check the gas limits a little differently.
type ProvSetUpContextDecorator struct {
	ffk FlatFeesKeeper
}

// NewProvSetUpContextDecorator creates a new ProvSetUpContextDecorator with the given FlatFeesKeeper.
func NewProvSetUpContextDecorator(ffk FlatFeesKeeper) ProvSetUpContextDecorator {
	return ProvSetUpContextDecorator{ffk: ffk}
}

// AnteHandle implements the ante handler logic for ProvSetUpContextDecorator.
func (d ProvSetUpContextDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) {
	ctx.Logger().Debug("Starting ProvSetUpContextDecorator.AnteHandle.", "simulate", simulate, "IsCheckTx", ctx.IsCheckTx())

	// All transactions must implement FeeTx.
	feeTx, err := GetFeeTx(tx)
	if err != nil {
		// Set a gas meter with limit 0 as to prevent an infinite gas meter attack during runTx.
		newCtx = ante.SetGasMeter(simulate, ctx, 0)
		return newCtx, err
	}

	// Get the actual gas wanted for this tx (accounting for our custom simulation process).
	gasWanted, err := GetGasWanted(ctx.Logger(), feeTx)
	if err != nil {
		// Set a gas meter with limit 0 as to prevent an infinite gas meter attack during runTx.
		newCtx = ante.SetGasMeter(simulate, ctx, 0)
		return newCtx, err
	}

	var maxBlockGas int64
	if bp := ctx.ConsensusParams().Block; bp != nil {
		maxBlockGas = bp.GetMaxGas()
	}
	txIsLimited := txGasLimitShouldApply(ctx.ChainID(), tx.GetMsgs())

	// Determine how much gas to give the gas meter.
	// By default, it's the TxGasLimit.
	// If the user provided a gas limit, use that instead.
	// Or, if the tx isn't limited, and the user didn't provide their own limit, use half the block gas limit.
	// This allows gov props to use a higher gas limit than the usual Tx gas limit.
	gasLimit := TxGasLimit
	switch {
	case gasWanted != DefaultGasLimit:
		gasLimit = gasWanted
	case !txIsLimited && maxBlockGas > 0:
		gasLimit = uint64(maxBlockGas / 2) //nolint:gosec // safe: maxBlockGas is non-negative
	}

	// Note that SetGasMeter uses an infinite gas meter if simulating or at height 0 (init genesis).
	newCtx = ante.SetGasMeter(simulate, ctx, gasLimit)
	// Now wrap that gas meter in our flat-fee gas meter.
	ffgm := NewFlatFeeGasMeter(newCtx.GasMeter(), newCtx.Logger(), d.ffk)
	newCtx = ctx.WithGasMeter(ffgm)
	// Add a second copy to the context as a backup in case the main gas meter is overwritten.
	newCtx = SetFlatFeeGasMeterBackup(newCtx, ffgm)
	// Note: We don't set the costs yet because we want to check a few more things before doing that work.

	// Ensure that the requested gas does not exceed either the configured block maximum, or the tx maximum.
	// If there's no block maximum defined, we can't do that check, and we interpret that as an indication
	// that there shouldn't be a tx limit either.
	if maxBlockGas > 0 {
		if gasWanted > uint64(maxBlockGas) {
			return newCtx, sdkerrors.ErrInvalidGasLimit.Wrapf("tx gas limit %d exceeds block max gas %d", gasWanted, maxBlockGas)
		}
		if txIsLimited && gasWanted > TxGasLimit {
			return newCtx, sdkerrors.ErrInvalidGasLimit.Wrapf("tx gas limit %d exceeds tx max gas %d", gasWanted, TxGasLimit)
		}
	}

	// Decorator will catch an OutOfGasPanic caused in the next antehandler
	// AnteHandlers must have their own defer/recover in order for the BaseApp
	// to know how much gas was used! This is because the GasMeter is created in
	// the AnteHandler, but if it panics the context won't be set properly in
	// runTx's recover call.
	defer func() {
		if r := recover(); r != nil {
			switch rType := r.(type) {
			case storetypes.ErrorOutOfGas:
				err = sdkerrors.ErrOutOfGas.Wrapf("out of gas in location: %v; gasWanted: %d, gasUsed: %d",
					rType.Descriptor, newCtx.GasMeter().Limit(), newCtx.GasMeter().GasConsumed())
			default:
				panic(r)
			}
		}
	}()

	return next(newCtx, tx, simulate)
}
