"""Cookie-Policy-Agent v3 — baut auf doc_check_controls (381 DB-MCs).

Sprint 1.12 Phase 2 — analog zu impressum/agent.py:
  Layer 0 — Regex-Boost (meine 12 Patterns aus mcs.py)
  Layer 1 — Keyword-Match aus pass_criteria der 381 Cookie-MCs
  Layer 2 — BGE-M3 Embedding-Match
  Layer 3 — Semantic-Validator (LLM) + Auto-Learning-Library

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 .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,
}


def _build_measure(label: str, norm: str) -> str:
    """Maßnahme (Imperativ) statt Pruef-Frage als action. Das Tool
    definiert Maßnahmen im Frontend — es stellt keine Fragen."""
    base = (label or "").strip().rstrip(".")
    if not base:
        return ("Cookie-Angabe ergänzen und gegen die gesetzlichen "
                "Vorgaben prüfen.")
    msg = f"Cookie-Angabe ergänzen: {base}."
    if norm:
        msg += f" Rechtsgrundlage: {norm}."
    return msg


class CookiePolicyAgent(BaseSpecialistAgent):
    agent_id = "cookie_policy"
    agent_version = "3.0"
    doc_type = "cookie"
    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 [])
        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",
                    reason="text too short",
                ))
            return self._finalize(
                start, findings, esc_logs, coverage,
                confidence=0.0,
                notes="Cookie-Policy-Text zu kurz oder leer.",
            )

        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_0_field_hits', 0)} Pattern-Boosts · "
            f"{telemetry.get('layer_0_boost_overrides', 0)} Boost-Overrides"
        )
        if telemetry.get("sector_dropped"):
            notes_parts.append(
                f"Scope-Filter: {telemetry['sector_dropped']} "
                "Branchen-MCs entfernt"
            )

        # Layer 3 — Sufficiency-Judge (Haiku) auf die embedding/boost-rescued
        # Controls: Embedding findet das Thema, Claude entscheidet ob die Pflicht
        # konkret erfuellt ist. Nur im vollen Check (nicht skip_llm).
        skip_llm = bool((agent_input.context or {}).get("skip_llm"))
        if not skip_llm:
            try:
                from ._sufficiency_judge import judge_rescued
                corrected = await judge_rescued(text, results)
                if corrected:
                    notes_parts.append(
                        f"layer-3 sufficiency-judge: {corrected} Rescues "
                        "zurückgenommen"
                    )
            except Exception as e:
                logger.warning("cookie layer-3 judge skipped: %s", e)

        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,
            )
            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 "")
            if r.get("article"):
                norm_str = (norm_str + f" Art. {r.get('article')}").strip()
            findings.append(Finding(
                check_id=f"DBMC-{mc_id}",
                agent=self.agent_id,
                agent_version=self.agent_version,
                field_id=mc_id,
                severity=sev,
                severity_reason="db_mc_failed",
                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_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",
                reason=("regex-boost hit"
                        if mc.field_id in boost_ids
                        else "kein Pattern-Treffer (kein Veto)"),
            ))

        await self._semantic_demote(text, findings, coverage)

        kb_findings = self._kb_layer(text, agent_input.context or {})
        findings.extend(kb_findings)

        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),
        )

    def _kb_layer(self, text: str, context: dict) -> list[Finding]:
        """Wenn cmp_vendors im Kontext: prüfe ob alle in Policy genannt."""
        cmp_vendors = context.get("cmp_vendors") or []
        if not cmp_vendors:
            return []
        text_lc = text.lower()
        seen_names: set[str] = set()
        for v in cmp_vendors:
            if not isinstance(v, dict):
                continue
            name = (v.get("name") or v.get("vendor") or "").strip()
            if name and len(name) > 2:
                seen_names.add(name)
        missing = [n for n in sorted(seen_names)
                   if n.lower() not in text_lc]
        if not missing:
            return []
        sample = missing[:8]
        return [Finding(
            check_id="COOKIE-POLICY-AGENT-CMP-VS-POLICY",
            agent=self.agent_id,
            agent_version=self.agent_version,
            field_id="vendor_consistency",
            severity=Severity.MEDIUM,
            severity_reason="cmp_observed_vendors_not_in_policy",
            title=(
                f"{len(missing)} im CMP beobachtete Vendoren "
                "fehlen in der Cookie-Policy"
            ),
            norm="DSGVO Art. 13 Abs. 1 lit. e (Empfänger vollständig)",
            evidence=f"Fehlend: {', '.join(sample)}"
                       + (" …" if len(missing) > 8 else ""),
            action=(
                "Die im Cookie-Consent-Banner beobachteten Vendoren "
                "müssen vollständig in der Cookie-Richtlinie genannt sein."
            ),
            confidence=0.88,
            sources=[EvidenceSource(
                source_type=SourceType.CROSS,
                source_id="CMP-CROSS-CHECK",
                detail=f"{len(missing)} missing of {len(seen_names)}",
            )],
        )]

    async def _semantic_demote(
        self, text: str, findings: list[Finding],
        coverage: list[McCoverage],
    ) -> None:
        candidates = [
            f for f in findings
            if f.severity in (Severity.HIGH.value, Severity.MEDIUM.value)
            and f.severity_reason == "db_mc_failed"
        ]
        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 == f"DBMC-{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)
