package internal

import (
	"fmt"

	errorsmod "cosmossdk.io/errors"
	storetypes "cosmossdk.io/store/types"

	sdk "git.cw.tr/mukan-network/mukan-sdk/types"

	"git.cw.tr/mukan-network/mukan-ibc/modules/apps/callbacks/types"
)

// ProcessCallback executes the callbackExecutor and reverts contract changes if the callbackExecutor fails.
//
// Error Precedence and Returns:
//   - oogErr: Takes the highest precedence. If the callback runs out of gas, an error wrapped with types.ErrCallbackOutOfGas is returned.
//   - panicErr: Takes the second-highest precedence. If a panic occurs and it is not propagated, an error wrapped with types.ErrCallbackPanic is returned.
//   - callbackErr: If the callbackExecutor returns an error, it is returned as-is.
//
// panics if
//   - the contractExecutor panics for any reason, and the callbackType is SendPacket, or
//   - the contractExecutor runs out of gas and the relayer has not reserved gas grater than or equal to
//     CommitGasLimit.
func ProcessCallback(
	ctx sdk.Context, callbackType types.CallbackType,
	callbackData types.CallbackData, callbackExecutor func(sdk.Context) error,
) (err error) {
	cachedCtx, writeFn := ctx.CacheContext()
	cachedCtx = cachedCtx.WithGasMeter(storetypes.NewGasMeter(callbackData.ExecutionGasLimit))

	defer func() {
		// consume the minimum of g.consumed and g.limit
		ctx.GasMeter().ConsumeGas(cachedCtx.GasMeter().GasConsumedToLimit(), fmt.Sprintf("ibc %s callback", callbackType))

		// recover from all panics except during SendPacket callbacks
		if r := recover(); r != nil {
			if callbackType == types.CallbackTypeSendPacket {
				panic(r)
			}
			err = errorsmod.Wrapf(types.ErrCallbackPanic, "ibc %s callback panicked with: %v", callbackType, r)
		}

		// if the callback ran out of gas and the relayer has not reserved enough gas, then revert the state
		if cachedCtx.GasMeter().IsPastLimit() {
			if callbackData.AllowRetry() {
				panic(storetypes.ErrorOutOfGas{Descriptor: fmt.Sprintf("ibc %s callback out of gas; commitGasLimit: %d", callbackType, callbackData.CommitGasLimit)})
			}
			err = errorsmod.Wrapf(types.ErrCallbackOutOfGas, "ibc %s callback out of gas", callbackType)
		}

		// allow the transaction to be committed, continuing the packet lifecycle
	}()

	err = callbackExecutor(cachedCtx)
	if err == nil {
		writeFn()
	}

	return err
}
