package types

import (
	fmt "fmt"
	math "math"
	"slices"
	"strings"
	time "time"

	abci "github.com/cometbft/cometbft/abci/types"

	codectypes "github.com/cosmos/cosmos-sdk/codec/types"
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdktx "github.com/cosmos/cosmos-sdk/types/tx"
	proto "github.com/cosmos/gogoproto/proto"
)

type TriggerID = uint64

const (
	BlockHeightPrefix = "block-height"
	BlockTimePrefix   = "block-time"
)

type TriggerEventI interface {
	proto.Message
	GetEventPrefix() string
	GetEventOrder() uint64
	Validate() error
	ValidateContext(ctx sdk.Context) error
}

var _ TriggerEventI = &TransactionEvent{}
var _ TriggerEventI = &BlockHeightEvent{}
var _ TriggerEventI = &BlockTimeEvent{}
var _ codectypes.UnpackInterfacesMessage = (*Trigger)(nil)
var _ codectypes.UnpackInterfacesMessage = (*QueuedTrigger)(nil)

// Matches checks if two TransactionEvents have the same type and matching attributes.
func (e TransactionEvent) Matches(other abci.Event) bool {
	if e.Name != other.GetType() {
		return false
	}

	for _, attr := range e.Attributes {
		if !slices.ContainsFunc(other.Attributes, attr.Matches) {
			return false
		}
	}

	return true
}

// Matches checks if two Attributes have the same name and an equal value if one is specified.
func (a Attribute) Matches(other abci.EventAttribute) bool {
	if a.GetName() != other.GetKey() {
		return false
	}

	if a.GetValue() != "" && a.GetValue() != other.GetValue() {
		return false
	}

	return true
}

// GetEventPrefix gets the prefix for a TransactionEvent.
func (e TransactionEvent) GetEventPrefix() string {
	return e.Name
}

// GetEventOrder gets the order for which this event should be processed
func (e TransactionEvent) GetEventOrder() uint64 {
	return 0
}

// Validate checks if the event data is valid.
func (e TransactionEvent) Validate() error {
	if strings.TrimSpace(e.Name) == "" {
		return fmt.Errorf("empty event name")
	}
	for _, attribute := range e.Attributes {
		if strings.TrimSpace(attribute.Name) == "" {
			return fmt.Errorf("empty attribute name")
		}
	}
	return nil
}

// Validate checks if this event is valid with the current context.
func (e TransactionEvent) ValidateContext(_ sdk.Context) error {
	return nil
}

// GetEventPrefix gets the prefix for a BlockHeightEvent.
func (e BlockHeightEvent) GetEventPrefix() string {
	return BlockHeightPrefix
}

// GetEventOrder gets the order for which this event should be processed
func (e BlockHeightEvent) GetEventOrder() uint64 {
	return e.BlockHeight
}

// Validate checks if the event data is valid.
func (e BlockHeightEvent) Validate() error {
	return nil
}

// ValidateContext checks if this event is valid with the current context.
func (e BlockHeightEvent) ValidateContext(ctx sdk.Context) error {
	if e.BlockHeight <= uint64(ctx.BlockHeight()) { //nolint:gosec // block height is always >= 0
		return ErrInvalidBlockHeight
	}
	return nil
}

// GetEventPrefix gets the prefix for a BlockTimeEvent.
func (e BlockTimeEvent) GetEventPrefix() string {
	return BlockTimePrefix
}

// GetEventOrder gets the order for which this event should be processed
func (e BlockTimeEvent) GetEventOrder() uint64 {
	return uint64(e.Time.UnixNano()) //nolint:gosec // time always >= Unix epoch
}

// Validate checks if the event data is valid.
func (e BlockTimeEvent) Validate() error {
	return nil
}

// Validate checks if this event is valid with the current context.
func (e BlockTimeEvent) ValidateContext(ctx sdk.Context) error {
	if e.Time.UTC().Equal(ctx.BlockTime().UTC()) || e.Time.Before(ctx.BlockTime().UTC()) {
		return ErrInvalidBlockTime
	}
	// Reject times outside int64 UnixNano range (~1678–2262).
	// Otherwise GetEventOrder() may overflow to a small value,
	// causing the event to sort before valid triggers and block execution.
	if e.Time.After(time.Unix(0, math.MaxInt64).UTC()) {
		return ErrInvalidBlockTime
	}
	return nil
}

// NewTrigger creates a new trigger.
func NewTrigger(id TriggerID, owner string, event *codectypes.Any, action []*codectypes.Any) Trigger {
	return Trigger{
		id,
		owner,
		event,
		action,
	}
}

// UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces
func (m Trigger) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error {
	if m.Event != nil {
		var event TriggerEventI
		err := unpacker.UnpackAny(m.Event, &event)
		if err != nil {
			return err
		}
	}
	return sdktx.UnpackInterfaces(unpacker, m.Actions)
}

// NewQueuedTrigger creates a new trigger for queueing.
func NewQueuedTrigger(trigger Trigger, blockTime time.Time, blockHeight uint64) QueuedTrigger {
	return QueuedTrigger{
		Time:        blockTime,
		BlockHeight: blockHeight,
		Trigger:     trigger,
	}
}

// UnpackInterfaces implements UnpackInterfacesMessage.UnpackInterfaces
func (m QueuedTrigger) UnpackInterfaces(unpacker codectypes.AnyUnpacker) error {
	if m.Trigger.Event != nil {
		var event TriggerEventI
		err := unpacker.UnpackAny(m.Trigger.Event, &event)
		if err != nil {
			return err
		}
	}
	return sdktx.UnpackInterfaces(unpacker, m.Trigger.Actions)
}

// GetTriggerEventI returns unpacked TriggerEvent
func (m Trigger) GetTriggerEventI() (TriggerEventI, error) {
	event, ok := m.GetEvent().GetCachedValue().(TriggerEventI)
	if !ok {
		return nil, ErrNoTriggerEvent.Wrap("failed to get event")
	}

	return event, nil
}
