package routing

import (
	"sort"
	"strings"

	"chat-switchboard/models"
)

// Evaluator processes routing policies to produce a ranked candidate list.
type Evaluator struct{}

// NewEvaluator creates a new routing evaluator.
func NewEvaluator() *Evaluator {
	return &Evaluator{}
}

// Evaluate applies active policies to the available candidates and returns
// a ranked list. Candidates are filtered and reordered based on policy rules
// and health status. The first candidate in the result is the preferred choice.
//
// Policies are evaluated in priority order (lower priority number = first).
// The first matching policy wins — subsequent policies of the same type are skipped.
func (e *Evaluator) Evaluate(ctx *Context, policies []Policy, candidates []Candidate) ([]Candidate, *Decision) {
	if len(candidates) == 0 {
		return nil, nil
	}

	// Start with all candidates
	result := make([]Candidate, len(candidates))
	copy(result, candidates)

	var decision Decision
	decision.Candidates = len(candidates)

	// Sort policies by priority
	sorted := make([]Policy, len(policies))
	copy(sorted, policies)
	sort.Slice(sorted, func(i, j int) bool {
		return sorted[i].Priority < sorted[j].Priority
	})

	// Evaluate policies in priority order
	appliedTypes := map[PolicyType]bool{}
	for _, p := range sorted {
		if !p.IsActive {
			continue
		}
		// Skip if a policy of this type already matched
		if appliedTypes[p.Type] {
			continue
		}
		// Scope check: global applies to all; team only to matching teams
		if p.Scope == "team" && p.TeamID != nil {
			if !contains(ctx.TeamIDs, *p.TeamID) {
				continue
			}
		}

		matched := false
		switch p.Type {
		case PolicyProviderPrefer:
			result, matched = applyProviderPrefer(result, p.Config)
		case PolicyTeamRoute:
			result, matched = applyTeamRoute(result, p.Config)
		case PolicyCostLimit:
			result, matched = applyCostLimit(result, p.Config, ctx)
		case PolicyModelAlias:
			result, matched = applyModelAlias(result, p.Config, ctx)
		case PolicyCapabilityMatch:
			result, matched = applyCapabilityMatch(result, p.Config, ctx)
		}

		if matched {
			appliedTypes[p.Type] = true
			decision.PolicyID = p.ID
			decision.PolicyName = p.Name
			decision.PolicyType = string(p.Type)
		}
	}

	// Global health-aware sorting: healthy > degraded > unknown > down.
	// Also skip "down" providers if any policy requested it, or by default
	// when there are healthy alternatives.
	skipDown := anyPolicySkipsDown(sorted)
	result = sortByHealth(result, ctx.HealthStatus, skipDown)

	if len(result) > 0 {
		decision.Selected = result[0].ConfigID
	}

	return result, &decision
}

// ── Policy Implementations ──────────────────

// applyProviderPrefer reorders candidates to match the preferred provider list.
// Providers not in the preferred list are appended after, in original order.
func applyProviderPrefer(candidates []Candidate, cfg PolicyConfig) ([]Candidate, bool) {
	if len(cfg.PreferredProviders) == 0 {
		return candidates, false
	}

	// Build index: configID → position in preferred list
	rank := make(map[string]int, len(cfg.PreferredProviders))
	for i, id := range cfg.PreferredProviders {
		rank[id] = i
	}

	preferred := make([]Candidate, 0, len(candidates))
	rest := make([]Candidate, 0)

	for _, c := range candidates {
		if _, ok := rank[c.ConfigID]; ok {
			c.Reason = "preferred provider"
			preferred = append(preferred, c)
		} else {
			c.Reason = "fallback"
			rest = append(rest, c)
		}
	}

	// Sort preferred by their order in the preference list
	sort.Slice(preferred, func(i, j int) bool {
		return rank[preferred[i].ConfigID] < rank[preferred[j].ConfigID]
	})

	return append(preferred, rest...), len(preferred) > 0
}

// applyTeamRoute filters candidates to only allowed providers for this team.
func applyTeamRoute(candidates []Candidate, cfg PolicyConfig) ([]Candidate, bool) {
	if len(cfg.AllowedProviders) == 0 {
		return candidates, false
	}

	allowed := make(map[string]bool, len(cfg.AllowedProviders))
	for _, id := range cfg.AllowedProviders {
		allowed[id] = true
	}

	filtered := make([]Candidate, 0, len(candidates))
	for _, c := range candidates {
		if allowed[c.ConfigID] {
			c.Reason = "team-allowed provider"
			filtered = append(filtered, c)
		}
	}

	// Only apply if at least one allowed provider is available
	if len(filtered) > 0 {
		return filtered, true
	}
	return candidates, false
}

// applyCostLimit removes candidates whose estimated cost exceeds the limit.
func applyCostLimit(candidates []Candidate, cfg PolicyConfig, ctx *Context) ([]Candidate, bool) {
	if cfg.MaxCostUSD == nil || *cfg.MaxCostUSD <= 0 {
		return candidates, false
	}

	// For cost estimation we'd need request token count, which isn't
	// available at routing time. Instead, filter out providers with
	// pricing above the threshold per-million-tokens as a heuristic.
	// A 4K-token request at $10/M = $0.04, so if max is $0.10 we'd
	// filter out providers above ~$25/M. For now, just ensure pricing
	// exists and tag the decision.
	maxPerM := *cfg.MaxCostUSD * 1_000_000 / 4096 // rough heuristic: 4K tokens avg

	filtered := make([]Candidate, 0, len(candidates))
	for _, c := range candidates {
		key := c.ConfigID + ":" + c.Model
		if pricing, ok := ctx.Pricing[key]; ok && pricing != nil {
			if pricing.InputPerM > maxPerM {
				continue // too expensive
			}
		}
		filtered = append(filtered, c)
	}

	if len(filtered) > 0 && len(filtered) < len(candidates) {
		return filtered, true
	}
	return candidates, false
}

// applyModelAlias replaces the model in matching candidates.
func applyModelAlias(candidates []Candidate, cfg PolicyConfig, ctx *Context) ([]Candidate, bool) {
	if cfg.AliasFrom == "" || cfg.AliasTo == "" {
		return candidates, false
	}

	// Only apply if the requested model matches the alias source
	if !strings.EqualFold(ctx.Model, cfg.AliasFrom) {
		return candidates, false
	}

	// If a specific provider is targeted, reorder to prefer it
	if cfg.AliasProvider != "" {
		for i, c := range candidates {
			if c.ConfigID == cfg.AliasProvider {
				candidates[i].Model = cfg.AliasTo
				candidates[i].Reason = "model alias: " + cfg.AliasFrom + " → " + cfg.AliasTo
				// Move to front
				reordered := make([]Candidate, 0, len(candidates))
				reordered = append(reordered, candidates[i])
				reordered = append(reordered, candidates[:i]...)
				reordered = append(reordered, candidates[i+1:]...)
				return reordered, true
			}
		}
	}

	// No specific provider — just rewrite the model on all candidates
	for i := range candidates {
		candidates[i].Model = cfg.AliasTo
		candidates[i].Reason = "model alias: " + cfg.AliasFrom + " → " + cfg.AliasTo
	}
	return candidates, true
}

// applyCapabilityMatch filters candidates to those whose model has all
// required capabilities. Optionally sorts remaining candidates by cheapest
// output price when PreferCheapest is set.
//
// Required capabilities map to ModelCapabilities boolean fields:
//   "tool_calling", "vision", "thinking", "reasoning",
//   "code_optimized", "web_search", "streaming"
func applyCapabilityMatch(candidates []Candidate, cfg PolicyConfig, ctx *Context) ([]Candidate, bool) {
	if len(cfg.RequiredCaps) == 0 {
		return candidates, false
	}
	if ctx.Capabilities == nil {
		return candidates, false
	}

	filtered := make([]Candidate, 0, len(candidates))
	for _, c := range candidates {
		key := c.ConfigID + ":" + c.Model
		caps, ok := ctx.Capabilities[key]
		if !ok {
			continue // no capability data — skip (conservative)
		}
		if hasAllCaps(caps, cfg.RequiredCaps) {
			c.Reason = "capability match"
			filtered = append(filtered, c)
		}
	}

	if len(filtered) == 0 {
		// No candidates match — fall back to original set rather than
		// returning empty (better to try than fail immediately).
		return candidates, false
	}

	// Sort by cheapest output price if requested
	if cfg.PreferCheapest && ctx.Pricing != nil {
		sort.SliceStable(filtered, func(i, j int) bool {
			ki := filtered[i].ConfigID + ":" + filtered[i].Model
			kj := filtered[j].ConfigID + ":" + filtered[j].Model
			pi, oki := ctx.Pricing[ki]
			pj, okj := ctx.Pricing[kj]
			if !oki || pi == nil {
				return false // unknown price sorts last
			}
			if !okj || pj == nil {
				return true
			}
			return pi.OutputPerM < pj.OutputPerM
		})
	}

	return filtered, true
}

// hasAllCaps checks whether a ModelCapabilities struct has all requested
// capability flags set. Unrecognized capability names are ignored.
func hasAllCaps(caps *models.ModelCapabilities, required []string) bool {
	if caps == nil {
		return false
	}
	for _, req := range required {
		switch req {
		case "tool_calling":
			if !caps.ToolCalling {
				return false
			}
		case "vision":
			if !caps.Vision {
				return false
			}
		case "thinking":
			if !caps.Thinking {
				return false
			}
		case "reasoning":
			if !caps.Reasoning {
				return false
			}
		case "code_optimized":
			if !caps.CodeOptimized {
				return false
			}
		case "web_search":
			if !caps.WebSearch {
				return false
			}
		case "streaming":
			if !caps.Streaming {
				return false
			}
		// Unrecognized caps are ignored (forward-compatible)
		}
	}
	return true
}

// ── Health Sorting ──────────────────────────

var statusOrder = map[models.ProviderStatus]int{
	models.StatusHealthy:  0,
	models.StatusDegraded: 1,
	models.StatusUnknown:  2,
	models.StatusDown:     3,
}

func sortByHealth(candidates []Candidate, health map[string]models.ProviderStatus, skipDown bool) []Candidate {
	// Annotate status
	for i, c := range candidates {
		if s, ok := health[c.ConfigID]; ok {
			candidates[i].Status = s
		} else {
			candidates[i].Status = models.StatusUnknown
		}
	}

	// Filter down if requested and alternatives exist
	if skipDown {
		healthy := make([]Candidate, 0, len(candidates))
		for _, c := range candidates {
			if c.Status != models.StatusDown {
				healthy = append(healthy, c)
			}
		}
		if len(healthy) > 0 {
			candidates = healthy
		}
		// If all are down, keep them — better to try than fail immediately.
	}

	// Stable sort by health status
	sort.SliceStable(candidates, func(i, j int) bool {
		return statusOrder[candidates[i].Status] < statusOrder[candidates[j].Status]
	})

	return candidates
}

// ── Helpers ─────────────────────────────────

func contains(slice []string, item string) bool {
	for _, s := range slice {
		if s == item {
			return true
		}
	}
	return false
}

func anyPolicySkipsDown(policies []Policy) bool {
	for _, p := range policies {
		if p.IsActive && p.Config.SkipDown {
			return true
		}
	}
	return false
}
