"""Kanonisierung und domaenengetrennte Ed25519-Signaturen.

Warum eine eigene Datei: Signieren und Verifizieren muessen auf Client- und
Master-Seite bitidentisch arbeiten. Im Vorgaengerprojekt wurde das durch eine
handkopierte ``licensing.py`` in beiden Repos geloest -- hier ist es *ein*
installierbares Paket, das beide Anwendungen als Abhaengigkeit einbinden.

Sicherheitsrelevante Abweichung vom Vorgaenger: jede Signatur ist
**domaenengetrennt**. Vor die kanonischen Bytes wird ein Kontext-Praefix
gestellt (``SIG_CONTEXT_*``). Eine Signatur ueber eine Lizenz-Payload kann
dadurch nie als Signatur ueber eine Aktivierungsantwort durchgehen, selbst wenn
ein Angreifer Feldnamen so waehlen koennte, dass zwei Strukturen identische
kanonische Bytes ergeben.
"""

from __future__ import annotations

import base64
import binascii
import json
from typing import Any, Mapping

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric import ed25519

# Domaenen-Praefixe. Aenderungen hieran sind ein Protokollbruch.
SIG_CONTEXT_LICENSE = b"tesm/license-file/v2\x00"
SIG_CONTEXT_REQUEST = b"tesm/client-request/v2\x00"
SIG_CONTEXT_RESPONSE = b"tesm/master-response/v2\x00"


def canonical_bytes(payload: Mapping[str, Any]) -> bytes:
    """Deterministische JSON-Serialisierung: sortierte Schluessel, keine Whitespaces.

    ``ensure_ascii=True`` (Default) ist hier gewollt: die Bytes bleiben damit
    unabhaengig von der Unicode-Normalform der Eingabe reproduzierbar.
    """
    return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")


def _b64d(value: str, *, field: str) -> bytes:
    try:
        return base64.b64decode(value, validate=True)
    except (binascii.Error, ValueError, TypeError) as exc:  # pragma: no cover - defensiv
        raise ValueError(f"{field} ist kein gueltiges Base64") from exc






def sign(payload: Mapping[str, Any], private_key_b64: str, context: bytes) -> str:
    """Signiert ``payload`` domaenengetrennt. Rueckgabe: Base64-Signatur."""
    key = ed25519.Ed25519PrivateKey.from_private_bytes(
        _b64d(private_key_b64, field="private_key")
    )
    return base64.b64encode(key.sign(context + canonical_bytes(payload))).decode("ascii")


def verify(
    payload: Mapping[str, Any],
    signature_b64: str,
    public_key_b64: str,
    context: bytes,
) -> bool:
    """Verifiziert eine Signatur.

    Gibt ``False`` zurueck statt zu werfen -- der Aufrufer kann "ungueltig" so
    nie mit "abgestuerzt" verwechseln. Das war im Vorgaengerprojekt bereits eine
    bewusste Entscheidung und bleibt es.
    """
    if not signature_b64 or not public_key_b64:
        return False
    try:
        key = ed25519.Ed25519PublicKey.from_public_bytes(
            _b64d(public_key_b64, field="public_key")
        )
        key.verify(_b64d(signature_b64, field="signature"), context + canonical_bytes(payload))
        return True
    except (InvalidSignature, ValueError, TypeError, KeyError):
        return False


def encode_code(data: Mapping[str, Any]) -> str:
    """Kodiert ein Protokollobjekt als kopierbaren Offline-Code (Base64url, ohne Padding)."""
    raw = base64.urlsafe_b64encode(canonical_bytes(data)).decode("ascii")
    return raw.rstrip("=")


def decode_code(code: str) -> dict[str, Any]:
    """Dekodiert einen Offline-Code. Toleriert fehlendes Padding und Whitespace/Umbrueche."""
    cleaned = "".join(code.split())
    if not cleaned:
        raise ValueError("Leerer Code")
    cleaned += "=" * (-len(cleaned) % 4)
    try:
        data = json.loads(base64.urlsafe_b64decode(cleaned.encode("ascii")).decode("utf-8"))
    except (binascii.Error, ValueError, UnicodeDecodeError) as exc:
        raise ValueError("Code ist unlesbar oder beschaedigt") from exc
    if not isinstance(data, dict):
        raise ValueError("Code enthaelt kein Objekt")
    return data
