package psvc

import (
	"strings"

	"github.com/shopspring/decimal"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/discovery"
)

// BatchOptimizationMode defines how ExecuteBatchPayment compacts batch operations.
type BatchOptimizationMode string

const (
	BatchOptimizationModeUnspecified        BatchOptimizationMode = ""
	BatchOptimizationModeMergeByDestination BatchOptimizationMode = "merge_by_destination"
	BatchOptimizationModeNoOptimization     BatchOptimizationMode = "no_optimization"
)

// BatchOptimizationGrouping defines which target key is used for merge grouping.
type BatchOptimizationGrouping string

const (
	BatchOptimizationGroupingUnspecified          BatchOptimizationGrouping = ""
	BatchOptimizationGroupingOperationDestination BatchOptimizationGrouping = "operation_destination"
	BatchOptimizationGroupingOperationSource      BatchOptimizationGrouping = "operation_source"
	BatchOptimizationGroupingRailTarget           BatchOptimizationGrouping = "rail_target"
)

// BatchOptimizationPolicy configures batch operation compaction behavior.
//
// Rules are evaluated by "best match":
// 1. all specified match fields must match the item context;
// 2. winner is highest specificity (number of optional match fields present);
// 3. ties are resolved by larger Priority;
// 4. remaining ties keep the first rule order from config.
//
// Rail is mandatory per rule. Other match fields are optional.
type BatchOptimizationPolicy struct {
	DefaultMode BatchOptimizationMode
	Rules       []BatchOptimizationRule
}

// BatchOptimizationRule defines one matching rule.
type BatchOptimizationRule struct {
	ID       string
	Enabled  *bool
	Priority int
	Mode     BatchOptimizationMode
	GroupBy  BatchOptimizationGrouping
	Match    BatchOptimizationMatch
}

// BatchOptimizationMatch defines selectors for one rule.
type BatchOptimizationMatch struct {
	Rail       model.Rail
	Providers  []string
	Networks   []string
	Currencies []string
	Amount     *BatchOptimizationAmountRange
}

// BatchOptimizationAmountRange defines optional amount matching boundaries.
type BatchOptimizationAmountRange struct {
	Min      string
	Max      string
	Currency string
}

// BatchOptimizationContext is runtime data used for matching one batch item.
type BatchOptimizationContext struct {
	Rail     model.Rail
	Provider string
	Network  string
	Currency string
	Amount   string
}

// BatchOptimizationSelection is the selected optimization decision.
type BatchOptimizationSelection struct {
	Mode      BatchOptimizationMode
	RuleID    string
	Matched   bool
	GroupBy   BatchOptimizationGrouping
	MatchRail model.Rail
}

func defaultBatchOptimizationPolicy() BatchOptimizationPolicy {
	return BatchOptimizationPolicy{
		DefaultMode: BatchOptimizationModeNoOptimization,
	}
}

func normalizeBatchOptimizationPolicy(in BatchOptimizationPolicy) BatchOptimizationPolicy {
	out := defaultBatchOptimizationPolicy()
	if normalizedDefault := normalizeBatchOptimizationMode(in.DefaultMode); normalizedDefault != BatchOptimizationModeUnspecified {
		out.DefaultMode = normalizedDefault
	}
	if len(in.Rules) == 0 {
		return out
	}

	rules := make([]BatchOptimizationRule, 0, len(in.Rules))
	for i := range in.Rules {
		rule, ok := normalizeBatchOptimizationRule(in.Rules[i])
		if !ok {
			continue
		}
		rules = append(rules, rule)
	}
	if len(rules) > 0 {
		out.Rules = rules
	}
	return out
}

func normalizeBatchOptimizationRule(in BatchOptimizationRule) (BatchOptimizationRule, bool) {
	out := BatchOptimizationRule{
		ID:       strings.TrimSpace(in.ID),
		Enabled:  in.Enabled,
		Priority: in.Priority,
		Mode:     normalizeBatchOptimizationMode(in.Mode),
		GroupBy:  normalizeBatchOptimizationGrouping(in.GroupBy),
	}
	if out.Mode == BatchOptimizationModeUnspecified {
		return BatchOptimizationRule{}, false
	}
	if out.GroupBy == BatchOptimizationGroupingUnspecified {
		out.GroupBy = BatchOptimizationGroupingOperationDestination
	}

	match, ok := normalizeBatchOptimizationMatch(in.Match)
	if !ok {
		return BatchOptimizationRule{}, false
	}
	out.Match = match
	return out, true
}

func normalizeBatchOptimizationMatch(in BatchOptimizationMatch) (BatchOptimizationMatch, bool) {
	out := BatchOptimizationMatch{
		Rail:       model.ParseRail(string(in.Rail)),
		Providers:  normalizeOptimizationTokens(in.Providers),
		Networks:   normalizeOptimizationTokens(in.Networks),
		Currencies: normalizeOptimizationCurrencies(in.Currencies),
	}
	if out.Rail == discovery.RailUnspecified {
		return BatchOptimizationMatch{}, false
	}
	if in.Amount != nil {
		normalizedAmount, ok := normalizeBatchOptimizationAmountRange(*in.Amount)
		if !ok {
			return BatchOptimizationMatch{}, false
		}
		out.Amount = &normalizedAmount
	}
	return out, true
}

func normalizeBatchOptimizationAmountRange(in BatchOptimizationAmountRange) (BatchOptimizationAmountRange, bool) {
	out := BatchOptimizationAmountRange{
		Min:      strings.TrimSpace(in.Min),
		Max:      strings.TrimSpace(in.Max),
		Currency: normalizeOptimizationCurrency(in.Currency),
	}
	if out.Min == "" && out.Max == "" {
		return BatchOptimizationAmountRange{}, false
	}

	var (
		min decimal.Decimal
		max decimal.Decimal
		err error
	)
	if out.Min != "" {
		min, err = decimal.NewFromString(out.Min)
		if err != nil {
			return BatchOptimizationAmountRange{}, false
		}
	}
	if out.Max != "" {
		max, err = decimal.NewFromString(out.Max)
		if err != nil {
			return BatchOptimizationAmountRange{}, false
		}
	}
	if out.Min != "" && out.Max != "" && min.GreaterThan(max) {
		return BatchOptimizationAmountRange{}, false
	}
	return out, true
}

func (p BatchOptimizationPolicy) Select(in BatchOptimizationContext) BatchOptimizationSelection {
	return p.SelectMany([]BatchOptimizationContext{in})
}

func (p BatchOptimizationPolicy) SelectMany(inputs []BatchOptimizationContext) BatchOptimizationSelection {
	policy := normalizeBatchOptimizationPolicy(p)
	contexts := make([]BatchOptimizationContext, 0, len(inputs))
	for i := range inputs {
		ctx := normalizeBatchOptimizationContext(inputs[i])
		if ctx.Rail == discovery.RailUnspecified {
			continue
		}
		contexts = append(contexts, ctx)
	}

	selected := BatchOptimizationSelection{
		Mode:      policy.DefaultMode,
		GroupBy:   BatchOptimizationGroupingOperationDestination,
		MatchRail: discovery.RailUnspecified,
	}
	if len(contexts) == 0 {
		return selected
	}

	bestSpecificity := -1
	bestPriority := 0
	bestOrder := len(policy.Rules) + 1
	bestContext := len(contexts) + 1
	for cIdx := range contexts {
		ctx := contexts[cIdx]
		for rIdx := range policy.Rules {
			rule := policy.Rules[rIdx]
			if rule.Enabled != nil && !*rule.Enabled {
				continue
			}
			specificity, ok := rule.matches(ctx)
			if !ok {
				continue
			}
			if specificity > bestSpecificity ||
				(specificity == bestSpecificity && rule.Priority > bestPriority) ||
				(specificity == bestSpecificity && rule.Priority == bestPriority && rIdx < bestOrder) ||
				(specificity == bestSpecificity && rule.Priority == bestPriority && rIdx == bestOrder && cIdx < bestContext) {
				selected = BatchOptimizationSelection{
					Mode:      rule.Mode,
					RuleID:    rule.ID,
					Matched:   true,
					GroupBy:   rule.GroupBy,
					MatchRail: rule.Match.Rail,
				}
				bestSpecificity = specificity
				bestPriority = rule.Priority
				bestOrder = rIdx
				bestContext = cIdx
			}
		}
	}
	return selected
}

func normalizeBatchOptimizationGrouping(raw BatchOptimizationGrouping) BatchOptimizationGrouping {
	switch strings.ToLower(strings.TrimSpace(string(raw))) {
	case "operation_destination", "operation-destination", "destination", "intent_destination", "intent-destination":
		return BatchOptimizationGroupingOperationDestination
	case "operation_source", "operation-source", "source", "intent_source", "intent-source":
		return BatchOptimizationGroupingOperationSource
	case "rail_target", "rail-target", "by_rail_target", "by-rail-target":
		return BatchOptimizationGroupingRailTarget
	default:
		return BatchOptimizationGroupingUnspecified
	}
}

func (r BatchOptimizationRule) matches(ctx BatchOptimizationContext) (int, bool) {
	match := r.Match
	if match.Rail == discovery.RailUnspecified || ctx.Rail == discovery.RailUnspecified {
		return 0, false
	}
	if match.Rail != ctx.Rail {
		return 0, false
	}

	specificity := 0
	if len(match.Providers) > 0 {
		specificity++
		if !containsOptimizationToken(match.Providers, ctx.Provider) {
			return 0, false
		}
	}
	if len(match.Networks) > 0 {
		specificity++
		if !containsOptimizationToken(match.Networks, ctx.Network) {
			return 0, false
		}
	}
	if len(match.Currencies) > 0 {
		specificity++
		if !containsOptimizationToken(match.Currencies, ctx.Currency) {
			return 0, false
		}
	}
	if match.Amount != nil {
		specificity++
		if !amountRangeMatches(*match.Amount, ctx.Amount, ctx.Currency) {
			return 0, false
		}
	}
	return specificity, true
}

func amountRangeMatches(rule BatchOptimizationAmountRange, amountRaw string, currencyRaw string) bool {
	value, err := decimal.NewFromString(strings.TrimSpace(amountRaw))
	if err != nil {
		return false
	}
	if ruleCurrency := normalizeOptimizationCurrency(rule.Currency); ruleCurrency != "" && ruleCurrency != normalizeOptimizationCurrency(currencyRaw) {
		return false
	}
	if rule.Min != "" {
		min, parseErr := decimal.NewFromString(rule.Min)
		if parseErr != nil || value.LessThan(min) {
			return false
		}
	}
	if rule.Max != "" {
		max, parseErr := decimal.NewFromString(rule.Max)
		if parseErr != nil || value.GreaterThan(max) {
			return false
		}
	}
	return true
}

func normalizeBatchOptimizationContext(in BatchOptimizationContext) BatchOptimizationContext {
	return BatchOptimizationContext{
		Rail:     model.ParseRail(string(in.Rail)),
		Provider: normalizeOptimizationToken(in.Provider),
		Network:  normalizeOptimizationToken(in.Network),
		Currency: normalizeOptimizationCurrency(in.Currency),
		Amount:   strings.TrimSpace(in.Amount),
	}
}

func normalizeBatchOptimizationMode(raw BatchOptimizationMode) BatchOptimizationMode {
	switch strings.ToLower(strings.TrimSpace(string(raw))) {
	case "merge_by_destination", "merge-by-destination", "by_destination", "by-destination":
		return BatchOptimizationModeMergeByDestination
	case "no_optimization", "no-optimization", "none", "never_merge", "never-merge", "never":
		return BatchOptimizationModeNoOptimization
	default:
		return BatchOptimizationModeUnspecified
	}
}

func normalizeOptimizationTokens(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(values))
	out := make([]string, 0, len(values))
	for i := range values {
		token := normalizeOptimizationToken(values[i])
		if token == "" {
			continue
		}
		if _, exists := seen[token]; exists {
			continue
		}
		seen[token] = struct{}{}
		out = append(out, token)
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func normalizeOptimizationCurrencies(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(values))
	out := make([]string, 0, len(values))
	for i := range values {
		token := normalizeOptimizationCurrency(values[i])
		if token == "" {
			continue
		}
		if _, exists := seen[token]; exists {
			continue
		}
		seen[token] = struct{}{}
		out = append(out, token)
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func normalizeOptimizationToken(value string) string {
	return strings.ToUpper(strings.TrimSpace(value))
}

func normalizeOptimizationCurrency(value string) string {
	return strings.ToUpper(strings.TrimSpace(value))
}

func containsOptimizationToken(values []string, value string) bool {
	if len(values) == 0 {
		return false
	}
	normalized := normalizeOptimizationToken(value)
	if normalized == "" {
		return false
	}
	for i := range values {
		if values[i] == normalized {
			return true
		}
	}
	return false
}
