package promotions

import (
	"errors"
	"fmt"
	"slices"
	"strings"
	"time"

	"git.tornberg.me/go-cart-actor/pkg/cart"
)

var errInvalidTimeFormat = errors.New("invalid time format")

// Tracer allows callers to receive structured debug information during evaluation.
type Tracer interface {
	Trace(event string, data map[string]any)
}

// NoopTracer is used when no tracer provided.
type NoopTracer struct{}

func (NoopTracer) Trace(string, map[string]any) {}

// PromotionItem is a lightweight abstraction derived from cart.CartItem
// for the purpose of promotion condition evaluation.
type PromotionItem struct {
	SKU         string
	Quantity    int
	Category    string
	PriceIncVat int64
}

// PromotionEvalContext carries all dynamic data required to evaluate promotion
// conditions. It can be constructed from a cart.CartGrain plus optional
// customer/order metadata.
type PromotionEvalContext struct {
	CartTotalIncVat       int64
	TotalItemQuantity     int
	Items                 []PromotionItem
	CustomerSegment       string
	CustomerLifetimeValue float64
	OrderCount            int
	Now                   time.Time
}

// ContextOption allows customization of fields when building a PromotionEvalContext.
type ContextOption func(*PromotionEvalContext)

// WithCustomerSegment sets the customer segment.
func WithCustomerSegment(seg string) ContextOption {
	return func(c *PromotionEvalContext) { c.CustomerSegment = seg }
}

// WithCustomerLifetimeValue sets lifetime value metric.
func WithCustomerLifetimeValue(v float64) ContextOption {
	return func(c *PromotionEvalContext) { c.CustomerLifetimeValue = v }
}

// WithOrderCount sets historical order count.
func WithOrderCount(n int) ContextOption {
	return func(c *PromotionEvalContext) { c.OrderCount = n }
}

// WithNow overrides the timestamp used for date/time related conditions.
func WithNow(t time.Time) ContextOption {
	return func(c *PromotionEvalContext) { c.Now = t }
}

// NewContextFromCart builds a PromotionEvalContext from a CartGrain and optional metadata.
func NewContextFromCart(g *cart.CartGrain, opts ...ContextOption) *PromotionEvalContext {
	ctx := &PromotionEvalContext{
		Items:             make([]PromotionItem, 0, len(g.Items)),
		CartTotalIncVat:   0,
		TotalItemQuantity: 0,
		Now:               time.Now(),
	}
	if g.TotalPrice != nil {
		ctx.CartTotalIncVat = g.TotalPrice.IncVat
	}
	for _, it := range g.Items {
		category := ""
		if it.Meta != nil {
			category = it.Meta.Category
		}
		ctx.Items = append(ctx.Items, PromotionItem{
			SKU:         it.Sku,
			Quantity:    it.Quantity,
			Category:    strings.ToLower(category),
			PriceIncVat: it.Price.IncVat,
		})
		ctx.TotalItemQuantity += it.Quantity
	}
	for _, o := range opts {
		o(ctx)
	}
	return ctx
}

// PromotionService evaluates PromotionRules against a PromotionEvalContext.
type PromotionService struct {
	tracer Tracer
}

// NewPromotionService constructs a PromotionService with an optional tracer.
func NewPromotionService(t Tracer) *PromotionService {
	if t == nil {
		t = NoopTracer{}
	}
	return &PromotionService{tracer: t}
}

// EvaluationResult holds the outcome of evaluating a single rule.
type EvaluationResult struct {
	Rule           PromotionRule
	Applicable     bool
	FailedReason   string
	MatchedActions []Action
}

// EvaluateRule determines if a single PromotionRule applies to the provided context.
// Returns an EvaluationResult with applicability and actions (if applicable).
func (s *PromotionService) EvaluateRule(rule PromotionRule, ctx *PromotionEvalContext) EvaluationResult {
	s.tracer.Trace("rule_start", map[string]any{
		"rule_id":   rule.ID,
		"priority":  rule.Priority,
		"status":    rule.Status,
		"startDate": rule.StartDate,
		"endDate":   rule.EndDate,
	})
	// Status gate
	now := ctx.Now
	switch rule.Status {
	case StatusInactive, StatusExpired:
		s.tracer.Trace("rule_skip_status", map[string]any{"rule_id": rule.ID, "status": rule.Status})
		return EvaluationResult{Rule: rule, Applicable: false, FailedReason: "status"}
	case StatusScheduled:
		// Allow scheduled only if current time >= startDate (and within endDate if present)
	}
	// Date window checks (if parseable)
	if rule.StartDate != "" {
		if tStart, err := parseDate(rule.StartDate); err == nil {
			if now.Before(tStart) {
				s.tracer.Trace("rule_skip_before_start", map[string]any{"rule_id": rule.ID})
				return EvaluationResult{Rule: rule, Applicable: false, FailedReason: "before_start"}
			}
		}
	}
	if rule.EndDate != nil && *rule.EndDate != "" {
		if tEnd, err := parseDate(*rule.EndDate); err == nil {
			if now.After(tEnd.Add(23*time.Hour + 59*time.Minute + 59*time.Second)) { // inclusive day
				s.tracer.Trace("rule_skip_after_end", map[string]any{"rule_id": rule.ID})
				return EvaluationResult{Rule: rule, Applicable: false, FailedReason: "after_end"}
			}
		}
	}
	// Usage limit
	if rule.UsageLimit != nil && rule.UsageCount >= *rule.UsageLimit {
		s.tracer.Trace("rule_skip_usage_limit", map[string]any{"rule_id": rule.ID, "usageCount": rule.UsageCount, "limit": *rule.UsageLimit})
		return EvaluationResult{Rule: rule, Applicable: false, FailedReason: "usage_limit_exhausted"}
	}

	if !evaluateConditionsTrace(rule.Conditions, ctx, s.tracer, rule.ID) {
		s.tracer.Trace("rule_conditions_failed", map[string]any{"rule_id": rule.ID})
		return EvaluationResult{Rule: rule, Applicable: false, FailedReason: "conditions"}
	}

	s.tracer.Trace("rule_applicable", map[string]any{"rule_id": rule.ID})
	return EvaluationResult{
		Rule:           rule,
		Applicable:     true,
		MatchedActions: rule.Actions,
	}
}

// EvaluateAll returns all applicable promotion actions given a list of rules and context.
// Rules marked Applicable are sorted by Priority (ascending: lower number = higher precedence).
func (s *PromotionService) EvaluateAll(rules []PromotionRule, ctx *PromotionEvalContext) ([]EvaluationResult, []Action) {
	results := make([]EvaluationResult, 0, len(rules))
	for _, r := range rules {
		res := s.EvaluateRule(r, ctx)
		results = append(results, res)
	}
	actions := make([]Action, 0)
	for _, res := range orderedByPriority(results) {
		if res.Applicable {
			s.tracer.Trace("actions_add", map[string]any{
				"rule_id":  res.Rule.ID,
				"count":    len(res.MatchedActions),
				"priority": res.Rule.Priority,
			})
			actions = append(actions, res.MatchedActions...)
		}
	}
	s.tracer.Trace("evaluation_complete", map[string]any{
		"rules_total": len(rules),
		"actions":     len(actions),
	})
	return results, actions
}

// orderedByPriority returns results sorted by PromotionRule.Priority ascending (stable).
func orderedByPriority(in []EvaluationResult) []EvaluationResult {
	out := make([]EvaluationResult, len(in))
	copy(out, in)
	for i := 1; i < len(out); i++ {
		j := i
		for j > 0 && out[j-1].Rule.Priority > out[j].Rule.Priority {
			out[j-1], out[j] = out[j], out[j-1]
			j--
		}
	}
	return out
}

// ----------------------------
// Condition evaluation (with tracing)
// ----------------------------

func evaluateConditionsTrace(conds Conditions, ctx *PromotionEvalContext, t Tracer, ruleID string) bool {
	for idx, c := range conds {
		if !evaluateConditionTrace(c, ctx, t, ruleID, fmt.Sprintf("root[%d]", idx)) {
			return false
		}
	}
	return true
}

func evaluateConditionTrace(c Condition, ctx *PromotionEvalContext, t Tracer, ruleID, path string) bool {
	if grp, ok := c.(ConditionGroup); ok {
		return evaluateGroupTrace(grp, ctx, t, ruleID, path)
	}
	bc, ok := c.(BaseCondition)
	if !ok {
		t.Trace("cond_invalid_type", map[string]any{"rule_id": ruleID, "path": path})
		return false
	}
	res := evaluateBaseCondition(bc, ctx)
	t.Trace("cond_base", map[string]any{
		"rule_id": ruleID,
		"path":    path,
		"type":    bc.Type,
		"op":      bc.Operator,
		"value":   bc.Value.String(),
		"result":  res,
	})
	return res
}

func evaluateGroupTrace(g ConditionGroup, ctx *PromotionEvalContext, t Tracer, ruleID, path string) bool {
	op := normalizeLogicOperator(string(g.Operator))
	if len(g.Conditions) == 0 {
		t.Trace("cond_group_empty", map[string]any{"rule_id": ruleID, "path": path})
		return true
	}
	if op == string(LogicAND) {
		for i, child := range g.Conditions {
			if !evaluateConditionTrace(child, ctx, t, ruleID, path+fmt.Sprintf(".AND[%d]", i)) {
				t.Trace("cond_group_and_fail", map[string]any{"rule_id": ruleID, "path": path})
				return false
			}
		}
		t.Trace("cond_group_and_pass", map[string]any{"rule_id": ruleID, "path": path})
		return true
	}
	for i, child := range g.Conditions {
		if evaluateConditionTrace(child, ctx, t, ruleID, path+fmt.Sprintf(".OR[%d]", i)) {
			t.Trace("cond_group_or_pass", map[string]any{"rule_id": ruleID, "path": path})
			return true
		}
	}
	t.Trace("cond_group_or_fail", map[string]any{"rule_id": ruleID, "path": path})
	return false
}

// Fallback non-traced evaluation (used internally by traced path)
func evaluateConditions(conds Conditions, ctx *PromotionEvalContext) bool {
	for _, c := range conds {
		if !evaluateCondition(c, ctx) {
			return false
		}
	}
	return true
}

func evaluateCondition(c Condition, ctx *PromotionEvalContext) bool {
	if grp, ok := c.(ConditionGroup); ok {
		return evaluateGroup(grp, ctx)
	}
	bc, ok := c.(BaseCondition)
	if !ok {
		return false
	}
	return evaluateBaseCondition(bc, ctx)
}

func evaluateGroup(g ConditionGroup, ctx *PromotionEvalContext) bool {
	op := normalizeLogicOperator(string(g.Operator))
	if len(g.Conditions) == 0 {
		return true
	}
	if op == string(LogicAND) {
		for _, child := range g.Conditions {
			if !evaluateCondition(child, ctx) {
				return false
			}
		}
		return true
	}
	for _, child := range g.Conditions {
		if evaluateCondition(child, ctx) {
			return true
		}
	}
	return false
}

func evaluateBaseCondition(b BaseCondition, ctx *PromotionEvalContext) bool {
	switch b.Type {
	case CondCartTotal:
		return evalNumberCompare(float64(ctx.CartTotalIncVat), b)
	case CondItemQuantity:
		return evalNumberCompare(float64(ctx.TotalItemQuantity), b)
	case CondCustomerSegment:
		return evalStringCompare(ctx.CustomerSegment, b)
	case CondProductCategory:
		return evalAnyItemMatch(func(it PromotionItem) bool {
			return evalValueAgainstTarget(strings.ToLower(it.Category), b)
		}, b, ctx)
	case CondProductID:
		return evalAnyItemMatch(func(it PromotionItem) bool {
			return evalValueAgainstTarget(strings.ToLower(it.SKU), b)
		}, b, ctx)
	case CondCustomerLifetime:
		return evalNumberCompare(ctx.CustomerLifetimeValue, b)
	case CondOrderCount:
		return evalNumberCompare(float64(ctx.OrderCount), b)
	case CondDateRange:
		return evalDateRange(ctx.Now, b)
	case CondDayOfWeek:
		return evalDayOfWeek(ctx.Now, b)
	case CondTimeOfDay:
		return evalTimeOfDay(ctx.Now, b)
	default:
		return false
	}
}

func evalAnyItemMatch(pred func(PromotionItem) bool, b BaseCondition, ctx *PromotionEvalContext) bool {
	if slices.ContainsFunc(ctx.Items, pred) {
		return true
	}
	switch normalizeOperator(string(b.Operator)) {
	case string(OpNotIn), string(OpNotContains):
		return true
	}
	return false
}

// ----------------------------
// Primitive evaluators
// ----------------------------

func evalNumberCompare(target float64, b BaseCondition) bool {
	op := normalizeOperator(string(b.Operator))
	cond, ok := b.Value.AsFloat64()
	if !ok {
		if list, okL := b.Value.AsFloat64Slice(); okL && (op == string(OpIn) || op == string(OpNotIn)) {
			found := sliceFloatContains(list, target)
			if op == string(OpIn) {
				return found
			}
			return !found
		}
		return false
	}
	switch op {
	case string(OpEquals):
		return target == cond
	case string(OpNotEquals):
		return target != cond
	case string(OpGreaterThan):
		return target > cond
	case string(OpLessThan):
		return target < cond
	case string(OpGreaterOrEqual):
		return target >= cond
	case string(OpLessOrEqual):
		return target <= cond
	default:
		return false
	}
}

func evalStringCompare(target string, b BaseCondition) bool {
	op := normalizeOperator(string(b.Operator))
	targetLower := strings.ToLower(target)

	if s, ok := b.Value.AsString(); ok {
		condLower := strings.ToLower(s)
		switch op {
		case string(OpEquals):
			return targetLower == condLower
		case string(OpNotEquals):
			return targetLower != condLower
		case string(OpContains):
			return strings.Contains(targetLower, condLower)
		case string(OpNotContains):
			return !strings.Contains(targetLower, condLower)
		}
	}

	if arr, ok := b.Value.AsStringSlice(); ok {
		switch op {
		case string(OpIn):
			for _, v := range arr {
				if targetLower == strings.ToLower(v) {
					return true
				}
			}
			return false
		case string(OpNotIn):
			for _, v := range arr {
				if targetLower == strings.ToLower(v) {
					return false
				}
			}
			return true
		case string(OpContains):
			for _, v := range arr {
				if strings.Contains(targetLower, strings.ToLower(v)) {
					return true
				}
			}
			return false
		case string(OpNotContains):
			for _, v := range arr {
				if strings.Contains(targetLower, strings.ToLower(v)) {
					return false
				}
			}
			return true
		}
	}

	return false
}

func evalValueAgainstTarget(target string, b BaseCondition) bool {
	op := normalizeOperator(string(b.Operator))
	tLower := strings.ToLower(target)

	if s, ok := b.Value.AsString(); ok {
		vLower := strings.ToLower(s)
		switch op {
		case string(OpEquals):
			return tLower == vLower
		case string(OpNotEquals):
			return tLower != vLower
		case string(OpContains):
			return strings.Contains(tLower, vLower)
		case string(OpNotContains):
			return !strings.Contains(tLower, vLower)
		case string(OpIn):
			return tLower == vLower
		case string(OpNotIn):
			return tLower != vLower
		}
	}

	if list, ok := b.Value.AsStringSlice(); ok {
		found := false
		for _, v := range list {
			if tLower == strings.ToLower(v) {
				found = true
				break
			}
		}
		switch op {
		case string(OpIn):
			return found
		case string(OpNotIn):
			return !found
		case string(OpContains):
			for _, v := range list {
				if strings.Contains(tLower, strings.ToLower(v)) {
					return true
				}
			}
			return false
		case string(OpNotContains):
			for _, v := range list {
				if strings.Contains(tLower, strings.ToLower(v)) {
					return false
				}
			}
			return true
		}
	}

	return false
}

func evalDateRange(now time.Time, b BaseCondition) bool {
	var start, end time.Time
	if ss, ok := b.Value.AsStringSlice(); ok && len(ss) == 2 {
		t0, e0 := parseDate(ss[0])
		t1, e1 := parseDate(ss[1])
		if e0 != nil || e1 != nil {
			return false
		}
		start, end = t0, t1
	} else if s, ok := b.Value.AsString(); ok {
		parts := strings.Split(s, "..")
		if len(parts) != 2 {
			return false
		}
		t0, e0 := parseDate(parts[0])
		t1, e1 := parseDate(parts[1])
		if e0 != nil || e1 != nil {
			return false
		}
		start, end = t0, t1
	} else {
		return false
	}
	endInclusive := end.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
	return (now.Equal(start) || now.After(start)) && (now.Equal(endInclusive) || now.Before(endInclusive))
}

func evalDayOfWeek(now time.Time, b BaseCondition) bool {
	dow := int(now.Weekday())
	allowed := make(map[int]struct{})
	if arr, ok := b.Value.AsStringSlice(); ok {
		for _, v := range arr {
			if idx, ok := parseDayOfWeek(v); ok {
				allowed[idx] = struct{}{}
			}
		}
	} else if s, ok := b.Value.AsString(); ok {
		for _, part := range strings.Split(s, "|") {
			if idx, ok := parseDayOfWeek(strings.TrimSpace(part)); ok {
				allowed[idx] = struct{}{}
			}
		}
	} else {
		return false
	}

	if len(allowed) == 0 {
		return false
	}
	op := normalizeOperator(string(b.Operator))
	_, present := allowed[dow]
	if op == string(OpIn) || op == string(OpEquals) || op == "" {
		return present
	}
	if op == string(OpNotIn) || op == string(OpNotEquals) {
		return !present
	}
	return present
}

func evalTimeOfDay(now time.Time, b BaseCondition) bool {
	var startMin, endMin int
	if arr, ok := b.Value.AsStringSlice(); ok && len(arr) == 2 {
		s0, e0 := parseClock(arr[0])
		s1, e1 := parseClock(arr[1])
		if e0 != nil || e1 != nil {
			return false
		}
		startMin, endMin = s0, s1
	} else if s, ok := b.Value.AsString(); ok {
		parts := strings.Split(s, "-")
		if len(parts) != 2 {
			return false
		}
		s0, e0 := parseClock(parts[0])
		s1, e1 := parseClock(parts[1])
		if e0 != nil || e1 != nil {
			return false
		}
		startMin, endMin = s0, s1
	} else {
		return false
	}
	todayMin := now.Hour()*60 + now.Minute()
	if startMin > endMin {
		return todayMin >= startMin || todayMin <= endMin
	}
	return todayMin >= startMin && todayMin <= endMin
}

// ----------------------------
// Parsing helpers
// ----------------------------

func parseDate(s string) (time.Time, error) {
	layouts := []string{
		"2006-01-02",
		time.RFC3339,
		"2006-01-02T15:04:05Z07:00",
	}
	for _, l := range layouts {
		if t, err := time.Parse(l, strings.TrimSpace(s)); err == nil {
			return t, nil
		}
	}
	return time.Time{}, errInvalidTimeFormat
}

func parseDayOfWeek(s string) (int, bool) {
	sl := strings.ToLower(strings.TrimSpace(s))
	switch sl {
	case "sun", "sunday", "0":
		return 0, true
	case "mon", "monday", "1":
		return 1, true
	case "tue", "tues", "tuesday", "2":
		return 2, true
	case "wed", "weds", "wednesday", "3":
		return 3, true
	case "thu", "thur", "thurs", "thursday", "4":
		return 4, true
	case "fri", "friday", "5":
		return 5, true
	case "sat", "saturday", "6":
		return 6, true
	default:
		return 0, false
	}
}

func parseClock(s string) (int, error) {
	s = strings.TrimSpace(s)
	parts := strings.Split(s, ":")
	if len(parts) != 2 {
		return 0, errInvalidTimeFormat
	}
	h, err := parsePositiveInt(parts[0])
	if err != nil {
		return 0, err
	}
	m, err := parsePositiveInt(parts[1])
	if err != nil {
		return 0, err
	}
	if h < 0 || h > 23 || m < 0 || m > 59 {
		return 0, errInvalidTimeFormat
	}
	return h*60 + m, nil
}

func parsePositiveInt(s string) (int, error) {
	n := 0
	for _, r := range s {
		if r < '0' || r > '9' {
			return 0, errInvalidTimeFormat
		}
		n = n*10 + int(r-'0')
	}
	return n, nil
}

func normalizeOperator(op string) string {
	o := strings.ToLower(strings.TrimSpace(op))
	switch o {
	case "=", "equals", "eq":
		return string(OpEquals)
	case "!=", "not_equals", "neq":
		return string(OpNotEquals)
	case ">", "greater_than", "gt":
		return string(OpGreaterThan)
	case "<", "less_than", "lt":
		return string(OpLessThan)
	case ">=", "greater_or_equal", "ge", "gte":
		return string(OpGreaterOrEqual)
	case "<=", "less_or_equal", "le", "lte":
		return string(OpLessOrEqual)
	case "contains":
		return string(OpContains)
	case "not_contains":
		return string(OpNotContains)
	case "in":
		return string(OpIn)
	case "not_in":
		return string(OpNotIn)
	default:
		return o
	}
}

func normalizeLogicOperator(op string) string {
	o := strings.ToLower(strings.TrimSpace(op))
	switch o {
	case "&&", "and":
		return string(LogicAND)
	case "||", "or":
		return string(LogicOR)
	default:
		return o
	}
}

func sliceFloatContains(list []float64, v float64) bool {
	return slices.Contains(list, v)
}

// ----------------------------
// Potential extension hooks
// ----------------------------
//
// Future ideas:
//   - Conflict resolution strategies (e.g., best discount wins, stackable tags)
//   - Action transformation (e.g., applying tiered logic or bundles carefully)
//   - Recording evaluation traces for debugging / analytics.
//   - Tracing instrumentation for condition evaluation.
//
// These can be integrated by adding strategy interfaces or injecting evaluators
// into PromotionService.
//
// ----------------------------
// End of file
// ----------------------------
