"""BaseSpecialistAgent — Contract für alle Doc-Type-Agenten.

Architektur-Vertrag (User-Vorgabe 2026-06-08):
  - Jeder Agent ist ein hochspezialisierter Anwalt für sein Thema.
  - Standard: deterministische MCs + Regex + FAQ-KB.
  - Eskalation bei Unsicherheit: qwen2.5:7b → OVH 120b → Claude (TBD).
  - Jeder Output ist auditfest: Quelle pro Finding (MC-ID / Regex /
    FAQ / LLM-Stufe + Prompt-Hash).
  - Output enthält NIEMALS "rechtssicher" / "garantiert" — Linter
    verbietet diese Wörter.
  - Token-Budget pro Agent (Cost-Cap).
"""

from __future__ import annotations

import abc
import hashlib
from datetime import datetime
from enum import Enum
from typing import Any

from pydantic import BaseModel, ConfigDict, Field


class Severity(str, Enum):
    HIGH = "HIGH"
    MEDIUM = "MEDIUM"
    LOW = "LOW"
    INFO = "INFO"


class CheckStatus(str, Enum):
    """Verdikt eines Checks — GETRENNT vom Risiko (severity).

    User-Vorgabe 2026-06-10 (kanonisches Datenmodell):
      - Applicability ≠ Compliance: NOT_APPLICABLE ist KEIN FAIL.
      - Unknown ≠ Fail: nicht bestimmbar → INSUFFICIENT_EVIDENCE, kein FAIL.
    severity bleibt die Risiko-Achse (HIGH/…/INFO); status ist das Urteil.
    """
    PASS = "pass"
    FAIL = "fail"
    NOT_APPLICABLE = "not_applicable"
    INSUFFICIENT_EVIDENCE = "insufficient_evidence"
    POSSIBLY_APPLICABLE = "possibly_applicable"


class SourceType(str, Enum):
    """Wo kommt das Finding her? Für die auditfeste Beweiskette."""
    MC = "mc"                       # Machine-Check (deterministisch)
    REGEX = "regex"                 # Pattern-Match
    KB_FAQ = "kb_faq"               # Knowledge-Base / kuratiert
    LLM_LOCAL = "llm_local"         # qwen2.5:7b oder qwen3
    LLM_LOCAL_BIG = "llm_local_big"  # OVH 120b
    LLM_CLOUD = "llm_cloud"         # Claude API (Anonymisierung Pflicht)
    CROSS = "cross"                 # Cross-Doc-Agent


class EvidenceSource(BaseModel):
    """Eine Quelle in der Beweiskette eines Findings."""
    source_type: SourceType
    source_id: str = ""             # mc_id, regex_name, faq_id, model
    detail: str = ""                # Snippet, Prompt-Hash, etc.
    confidence: float = 1.0         # 0.0 – 1.0


class Finding(BaseModel):
    """Ein einzelnes Audit-Finding aus einem Specialist-Agent."""
    model_config = ConfigDict(use_enum_values=True, validate_default=True)

    check_id: str                   # z.B. IMPRESSUM-AGENT-HANDELSREGISTER
    agent: str                      # impressum_v2
    agent_version: str              # 2.0
    field_id: str = ""              # field-key innerhalb des Agenten
    # Verdikt (was IST der Fall) — getrennt vom Risiko (severity).
    status: CheckStatus = CheckStatus.FAIL
    severity: Severity
    severity_reason: str = ""
    title: str
    norm: str = ""
    evidence: str = ""
    action: str = ""
    confidence: float = 1.0         # 0.0 – 1.0; treibt Eskalation
    sources: list[EvidenceSource] = Field(default_factory=list)


class Recommendation(BaseModel):
    """Konsolidierte Maßnahme. Kann aus 1..N Findings stammen
    (Rollup-Dedup-Schritt bündelt ähnliche Maßnahmen)."""
    recommendation_id: str          # stable hash der canonical_text
    title: str
    body: str
    severity: Severity              # höchste Severity der Quell-Findings
    related_finding_ids: list[str] = Field(default_factory=list)
    estimated_effort_hours: float = 0.0


class McCoverage(BaseModel):
    """Welche MC hat der Agent geprüft + Ergebnis.

    status-Vokabular (mappt auf CheckStatus):
      ok                    → PASS
      high | medium | low   → FAIL (Risiko = severity der Quelle)
      na                    → NOT_APPLICABLE (Rechtsform/Branche)
      insufficient_evidence → INSUFFICIENT_EVIDENCE (nicht bestimmbar)
      skipped               → Dokument nicht ladbar / zu kurz
    """
    mc_id: str
    status: str
    reason: str = ""
    # Menschlicher Feldname (für die Pflichtangaben-Tabelle im Frontend —
    # NICHT die mc_id zeigen, sonst Reverse-Engineering der MC-Bibliothek).
    label: str = ""
    # Der tatsächlich gefundene Text/Wert (Snippet) bei status=ok.
    found: str = ""


class EscalationLog(BaseModel):
    """Ein Eskalations-Schritt mit Modell + Latenz + Kosten."""
    stage: SourceType
    model: str
    duration_ms: int = 0
    tokens_in: int = 0
    tokens_out: int = 0
    success: bool = True
    error: str = ""


class AgentInput(BaseModel):
    """Was ein Agent zur Bearbeitung bekommt."""
    doc_type: str
    text: str
    url: str = ""
    business_scope: list[str] = Field(default_factory=list)
    company_name: str = ""
    origin_domain: str = ""
    # extra context z.B. extracted_profile, multi-doc-context
    context: dict[str, Any] = Field(default_factory=dict)


class AgentOutput(BaseModel):
    """Was ein Agent zurückgibt — auditfestes Resultat."""
    agent: str
    agent_version: str
    started_at: datetime
    finished_at: datetime
    duration_ms: int
    findings: list[Finding] = Field(default_factory=list)
    recommendations: list[Recommendation] = Field(default_factory=list)
    mc_coverage: list[McCoverage] = Field(default_factory=list)
    escalation_log: list[EscalationLog] = Field(default_factory=list)
    confidence: float = 1.0
    notes: str = ""
    # Speed-O-Meter aggregates (vorberechnet fürs Frontend)
    mc_total: int = 0
    mc_ok: int = 0
    mc_na: int = 0
    mc_high: int = 0
    mc_medium: int = 0
    mc_low: int = 0
    mc_insufficient: int = 0
    mc_possibly: int = 0


# Verbotene Wörter im Output — sicherheitshalber, damit kein Agent
# (oder LLM-Stufe) "rechtssicher" suggeriert. User-Vorgabe.
FORBIDDEN_OUTPUT_TERMS = (
    "rechtssicher",
    "rechts-sicher",
    "garantiert",
    "garantieren",
    "garantie",
    "gesetzeskonform",
    "gesetzes-konform",
    "100% konform",
    "100 % konform",
    "vollständig konform",
    "absolut sicher",
)


class DisclaimerViolation(ValueError):
    """Erhoben wenn ein Agent-Output ein verbotenes Wort enthält."""


def lint_output(output: AgentOutput) -> AgentOutput:
    """Prüft den Output gegen FORBIDDEN_OUTPUT_TERMS. Sanitisiert
    titles/actions inline — ersetzt verbotene Begriffe durch
    [→ neutraler Wortlaut empfohlen]-Marker. Loggt aber NICHT in
    den Datenbestand, sondern als notes."""
    issues: list[str] = []
    for f in output.findings:
        for field_name in ("title", "evidence", "action"):
            v = getattr(f, field_name) or ""
            for term in FORBIDDEN_OUTPUT_TERMS:
                if _has_term(v, term):
                    issues.append(f"Finding {f.check_id}.{field_name}: '{term}'")
                    v = _scrub(v, term)
                    setattr(f, field_name, v)
    for r in output.recommendations:
        for field_name in ("title", "body"):
            v = getattr(r, field_name) or ""
            for term in FORBIDDEN_OUTPUT_TERMS:
                if _has_term(v, term):
                    issues.append(f"Rec {r.recommendation_id}.{field_name}: '{term}'")
                    v = _scrub(v, term)
                    setattr(r, field_name, v)
    if issues:
        prefix = output.notes + " · " if output.notes else ""
        output.notes = prefix + "linter scrubbed: " + "; ".join(issues[:5])
    return output


def _has_term(text: str, term: str) -> bool:
    """Verbotenes Wort an WORT-GRENZE (nicht Substring) — blockt 'garantiert'/
    'garantie', lässt aber den Rechtsbegriff 'Schutzgarantien'/'geeignete
    Garantien' (Art. 46 DSGVO) passieren."""
    import re as _re
    return bool(_re.search(
        r"\b" + _re.escape(term) + r"\b", text, _re.IGNORECASE))


def _scrub(text: str, term: str) -> str:
    """Case-insensitive replace mit Marker — nur das eigenständige Wort."""
    import re as _re
    return _re.sub(
        r"\b" + _re.escape(term) + r"\b", "[→ neutraler Wortlaut]",
        text, flags=_re.IGNORECASE,
    )


def stable_recommendation_id(canonical_text: str) -> str:
    """Stable hash der canonical recommendation für Dedup-Rollup."""
    h = hashlib.sha256(canonical_text.lower().strip().encode("utf-8"))
    return h.hexdigest()[:16]


class BaseSpecialistAgent(abc.ABC):
    """Abstract base — jeder Agent erbt davon."""

    agent_id: str = ""           # "impressum", "dse", ...
    agent_version: str = "0.0"
    doc_type: str = ""           # Doc-Type den der Agent owned
    owned_mc_ids: tuple[str, ...] = ()

    @abc.abstractmethod
    async def evaluate(self, agent_input: AgentInput) -> AgentOutput:
        """Run the agent. MUST return a complete AgentOutput.
        Implementations should call lint_output() before returning."""
