"""Deterministische Klassifikatoren: Datenschutzklasse und Aufgabenklasse.

Grundsatz: Default ist UNKNOWN. Regeln können nur verschärfen.
- Secrets/Token, Kunden-/Partnernamen, personenbezogene Daten -> CONFIDENTIAL
- Interne Merkmale (Pfade, "intern") -> INTERNAL
- PUBLIC nur konservativ: ausdrückliche Markierung am Anfang des Prompts
  (#public / #öffentlich / [public]), und nur, wenn KEINE andere Regel greift,
  keine Anhänge vorliegen und alle Memory-Ausschnitte selbst PUBLIC sind.

Die Markierung ist eine Dateneinstufung durch den Absender, keine
Autorisierung: Wer Codex nutzen darf, entscheiden Authentifizierung,
Policy-Matrix und HMAC-Clearance.
"""
from __future__ import annotations

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

from .context import Attachment, ContextEnvelope, MemorySnippet
from .core import PrivacyTier, most_sensitive

# Secret-Muster. Werden auch von result_check (Schwärzung) benutzt.
SECRET_RULES: tuple[tuple[str, Pattern[str]], ...] = (
    ("secret.private_key_block", re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)")),
    # Grenzen über Zeichenklassen statt \b: Tokens dürfen auf "-" enden und stehen
    # in URLs direkt hinter "bot" (https://api.telegram.org/bot<ID>:<TOKEN>/...).
    ("secret.telegram_bot_token", re.compile(r"(?<![0-9])\d{8,10}:[A-Za-z0-9_-]{35}(?![A-Za-z0-9_-])")),
    ("secret.anthropic_key", re.compile(r"\bsk-ant-[A-Za-z0-9_-]{20,}")),
    ("secret.openai_key", re.compile(r"\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}")),
    ("secret.github_token", re.compile(r"\b(?:ghp|gho|ghs|ghu|ghr)_[A-Za-z0-9]{30,}|\bgithub_pat_[A-Za-z0-9_]{20,}")),
    ("secret.slack_token", re.compile(r"\bxox[abposr]-[A-Za-z0-9-]{10,}")),
    ("secret.aws_access_key", re.compile(r"\b(?:AKIA|ASIA)[0-9A-Z]{16}\b")),
    ("secret.jwt", re.compile(r"\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}")),
    ("secret.supabase_key", re.compile(r"\bsb_(?:secret|publishable)_[A-Za-z0-9_-]{10,}")),
    ("secret.bearer", re.compile(r"(?i)\bbearer\s+[A-Za-z0-9._~+/=-]{20,}")),
    ("secret.assignment", re.compile(
        r"(?i)\b(?:api[_-]?key|secret|token|password|passwort|kennwort|pwd|client[_-]?secret)\b"
        r"\s*[:=]\s*['\"]?(?!\[GESCHWÄRZT:)[^\s'\"]{8,}")),
)

# Wortgrenzen über Buchstaben statt \b: "_" und Ziffern trennen, damit
# Dateinamen und Bezeichner (BMW_Angebot.pdf, Kundendaten_2026) erkannt werden.
_L = r"(?<![A-Za-zÄÖÜäöüß])"
_R = r"(?![A-Za-zÄÖÜäöüß])"

PII_RULES: tuple[tuple[str, Pattern[str]], ...] = (
    ("pii.email", re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}\b")),
    ("pii.phone", re.compile(r"(?<![\w+])(?:\+|00)[1-9]\d(?:[ ./-]?\d){6,13}\b|(?<![\w+])0\d{2,5}[ /-]?\d{4,10}\b")),
    ("pii.iban", re.compile(r"\b[A-Z]{2}\d{2}(?: ?[A-Z0-9]{4}){3,7}(?: ?[A-Z0-9]{1,3})?\b")),
    ("pii.keyword", re.compile(
        _L + r"(?:personenbezogen|dsgvo|gdpr|geburtsdatum|geburtstag|steuer-?id|steuernummer|"
        r"sozialversicherung|krankheit|diagnose|gehalt|gehälter|lohnabrechnung|kontonummer|"
        r"personalausweis|reisepass|patient|mitarbeiterdaten|personaldaten|"
        r"privatadresse|wohnadresse|social security)", re.IGNORECASE)),
)

DEFAULT_CONFIDENTIAL_KEYWORDS: tuple[tuple[str, str], ...] = (
    # Kunden-/Partnernamen als Präfix (konservativ): BMW, BMW_Angebot, BMWGroup.
    ("customer.bmw", _L + r"bmw"),
    ("customer.adesso", _L + r"adesso"),
    ("customer.supplierpulse", _L + r"supplier[\s_-]*pulse"),
    ("customer.kundendaten", _L + r"kund(?:e|in)"),
    ("customer.customer", _L + r"customers?" + _R),
    ("customer.mandant", _L + r"mandant"),
    ("confidential.marker", _L + r"(?:vertraulich|confidential|geheim|verschwiegenheit|nda" + _R + r")"),
)

DEFAULT_INTERNAL_KEYWORDS: tuple[tuple[str, str], ...] = (
    ("internal.path", r"(?:/home/aria\b|/root\b|/etc/|~/\.claude|\.env\b)"),
    ("internal.marker", _L + r"(?:intern|interne[nmrs]?|internal|nicht öffentlich)" + _R),
)

PUBLIC_MARKER = re.compile(r"^\s*(?:#public|#öffentlich|#oeffentlich|\[public\])(?=\s|$)", re.IGNORECASE)


@dataclass(frozen=True)
class PrivacyAssessment:
    tier: PrivacyTier
    rule_ids: tuple[str, ...]
    parts: tuple[tuple[str, PrivacyTier], ...]


def _compile(entries: Iterable[tuple[str, str]], prefix: str) -> tuple[tuple[str, Pattern[str]], ...]:
    compiled = []
    for rule_id, pattern in entries:
        if not rule_id.startswith(prefix):
            rule_id = prefix + rule_id
        compiled.append((rule_id, re.compile(pattern, re.IGNORECASE)))
    return tuple(compiled)


class PrivacyClassifier:
    def __init__(self, *, public_marker_enabled: bool = True,
                 extra_confidential_keywords: Iterable[str] = (),
                 extra_internal_keywords: Iterable[str] = ()):
        self.public_marker_enabled = bool(public_marker_enabled)
        extra_conf = tuple((f"custom.{i}", _L + re.escape(k) + _R)
                           for i, k in enumerate(extra_confidential_keywords))
        extra_int = tuple((f"custom.{i}", _L + re.escape(k) + _R)
                          for i, k in enumerate(extra_internal_keywords))
        self._confidential = SECRET_RULES + PII_RULES + _compile(
            DEFAULT_CONFIDENTIAL_KEYWORDS + extra_conf, "")
        self._internal = _compile(DEFAULT_INTERNAL_KEYWORDS + extra_int, "")

    def _scan(self, text: str) -> tuple[list[str], list[str]]:
        confidential = [rule for rule, pattern in self._confidential if pattern.search(text)]
        internal = [rule for rule, pattern in self._internal if pattern.search(text)]
        return confidential, internal

    def _classify_text(self, text: str, *, allow_public: bool) -> tuple[PrivacyTier, list[str]]:
        confidential, internal = self._scan(text)
        if confidential:
            return PrivacyTier.CONFIDENTIAL, confidential + internal
        if internal:
            return PrivacyTier.INTERNAL, internal
        if allow_public:
            return PrivacyTier.PUBLIC, ["public.marker"]
        return PrivacyTier.UNKNOWN, []

    def has_public_marker(self, prompt: str) -> bool:
        return self.public_marker_enabled and bool(PUBLIC_MARKER.match(prompt))

    def _classify_memory(self, snippet: MemorySnippet) -> tuple[PrivacyTier, list[str]]:
        # Review #16: render_prompt uebertraegt auch die QUELLENBEZEICHNUNG (z. B. "BMW-Geheimprojekt")
        # an das Backend -> sie muss mitklassifiziert werden, sonst umgeht ein vertraulicher Quellname
        # die Datenschutzmatrix. Text UND source scannen und die sensiblere Einstufung nehmen.
        text_tier, text_rules = self._classify_text(snippet.text, allow_public=False)
        source_tier, source_rules = self._classify_text(snippet.source, allow_public=False)
        tier = most_sensitive((text_tier, source_tier))
        rules = text_rules + [f"source.{r}" for r in source_rules]
        if tier is PrivacyTier.UNKNOWN and snippet.declared_tier is not PrivacyTier.UNKNOWN:
            # Quelle darf einstufen, der Klassifikator nur verschärfen.
            return snippet.declared_tier, rules + [f"memory.declared_{snippet.declared_tier.value}"]
        if tier is not PrivacyTier.CONFIDENTIAL and snippet.declared_tier is PrivacyTier.CONFIDENTIAL:
            return PrivacyTier.CONFIDENTIAL, rules + ["memory.declared_confidential"]
        return tier, rules

    def _classify_attachment(self, attachment: Attachment) -> tuple[PrivacyTier, list[str]]:
        metadata = " ".join(v for v in (attachment.name, attachment.mime) if v)
        tier, rules = self._classify_text(metadata, allow_public=False)
        if attachment.data is not None:
            try:
                decoded = attachment.data.decode("utf-8")
            except UnicodeDecodeError:
                decoded = None
            if decoded is not None:
                content_tier, content_rules = self._classify_text(decoded, allow_public=False)
                tier = most_sensitive((tier, content_tier))
                rules = rules + content_rules
        # Ein Anhang ist nie PUBLIC: Inhalt ist nicht vollständig prüfbar.
        if tier is PrivacyTier.PUBLIC:
            tier = PrivacyTier.UNKNOWN
        return tier, rules + ["attachment.never_public"]

    def classify(self, envelope: ContextEnvelope) -> PrivacyAssessment:
        if not isinstance(envelope, ContextEnvelope):
            raise TypeError("INVALID_ENVELOPE")
        parts: list[tuple[str, PrivacyTier]] = []
        rules: list[str] = []
        prompt_tier, prompt_rules = self._classify_text(
            envelope.prompt, allow_public=self.has_public_marker(envelope.prompt))
        parts.append(("prompt", prompt_tier))
        rules.extend(prompt_rules)
        for index, snippet in enumerate(envelope.memory):
            tier, snippet_rules = self._classify_memory(snippet)
            parts.append((f"memory[{index}]", tier))
            rules.extend(snippet_rules)
        for index, attachment in enumerate(envelope.attachments):
            tier, attachment_rules = self._classify_attachment(attachment)
            parts.append((f"attachment[{index}]", tier))
            rules.extend(attachment_rules)
        overall = most_sensitive(tier for _, tier in parts)
        return PrivacyAssessment(overall, tuple(sorted(set(rules))), tuple(parts))


# ---------------------------------------------------------------------------
# Aufgabenklasse und benötigte Fähigkeiten
# ---------------------------------------------------------------------------

TASK_RULES: tuple[tuple[str, Pattern[str]], ...] = (
    ("review", re.compile(r"(?i)\b(?:review\w*|code-?review|gegenles\w*|begutacht\w*|prüf\w*|pruef\w*)\b")),
    ("coding", re.compile(
        r"(?i)```|\b(?:code|coden|python|javascript|typescript|bash|shell|skript\w*|script\w*|"
        r"funktion\w*|function\w*|bug\w*|debug\w*|stack ?trace|exception|traceback|refactor\w*|"
        r"kompilier\w*|compile\w*|regex|sql|endpoint\w*|unit-?test\w*|pytest|git|commit\w*|"
        r"programmier\w*|implementier\w*)\b")),
    ("planning", re.compile(r"(?i)\b(?:plan\w*|roadmap|strategie\w*|konzept\w*|architektur\w*)\b")),
    ("writing", re.compile(
        r"(?i)\b(?:schreib\w*|formulier\w*|text\w*|mail\w*|e-mail\w*|brief\w*|übersetz\w*|"
        r"uebersetz\w*|zusammenfass\w*)\b")),
)

IMAGE_KINDS = frozenset({"photo"})


@dataclass(frozen=True)
class TaskAssessment:
    task_class: str
    capabilities: frozenset[str]
    rule: str


def classify_task(envelope: ContextEnvelope, default_task: str = "analysis") -> TaskAssessment:
    capabilities = {"text"}
    has_image = any(a.kind in IMAGE_KINDS or (a.mime or "").startswith("image/") for a in envelope.attachments)
    has_file = any(not (a.kind in IMAGE_KINDS or (a.mime or "").startswith("image/")) for a in envelope.attachments)
    if has_image:
        capabilities.add("vision")
    if has_file:
        capabilities.add("files")
    if has_image:
        return TaskAssessment("vision", frozenset(capabilities), "task.image_attachment")
    for task, pattern in TASK_RULES:
        if pattern.search(envelope.prompt):
            if task in ("coding", "review"):
                capabilities.add("code")
            return TaskAssessment(task, frozenset(capabilities), f"task.{task}")
    return TaskAssessment(default_task, frozenset(capabilities), "task.default")
