"""Routing decision transparency engine.

Converts internal :class:`~llmhost.router.models.RoutingDecision` data into
clear, human-readable explanations for the dashboard and streaming headers.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any

from pydantic import BaseModel, Field

if TYPE_CHECKING:
    from llmhosts.router.models import AlternativeRoute, RoutingDecision

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Pydantic v2 response models
# ---------------------------------------------------------------------------

_TIER_NAMES: dict[str, str] = {
    "rule": "Rule-Based",
    "knn": "kNN",
    "modernbert": "ModernBERT",
    "qwen": "Qwen",
}


class TierInfo(BaseModel):
    """Human-readable tier descriptor."""

    name: str  # "Rule-Based", "kNN", "ModernBERT", "Qwen"
    tier_number: int  # 0-3


class FactorDetail(BaseModel):
    """A single scored factor that contributed to the routing decision."""

    score: float
    detail: str
    weight: float = 1.0


class AlternativeDetail(BaseModel):
    """An alternative route that was considered but not selected."""

    model: str
    provider: str
    estimated_cost: str
    why_not_chosen: str


class CostBreakdown(BaseModel):
    """Actual vs. cloud-equivalent cost breakdown."""

    actual: float
    cloud_equivalent: float
    saved: float
    currency: str = "USD"


class TransparentDecision(BaseModel):
    """Full transparent routing explanation exposed via the dashboard API.

    Contains everything a user needs to understand *why* the router
    picked a particular backend and model for their request.
    """

    summary: str
    tier: TierInfo
    confidence: float = Field(ge=0.0, le=1.0)
    reasoning_steps: list[str] = Field(default_factory=list)
    factors: dict[str, FactorDetail] = Field(default_factory=dict)
    alternatives: list[AlternativeDetail] = Field(default_factory=list)
    cost_breakdown: CostBreakdown


# ---------------------------------------------------------------------------
# Transparency Engine
# ---------------------------------------------------------------------------

# Map tier identifiers to numeric tier numbers
_TIER_NUMBERS: dict[str, int] = {
    "rule": 0,
    "knn": 1,
    "modernbert": 2,
    "qwen": 3,
}


class TransparencyEngine:
    """Makes routing decisions human-readable and debuggable.

    Converts internal :class:`RoutingDecision` data into clear,
    understandable explanations for the dashboard.
    """

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def format_decision(
        self,
        decision: RoutingDecision,
        *,
        model_requested: str = "",
        cloud_equivalent_cost: float = 0.0,
    ) -> TransparentDecision:
        """Convert a :class:`RoutingDecision` to a human-readable explanation.

        Parameters
        ----------
        decision:
            The raw routing decision from the router engine.
        model_requested:
            The model originally requested by the caller (e.g. ``"gpt-4o-mini"``).
        cloud_equivalent_cost:
            Estimated cost if this request had been sent to the cloud.

        Returns
        -------
        TransparentDecision
            Fully populated transparency object ready for JSON serialisation.
        """
        tier_info = self._build_tier_info(decision.tier)
        actual_cost = self._extract_actual_cost(decision)
        saved = round(cloud_equivalent_cost - actual_cost, 6)

        cost_breakdown = CostBreakdown(
            actual=actual_cost,
            cloud_equivalent=cloud_equivalent_cost,
            saved=max(saved, 0.0),
        )

        factors = self._build_factors(decision)
        alternatives = self._build_alternatives(decision.alternatives)
        reasoning_steps = self._build_reasoning_steps(
            decision=decision,
            model_requested=model_requested,
            actual_cost=actual_cost,
            cloud_equivalent_cost=cloud_equivalent_cost,
        )
        summary = self._build_summary(decision, actual_cost)

        return TransparentDecision(
            summary=summary,
            tier=tier_info,
            confidence=min(max(decision.confidence, 0.0), 1.0),
            reasoning_steps=reasoning_steps,
            factors=factors,
            alternatives=alternatives,
            cost_breakdown=cost_breakdown,
        )

    def format_streaming_header(self, decision: RoutingDecision) -> str:
        """One-line summary suitable for an SSE stream header comment.

        Returns a compact string like::

            [Rule-Based | llama3.2:8b via ollama | confidence=0.95 | local]
        """
        tier_name = _TIER_NAMES.get(decision.tier, decision.tier)
        locality = "local" if self._is_local(decision.backend_type) else "cloud"
        return (
            f"[{tier_name} | {decision.model} via {decision.backend_type}"
            f" | confidence={decision.confidence:.2f} | {locality}]"
        )

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _build_tier_info(tier: str) -> TierInfo:
        """Map a tier identifier to a :class:`TierInfo`."""
        return TierInfo(
            name=_TIER_NAMES.get(tier, tier.title()),
            tier_number=_TIER_NUMBERS.get(tier, -1),
        )

    @staticmethod
    def _is_local(backend_type: str) -> bool:
        """Return ``True`` when the backend is a local inference engine."""
        return backend_type.lower() in {"ollama", "vllm", "exo", "local"}

    @staticmethod
    def _extract_actual_cost(decision: RoutingDecision) -> float:
        """Pull the actual cost from the decision's factors dict, default 0."""
        cost_factor: Any = decision.factors.get("cost", {})
        if isinstance(cost_factor, dict):
            return float(cost_factor.get("actual", 0.0))
        return 0.0

    def _build_factors(self, decision: RoutingDecision) -> dict[str, FactorDetail]:
        """Normalise raw factor dicts into :class:`FactorDetail` objects."""
        result: dict[str, FactorDetail] = {}
        raw = decision.factors

        # Cost factor
        cost_raw = raw.get("cost")
        if isinstance(cost_raw, dict) and cost_raw:
            result["cost"] = FactorDetail(
                score=float(cost_raw.get("score", 0.0)),
                detail=str(cost_raw.get("detail", self._cost_detail(decision))),
                weight=float(cost_raw.get("weight", 1.0)),
            )
        else:
            result["cost"] = FactorDetail(
                score=1.0 if self._is_local(decision.backend_type) else 0.5,
                detail=self._cost_detail(decision),
            )

        # Quality factor
        quality_raw = raw.get("quality")
        if isinstance(quality_raw, dict) and quality_raw:
            result["quality"] = FactorDetail(
                score=float(quality_raw.get("score", 0.0)),
                detail=str(quality_raw.get("detail", "No quality data")),
                weight=float(quality_raw.get("weight", 1.0)),
            )
        else:
            result["quality"] = FactorDetail(score=0.85, detail="Sufficient for this task")

        # Latency factor
        latency_raw = raw.get("latency")
        if isinstance(latency_raw, dict) and latency_raw:
            result["latency"] = FactorDetail(
                score=float(latency_raw.get("score", 0.0)),
                detail=str(latency_raw.get("detail", f"{decision.latency_ms:.0f}ms")),
                weight=float(latency_raw.get("weight", 1.0)),
            )
        else:
            latency_score = self._latency_score(decision.latency_ms)
            label = self._latency_label(decision.latency_ms)
            result["latency"] = FactorDetail(
                score=latency_score,
                detail=f"{decision.latency_ms:.0f}ms ({label})",
            )

        # Privacy factor
        privacy_raw = raw.get("privacy")
        if isinstance(privacy_raw, dict) and privacy_raw:
            result["privacy"] = FactorDetail(
                score=float(privacy_raw.get("score", 0.0)),
                detail=str(privacy_raw.get("detail", "")),
                weight=float(privacy_raw.get("weight", 1.0)),
            )
        else:
            is_local = self._is_local(decision.backend_type)
            result["privacy"] = FactorDetail(
                score=1.0 if is_local else 0.3,
                detail="Data stays local" if is_local else "Data sent to cloud provider",
            )

        # Pass through any extra factors from the router
        known_keys = {"cost", "quality", "latency", "privacy"}
        for key, val in raw.items():
            if key in known_keys:
                continue
            if isinstance(val, dict):
                result[key] = FactorDetail(
                    score=float(val.get("score", 0.0)),
                    detail=str(val.get("detail", "")),
                    weight=float(val.get("weight", 1.0)),
                )

        return result

    def _cost_detail(self, decision: RoutingDecision) -> str:
        """Build a short cost description string."""
        if self._is_local(decision.backend_type):
            return "Free (local)"
        cost = self._extract_actual_cost(decision)
        if cost > 0:
            return f"${cost:.4f}"
        return "Estimated"

    @staticmethod
    def _latency_score(latency_ms: float) -> float:
        """Score latency on a 0-1 scale (lower latency = higher score)."""
        if latency_ms <= 0:
            return 0.5  # unknown
        if latency_ms < 100:
            return 1.0
        if latency_ms < 300:
            return 0.9
        if latency_ms < 1000:
            return 0.7
        if latency_ms < 3000:
            return 0.4
        return 0.2

    @staticmethod
    def _latency_label(latency_ms: float) -> str:
        """Human label for a latency measurement."""
        if latency_ms <= 0:
            return "unknown"
        if latency_ms < 100:
            return "excellent"
        if latency_ms < 300:
            return "good"
        if latency_ms < 1000:
            return "moderate"
        if latency_ms < 3000:
            return "slow"
        return "very slow"

    @staticmethod
    def _build_alternatives(alternatives: list[AlternativeRoute]) -> list[AlternativeDetail]:
        """Convert router :class:`AlternativeRoute` objects to dashboard-friendly form."""
        result: list[AlternativeDetail] = []
        for alt in alternatives:
            result.append(
                AlternativeDetail(
                    model=alt.model,
                    provider=alt.backend_type,
                    estimated_cost=f"${alt.score:.4f}" if alt.score > 0 else "N/A",
                    why_not_chosen=alt.reason_not_chosen,
                )
            )
        return result

    def _build_reasoning_steps(
        self,
        *,
        decision: RoutingDecision,
        model_requested: str,
        actual_cost: float,
        cloud_equivalent_cost: float,
    ) -> list[str]:
        """Assemble numbered human-readable reasoning steps."""
        steps: list[str] = []
        step = 0

        # Step 1: What was requested
        step += 1
        if model_requested:
            steps.append(f"{step}. Model '{model_requested}' requested")
        else:
            steps.append(f"{step}. Request received")

        # Step 2: What was found / resolved
        step += 1
        is_local = self._is_local(decision.backend_type)
        if is_local and model_requested and model_requested != decision.model:
            steps.append(f"{step}. Local equivalent found: {decision.model}")
        elif is_local:
            steps.append(f"{step}. Model available locally: {decision.model}")
        else:
            steps.append(f"{step}. Routed to cloud: {decision.model} via {decision.backend_type}")

        # Step 3: Health / availability
        step += 1
        if decision.latency_ms > 0:
            label = self._latency_label(decision.latency_ms)
            steps.append(f"{step}. Backend is healthy (latency: {decision.latency_ms:.0f}ms avg, {label})")
        else:
            steps.append(f"{step}. Backend is available")

        # Step 4: Cost comparison
        step += 1
        if cloud_equivalent_cost > 0:
            steps.append(f"{step}. Cost: ${actual_cost:.4f} vs ${cloud_equivalent_cost:.4f} cloud estimate")
        else:
            steps.append(f"{step}. Cost: ${actual_cost:.4f}")

        # Step 5: Final decision
        step += 1
        saved = round(cloud_equivalent_cost - actual_cost, 6)
        tier_name = _TIER_NAMES.get(decision.tier, decision.tier)
        if is_local and saved > 0:
            steps.append(f"{step}. Decision: route locally via {tier_name} (saves ${saved:.4f})")
        elif is_local:
            steps.append(f"{step}. Decision: route locally via {tier_name}")
        else:
            steps.append(f"{step}. Decision: route to {decision.backend_type} via {tier_name}")

        # Append raw reasoning from the router if it adds value
        if decision.reasoning and decision.reasoning not in steps:
            step += 1
            steps.append(f"{step}. Router note: {decision.reasoning}")

        return steps

    def _build_summary(self, decision: RoutingDecision, actual_cost: float) -> str:
        """Build a concise one-line summary string."""
        is_local = self._is_local(decision.backend_type)
        if is_local:
            return f"Routed to local {decision.model} (cost: ${actual_cost:.2f})"
        return f"Routed to {decision.model} via {decision.backend_type} (cost: ${actual_cost:.4f})"
