package erecon

import (
	"strings"
	"time"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/oshared"
	svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared"
	"github.com/tech/sendico/pkg/merrors"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
)

type normalizedEvent struct {
	stepRef        string            `bson:"stepRef"`
	matchRefs      []agg.ExternalRef `bson:"matchRefs"`
	appendRefs     []agg.ExternalRef `bson:"appendRefs"`
	targetState    agg.StepState     `bson:"targetState"`
	failureInfo    *failureInfo      `bson:"failure,omitempty"`
	forceAggregate *forceAggregate   `bson:"forceAggregate,omitempty"`
	executedMoney  *paymenttypes.Money
}

type failureInfo struct {
	code       string     `bson:"code"`
	msg        string     `bson:"message"`
	occurredAt *time.Time `bson:"occurredAt,omitempty"`
}

type forceAggregate struct {
	failed         bool `bson:"failed"`
	needsAttention bool `bson:"needsAttention"`
}

func normalizeEvent(event Event) (*normalizedEvent, error) {
	if countPayloads(event) != 1 {
		return nil, merrors.InvalidArgument("exactly one event payload is required")
	}

	if event.Gateway != nil {
		return normalizeGatewayEvent(*event.Gateway)
	}
	if event.Ledger != nil {
		return normalizeLedgerEvent(*event.Ledger)
	}
	return normalizeCardEvent(*event.Card)
}

func countPayloads(event Event) int {
	count := 0
	if event.Gateway != nil {
		count++
	}
	if event.Ledger != nil {
		count++
	}
	if event.Card != nil {
		count++
	}
	return count
}

func (e *normalizedEvent) failureCodeValue() string {
	if e == nil || e.failureInfo == nil {
		return ""
	}
	return strings.TrimSpace(e.failureInfo.code)
}

func (e *normalizedEvent) failureMsgValue() string {
	if e == nil || e.failureInfo == nil {
		return ""
	}
	return strings.TrimSpace(e.failureInfo.msg)
}

func (e *normalizedEvent) occurredAtValue() *time.Time {
	if e == nil || e.failureInfo == nil {
		return nil
	}
	return e.failureInfo.occurredAt
}

func (e *normalizedEvent) forceAggregateFailedValue() bool {
	return e != nil && e.forceAggregate != nil && e.forceAggregate.failed
}

func (e *normalizedEvent) forceAggregateNeedsAttentionValue() bool {
	return e != nil && e.forceAggregate != nil && e.forceAggregate.needsAttention
}

func buildFailureInfo(code, msg string, occurredAt *time.Time) *failureInfo {
	if code == "" && msg == "" && occurredAt == nil {
		return nil
	}
	return &failureInfo{
		code:       code,
		msg:        msg,
		occurredAt: occurredAt,
	}
}

func buildForceAggregate(failed, needsAttention bool) *forceAggregate {
	if !failed && !needsAttention {
		return nil
	}
	return &forceAggregate{
		failed:         failed,
		needsAttention: needsAttention,
	}
}

func normalizeGatewayEvent(src GatewayEvent) (*normalizedEvent, error) {
	status, ok := normalizeGatewayStatus(src.Status)
	if !ok {
		return nil, merrors.InvalidArgument("gateway status is invalid")
	}

	target, needsAttention := mapFailureTarget(status, src.Retryable)
	failureCode := strings.TrimSpace(src.FailureCode)
	failureMsg := strings.TrimSpace(src.FailureMsg)
	if target == agg.StepStateFailed && failureMsg == "" {
		failureMsg = "gateway operation failed"
	}
	ev := &normalizedEvent{
		stepRef:        strings.TrimSpace(src.StepRef),
		targetState:    target,
		failureInfo:    buildFailureInfo(failureCode, failureMsg, oshared.CloneTimeUTC(src.OccurredAt)),
		forceAggregate: buildForceAggregate(src.TerminalFailure, needsAttention),
		executedMoney:  svcshared.CloneMoneyTrimNonEmpty(src.ExecutedMoney),
	}
	ev.matchRefs = normalizeRefList([]agg.ExternalRef{
		{
			GatewayInstanceID: strings.TrimSpace(src.GatewayInstanceID),
			Kind:              ExternalRefKindOperation,
			Ref:               strings.TrimSpace(src.OperationRef),
		},
		{
			GatewayInstanceID: strings.TrimSpace(src.GatewayInstanceID),
			Kind:              ExternalRefKindTransfer,
			Ref:               strings.TrimSpace(src.TransferRef),
		},
	})
	ev.appendRefs = cloneRefs(ev.matchRefs)

	if ev.stepRef == "" && len(ev.matchRefs) == 0 {
		return nil, merrors.InvalidArgument("gateway event must include step_ref or operation/transfer reference")
	}
	return ev, nil
}

func normalizeLedgerEvent(src LedgerEvent) (*normalizedEvent, error) {
	status, ok := normalizeLedgerStatus(src.Status)
	if !ok {
		return nil, merrors.InvalidArgument("ledger status is invalid")
	}

	target, needsAttention := mapFailureTarget(status, src.Retryable)
	failureCode := strings.TrimSpace(src.FailureCode)
	failureMsg := strings.TrimSpace(src.FailureMsg)
	if target == agg.StepStateFailed && failureMsg == "" {
		failureMsg = "ledger operation failed"
	}
	ev := &normalizedEvent{
		stepRef:        strings.TrimSpace(src.StepRef),
		targetState:    target,
		failureInfo:    buildFailureInfo(failureCode, failureMsg, oshared.CloneTimeUTC(src.OccurredAt)),
		forceAggregate: buildForceAggregate(src.TerminalFailure, needsAttention),
	}
	ev.matchRefs = normalizeRefList([]agg.ExternalRef{
		{
			Kind: ExternalRefKindLedger,
			Ref:  strings.TrimSpace(src.EntryRef),
		},
	})
	ev.appendRefs = cloneRefs(ev.matchRefs)

	if ev.stepRef == "" && len(ev.matchRefs) == 0 {
		return nil, merrors.InvalidArgument("ledger event must include step_ref or entry_ref")
	}
	return ev, nil
}

func normalizeCardEvent(src CardEvent) (*normalizedEvent, error) {
	status, ok := normalizeCardStatus(src.Status)
	if !ok {
		return nil, merrors.InvalidArgument("card status is invalid")
	}

	target, needsAttention := mapFailureTarget(status, src.Retryable)
	failureCode := strings.TrimSpace(src.FailureCode)
	failureMsg := strings.TrimSpace(src.FailureMsg)
	if target == agg.StepStateFailed && failureMsg == "" {
		failureMsg = "card payout failed"
	}
	ev := &normalizedEvent{
		stepRef:        strings.TrimSpace(src.StepRef),
		targetState:    target,
		failureInfo:    buildFailureInfo(failureCode, failureMsg, oshared.CloneTimeUTC(src.OccurredAt)),
		forceAggregate: buildForceAggregate(src.TerminalFailure, needsAttention),
	}
	ev.matchRefs = normalizeRefList([]agg.ExternalRef{
		{
			GatewayInstanceID: strings.TrimSpace(src.GatewayInstanceID),
			Kind:              ExternalRefKindCardPayout,
			Ref:               strings.TrimSpace(src.PayoutRef),
		},
	})
	ev.appendRefs = cloneRefs(ev.matchRefs)

	if ev.stepRef == "" && len(ev.matchRefs) == 0 {
		return nil, merrors.InvalidArgument("card event must include step_ref or payout_ref")
	}
	return ev, nil
}

func normalizeGatewayStatus(status GatewayStatus) (GatewayStatus, bool) {
	switch strings.ToLower(strings.TrimSpace(string(status))) {
	case string(GatewayStatusCreated):
		return GatewayStatusCreated, true
	case string(GatewayStatusProcessing):
		return GatewayStatusProcessing, true
	case string(GatewayStatusWaiting):
		return GatewayStatusWaiting, true
	case string(GatewayStatusSuccess):
		return GatewayStatusSuccess, true
	case string(GatewayStatusFailed):
		return GatewayStatusFailed, true
	case string(GatewayStatusCancelled):
		return GatewayStatusCancelled, true
	default:
		return GatewayStatusUnspecified, false
	}
}

func normalizeLedgerStatus(status LedgerStatus) (LedgerStatus, bool) {
	switch strings.ToLower(strings.TrimSpace(string(status))) {
	case string(LedgerStatusPending):
		return LedgerStatusPending, true
	case string(LedgerStatusProcessing):
		return LedgerStatusProcessing, true
	case string(LedgerStatusPosted):
		return LedgerStatusPosted, true
	case string(LedgerStatusFailed):
		return LedgerStatusFailed, true
	case string(LedgerStatusCancelled):
		return LedgerStatusCancelled, true
	default:
		return LedgerStatusUnspecified, false
	}
}

func normalizeCardStatus(status CardStatus) (CardStatus, bool) {
	switch strings.ToLower(strings.TrimSpace(string(status))) {
	case string(CardStatusCreated):
		return CardStatusCreated, true
	case string(CardStatusProcessing):
		return CardStatusProcessing, true
	case string(CardStatusWaiting):
		return CardStatusWaiting, true
	case string(CardStatusSuccess):
		return CardStatusSuccess, true
	case string(CardStatusFailed):
		return CardStatusFailed, true
	case string(CardStatusCancelled):
		return CardStatusCancelled, true
	default:
		return CardStatusUnspecified, false
	}
}

func normalizeEventMoney(money *paymenttypes.Money) *paymenttypes.Money {
	return svcshared.CloneMoneyTrimNonEmpty(money)
}

func mapFailureTarget(status any, retryable *bool) (agg.StepState, bool) {
	switch status {
	case GatewayStatusCreated, GatewayStatusProcessing, GatewayStatusWaiting:
		return agg.StepStateRunning, false
	case LedgerStatusPending, LedgerStatusProcessing:
		return agg.StepStateRunning, false
	case CardStatusCreated, CardStatusProcessing, CardStatusWaiting:
		return agg.StepStateRunning, false
	case GatewayStatusSuccess, LedgerStatusPosted, CardStatusSuccess:
		return agg.StepStateCompleted, false
	case GatewayStatusFailed, GatewayStatusCancelled, LedgerStatusFailed, LedgerStatusCancelled, CardStatusFailed, CardStatusCancelled:
		if retryable != nil && !*retryable {
			return agg.StepStateNeedsAttention, true
		}
		return agg.StepStateFailed, false
	default:
		return agg.StepStateUnspecified, false
	}
}

func normalizeRefList(refs []agg.ExternalRef) []agg.ExternalRef {
	if len(refs) == 0 {
		return nil
	}
	out := make([]agg.ExternalRef, 0, len(refs))
	seen := map[string]struct{}{}
	for i := range refs {
		ref := refs[i]
		ref.GatewayInstanceID = strings.TrimSpace(ref.GatewayInstanceID)
		ref.Kind = strings.TrimSpace(ref.Kind)
		ref.Ref = strings.TrimSpace(ref.Ref)
		if ref.Kind == "" || ref.Ref == "" {
			continue
		}
		key := ref.GatewayInstanceID + "\x1f" + strings.ToLower(ref.Kind) + "\x1f" + strings.ToLower(ref.Ref)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, ref)
	}
	return out
}

func cloneRefs(refs []agg.ExternalRef) []agg.ExternalRef {
	if len(refs) == 0 {
		return nil
	}
	out := make([]agg.ExternalRef, 0, len(refs))
	out = append(out, refs...)
	return out
}
