package keeper

import (
	"fmt"

	storetypes "cosmossdk.io/store/types"

	"github.com/cosmos/cosmos-sdk/baseapp"
	codectypes "github.com/cosmos/cosmos-sdk/codec/types"
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdktx "github.com/cosmos/cosmos-sdk/types/tx"

	"github.com/provenance-io/provenance/internal/antewrapper"
	"github.com/provenance-io/provenance/x/trigger/types"
)

const (
	MaximumActions uint64 = 5
)

// ProcessTriggers Reads triggers from queues and attempts to run them.
func (k Keeper) ProcessTriggers(ctx sdk.Context) {
	var actionsProcessed uint64

	for !k.QueueIsEmpty(ctx) && actionsProcessed < MaximumActions {
		item := k.QueuePeek(ctx)
		triggerID := item.GetTrigger().Id
		k.Logger(ctx).Debug(fmt.Sprintf("Processing trigger %d", triggerID))

		actionsProcessed++

		k.Dequeue(ctx)

		actions := item.GetTrigger().Actions
		err := k.runActions(ctx, actions)
		k.emitTriggerExecuted(ctx, item.GetTrigger(), err == nil)
	}
}

// RunActions Runs all the actions.
func (k Keeper) runActions(ctx sdk.Context, actions []*codectypes.Any) error {
	cacheCtx, writeCache := ctx.CacheContext()

	msgs, err := sdktx.GetMsgs(actions, "RunActions")
	if err != nil {
		k.Logger(ctx).Error(
			"GetMsgs",
			"actions", actions,
			"error", err,
		)
		return err
	}
	results, err := k.handleMsgs(cacheCtx, msgs)
	if err != nil {
		k.Logger(ctx).Error(
			"HandleMsgs",
			"error", err,
		)
		return err
	}

	writeCache()
	for _, res := range results {
		ctx.EventManager().EmitEvents(res.GetEvents())
	}

	return nil
}

// handleMsgs Handles each message and verifies gas limit has not been exceeded.
func (k Keeper) handleMsgs(ctx sdk.Context, msgs []sdk.Msg) ([]sdk.Result, error) {
	results := make([]sdk.Result, len(msgs))
	// Each set of Msgs gets the same amount of gas as if they were supplied in a tx.
	// Because we're in a BeginBlock, the existing gas meter is either a no-op or infinite, and doesn't matter.
	// We don't use a flat-fee gas meter here because the fees were already paid, and we don't need to track them again.
	ctx = ctx.WithGasMeter(storetypes.NewGasMeter(antewrapper.TxGasLimit))

	for i, msg := range msgs {
		handler := k.router.Handler(msg)
		if handler == nil {
			return nil, fmt.Errorf("no message handler found for message %s at position %d", sdk.MsgTypeURL(msg), i)
		}
		k.Logger(ctx).Debug(fmt.Sprintf("Executing %s at position %d", sdk.MsgTypeURL(msg), i))
		r, err := k.safeHandle(ctx, msg, handler)
		if err != nil {
			return nil, fmt.Errorf("error processing message %s at position %d: %w", sdk.MsgTypeURL(msg), i, err)
		}
		// Handler should always return non-nil sdk.Result.
		if r == nil {
			return nil, fmt.Errorf("got nil sdk.Result for message %s at position %d", sdk.MsgTypeURL(msg), i)
		}
		k.Logger(ctx).Debug(fmt.Sprintf("Successfully executed %s at position %d", sdk.MsgTypeURL(msg), i))

		results[i] = *r
	}
	return results, nil
}

// safeHandle Handles one message and safely returns an error if it panics
func (k Keeper) safeHandle(ctx sdk.Context, msg sdk.Msg, handler baseapp.MsgServiceHandler) (result *sdk.Result, err error) {
	defer func() {
		if e := recover(); e != nil {
			// If it's an out-of-gas panic, convert it to a nicer error.
			if _, ok := e.(storetypes.ErrorOutOfGas); ok {
				result = nil
				err = fmt.Errorf("gas %d exceeded limit %d for message %q", ctx.GasMeter().GasConsumed(), ctx.GasMeter().Limit(), sdk.MsgTypeURL(msg))
				return
			}

			// If it's some other error, wrap it up and return it (instead of panicking).
			if er, ok := e.(error); ok {
				err = fmt.Errorf("panic (recovered) processing msg: %w", er)
				return
			}

			// Otherwise, it's some other panic. Just create a new error for it.
			err = fmt.Errorf("panic (recovered) processing msg: %v", e)
		}
	}()
	return handler(ctx, msg)
}

// emitTriggerExecuted Emits an EventTriggerExecuted for the provided trigger.
func (k Keeper) emitTriggerExecuted(ctx sdk.Context, trigger types.Trigger, success bool) {
	eventErr := ctx.EventManager().EmitTypedEvent(&types.EventTriggerExecuted{
		TriggerId: fmt.Sprintf("%d", trigger.GetId()),
		Owner:     trigger.Owner,
		Success:   success,
	})
	if eventErr != nil {
		ctx.Logger().Error("unable to emit EventTriggerExecuted", "err", eventErr)
	}
}
