"""Machine-Check-Definitionen für den Impressum-Agent.

Eine MC = ein abgegrenzter, deterministischer Check über das
Impressum-Dokument. Owner = impressum-agent.

Quelle: § 5 TMG / DDG + § 18 MStV + § 36 VSBG + Art. 14 EU-VO 524/2013.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from typing import Pattern


@dataclass(frozen=True)
class MC:
    """Eine Machine-Check-Definition."""
    mc_id: str                      # IMP-MC-001 ...
    field_id: str                   # name_anbieter, handelsregister, ...
    label: str
    norm: str
    patterns: tuple[Pattern[str], ...] = field(default_factory=tuple)
    severity_if_missing: str = "MEDIUM"  # HIGH | MEDIUM | LOW | INFO
    requires_scope: tuple[str, ...] = field(default_factory=tuple)
    # Opt-out: NICHT anwendbar, wenn eines dieser Tokens im Scope liegt
    # (z.B. Einzelunternehmer für Vertretungsberechtigte). Default = immer
    # anwendbar → Entry-Points ohne legal_form verschlucken nichts.
    excludes_scope: tuple[str, ...] = field(default_factory=tuple)
    # Wenn True: bei Scope-Mismatch nicht-applicable melden, sonst skip
    explicit_na: bool = True
    # Wenn True: fehlt die Angabe → KEIN Finding (z.B. USt-IdNr —
    # Kleinunternehmer §19 haben legitim keine). Nur wenn vorhanden relevant.
    optional: bool = False
    # Wenn True: die Pflicht haengt an der Rechtsform (Handelsregister,
    # Vertretungsberechtigte). Ist die Rechtsform weder im Text noch im
    # Scope bestimmbar → INSUFFICIENT_EVIDENCE statt hartem FAIL.
    legal_form_dependent: bool = False
    # Graubereich: liegt eines dieser Tokens vor (aber NICHT requires_scope),
    # ist die MC NICHT hart anwendbar, sondern POSSIBLY_APPLICABLE — Pruef-
    # Hinweis (severity LOW) statt FAIL. Z.B. Corporate-Blog (§18 MStV evtl.).
    possibly_applies_scope: tuple[str, ...] = field(default_factory=tuple)
    # MC-spezifischer Pruef-Hinweis fuer den POSSIBLY_APPLICABLE-Fall
    # (warum Graubereich + was der Nutzer pruefen soll).
    possibly_hint: str = ""


MCS: tuple[MC, ...] = (
    MC(
        mc_id="IMP-MC-001",
        field_id="name_anbieter",
        label="Name + Anschrift des Anbieters",
        norm="§ 5 Abs. 1 Nr. 1 TMG",
        severity_if_missing="HIGH",
        patterns=(
            re.compile(
                r"\b(?:Anbieter|Diensteanbieter|"
                r"Verantwortlich(?:er Anbieter)?)\s*[:.\s]",
                re.IGNORECASE,
            ),
            # Label-free fallback: Firma (Rechtsform) + Adresse
            re.compile(
                r"\b[A-ZÄÖÜ][\w\-\& ]{1,80}?\s+"
                r"(?:Aktiengesellschaft|GmbH|AG|UG|KG|SE|GbR|OHG|"
                r"Limited|Ltd|LLC)\s*"
                r"[\s\S]{0,400}?"
                r"\b\d{5}\s+[A-ZÄÖÜ]",
                re.IGNORECASE,
            ),
        ),
    ),
    MC(
        mc_id="IMP-MC-002",
        field_id="kontakt_email",
        label="Email-Adresse",
        norm="§ 5 Abs. 1 Nr. 2 TMG",
        severity_if_missing="HIGH",
        patterns=(re.compile(r"\b[\w.+-]+@[\w-]+\.[a-z]{2,}\b",
                              re.IGNORECASE),),
    ),
    MC(
        mc_id="IMP-MC-003",
        field_id="kontakt_telefon",
        label="Telefon",
        norm="§ 5 Abs. 1 Nr. 2 TMG",
        severity_if_missing="MEDIUM",
        patterns=(re.compile(
            r"(?:Tel(?:efon(?:nummer)?)?|Phone|Fon)\.?\s*[:.\s]\s*"
            r"[\+\d][\d\s/\-()]{5,}",
            re.IGNORECASE,
        ),),
    ),
    MC(
        mc_id="IMP-MC-004",
        field_id="handelsregister",
        label="Handelsregister-Eintrag",
        norm="§ 5 Abs. 1 Nr. 4 TMG",
        severity_if_missing="HIGH",
        excludes_scope=("kein_handelsregister",),
        legal_form_dependent=True,
        patterns=(
            re.compile(r"\bHR[BA]\s+\d+", re.IGNORECASE),
            re.compile(r"Handelsregister", re.IGNORECASE),
        ),
    ),
    MC(
        mc_id="IMP-MC-005",
        field_id="ust_id",
        label="USt-IdNr",
        norm="§ 5 Abs. 1 Nr. 6 TMG",
        severity_if_missing="MEDIUM",
        optional=True,
        patterns=(
            re.compile(
                r"\b(?:USt-?Id(?:Nr)?\.?|VAT(?:-?Id)?)\s*[:.\s]",
                re.IGNORECASE,
            ),
            re.compile(r"\bDE\d{9}\b"),
        ),
    ),
    MC(
        mc_id="IMP-MC-006",
        field_id="vertretungsberechtigte",
        label="Vertretungsberechtigte Person",
        norm="§ 5 Abs. 1 Nr. 1 TMG (juristische Personen)",
        severity_if_missing="HIGH",
        excludes_scope=("keine_vertretung",),
        legal_form_dependent=True,
        patterns=(
            re.compile(
                r"(?:Gesch(?:ae|ä)ftsf(?:ue|ü)hr(?:er|ung|erin)|"
                r"Vorstand(?:svorsitzend(?:er|e))?|"
                r"Vertretungsberechtigt|vertreten\s+durch|"
                r"Inhaber(?:in)?|"
                r"Pers(?:ö|oe)nlich\s+haftend)"
                r"\s*[:.\s(]",
                re.IGNORECASE,
            ),
            re.compile(r"\bManagement\s*[:.\s]\s*[A-ZÄÖÜ]",
                         re.IGNORECASE),
            re.compile(r"\bDirector(?:s|en)?\s*[:.\s]\s*[A-ZÄÖÜ]",
                         re.IGNORECASE),
        ),
    ),
    MC(
        mc_id="IMP-MC-007",
        field_id="vertretungsberechtigte_label_korrekt",
        label="Deutsches Label 'Geschäftsführer' statt 'Management'",
        norm="§ 5 Abs. 1 Nr. 1 TMG (Deutsch-Pflicht, gerichtsfest)",
        severity_if_missing="MEDIUM",
        patterns=(re.compile(
            r"(?:Gesch(?:ae|ä)ftsf(?:ue|ü)hr(?:er|ung|erin)|"
            r"Vorstand(?:svorsitzend(?:er|e))?|"
            r"Vertretungsberechtigt|vertreten\s+durch|"
            r"Inhaber(?:in)?|"
            r"Pers(?:ö|oe)nlich\s+haftend)"
            r"\s*[:.\s]",
            re.IGNORECASE,
        ),),
    ),
    MC(
        mc_id="IMP-MC-008",
        field_id="aufsichtsbehoerde",
        label="Aufsichtsbehörde (regulierte Branchen)",
        norm="§ 5 Abs. 1 Nr. 3 TMG (Branchen-bedingt)",
        severity_if_missing="LOW",
        requires_scope=("regulated_profession", "financial_services",
                          "insurance", "automotive"),
        patterns=(
            re.compile(r"Aufsichtsbeh(?:ö|oe)rde\s*[:.\s]",
                         re.IGNORECASE),
            re.compile(
                r"\bBAFin\b|\bBNetzA\b|\bLKA\b|\bKBA\b|"
                r"Kraftfahrt-?Bundesamt|Bundesnetzagentur|"
                r"Bundesanstalt\s+f(?:ü|ue)r",
                re.IGNORECASE,
            ),
        ),
    ),
    MC(
        mc_id="IMP-MC-009",
        field_id="verantwortlicher_redaktion",
        label="Verantwortlicher § 18 MStV (journalistisch-redaktionell)",
        norm="§ 18 MStV (bei Blog/News/Magazin/Newsroom Pflicht)",
        severity_if_missing="MEDIUM",
        requires_scope=("editorial",),
        possibly_applies_scope=("editorial_possible",),
        possibly_hint=(
            "Bei journalistisch-redaktionellen Inhalten (Nachrichten/Magazin) "
            "ist ein Verantwortlicher nach § 18 MStV anzugeben. Bei reinem "
            "Corporate-Blog meist nicht erforderlich — bitte prüfen."
        ),
        patterns=(re.compile(
            r"(?:Verantwortlich(?:er|e)?\s+(?:f(?:ue|ü)r|i\.S\.d\.|"
            r"nach|gem(?:ae|ä)ß)\s+§\s*18|"
            r"V\.i\.S\.d\.\s*§?\s*18|"
            r"redaktionell\s+Verantwortlich)",
            re.IGNORECASE,
        ),),
    ),
    MC(
        mc_id="IMP-MC-010",
        field_id="verbraucher_streitbeilegung",
        label="Verbraucher-Streitbeilegung-Hinweis",
        norm="§ 36 VSBG (Verbraucherverträge über die Website)",
        severity_if_missing="MEDIUM",
        # Hart nur bei echtem Online-Verkauf; reine B2C-Orientierung (z.B.
        # OEM-Markenseite, Verkauf über Händler) = Graubereich → Prüf-Hinweis.
        requires_scope=("ecommerce",),
        possibly_applies_scope=("b2c",),
        possibly_hint=(
            "§ 36 VSBG gilt, wenn auf dieser Seite Verbraucherverträge "
            "geschlossen werden. Bei reiner Marken-/Info-Seite (Verkauf über "
            "Händler/Vertragspartner) meist nicht erforderlich — bitte prüfen."
        ),
        patterns=(re.compile(
            r"(?:Verbraucherschlichtungs|VSBG|"
            r"Streitbeilegung|"
            r"Schlichtungsstelle|"
            r"alternative\s+Streit(?:beilegung|schlichtung))",
            re.IGNORECASE,
        ),),
    ),
    MC(
        mc_id="IMP-MC-011",
        field_id="berufsangaben",
        label="Berufsbezeichnung + Berufsrechtliche Angaben",
        norm="§ 5 Abs. 1 Nr. 5 TMG (Kammerberufe)",
        severity_if_missing="LOW",
        requires_scope=("regulated_profession",),
        patterns=(re.compile(
            r"Berufsbezeichnung|Berufsordnung|Kammer", re.IGNORECASE,
        ),),
    ),
    MC(
        mc_id="IMP-MC-012",
        field_id="odr_link",
        label="OS-Link auf EU-Plattform",
        norm="Art. 14 EU-VO 524/2013 (B2C-Onlineshops)",
        severity_if_missing="MEDIUM",
        requires_scope=("ecommerce",),
        patterns=(re.compile(r"ec\.europa\.eu/consumers/odr",
                              re.IGNORECASE),),
    ),
)


# Public list of all MC-IDs for the Registry
MC_IDS: tuple[str, ...] = tuple(m.mc_id for m in MCS)


def scope_matches(mc: MC, scope: set[str], is_automotive: bool) -> bool:
    """Entscheidet ob die MC auf den Business-Scope anwendbar ist."""
    # Opt-out zuerst: explizit ausgeschlossene Rechtsformen (z.B.
    # Einzelunternehmer für Vertretungsberechtigte) → nicht anwendbar.
    if mc.excludes_scope and any(s in scope for s in mc.excludes_scope):
        return False
    if not mc.requires_scope:
        return True
    if mc.field_id == "aufsichtsbehoerde" and is_automotive:
        return True
    return any(s in scope for s in mc.requires_scope)


_LEGAL_FORM_RE = re.compile(
    r"(?:\bGmbH\b|\bgGmbH\b|\bmbH\b|\bUG\b|\bAG\b|\bSE\b|\bKGaA\b|"
    r"\bKG\b|\bOHG\b|\bGbR\b|\bPartG(?:mbB)?\b|"
    r"\be\.?\s?K(?:fm|fr)?\.?\b|\be\.?\s?V\.?\b|\bStiftung\b|"
    r"\bLtd\.?\b|\bLimited\b|\bLLC\b|\bS\.A\.|\bN\.V\.|\bB\.V\.|"
    r"\bEinzelunternehm\w*|\bKaufmann\b|\bKauffrau\b|\bFreiberuf\w*)",
    re.IGNORECASE,
)


def scope_disposition(mc: MC, scope: set[str], is_automotive: bool) -> str:
    """3-Wege-Anwendbarkeit: 'applies' (hart) | 'possible' (Graubereich) |
    'na' (nicht anwendbar).

    'possible' nur, wenn die MC NICHT hart anwendbar ist, aber ein
    possibly_applies_scope-Token vorliegt (z.B. Corporate-Blog → §18 MStV
    evtl. relevant) → POSSIBLY_APPLICABLE statt FAIL/NA."""
    if mc.excludes_scope and any(s in scope for s in mc.excludes_scope):
        return "na"
    if scope_matches(mc, scope, is_automotive):
        return "applies"
    if mc.possibly_applies_scope and any(
        s in scope for s in mc.possibly_applies_scope
    ):
        return "possible"
    return "na"


def detect_legal_form_present(text: str) -> bool:
    """Nennt der Text ueberhaupt eine Rechtsform?

    Grundlage fuer INSUFFICIENT_EVIDENCE: ohne erkennbare Rechtsform (und
    ohne legal_form im Scope) kann der Agent die Register-/Vertretungs-
    pflicht nicht belastbar behaupten → kein hartes FAIL, sondern
    'unzureichende Evidenz' (User-Vorgabe 2026-06-10: 'Muster Consulting'
    ohne Rechtsform darf kein 'Handelsregister fehlt' ausloesen)."""
    return bool(_LEGAL_FORM_RE.search(text or ""))


def detect_automotive(text: str) -> bool:
    """KFZ-Hersteller/-Vertrieb → triggert KBA-Hint."""
    if re.search(
        r"\b(?:KFZ|Fahrzeug(?:e|herstellung|verkauf)?|Automobil|"
        r"E-Auto|Elektroauto|Auto-?Konfigurator|"
        r"Elektrofahrzeug|Hybrid-?Fahrzeug)\b",
        text, re.IGNORECASE,
    ):
        return True
    return bool(re.search(
        r"\b(?:Tesla|BMW|Mercedes-?Benz|Audi|Volkswagen|Porsche|"
        r"Volvo|Stellantis|Skoda|Seat|Cupra|MINI|Smart|"
        r"Opel|Ford\s+Deutschland|Hyundai|Kia|Toyota|Mazda|"
        r"Nissan|Honda|Subaru|Lexus|Polestar|NIO|BYD|Rivian|"
        r"Lucid)\s+(?:Germany|Deutschland|Group|Holding|AG|"
        r"GmbH|S(?:E|\.A\.))\b",
        text, re.IGNORECASE,
    ))
