"""ChecklistAgent — generischer Doc-Agent über eine kuratierte Pflichtangaben-
Checkliste (L1 „erwähnt?" + L2 „vollständig?").

Basis für DSE/AGB/Widerruf/… : läuft die Checkliste deterministisch über den
Text → strukturierter AgentOutput (mc_coverage + Findings + Maßnahmen). BEWUSST
KEIN Library-Firehose. Subklassen setzen nur CHECKLIST/agent_id/doc_type und
können per _severity_override() die Severity kontextabhängig anheben.
"""

from __future__ import annotations

import re
from datetime import datetime, timezone

from ._base import (
    AgentInput,
    AgentOutput,
    BaseSpecialistAgent,
    CheckStatus,
    EvidenceSource,
    Finding,
    McCoverage,
    Severity,
    SourceType,
    lint_output,
)
from ._rollup import rollup

_SEV = {"HIGH": Severity.HIGH, "MEDIUM": Severity.MEDIUM,
        "LOW": Severity.LOW, "INFO": Severity.INFO}
# Coverage-Status bei FAIL spiegelt die Risiko-Achse (severity) der Quelle.
_COV_FAIL = {"HIGH": "high", "MEDIUM": "medium", "LOW": "low", "INFO": "low"}
# Norm-Referenz aus dem Label ziehen: '(Art. 13(1)(a))' oder '(§305 BGB)'.
_NORM_RE = re.compile(r"\((Art\.[^)]+|§\s*\d+[^)]*)\)")


def _match_value(text: str, start: int, end: int) -> str:
    """Exakter Treffer-Wert (nicht die umgebende Passage), normalisiert + gekappt."""
    return " ".join(text[start:end].split())[:120]


def _norm_of(label: str) -> str:
    m = _NORM_RE.search(label or "")
    return m.group(1).strip() if m else ""


def _compiled(check: dict) -> list:
    out = []
    for p in check.get("patterns", []):
        try:
            out.append(re.compile(p, re.IGNORECASE | re.MULTILINE))
        except re.error:
            continue
    return out


def _search(patterns: list, text: str):
    for p in patterns:
        m = p.search(text)
        if m:
            return m
    return None


class ChecklistAgent(BaseSpecialistAgent):
    CHECKLIST: list[dict] = []
    agent_id = ""
    agent_version = "1.0"
    doc_type = ""

    def _severity_override(self, c: dict, agent_input: AgentInput):
        """Hook: Subklasse kann die Checklist-Severity kontextabhängig anheben
        (z.B. DSE: Drittland → HIGH bei dokumentiertem Transfer). None = keine."""
        return None

    def _eff_sev(self, c: dict, agent_input: AgentInput) -> str:
        return self._severity_override(c, agent_input) or c.get("severity", "MEDIUM")

    async def evaluate(self, agent_input: AgentInput) -> AgentOutput:
        start = datetime.now(timezone.utc)
        text = (agent_input.text or "").strip()
        coverage: list[McCoverage] = []
        findings: list[Finding] = []

        if len(text) < 100:
            for c in self.CHECKLIST:
                coverage.append(McCoverage(
                    mc_id=c["id"], status="skipped",
                    label=c["label"], reason="Text zu kurz"))
            return self._finalize(start, findings, coverage, 0.0,
                                  f"{self.doc_type}-Text zu kurz oder leer.")

        # L1 (Pflichtangabe erwähnt?) zuerst — Ergebnis steuert L2.
        l1_present: dict[str, bool] = {}
        for c in self.CHECKLIST:
            if c.get("level", 1) != 1:
                continue
            m = _search(_compiled(c), text)
            l1_present[c["id"]] = m is not None
            coverage.append(self._cov(c, m, text, agent_input))
            if m is None:
                findings.append(self._finding(c, False, agent_input))

        # L2 (vollständig?) — nur wenn die übergeordnete L1 da ist. Fehlt die L1,
        # deckt deren Finding die Lücke ab → KEIN irreführendes 'na' (nicht
        # anwendbar) für das Detail.
        for c in self.CHECKLIST:
            if c.get("level", 1) != 2:
                continue
            parent = c.get("parent")
            if parent and not l1_present.get(parent, False):
                continue
            m = _search(_compiled(c), text)
            coverage.append(self._cov(c, m, text, agent_input))
            if m is None:
                findings.append(self._finding(c, True, agent_input))

        return self._finalize(start, findings, coverage, 0.7, "")

    def _cov(self, c: dict, m, text: str, ai: AgentInput) -> McCoverage:
        if m is not None:
            return McCoverage(
                mc_id=c["id"], status="ok", label=c["label"],
                reason="Pattern-Treffer",
                found=_match_value(text, m.start(), m.end()))
        sev = self._eff_sev(c, ai)
        return McCoverage(
            mc_id=c["id"], status=_COV_FAIL.get(sev, "medium"),
            label=c["label"],
            reason="fehlt" if c.get("level", 1) == 1 else "Detail unvollständig")

    def _finding(self, c: dict, present: bool, ai: AgentInput) -> Finding:
        sev = self._eff_sev(c, ai)
        # Titel + Maßnahme bewusst KURZ (treibt den Recommendation-Titel); die
        # ausführliche Begründung steht als evidence auf der Finding-Karte.
        title = (f"{c['label']}: Detail unvollständig" if present
                 else f"{c['label']} fehlt")
        action = (f"{c['label']} präzisieren." if present
                  else f"{c['label']} ergänzen.")
        return Finding(
            check_id=f"{self.agent_id.upper()}-{c['id']}",
            agent=self.agent_id, agent_version=self.agent_version,
            field_id=c["id"], status=CheckStatus.FAIL,
            severity=_SEV.get(sev, Severity.MEDIUM),
            severity_reason=("detail_incomplete" if present
                             else "pflichtangabe_missing"),
            title=title, norm=_norm_of(c["label"]),
            action=action, evidence=(c.get("hint") or "")[:280], confidence=0.7,
            sources=[EvidenceSource(
                source_type=SourceType.REGEX, source_id=c["id"],
                detail="kein Pattern-Treffer", confidence=0.7)],
        )

    def _finalize(self, start, findings, coverage, confidence, notes):
        end = datetime.now(timezone.utc)
        recs = rollup([f for f in findings
                       if f.status == CheckStatus.FAIL.value])
        out = AgentOutput(
            agent=self.agent_id, agent_version=self.agent_version,
            started_at=start, finished_at=end,
            duration_ms=int((end - start).total_seconds() * 1000),
            findings=findings, recommendations=recs, mc_coverage=coverage,
            confidence=confidence, notes=notes,
            mc_total=len(coverage),
            mc_ok=sum(1 for c in coverage if c.status == "ok"),
            mc_na=sum(1 for c in coverage if c.status == "na"),
            mc_high=sum(1 for c in coverage if c.status == "high"),
            mc_medium=sum(1 for c in coverage if c.status == "medium"),
            mc_low=sum(1 for c in coverage if c.status == "low"),
            mc_insufficient=sum(
                1 for c in coverage if c.status == "insufficient_evidence"),
            mc_possibly=sum(
                1 for c in coverage if c.status == "possibly_applicable"),
        )
        return lint_output(out)
