"""
DebateVerifier — Multi-Agent Structured Debate Triage
======================================================

When multiple agents produce conflicting answers, a Haiku mediator evaluates
their reasoning and returns a verdict + confidence.  This is a high-precision
TRIAGE signal, not a full-chain AUROC ranker.

Validated performance (exp_debate_verification, HP-200, n_debate=87):
  Precision when mediator overrides Agent 0:  85.4%  (baseline 58.0%)
  Odds ratio:                                  6.01   Fisher exact p < 0.0001
  Mediator calibration (Spearman):            -0.278  p = 0.0001
  J_debate AUROC (J5+agree+debate):           0.763   CI [0.695, 0.832]

Design
------
  Trigger condition: mean pairwise token-F1 between answers < disagree_threshold.
  If agents agree → returns DebateResult(verdict=0, confidence=0.95, routing="trusted").
  If agents disagree → calls Haiku mediator once → returns structured verdict.

  Risk signal: 1 - mediator_confidence  (low confidence = high risk).
  Override signal: verdict != 0 → Agent 0 is likely wrong (precision 85.4%).

Caching
-------
  All mediator responses are cached by default (cache_dir parameter).
  Set cache_dir=None to disable caching.

Quick start
-----------
    from llm_guard import DebateVerifier

    dv = DebateVerifier(api_key="sk-ant-...")
    result = dv.verify(
        question="Who founded the company that made the iPhone?",
        answers=["Steve Jobs", "Tim Cook", "Jony Ive"],
        evidences=["Founded Apple in 1976 with Wozniak", "", ""],
    )
    print(result.verdict)           # 0 (Agent 0 wins) / 1 / 2 / -1 (tie)
    print(result.confidence)        # 0.0 – 1.0
    print(result.risk)              # 1 - confidence
    print(result.routing)           # "trusted" | "uncertain" | "override_agent0"
    print(result.override_agent0)   # True when mediator prefers another agent
Purpose:    When agents disagree (token-F1 below threshold), a Haiku mediator evaluates their reasoning and returns a verdict. Precision 85.4% when overriding Agent 0; odds ratio 6.01 (Fisher p < 0.0001).
Exports:    DebateVerifier, DebateResult
Added:      v0.29.0
Status:     Validated
"""
from __future__ import annotations

import hashlib
import json
import re
import warnings
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional


@dataclass
class DebateResult:
    """
    Result from DebateVerifier.verify().

    verdict         : index of winning agent (0-based); -1 = tie.
    confidence      : mediator's self-reported confidence in [0, 1].
    risk            : 1 - confidence.  Low confidence → high risk.
    routing         : "trusted"        (agents agreed, no debate needed)
                      "uncertain"      (mediator uncertain, confidence < 0.5)
                      "override_agent0" (mediator prefers another agent → agent 0 likely wrong)
    override_agent0 : True when verdict != 0 and debate was triggered.
                      Precision 85.4% for identifying Agent 0 errors (exp_debate_verification).
    debate_triggered: False when agents agreed (disagree_threshold not reached).
    reason          : brief explanation from mediator.
    n_agents        : number of agents compared.
    """
    verdict:          int
    confidence:       float
    risk:             float
    routing:          str
    override_agent0:  bool
    debate_triggered: bool
    reason:           str
    n_agents:         int
    _raw:             Dict = field(default_factory=dict, repr=False)


class DebateVerifier:
    """
    Multi-agent structured debate mediator.

    Parameters
    ----------
    api_key : str, optional
        Anthropic API key.  Falls back to ANTHROPIC_API_KEY env var.
    model : str
        Model used as mediator.  Default: claude-haiku-4-5-20251001.
    disagree_threshold : float
        Mean pairwise token-F1 below this triggers debate.  Default 0.40.
    cache_dir : str or Path, optional
        Directory for caching mediator responses.  Default: ~/.cache/llm_guard/debate.
        Pass None to disable caching.
    uncertain_threshold : float
        Mediator confidence below this → routing="uncertain".  Default 0.50.
    """

    _SYSTEM = """\
You are an impartial mediator evaluating AI agents that gave different answers to the same question.
Determine which answer is most likely correct based on available evidence.

Return ONLY valid JSON (no markdown, no extra text):
{"verdict": 0 or 1 or 2 ... or "tie", "confidence": 0.0-1.0, "reason": "<one sentence>"}

Rules:
- verdict is the 0-based index of the best answer.
- "tie" only when genuinely impossible to choose.
- confidence < 0.5 means you are uncertain.
- Base judgment on factual evidence, not answer length or detail level.
- Do NOT default to the first answer without evidence."""

    def __init__(
        self,
        api_key:              Optional[str]  = None,
        model:                str            = "claude-haiku-4-5-20251001",
        disagree_threshold:   float          = 0.40,
        cache_dir:            Optional[str]  = None,
        uncertain_threshold:  float          = 0.50,
    ) -> None:
        import os
        self._api_key   = api_key or os.environ.get("ANTHROPIC_API_KEY", "")
        self._model     = model
        self._thresh    = disagree_threshold
        self._uncertain = uncertain_threshold
        self._cache_dir: Optional[Path] = (
            Path(cache_dir) if cache_dir is not None
            else Path.home() / ".cache" / "llm_guard" / "debate"
        )
        if self._cache_dir is not None:
            self._cache_dir.mkdir(parents=True, exist_ok=True)

    # ── public API ───────────────────────────────────────────────────────────

    def verify(
        self,
        question:  str,
        answers:   List[str],
        evidences: Optional[List[str]] = None,
    ) -> DebateResult:
        """
        Run a structured debate on conflicting agent answers.

        Parameters
        ----------
        question  : the shared question all agents answered.
        answers   : list of answers from each agent (Agent 0, 1, 2, ...).
        evidences : optional list of supporting evidence snippets (one per agent).
                    Pass None or [] to omit evidence.  Partial lists are OK.

        Returns
        -------
        DebateResult
        """
        if not answers:
            raise ValueError("answers must contain at least one entry.")
        if len(answers) == 1:
            return DebateResult(
                verdict=0, confidence=0.95, risk=0.05, routing="trusted",
                override_agent0=False, debate_triggered=False,
                reason="Single agent — no debate needed.", n_agents=1,
            )

        ev = list(evidences or [])
        ev += [""] * (len(answers) - len(ev))   # pad if shorter

        agree = self._mean_pairwise_f1(answers)

        # ── Agents agree — skip debate ────────────────────────────────────
        if agree >= self._thresh:
            return DebateResult(
                verdict=0, confidence=0.95, risk=0.05, routing="trusted",
                override_agent0=False, debate_triggered=False,
                reason="Agents agree — debate not triggered.", n_agents=len(answers),
            )

        # ── Run debate ────────────────────────────────────────────────────
        verdict_idx, conf, reason = self._call_mediator(question, answers, ev)

        risk = float(1.0 - conf)
        override = (verdict_idx != 0)

        if conf < self._uncertain:
            routing = "uncertain"
        elif override:
            routing = "override_agent0"
        else:
            routing = "trusted"

        return DebateResult(
            verdict=verdict_idx,
            confidence=conf,
            risk=risk,
            routing=routing,
            override_agent0=override,
            debate_triggered=True,
            reason=reason,
            n_agents=len(answers),
            _raw={"agree_score": agree},
        )

    def verify_batch(
        self,
        items: List[Dict],
    ) -> List[DebateResult]:
        """
        Batch verify.  Each item: {"question": ..., "answers": [...], "evidences": [...]}.
        """
        return [
            self.verify(
                item["question"],
                item["answers"],
                item.get("evidences"),
            )
            for item in items
        ]

    # ── internals ────────────────────────────────────────────────────────────

    @staticmethod
    def _toks(text: str):
        _sw = {"the","a","an","is","are","was","were","have","has","had","do","does","did",
               "will","would","could","should","of","in","on","at","to","for","with","by",
               "and","or","but","not","it","this","that","they","them","their"}
        return {w for w in re.findall(r"[a-zA-Z]+", text.lower())
                if w not in _sw and len(w) > 1}

    def _token_f1(self, a: str, b: str) -> float:
        pa, rb = self._toks(a), self._toks(b)
        if not pa or not rb: return 0.0
        c = pa & rb
        prec = len(c)/len(pa); rec = len(c)/len(rb)
        return 2*prec*rec/(prec+rec) if prec+rec > 0 else 0.0

    def _mean_pairwise_f1(self, answers: List[str]) -> float:
        import itertools
        pairs = list(itertools.combinations(range(len(answers)), 2))
        if not pairs: return 1.0
        return float(sum(self._token_f1(answers[i], answers[j]) for i, j in pairs) / len(pairs))

    def _build_prompt(self, question, answers, evidences):
        lines = [f"Question: {question}", ""]
        for idx, (ans, ev) in enumerate(zip(answers, evidences)):
            lines.append(f"Agent {idx} answered: \"{ans}\"")
            if ev:
                lines.append(f"  Evidence: {ev[:300]}")
        lines += ["", "The agents disagree. Which answer is most likely correct? Return JSON only."]
        return "\n".join(lines)

    def _cache_key(self, question, answers, evidences):
        payload = json.dumps({"q": question, "a": answers, "e": [e[:100] for e in evidences]})
        return hashlib.md5(payload.encode()).hexdigest()

    def _call_mediator(self, question, answers, evidences):
        """Returns (verdict_idx: int, confidence: float, reason: str)."""
        # Check cache
        if self._cache_dir is not None:
            key  = self._cache_key(question, answers, evidences)
            path = self._cache_dir / f"{key}.json"
            if path.exists():
                d = json.loads(path.read_text())
                return d["verdict"], d["confidence"], d.get("reason", "")

        prompt = self._build_prompt(question, answers, evidences)

        # Live API call
        try:
            import anthropic
            client = anthropic.Anthropic(api_key=self._api_key)
            msg = client.messages.create(
                model=self._model,
                max_tokens=150,
                system=self._SYSTEM,
                messages=[{"role": "user", "content": prompt}],
            )
            text = msg.content[0].text.strip()
        except Exception as e:
            warnings.warn(f"DebateVerifier API call failed: {e}", UserWarning)
            return 0, 0.5, f"error: {e}"

        verdict, conf, reason = self._parse_response(text, n_agents=len(answers))

        # Cache result
        if self._cache_dir is not None:
            path.write_text(json.dumps(
                {"verdict": verdict, "confidence": conf, "reason": reason, "raw": text}
            ))

        return verdict, conf, reason

    @staticmethod
    def _parse_response(text: str, n_agents: int):
        """Parse mediator JSON response. Robust to minor formatting issues."""
        text = text.strip()
        # Strip markdown code fences
        text = re.sub(r"^```(?:json)?\s*", "", text)
        text = re.sub(r"\s*```$", "", text)

        try:
            d    = json.loads(text)
            v    = d.get("verdict", "tie")
            conf = float(d.get("confidence", 0.5))
            reas = str(d.get("reason", ""))
        except (json.JSONDecodeError, KeyError, ValueError):
            v_m    = re.search(r'"verdict"\s*:\s*([0-9]+|"tie")', text, re.IGNORECASE)
            conf_m = re.search(r'"confidence"\s*:\s*([0-9.]+)', text)
            v    = v_m.group(1).strip('"') if v_m else "tie"
            conf = float(conf_m.group(1)) if conf_m else 0.5
            reas = ""

        # Convert verdict to int index
        if str(v).lower() in ("tie", "null", "none", ""):
            verdict_idx = -1
        else:
            try:
                verdict_idx = int(v)
                if verdict_idx < 0 or verdict_idx >= n_agents:
                    verdict_idx = -1
            except (ValueError, TypeError):
                verdict_idx = -1

        return verdict_idx, float(min(max(conf, 0.0), 1.0)), reas
