"""Impressum-Agent v3 — baut auf doc_check_controls (75 echte MCs aus DB).

Sprint 1.12 (User-Vorgabe 2026-06-09):

Architektur, 4 Layer:
  Layer 0  — Regex-Boost (meine 12 Patterns aus mcs.py + regex_boost.py)
  Layer 1  — Keyword-Match aus doc_check_controls.pass_criteria
              (rag_document_checker.check_document_with_controls)
  Layer 2  — BGE-M3 Embedding-Match (in rag_document_checker integriert)
  Layer 3  — Semantic-Validator (LLM) für noch immer offene FAILs +
              Auto-Learning der Labels

Output-Layer (mein Stack bleibt 1:1):
  - Disclaimer-Linter
  - Rollup-Dedup
  - Methodik-First-UI

Vorteil: 75 statt 12 Master-Controls; DB-konsistent; Embedding-Fallback
gratis; meine Konzepte bleiben als Boost + Output-Sanitizer.
"""

from __future__ import annotations

import logging
from datetime import datetime, timezone

from .._base import (
    AgentInput,
    AgentOutput,
    BaseSpecialistAgent,
    CheckStatus,
    EscalationLog,
    EvidenceSource,
    Finding,
    McCoverage,
    Severity,
    SourceType,
    lint_output,
)
from .._pattern_library import record as record_pattern
from .._rollup import rollup
from .._semantic_validator import build_rename_action, validate_present
from .mcs import (
    MC_IDS,
    MCS,
    detect_automotive,
    detect_legal_form_present,
    scope_disposition,
)

logger = logging.getLogger(__name__)


_SEV_TO_ENUM = {
    "CRITICAL": Severity.HIGH,
    "HIGH": Severity.HIGH,
    "MEDIUM": Severity.MEDIUM,
    "LOW": Severity.LOW,
    "INFO": Severity.INFO,
}


def _build_measure(label: str, norm: str) -> str:
    """Formuliert aus einer fehlenden Pflichtangabe eine konkrete Maßnahme
    (Imperativ) statt die Pruef-Frage auszugeben.

    Das Tool definiert Maßnahmen im Frontend — es stellt keine Fragen. Der
    check_question-Text ('Ist X angegeben?') wird daher NICHT mehr als
    action durchgereicht (sonst zeigt die Finding-Card eine Frage unter
    'Pflicht-Maßnahme')."""
    base = (label or "").strip().rstrip(".")
    if not base:
        return ("Pflichtangabe ergänzen und gegen die gesetzlichen "
                "Vorgaben prüfen.")
    msg = f"Pflichtangabe ergänzen: {base}."
    if norm:
        msg += f" Rechtsgrundlage: {norm}."
    return msg


def _match_value(text: str, start_pos: int, end_pos: int) -> str:
    """Der reine Treffer-Wert (E-Mail, USt-IdNr, Telefonnummer …) — exakt der
    gematchte Bereich, NICHT die umgebende Textpassage. Whitespace normalisiert
    + gekappt, damit die Pflichtangaben-Tabelle genau das zeigt, was erwartet
    wird (nur die Mail unter Email, nur die Anschrift unter Anschrift)."""
    return " ".join(text[start_pos:end_pos].split())[:120]


def _coverage(mc, status: str, reason: str, found: str = "") -> McCoverage:
    """McCoverage mit menschlichem Label (mc.label) — das Frontend zeigt
    NIE die mc_id (Reverse-Engineering-Schutz)."""
    return McCoverage(
        mc_id=mc.mc_id, status=status, reason=reason,
        label=mc.label, found=found,
    )


class ImpressumAgent(BaseSpecialistAgent):
    agent_id = "impressum"
    agent_version = "3.0"
    doc_type = "impressum"
    # Owner-Liste der eigenen Boost-Pattern-IDs (Layer 0).
    # Die DB-MC-IDs werden dynamisch ermittelt und sind kein Owner-
    # Konflikt: doc_check_controls.control_id-Werte sind global, der
    # Agent ist Caller, nicht Owner.
    owned_mc_ids = MC_IDS

    async def evaluate(self, agent_input: AgentInput) -> AgentOutput:
        start = datetime.now(timezone.utc)
        text = (agent_input.text or "").strip()
        scope = set(agent_input.business_scope or [])
        if detect_automotive(text):
            scope.add("automotive")

        coverage: list[McCoverage] = []
        findings: list[Finding] = []
        esc_logs: list[EscalationLog] = []
        notes_parts: list[str] = []

        if len(text) < 100:
            # Doc zu kurz — alle eigenen Pattern-IDs als skipped
            for mc in MCS:
                coverage.append(_coverage(mc, "skipped", "text too short"))
            return self._finalize(
                start, findings, esc_logs, coverage,
                confidence=0.0,
                notes="Impressum-Text zu kurz oder leer.",
            )

        # ── Findings aus den 12 autoritativen §5-TMG/DDG-MCs (mcs.py) ──
        # Die DB-Menge doc_type='impressum' ist verunreinigt (TRD/SEC/GOV-
        # Controls statt §5 TMG, fehl-klassifiziert von einer Vorgaenger-
        # Session), daher sind bis zum MC-Re-Filtering die 12 praezisen
        # Pattern-MCs die Findings-Quelle. field_id = semantisches Feld
        # (passt zum Semantic-Validator + den GT-Tests).
        is_auto = "automotive" in scope
        # 4-Status: ist die Rechtsform ueberhaupt bestimmbar (Wizard ODER
        # im Text genannt)? Wenn nicht, duerfen rechtsform-abhaengige Pflichten
        # NICHT hart als FAIL gewertet werden → INSUFFICIENT_EVIDENCE.
        form_known = (
            "legal_form_known" in scope or detect_legal_form_present(text)
        )
        for mc in MCS:
            disp = scope_disposition(mc, scope, is_auto)
            if disp == "na":
                coverage.append(_coverage(
                    mc, "na", "nicht anwendbar (Rechtsform/Branche)"))
                continue
            matched = None
            for p in mc.patterns:
                m = p.search(text)
                if m:
                    matched = m
                    break
            if matched is not None:
                coverage.append(_coverage(
                    mc, "ok", "Pattern-Treffer",
                    found=_match_value(text, matched.start(), matched.end()),
                ))
                continue
            if mc.optional:
                # fehlt + optional → KEIN Finding (z.B. USt-IdNr;
                # Kleinunternehmer §19 haben legitim keine).
                coverage.append(_coverage(
                    mc, "na", "optional — nicht angegeben"))
                continue
            if disp == "possible":
                # Graubereich (z.B. Corporate-Blog → §18, OEM-Markenseite →
                # §36 VSBG) → POSSIBLY_APPLICABLE: Pruef-Hinweis (LOW),
                # kein Verstoss. Hinweistext kommt MC-spezifisch.
                hint = mc.possibly_hint or (
                    f"Diese Angabe ist nur situativ Pflicht ({mc.norm}). "
                    "Bitte prüfen, ob sie auf Ihre Seite zutrifft."
                )
                findings.append(Finding(
                    check_id=f"IMP-{mc.field_id}",
                    agent=self.agent_id,
                    agent_version=self.agent_version,
                    field_id=mc.field_id,
                    status=CheckStatus.POSSIBLY_APPLICABLE,
                    severity=Severity.LOW,
                    severity_reason="graubereich",
                    title=f"{mc.label}: ggf. relevant — manuell prüfen",
                    norm=mc.norm,
                    evidence="",
                    action=hint,
                    confidence=0.5,
                    sources=[EvidenceSource(
                        source_type=SourceType.REGEX,
                        source_id=mc.mc_id,
                        detail="Graubereich-Signal (Blog/News), kein hartes Gate",
                        confidence=0.5,
                    )],
                ))
                coverage.append(_coverage(
                    mc, "possibly_applicable", "Graubereich — manuelle Prüfung"))
                continue
            if mc.legal_form_dependent and not form_known:
                # Rechtsform unbestimmt → kein hartes FAIL, sondern
                # 'unzureichende Evidenz' (severity INFO, Hinweis statt Verstoss).
                findings.append(Finding(
                    check_id=f"IMP-{mc.field_id}",
                    agent=self.agent_id,
                    agent_version=self.agent_version,
                    field_id=mc.field_id,
                    status=CheckStatus.INSUFFICIENT_EVIDENCE,
                    severity=Severity.INFO,
                    severity_reason="rechtsform_unbestimmt",
                    title=f"{mc.label}: Rechtsform nicht erkennbar",
                    norm=mc.norm,
                    evidence="",
                    action=(
                        "Rechtsform im Impressum nicht eindeutig erkennbar — "
                        "bitte pruefen, ob das Unternehmen registerpflichtig "
                        "ist; falls ja, die Pflichtangabe ergaenzen."
                    ),
                    confidence=0.4,
                    sources=[EvidenceSource(
                        source_type=SourceType.REGEX,
                        source_id=mc.mc_id,
                        detail="keine Rechtsform im Text + kein legal_form im Scope",
                        confidence=0.4,
                    )],
                ))
                coverage.append(_coverage(
                    mc, "insufficient_evidence", "Rechtsform unbestimmt"))
                continue
            sev = _SEV_TO_ENUM.get(mc.severity_if_missing, Severity.MEDIUM)
            findings.append(Finding(
                check_id=f"IMP-{mc.field_id}",
                agent=self.agent_id,
                agent_version=self.agent_version,
                field_id=mc.field_id,
                status=CheckStatus.FAIL,
                severity=sev,
                severity_reason="pflichtangabe_missing",
                title=f"Pflichtangabe fehlt: {mc.label}",
                norm=mc.norm,
                evidence="",
                action=_build_measure(mc.label, mc.norm),
                confidence=0.9,
                sources=[EvidenceSource(
                    source_type=SourceType.REGEX,
                    source_id=mc.mc_id,
                    detail="kein Pattern-Treffer im Text",
                    confidence=0.9,
                )],
            ))
            coverage.append(_coverage(
                mc, sev.value.lower(), "kein Pattern-Treffer"))
        n_fail = sum(1 for f in findings
                     if f.status == CheckStatus.FAIL.value)
        n_unklar = sum(1 for f in findings
                       if f.status == CheckStatus.INSUFFICIENT_EVIDENCE.value)
        notes_parts.append(
            f"{len(MCS)} §5-TMG-MCs geprüft · "
            f"{n_fail} Pflichtangabe(n) offen"
            + (f" · {n_unklar} unklar (Rechtsform)" if n_unklar else "")
        )

        # ── Layer 3: Semantic-Validator nur für HIGH/MEDIUM-Fails ──
        # In der interaktiven Snapshot-Ansicht (context skip_llm) übersprungen:
        # der LLM-Schritt kostet ~40s, das deterministische Ergebnis genügt dort.
        if not (agent_input.context or {}).get("skip_llm"):
            await self._semantic_demote(text, findings, coverage)

        # Confidence: harmonic mean der Findings (oder hoch wenn 0)
        confs = [f.confidence for f in findings if f.confidence] or [0.95]
        overall = sum(confs) / len(confs)

        return self._finalize(
            start, findings, esc_logs, coverage,
            confidence=overall,
            notes=" · ".join(notes_parts),
        )

    async def _semantic_demote(
        self,
        text: str,
        findings: list[Finding],
        coverage: list[McCoverage],
    ) -> None:
        """LLM-Layer für HIGH/MEDIUM-DB-MCs: Label-Mismatch-Check.
        Bei Fund → HIGH/MEDIUM → LOW + Rename-Action."""
        candidates = [
            f for f in findings
            if f.severity in (Severity.HIGH.value, Severity.MEDIUM.value)
            and f.severity_reason == "pflichtangabe_missing"
        ]
        if not candidates:
            return
        result = await validate_present(
            text, [(f.field_id, f.title[:80]) for f in candidates],
        )
        if not result:
            return
        for finding in candidates:
            row = result.get(finding.field_id)
            if not row or not row.get("found"):
                continue
            if row.get("confidence", 0) < 0.6:
                continue
            label_used = row.get("label_used") or "abweichendes Label"
            conf = float(row.get("confidence") or 0.8)
            finding.severity = Severity.LOW.value
            finding.severity_reason = "label_mismatch"
            finding.title = (
                f"Label '{label_used}' weicht von Standard ab"
            )
            finding.evidence = str(row.get("evidence") or "")[:200]
            finding.action = build_rename_action(
                finding.field_id, label_used,
            )
            finding.confidence = conf
            finding.sources.append(EvidenceSource(
                source_type=SourceType.LLM_LOCAL,
                source_id="semantic_validator",
                detail=f"LLM-confirmed: '{label_used}'",
                confidence=conf,
            ))
            # Coverage update + auto-learning (mc_id steckt in der Quelle)
            mc_id_for_cov = (finding.sources[0].source_id
                             if finding.sources else "")
            for c in coverage:
                if c.mc_id and c.mc_id == mc_id_for_cov:
                    c.status = "low"
                    c.reason = f"label_mismatch: '{label_used}'"
            try:
                record_pattern(
                    field_id=finding.field_id,
                    label_used=label_used,
                    confidence=conf,
                    agent_id=self.agent_id,
                )
            except Exception as e:
                logger.warning("pattern-library record failed: %s", e)

    def _finalize(
        self,
        start: datetime,
        findings: list[Finding],
        esc_logs: list[EscalationLog],
        coverage: list[McCoverage],
        confidence: float,
        notes: str = "",
    ) -> AgentOutput:
        end = datetime.now(timezone.utc)
        # Recommendations nur aus echten FAILs — INSUFFICIENT_EVIDENCE /
        # POSSIBLY_APPLICABLE sind Hinweise, keine Pflicht-Massnahmen
        # (User-Datenmodell: Finding → Remediation nur bei echtem Verstoss).
        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,
            escalation_log=esc_logs,
            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)
