"""Kontext eines Auftrags und seine kanonische Serialisierung.

Das ist die INTEGRITÄTSSCHICHT: Sie beantwortet nur "ist der Kontext noch
derselbe?" (SHA-256 über die kanonische Form). Sie beantwortet nicht, ob
jemand berechtigt ist. Autorisierung: authorization.py (HMAC-Clearance).
"""
from __future__ import annotations

import hashlib
import json
import re
from dataclasses import dataclass
from typing import Any, Optional

from .core import PrivacyTier

CONTEXT_VERSION = 1
_HEX64 = re.compile(r"^[0-9a-f]{64}$")
ATTACHMENT_KINDS = frozenset({
    "photo", "document", "voice", "audio", "video", "video_note", "sticker",
})
MAX_NAME_CHARS = 512


def canonical_json(value: Any) -> bytes:
    """Eine einzige kanonische Form: sortierte Schlüssel, keine Leerzeichen,
    UTF-8 ohne ASCII-Escaping, keine NaN/Infinity."""
    return json.dumps(
        value, sort_keys=True, ensure_ascii=False, separators=(",", ":"),
        allow_nan=False,
    ).encode("utf-8")


def sha256_hex(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def _optional_str(value: Any, field_name: str, limit: int = MAX_NAME_CHARS) -> None:
    if value is not None and (not isinstance(value, str) or len(value) > limit or "\x00" in value):
        raise ValueError(f"INVALID_ATTACHMENT_{field_name.upper()}")


@dataclass(frozen=True)
class MemorySnippet:
    """Ein Memory-Ausschnitt aus einer vertrauenswürdigen lokalen Quelle.

    declared_tier stammt von der Quelle (z. B. Brain-Index). Der Klassifikator
    kann die Einstufung nur verschärfen, nie lockern.
    """

    source: str
    text: str
    declared_tier: PrivacyTier = PrivacyTier.UNKNOWN

    def __post_init__(self):
        if not isinstance(self.source, str) or not self.source or len(self.source) > MAX_NAME_CHARS:
            raise ValueError("INVALID_MEMORY_SOURCE")
        if not isinstance(self.text, str):
            raise ValueError("INVALID_MEMORY")
        if not isinstance(self.declared_tier, PrivacyTier):
            raise ValueError("INVALID_MEMORY_TIER")

    def canonical(self) -> dict:
        return {"source": self.source, "text": self.text, "declared_tier": self.declared_tier.value}


@dataclass(frozen=True)
class Attachment:
    """Anhang: Metadaten plus Inhalts-Hash.

    data enthält die Bytes, wenn ein vertrauenswürdiger lokaler Lader sie
    bereitgestellt hat; dann wird der Hash IMMER aus den Bytes berechnet.
    Ohne Bytes bindet der vom Transport gemeldete sha256 bzw. die
    Telegram-file_id den Inhalt.
    """

    kind: str
    file_id: str = ""
    name: Optional[str] = None
    mime: Optional[str] = None
    size: Optional[int] = None
    sha256: Optional[str] = None
    local_path: Optional[str] = None
    data: Optional[bytes] = None

    def __post_init__(self):
        if self.kind not in ATTACHMENT_KINDS:
            raise ValueError("INVALID_ATTACHMENT_KIND")
        if not isinstance(self.file_id, str) or len(self.file_id) > MAX_NAME_CHARS:
            raise ValueError("INVALID_ATTACHMENT_FILE_ID")
        _optional_str(self.name, "name")
        _optional_str(self.mime, "mime", 255)
        _optional_str(self.local_path, "local_path", 4096)
        if self.size is not None and (type(self.size) is not int or self.size < 0):
            raise ValueError("INVALID_ATTACHMENT_SIZE")
        if self.sha256 is not None and (not isinstance(self.sha256, str) or not _HEX64.match(self.sha256)):
            raise ValueError("INVALID_ATTACHMENT_DIGEST")
        if self.data is not None:
            if type(self.data) is not bytes:
                raise ValueError("INVALID_ATTACHMENT_DATA")
            if self.sha256 is not None and sha256_hex(self.data) != self.sha256:
                raise ValueError("ATTACHMENT_HASH_MISMATCH")
            if self.size is not None and self.size != len(self.data):
                raise ValueError("ATTACHMENT_SIZE_MISMATCH")

    def content_sha256(self) -> Optional[str]:
        if self.data is not None:
            return sha256_hex(self.data)
        return self.sha256

    def effective_size(self) -> Optional[int]:
        if self.data is not None:
            return len(self.data)
        return self.size

    def canonical(self) -> dict:
        return {
            "kind": self.kind,
            "file_id": self.file_id,
            "name": self.name,
            "mime": self.mime,
            "size": self.effective_size(),
            "content_sha256": self.content_sha256(),
            "local_path": self.local_path,
            "content_loaded": self.data is not None,
        }


@dataclass(frozen=True)
class ContextEnvelope:
    prompt: str
    memory: tuple[MemorySnippet, ...] = ()
    attachments: tuple[Attachment, ...] = ()

    def __post_init__(self):
        if not isinstance(self.prompt, str) or "\x00" in self.prompt:
            raise ValueError("INVALID_PROMPT")
        if not isinstance(self.memory, tuple) or any(not isinstance(m, MemorySnippet) for m in self.memory):
            raise ValueError("INVALID_MEMORY")
        if not isinstance(self.attachments, tuple) or any(not isinstance(a, Attachment) for a in self.attachments):
            raise ValueError("INVALID_ATTACHMENTS")

    def canonical_dict(self) -> dict:
        return {
            "v": CONTEXT_VERSION,
            "prompt": self.prompt,
            "memory": [m.canonical() for m in self.memory],
            "attachments": [a.canonical() for a in self.attachments],
        }

    def canonical(self) -> bytes:
        return canonical_json(self.canonical_dict())

    def digest(self) -> str:
        """Integritäts-Digest des Kontexts (kein Berechtigungsnachweis)."""
        return sha256_hex(b"aria-v3/context/v1\n" + self.canonical())

    def size_bytes(self) -> int:
        total = len(self.prompt.encode("utf-8"))
        total += sum(len(m.text.encode("utf-8")) for m in self.memory)
        total += sum(a.effective_size() or 0 for a in self.attachments if a.data is not None)
        return total
