"""
TrustScorer — Composite 0-100 trust score with natural-language explanation.
=============================================================================

Combines existing AgentGuard signals into a single actionable trust score
and a human-readable verdict. No additional LLM calls.

Five dimensions
---------------
  process_trust        Chain structure quality (step count, failure mode)
  consistency_trust    Behavioral self-consistency (SC_OLD behavioral score)
  evidence_trust       Retrieval quality (observation length, search density)
  judge_trust          Judge assessment (judge_label, ptrue_risk if present)
  calibration_trust    Score confidence (distance from decision boundary)

Score mapping
-------------
  80–100  HIGHLY_RELIABLE   — Proceed with confidence
  60–79   LIKELY_RELIABLE   — Proceed with standard caution
  40–59   UNCERTAIN         — Verify before acting on this answer
  20–39   LIKELY_UNRELIABLE — High risk; request human review
  0–19    UNRELIABLE        — Do not use this answer

Quick start::

    from llm_guard.agent_guard import AgentGuard
    from llm_guard.trust_scorer import TrustScorer

    guard = AgentGuard()
    result = guard.score_chain(question, steps, answer)

    ts = TrustScorer()
    trust = ts.score(result)
    print(trust.score)        # e.g. 72
    print(trust.verdict)      # e.g. "LIKELY_RELIABLE"
    print(trust.explanation)  # Human-readable paragraph
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Dict, Optional


@dataclass
class TrustResult:
    """Composite trust assessment for a single chain."""
    score: int                           # 0–100, higher = more trustworthy
    verdict: str                         # HIGHLY_RELIABLE / LIKELY_RELIABLE / UNCERTAIN / LIKELY_UNRELIABLE / UNRELIABLE
    explanation: str                     # Natural-language explanation
    dimension_scores: Dict[str, int] = field(default_factory=dict)  # per-dimension 0-100 scores
    raw_risk: float = 0.0               # original risk_score from ChainTrustResult


class TrustScorer:
    """
    Converts a ChainTrustResult into a 0-100 TrustResult with explanation.

    Parameters
    ----------
    weights : dict, optional
        Per-dimension weights (must sum to 1.0). Defaults to balanced weights.
        Keys: process_trust, consistency_trust, evidence_trust,
              judge_trust, calibration_trust.
    """

    _VERDICTS = [
        (80, "HIGHLY_RELIABLE",   "Proceed with confidence."),
        (60, "LIKELY_RELIABLE",   "Proceed with standard caution."),
        (40, "UNCERTAIN",         "Verify before acting on this answer."),
        (20, "LIKELY_UNRELIABLE", "High risk — request human review."),
        (0,  "UNRELIABLE",        "Do not use this answer."),
    ]

    _DEFAULT_WEIGHTS = {
        "process_trust":     0.25,
        "consistency_trust": 0.30,
        "evidence_trust":    0.20,
        "judge_trust":       0.15,
        "calibration_trust": 0.10,
    }

    _DIMENSION_KEYS = frozenset(_DEFAULT_WEIGHTS)

    def __init__(self, weights: Optional[Dict[str, float]] = None) -> None:
        if weights is not None:
            missing = self._DIMENSION_KEYS - set(weights)
            if missing:
                raise ValueError(
                    f"weights missing required dimension(s): {sorted(missing)}. "
                    f"Required keys: {sorted(self._DIMENSION_KEYS)}"
                )
            total = sum(weights.values())
            if abs(total - 1.0) > 1e-6:
                raise ValueError(f"weights must sum to 1.0, got {total:.4f}")
            self._weights = dict(weights)
        else:
            self._weights = dict(self._DEFAULT_WEIGHTS)

    # ── Dimension scorers (each returns 0–100) ────────────────────────────────

    def _process_trust(self, result) -> int:
        """
        Chain structure quality.
        - failure_mode present → low
        - needs_alert → low
        - step_count 2-5 → optimal (more steps = more complete)
        - step_count 0-1 → suspicious (no retrieval)
        - step_count > 8 → suspicious (looping)
        """
        score = 70  # baseline

        # Step count signal
        # 0: no retrieval (suspicious); 1: minimal; 2-5: optimal;
        # 6-8: neutral (allowed, no bonus/penalty); >8: likely looping
        n = result.step_count
        if n == 0:
            score -= 30
        elif n == 1:
            score -= 10
        elif 2 <= n <= 5:
            score += 15
        elif n > 8:
            score -= 20

        # Failure mode penalty
        if result.failure_mode:
            penalties = {
                "repeated_query": -20,
                "empty_answer":   -35,
                "confident_wrong": -30,
                "long_chain":      -10,
            }
            score += penalties.get(result.failure_mode, -15)

        # needs_alert override
        if result.needs_alert:
            score = min(score, 30)

        return max(0, min(100, score))

    def _consistency_trust(self, result) -> int:
        """
        Behavioral self-consistency (SC_OLD). behavioral_score is a risk score
        [0,1] where lower = more consistent. Convert to 0-100 trust.
        """
        beh_risk = getattr(result, "behavioral_score", None)
        if beh_risk is None:
            return 50  # neutral if missing
        return max(0, min(100, int((1.0 - float(beh_risk)) * 100)))

    def _evidence_trust(self, result) -> int:
        """
        Retrieval/evidence quality from behavioral_components.
        Uses avg_obs_len (longer observations = better retrieval) and
        frac_search (more search steps = more evidence gathering).
        """
        bc = getattr(result, "behavioral_components", {}) or {}
        score = 55  # neutral baseline

        avg_obs_len = bc.get("avg_obs_len", None)
        if avg_obs_len is not None:
            # >200 chars per obs → good retrieval; <50 → very thin
            if avg_obs_len > 300:
                score += 20
            elif avg_obs_len > 150:
                score += 10
            elif avg_obs_len < 50:
                score -= 20

        frac_search = bc.get("frac_search", None)
        if frac_search is not None:
            # 0.5-0.9 fraction of steps are searches → good
            if 0.5 <= frac_search <= 0.9:
                score += 10
            elif frac_search < 0.3:
                score -= 10

        return max(0, min(100, score))

    def _judge_trust(self, result) -> int:
        """
        Judge assessment. Uses judge_label and ptrue_risk from behavioral_components.
        """
        bc = getattr(result, "behavioral_components", {}) or {}
        score = 50  # neutral baseline

        # judge_label signal — only GOOD/BORDERLINE/POOR carry scoring signal.
        # STRUCTURAL and LOCAL are shortcut labels (no quality judgment); treat as None.
        label = getattr(result, "judge_label", None)
        _scored_label = label if label in ("GOOD", "BORDERLINE", "POOR") else None
        if _scored_label == "GOOD":
            score += 35
        elif _scored_label == "BORDERLINE":
            score += 10
        elif _scored_label == "POOR":
            score -= 35

        # ptrue_risk if available
        ptrue_risk = bc.get("ptrue_risk", None)
        if ptrue_risk is not None:
            # ptrue_risk [0,1]: 0 = very likely correct, 1 = very likely wrong
            ptrue_trust = int((1.0 - float(ptrue_risk)) * 100)
            # Blend when a quality label is present; otherwise use ptrue directly
            if _scored_label is not None:
                score = int(0.6 * score + 0.4 * ptrue_trust)
            else:
                score = ptrue_trust

        return max(0, min(100, score))

    def _calibration_trust(self, result) -> int:
        """
        Directional score confidence — how far the risk score is from the 0.5
        boundary, factoring in direction.

        Mapping:
          risk=0.0 → 100 (very confident this chain is correct)
          risk=0.5 → 50  (on the decision boundary, uncertain)
          risk=1.0 → 0   (very confident this chain is wrong)

        Concretely: calibration_trust = int((1.0 - risk) * 100), but computed
        via distance from 0.5 to preserve the symmetric boundary intuition.
        """
        risk = float(getattr(result, "risk_score", 0.5))
        distance = abs(risk - 0.5)  # 0.0 = on boundary; 0.5 = maximally confident
        if risk < 0.5:
            # Low risk → confident correct → high trust
            return max(0, min(100, int(50 + distance * 100)))
        else:
            # High risk → confident wrong → low trust
            return max(0, min(100, int(50 - distance * 100)))

    # ── Verdict and explanation ────────────────────────────────────────────────

    def _verdict(self, score: int) -> tuple:
        for threshold, verdict, action in self._VERDICTS:
            if score >= threshold:
                return verdict, action
        return "UNRELIABLE", "Do not use this answer."

    def _explain(self, result, dims: Dict[str, int], score: int, verdict: str, action: str) -> str:
        parts = []

        # Opening sentence
        parts.append(
            f"Trust score: {score}/100 ({verdict}). {action}"
        )

        # Process trust narrative
        n = result.step_count
        if n == 0:
            parts.append("The agent produced no retrieval steps, which reduces confidence in factual grounding.")
        elif n == 1:
            parts.append(f"The agent completed {n} retrieval step.")
        else:
            parts.append(f"The agent completed {n} retrieval steps.")

        if result.failure_mode:
            mode_desc = {
                "repeated_query":  "A repeated-query failure was detected (the agent looped on the same search).",
                "empty_answer":    "An empty-answer failure was detected (the agent returned no answer).",
                "confident_wrong": "A confident-wrong failure was detected (the agent was highly confident but likely incorrect).",
                "long_chain":      "An unusually long chain was detected, suggesting possible reasoning loops.",
            }
            parts.append(mode_desc.get(result.failure_mode, f"Failure mode detected: {result.failure_mode}."))

        # Consistency narrative
        if dims["consistency_trust"] >= 70:
            parts.append("Behavioral signals indicate high self-consistency across the reasoning chain.")
        elif dims["consistency_trust"] <= 35:
            parts.append("Low behavioral self-consistency — the chain structure suggests unreliable reasoning.")

        # Judge narrative
        label = getattr(result, "judge_label", None)
        if label == "GOOD":
            parts.append("The quality judge rated this chain as GOOD.")
        elif label == "POOR":
            parts.append("The quality judge rated this chain as POOR.")
        elif label == "BORDERLINE":
            parts.append("The quality judge rated this chain as BORDERLINE.")

        return " ".join(parts)

    # ── Public API ─────────────────────────────────────────────────────────────

    def score(self, result) -> TrustResult:
        """
        Convert a ChainTrustResult into a TrustResult (0-100 score + explanation).

        Parameters
        ----------
        result : ChainTrustResult
            Output of AgentGuard.score_chain().

        Returns
        -------
        TrustResult
        """
        dims = {
            "process_trust":     self._process_trust(result),
            "consistency_trust": self._consistency_trust(result),
            "evidence_trust":    self._evidence_trust(result),
            "judge_trust":       self._judge_trust(result),
            "calibration_trust": self._calibration_trust(result),
        }

        composite = sum(
            self._weights[k] * v for k, v in dims.items()
        )
        trust_score = max(0, min(100, round(composite)))

        verdict, action = self._verdict(trust_score)
        explanation = self._explain(result, dims, trust_score, verdict, action)

        return TrustResult(
            score=trust_score,
            verdict=verdict,
            explanation=explanation,
            dimension_scores=dims,
            raw_risk=float(getattr(result, "risk_score", 0.0)),
        )
