package voucher

import (
	"errors"
	"fmt"
	"strconv"
	"strings"
	"unicode"
)

/*
Package voucher - rule parser

A lightweight parser for voucher rule expressions.

Supported rule kinds (case-insensitive keywords):

  sku=SKU1|SKU2|SKU3
    - At least one of the listed SKUs must be present in the cart.

  category=CatA|CatB|CatC
    - At least one of the listed categories must be present.

  min_total>=12345
    - Cart total (Inc VAT) must be at least this value (int64).

  min_item_price>=5000
    - At least one individual item (Inc VAT single unit price) must be at least this value (int64).

Rule list grammar (simplified):
  rules := rule (sep rule)*
  rule  := (sku|category) '=' valueList
         | (min_total|min_item_price) comparator number
  valueList := value ('|' value)*
  comparator := '>='   (only comparator currently supported for numeric rules)
  sep := ';' | ',' | newline

Whitespace is ignored around tokens.

Example:
  sku=ABC123|XYZ999; category=Shoes|Bags
  min_total>=10000
  min_item_price>=2500, category=Accessories

Parsing returns a RuleSet which can later be evaluated against a generic context.
The evaluation context uses simple Item abstractions to avoid tight coupling with
the cart implementation (which currently lives under cmd/cart and cannot be
imported due to being in package main).

This is intentionally conservative and extensible:
  * Adding new rule kinds: extend RuleKind constants, add parse + evaluate logic.
  * Supporting new operators: extend numeric rule parsing & evaluation.
*/

var (
	// ErrEmptyExpression is returned when the input string has only whitespace.
	ErrEmptyExpression = errors.New("voucher: empty rule expression")
	// ErrInvalidRule indicates a syntactic or semantic issue with a single rule fragment.
	ErrInvalidRule = errors.New("voucher: invalid rule")
)

// RuleKind enumerates supported rule kinds.
type RuleKind string

const (
	RuleSku          RuleKind = "sku"
	RuleCategory     RuleKind = "category"
	RuleMinTotal     RuleKind = "min_total"
	RuleMinItemPrice RuleKind = "min_item_price"
)

// ruleCondition represents a single, parsed rule.
type ruleCondition struct {
	Kind       RuleKind
	StringVals []string // For sku / category multi-value list
	MinValue   *int64   // For numeric threshold rules
	// Operator reserved for future (e.g., >, >=, ==). Currently always ">=" for numeric kinds.
	Operator string
}

// RuleSet groups multiple rule conditions (logical AND).
// All conditions must pass for Applies() to return true.
type RuleSet struct {
	Conditions []ruleCondition
	Source     string // original, trimmed source string
}

// Item is a minimal abstraction for evaluation (decoupled from cart domain structs).
type Item struct {
	Sku       string
	Category  string
	UnitPrice int64 // Inc VAT (single unit)
}

// EvalContext bundles cart-like data necessary for evaluation.
type EvalContext struct {
	Items        []Item
	CartTotalInc int64
}

// Applies returns true if all rule conditions pass for the context.
func (rs *RuleSet) Applies(ctx EvalContext) bool {
	for _, c := range rs.Conditions {
		switch c.Kind {
		case RuleSku:
			if !anyItem(ctx.Items, func(it Item) bool {
				return containsFold(c.StringVals, it.Sku)
			}) {
				return false
			}
		case RuleCategory:
			if !anyItem(ctx.Items, func(it Item) bool {
				return containsFold(c.StringVals, it.Category)
			}) {
				return false
			}
		case RuleMinTotal:
			if c.MinValue == nil || ctx.CartTotalInc < *c.MinValue {
				return false
			}
		case RuleMinItemPrice:
			if c.MinValue == nil {
				return false
			}
			if !anyItem(ctx.Items, func(it Item) bool {
				return it.UnitPrice >= *c.MinValue
			}) {
				return false
			}
		default:
			// Unknown kinds fail closed to avoid granting unintended discounts.
			return false
		}
	}
	return true
}

// anyItem returns true if predicate matches any item.
func anyItem(items []Item, pred func(Item) bool) bool {
	for _, it := range items {
		if pred(it) {
			return true
		}
	}
	return false
}

// ParseRules parses a rule expression into a RuleSet.
func ParseRules(input string) (*RuleSet, error) {
	trimmed := strings.TrimSpace(input)
	if trimmed == "" {
		return nil, ErrEmptyExpression
	}

	fragments := splitRuleFragments(trimmed)
	if len(fragments) == 0 {
		return nil, ErrInvalidRule
	}

	var conditions []ruleCondition
	for _, frag := range fragments {
		if frag == "" {
			continue
		}
		c, err := parseFragment(frag)
		if err != nil {
			return nil, fmt.Errorf("%w: %s (%v)", ErrInvalidRule, frag, err)
		}
		conditions = append(conditions, c)
	}

	if len(conditions) == 0 {
		return nil, ErrInvalidRule
	}

	return &RuleSet{
		Conditions: conditions,
		Source:     trimmed,
	}, nil
}

// splitRuleFragments splits on ; , or newline, while respecting basic structure.
func splitRuleFragments(s string) []string {
	// Normalize line endings
	s = strings.ReplaceAll(s, "\r\n", "\n")

	// We allow separators: newline, semicolon, comma.
	seps := func(r rune) bool {
		return r == ';' || r == '\n' || r == ','
	}
	raw := strings.FieldsFunc(s, seps)
	out := make([]string, 0, len(raw))
	for _, f := range raw {
		t := strings.TrimSpace(f)
		if t != "" {
			out = append(out, t)
		}
	}
	return out
}

// parseFragment parses an individual rule fragment.
func parseFragment(frag string) (ruleCondition, error) {
	lower := strings.ToLower(frag)

	// Numeric rules have form: <kind> >= number
	if strings.HasPrefix(lower, string(RuleMinTotal)) ||
		strings.HasPrefix(lower, string(RuleMinItemPrice)) {

		return parseNumericRule(frag)
	}

	// Key=Value list rules (sku / category).
	if i := strings.Index(frag, "="); i > 0 {
		key := strings.TrimSpace(frag[:i])
		valPart := strings.TrimSpace(frag[i+1:])
		if key == "" || valPart == "" {
			return ruleCondition{}, errors.New("empty key/value")
		}
		kind := RuleKind(strings.ToLower(key))
		switch kind {
		case RuleSku, RuleCategory:
			values := splitAndClean(valPart, "|")
			if len(values) == 0 {
				return ruleCondition{}, errors.New("empty value list")
			}
			return ruleCondition{
				Kind:       kind,
				StringVals: values,
			}, nil
		default:
			return ruleCondition{}, fmt.Errorf("unsupported key '%s'", key)
		}
	}

	return ruleCondition{}, fmt.Errorf("unrecognized fragment '%s'", frag)
}

func parseNumericRule(frag string) (ruleCondition, error) {
	// Support only '>=' for now.
	var kind RuleKind
	var rest string

	fragTrim := strings.TrimSpace(frag)

	switch {
	case strings.HasPrefix(strings.ToLower(fragTrim), string(RuleMinTotal)):
		kind = RuleMinTotal
		rest = strings.TrimSpace(fragTrim[len(RuleMinTotal):])
	case strings.HasPrefix(strings.ToLower(fragTrim), string(RuleMinItemPrice)):
		kind = RuleMinItemPrice
		rest = strings.TrimSpace(fragTrim[len(RuleMinItemPrice):])
	default:
		return ruleCondition{}, fmt.Errorf("unknown numeric rule '%s'", frag)
	}

	// Expect operator and number (>= <number>)
	rest = stripLeadingSpace(rest)
	if !strings.HasPrefix(rest, ">=") {
		return ruleCondition{}, fmt.Errorf("expected '>=' in '%s'", frag)
	}
	numStr := strings.TrimSpace(rest[2:])
	if numStr == "" {
		return ruleCondition{}, fmt.Errorf("missing numeric value in '%s'", frag)
	}

	value, err := strconv.ParseInt(numStr, 10, 64)
	if err != nil {
		return ruleCondition{}, fmt.Errorf("invalid number '%s': %v", numStr, err)
	}
	if value < 0 {
		return ruleCondition{}, fmt.Errorf("negative threshold %d", value)
	}

	return ruleCondition{
		Kind:     kind,
		MinValue: &value,
		Operator: ">=",
	}, nil
}

func stripLeadingSpace(s string) string {
	for len(s) > 0 && unicode.IsSpace(rune(s[0])) {
		s = s[1:]
	}
	return s
}

func splitAndClean(s string, sep string) []string {
	raw := strings.Split(s, sep)
	out := make([]string, 0, len(raw))
	for _, r := range raw {
		t := strings.TrimSpace(r)
		if t != "" {
			out = append(out, t)
		}
	}
	return out
}

func containsFold(list []string, candidate string) bool {
	for _, v := range list {
		if strings.EqualFold(v, candidate) {
			return true
		}
	}
	return false
}

// Describe returns a human-friendly summary of the parsed rule set.
func (rs *RuleSet) Describe() string {
	if rs == nil {
		return "<nil>"
	}
	var parts []string
	for _, c := range rs.Conditions {
		switch c.Kind {
		case RuleSku, RuleCategory:
			parts = append(parts, fmt.Sprintf("%s in (%s)", c.Kind, strings.Join(c.StringVals, "|")))
		case RuleMinTotal, RuleMinItemPrice:
			if c.MinValue != nil {
				parts = append(parts, fmt.Sprintf("%s %s %d", c.Kind, c.OperatorOr(">="), *c.MinValue))
			}
		default:
			parts = append(parts, fmt.Sprintf("unknown(%s)", c.Kind))
		}
	}
	return strings.Join(parts, " AND ")
}

func (c ruleCondition) OperatorOr(def string) string {
	if c.Operator == "" {
		return def
	}
	return c.Operator
}

// --- Convenience helpers for incremental adoption ---

// MustParseRules panics on parse error (useful in tests or static initialization).
func MustParseRules(expr string) *RuleSet {
	rs, err := ParseRules(expr)
	if err != nil {
		panic(err)
	}
	return rs
}
