package opagg

import (
	"strconv"
	"strings"
	"time"

	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"go.uber.org/zap"
)

const (
	keySep = "\x1f"

	attrAggregatedByRecipient = "orchestrator.v2.aggregated_by_recipient"
	attrAggregatedItems       = "orchestrator.v2.aggregated_items"
)

type svc struct {
	logger mlogger.Logger
}

type groupAccumulator struct {
	recipientKey string
	intentRefs   []string
	intent       model.PaymentIntent
	quote        *model.PaymentQuoteSnapshot
	members      []GroupMember
}

func (s *svc) Aggregate(in Input) (out *Output, err error) {
	logger := s.logger
	logger.Debug("Starting Aggregate", zap.Int("items_count", len(in.Items)))
	defer func(start time.Time) {
		fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
		if out != nil {
			fields = append(fields, zap.Int("groups_count", len(out.Groups)))
		}
		if err != nil {
			logger.Warn("Failed to aggregate", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Aggregate", fields...)
	}(time.Now())

	if len(in.Items) == 0 {
		return nil, merrors.InvalidArgument("items are required")
	}

	groups := make(map[string]*groupAccumulator, len(in.Items))
	order := make([]string, 0, len(in.Items))

	for i := range in.Items {
		item := in.Items[i]
		if err := validateItem(item); err != nil {
			return nil, merrors.InvalidArgument("items[" + itoa(i) + "]: " + err.Error())
		}

		key, recipientKey, err := groupingKey(item)
		if err != nil {
			return nil, merrors.InvalidArgument("items[" + itoa(i) + "]: " + err.Error())
		}
		if !isBatchingEligible(item.QuoteSnapshot) {
			key = key + keySep + "non_batching=" + itoa(i)
		}
		if normalizeMergeMode(item.MergeMode) == MergeModeNever {
			key = key + keySep + "merge_mode=never:" + itoa(i)
		}
		if policyTag := strings.TrimSpace(item.PolicyTag); policyTag != "" {
			key = key + keySep + "policy_tag=" + policyTag
		}

		intentRef := firstNonEmpty(
			strings.TrimSpace(item.IntentRef),
			strings.TrimSpace(item.IntentSnapshot.Ref),
			"intent-"+itoa(i+1),
		)

		acc, exists := groups[key]
		if !exists {
			intentSnapshot, cloneErr := cloneIntentSnapshot(item.IntentSnapshot)
			if cloneErr != nil {
				return nil, cloneErr
			}
			quoteSnapshot, cloneErr := cloneQuoteSnapshot(item.QuoteSnapshot)
			if cloneErr != nil {
				return nil, cloneErr
			}
			if quoteSnapshot == nil {
				return nil, merrors.InvalidArgument("items[" + itoa(i) + "].quote_snapshot is required")
			}
			member, memberErr := makeGroupMember(intentRef, item.IntentSnapshot, item.QuoteSnapshot)
			if memberErr != nil {
				return nil, memberErr
			}
			groupRecipientKey := recipientKey
			if normalizeMergeMode(item.MergeMode) == MergeModeByRecipient {
				if override := strings.TrimSpace(item.MergeKey); override != "" {
					groupRecipientKey = override
				}
			}

			groups[key] = &groupAccumulator{
				recipientKey: groupRecipientKey,
				intentRefs:   []string{intentRef},
				intent:       intentSnapshot,
				quote:        quoteSnapshot,
				members:      []GroupMember{member},
			}
			order = append(order, key)
			continue
		}

		if err := mergeIntentSnapshot(&acc.intent, item.IntentSnapshot); err != nil {
			return nil, merrors.InvalidArgument("items[" + itoa(i) + "]: " + err.Error())
		}
		if err := mergeQuoteSnapshot(acc.quote, item.QuoteSnapshot); err != nil {
			return nil, merrors.InvalidArgument("items[" + itoa(i) + "]: " + err.Error())
		}
		acc.intentRefs = append(acc.intentRefs, intentRef)
		member, memberErr := makeGroupMember(intentRef, item.IntentSnapshot, item.QuoteSnapshot)
		if memberErr != nil {
			return nil, memberErr
		}
		acc.members = append(acc.members, member)
	}

	out = &Output{
		Groups: make([]Group, 0, len(order)),
	}
	for _, key := range order {
		acc := groups[key]
		if acc == nil || acc.quote == nil {
			continue
		}
		finalIntent, err := cloneIntentSnapshot(acc.intent)
		if err != nil {
			return nil, err
		}
		finalQuote, err := cloneQuoteSnapshot(acc.quote)
		if err != nil {
			return nil, err
		}
		if len(acc.intentRefs) > 1 {
			if finalIntent.Attributes == nil {
				finalIntent.Attributes = map[string]string{}
			}
			finalIntent.Attributes[attrAggregatedByRecipient] = "true"
			finalIntent.Attributes[attrAggregatedItems] = strconv.Itoa(len(acc.intentRefs))
		}

		out.Groups = append(out.Groups, Group{
			RecipientKey:   acc.recipientKey,
			IntentRefs:     cloneStringSlice(acc.intentRefs),
			IntentSnapshot: finalIntent,
			QuoteSnapshot:  finalQuote,
			Members:        cloneGroupMembers(acc.members),
		})
	}

	if len(out.Groups) == 0 {
		return nil, merrors.InvalidArgument("aggregation produced no groups")
	}
	return out, nil
}

func validateItem(item Item) error {
	if isEmptyIntentSnapshot(item.IntentSnapshot) {
		return merrors.InvalidArgument("intent_snapshot is required")
	}
	if item.QuoteSnapshot == nil {
		return merrors.InvalidArgument("quote_snapshot is required")
	}
	if item.IntentSnapshot.Amount == nil {
		return merrors.InvalidArgument("intent_snapshot.amount is required")
	}
	if strings.TrimSpace(item.IntentSnapshot.Amount.Currency) == "" {
		return merrors.InvalidArgument("intent_snapshot.amount.currency is required")
	}
	return nil
}

func groupingKey(item Item) (string, string, error) {
	recipientKey, err := endpointKey(item.IntentSnapshot.Destination)
	if err != nil {
		return "", "", merrors.InvalidArgument("intent_snapshot.destination: " + err.Error())
	}

	if normalizeMergeMode(item.MergeMode) == MergeModeByRecipient {
		mergeKey := strings.TrimSpace(item.MergeKey)
		if mergeKey == "" {
			mergeKey = recipientKey
		}
		key := strings.Join([]string{
			"kind=" + strings.ToLower(strings.TrimSpace(string(item.IntentSnapshot.Kind))),
			"merge_key=" + mergeKey,
		}, keySep)
		return key, recipientKey, nil
	}

	sourceKey, err := endpointKey(item.IntentSnapshot.Source)
	if err != nil {
		return "", "", merrors.InvalidArgument("intent_snapshot.source: " + err.Error())
	}

	quote := item.QuoteSnapshot
	key := strings.Join([]string{
		"kind=" + strings.ToLower(strings.TrimSpace(string(item.IntentSnapshot.Kind))),
		"source=" + sourceKey,
		"recipient=" + recipientKey,
		"settlement_mode=" + strings.ToLower(strings.TrimSpace(string(item.IntentSnapshot.SettlementMode))),
		"settlement_currency=" + normalizeCurrency(item.IntentSnapshot.SettlementCurrency),
		"debit_currency=" + moneyCurrency(quote.DebitAmount),
		"settlement_amount_currency=" + moneyCurrency(quote.ExpectedSettlementAmount),
		"route=" + routeSignature(quote.Route),
		"fx=" + fxQuoteSignature(quote.FXQuote),
	}, keySep)

	return key, recipientKey, nil
}

func makeGroupMember(intentRef string, intent model.PaymentIntent, quote *model.PaymentQuoteSnapshot) (GroupMember, error) {
	intentSnapshot, err := cloneIntentSnapshot(intent)
	if err != nil {
		return GroupMember{}, err
	}
	quoteSnapshot, err := cloneQuoteSnapshot(quote)
	if err != nil {
		return GroupMember{}, err
	}
	return GroupMember{
		IntentRef:      strings.TrimSpace(intentRef),
		IntentSnapshot: intentSnapshot,
		QuoteSnapshot:  quoteSnapshot,
	}, nil
}

func isBatchingEligible(quote *model.PaymentQuoteSnapshot) bool {
	if quote == nil || quote.ExecutionConditions == nil {
		return true
	}
	return quote.ExecutionConditions.BatchingEligible
}

func normalizeMergeMode(mode MergeMode) MergeMode {
	switch strings.ToLower(strings.TrimSpace(string(mode))) {
	case strings.ToLower(string(MergeModeNever)):
		return MergeModeNever
	default:
		return MergeModeByRecipient
	}
}
