"""DSE-Agent v3 — Datenschutzerklärung / Datenschutzinformation (Art. 13/14
DSGVO), baut auf doc_check_controls (267 text-MCs aus DB).

Volle Parität zu impressum/ + cookie_policy/ (User-Vorgabe 2026-06-17):
  Layer 0  — Regex-Boost (kuratierte Art-13-Patterns aus mcs.py / ART13_CHECKLIST)
  Layer 1  — Keyword-Match aus pass_criteria der DSE-DB-MCs (deterministisch)
  Layer 2  — BGE-M3 Embedding-Match
  Layer 3  — Semantic-Validator (LLM) für offene HIGH/MEDIUM-Fails + Auto-Learning

Die kuratierten Patterns gehen NICHT verloren — sie boosten (Layer 0) die DB-
Controls (z.B. präzises "keine Drittlandübermittlung" → drittland-MC PASS, kein
False-Positive). DSE-Spezialität bleibt: Drittland → HIGH bei dokumentiertem
Transfer (scan_context).

Output-Layer (Linter / Rollup / Methodik-UI) bleibt 1:1.
"""

from __future__ import annotations

import logging
from datetime import datetime, timezone

from .._base import (
    AgentInput,
    AgentOutput,
    BaseSpecialistAgent,
    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
from .regex_boost import BOOST_KEYWORDS
from .v3_engine import run_v3_pipeline

logger = logging.getLogger(__name__)


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

# Drittland-Vokabeln für die scan_context-Heraufstufung (Art. 13(1)(f)).
_THIRD_COUNTRY_KW = tuple(set(
    BOOST_KEYWORDS.get("third_country", ())
    + BOOST_KEYWORDS.get("third_country_mechanism", ())
))


def _build_measure(label: str, norm: str) -> str:
    """Maßnahme (Imperativ) statt Pruef-Frage als action."""
    base = (label or "").strip().rstrip(".")
    if not base:
        return ("Datenschutz-Pflichtangabe ergänzen und gegen Art. 13/14 "
                "DSGVO prüfen.")
    msg = f"Pflichtangabe ergänzen: {base}."
    if norm:
        msg += f" Rechtsgrundlage: {norm}."
    return msg


def _is_third_country_topic(result: dict) -> bool:
    """Ist dieses DB-MC thematisch ein Drittland-Control?"""
    parts: list[str] = [str(result.get("label") or "").lower()]
    for c in (result.get("_pass_criteria") or []):
        if c:
            parts.append(str(c).lower())
    blob = " ".join(parts)
    hits = sum(1 for kw in _THIRD_COUNTRY_KW if kw in blob)
    return hits >= 1


class DSEAgent(BaseSpecialistAgent):
    agent_id = "dse"
    agent_version = "3.0"
    doc_type = "dse"
    owned_mc_ids = MC_IDS

    def _third_country_transfer(self, agent_input: AgentInput) -> bool:
        sc = (agent_input.context or {}).get("scan_context") or {}
        return str(sc.get("third_country_transfer", "")).lower() in (
            "yes", "true", "1", "ja")

    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 [])
        coverage: list[McCoverage] = []
        findings: list[Finding] = []
        esc_logs: list[EscalationLog] = []
        notes_parts: list[str] = []

        if len(text) < 100:
            for mc in MCS:
                coverage.append(McCoverage(
                    mc_id=mc.mc_id, status="skipped",
                    label=mc.label, reason="text too short",
                ))
            return self._finalize(
                start, findings, esc_logs, coverage,
                confidence=0.0,
                notes="DSE-Text zu kurz oder leer.",
            )

        tc_transfer = self._third_country_transfer(agent_input)
        # Embedding-Recall (Layer 2) läuft IMMER — deterministisch, gecacht
        # (pro Doc-Hash → Folge-Views instant) und Reachability-gegated
        # (kein Hang, wenn der Service fehlt). Ersetzt den über-passenden Boost.
        results, telemetry = await run_v3_pipeline(text, scope)
        notes_parts.append(
            f"v3-pipeline: {telemetry.get('total_mcs', 0)} DB-MCs · "
            f"{telemetry.get('layer_1_pass', 0)} Keyword-Treffer · "
            f"{telemetry.get('embedding_passes', 0)} semantisch (Embedding)"
        )
        if telemetry.get("sector_dropped") or telemetry.get("offtopic_dropped"):
            notes_parts.append(
                f"Scope-Filter: {telemetry.get('sector_dropped', 0)} "
                f"Branchen-MCs, {telemetry.get('offtopic_dropped', 0)} "
                "themenfremde MCs entfernt"
            )

        seen: set[str] = set()
        for r in results:
            mc_id = r.get("control_id") or ""
            if not mc_id or mc_id in seen:
                continue
            seen.add(mc_id)
            passed = bool(r.get("passed"))
            sev = _SEV_TO_ENUM.get(
                (r.get("severity") or "MEDIUM").upper(), Severity.MEDIUM,
            )
            # DSE-Spezialität: Drittland → HIGH bei dokumentiertem Transfer.
            sev_reason = "db_mc_failed"
            if tc_transfer and _is_third_country_topic(r):
                sev = Severity.HIGH
                sev_reason = "db_mc_failed_third_country_transfer"
            coverage.append(McCoverage(
                mc_id=mc_id,
                status="ok" if passed else sev.value.lower(),
                reason=str(r.get("matched_text") or r.get("hint") or "")[:120],
            ))
            if passed:
                continue
            label = r.get("label") or r.get("hint") or ""
            norm_str = str(r.get("regulation") or "").strip()
            if r.get("article"):
                norm_str = (norm_str + f" Art. {r.get('article')}").strip()
            if not norm_str:
                norm_str = "DSGVO Art. 13/14"
            findings.append(Finding(
                check_id=f"DSE-DBMC-{mc_id}",
                agent=self.agent_id,
                agent_version=self.agent_version,
                field_id=mc_id,
                severity=sev,
                severity_reason=sev_reason,
                title=str(label)[:200] or f"DB-MC {mc_id} nicht erfüllt",
                norm=norm_str,
                evidence="",
                action=_build_measure(str(label), norm_str)[:400],
                confidence=0.9,
                sources=[EvidenceSource(
                    source_type=SourceType.MC,
                    source_id=mc_id,
                    detail=str(r.get("source") or "keyword_match")[:120],
                    confidence=0.9,
                )],
            ))

        # Boost-Coverage: meine Pattern-Treffer (regex-boost field_ids).
        boost_ids = set(telemetry.get("layer_0_field_ids") or [])
        for mc in MCS:
            coverage.append(McCoverage(
                mc_id=mc.mc_id,
                status="ok" if mc.field_id in boost_ids else "na",
                label=mc.label,
                reason=("regex-boost hit"
                        if mc.field_id in boost_ids
                        else "kein Pattern-Treffer (kein Veto)"),
            ))

        if not (agent_input.context or {}).get("skip_llm"):
            await self._semantic_demote(text, findings, coverage)

        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 in (
                "db_mc_failed", "db_mc_failed_third_country_transfer")
        ]
        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,
            ))
            for c in coverage:
                if c.mc_id == finding.field_id:
                    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)
        recs = rollup(findings)
        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"),
        )
        return lint_output(out)
