"""
backup.py — Backup-Helper für den NotebookLM Kurator.

Zweck: Schreibt vor jedem geplanten Löschen einer NotebookLM-Quelle ein
vollständiges Backup nach `<backups_root>/<notebook-slug>/<YYYY-MM-DD>/`.
Das Backup enthält Fulltext, Metadaten, Löschgrund und einen Zeitstempel.

Zwei Haupt-Funktionen:
- `slugify_ascii(text, max_len)`: Erzeugt einen ASCII-Slug für Dateinamen.
- `write_backup(...)`: Schreibt die Backup-Datei atomar und verweigert Overwrite.

Wichtige Ausnahme-Regel:
`slugify_ascii` ist die EINZIGE Stelle im gesamten Kurator-Skill, an der
ASCII-Transliteration (ä→ae usw.) erlaubt ist. Grund: Das `notebooklm` CLI
zeigt auf Windows Umlaut-Quirks in Dateinamen. Darum bekommen nur
Backup-Dateinamen einen ASCII-Slug. Der Original-Titel mit Umlauten bleibt im
Backup-Frontmatter (`notebooklm-original-title`) und im Body (H1) unverändert
erhalten. Überall sonst im Skill gilt: echte Unicode-Umlaute verwenden.

`backups_root` ist Pflicht-Parameter — kein impliziter Vault-Default.
Der Aufrufer übergibt typischerweise `cfg.paths.kurator_state_root / "backups"`.

Atomares Schreiben via tempfile + os.replace() (gleiches Muster wie
`index_io.py`, Windows-tolerant). Nur stdlib, keine externen Abhängigkeiten.
"""

import os
import re
import tempfile
from datetime import datetime
from pathlib import Path


# Umlaut-Transliterationstabelle — nur für slugify_ascii zugelassen.
_UMLAUT_MAP = {
    "ä": "ae",
    "ö": "oe",
    "ü": "ue",
    "Ä": "Ae",
    "Ö": "Oe",
    "Ü": "Ue",
    "ß": "ss",
}


def slugify_ascii(text: str, max_len: int = 60) -> str:
    """Erzeugt einen ASCII-Slug für Dateinamen.

    **Einzige Stelle im Kurator-Skill, an der ASCII-Transliteration erlaubt
    ist.** Das `notebooklm` CLI zeigt auf Windows Umlaut-Quirks in
    Dateinamen, darum bekommen nur Backup-Dateinamen einen ASCII-Slug. Der
    Original-Titel mit Umlauten bleibt im Backup-Frontmatter unter
    `notebooklm-original-title` und im Body als H1-Heading unverändert
    erhalten.

    Regeln:
    - Umlaute werden transliteriert: ä→ae, ö→oe, ü→ue, Ä→Ae, Ö→Oe, Ü→Ue, ß→ss.
    - Alle anderen Nicht-ASCII-Zeichen werden zu `-` ersetzt.
    - Nur `[a-zA-Z0-9-]` bleibt erhalten, Rest (Leerzeichen, Satzzeichen) zu `-`.
    - Mehrfache `-` werden zu einem `-` zusammengefasst.
    - Führende und abschließende `-` werden entfernt.
    - Ergebnis wird komplett kleingeschrieben.
    - Auf `max_len` gekürzt, möglichst am letzten `-` vor `max_len`.
    - Wenn das Ergebnis leer wäre, wird `"untitled"` zurückgegeben.
    """
    if text is None:
        return "untitled"

    # 1) Umlaute transliterieren
    out_chars = []
    for ch in text:
        if ch in _UMLAUT_MAP:
            out_chars.append(_UMLAUT_MAP[ch])
        else:
            out_chars.append(ch)
    transliterated = "".join(out_chars)

    # 2) Nicht-ASCII-Zeichen durch `-` ersetzen
    ascii_only = "".join(c if ord(c) < 128 else "-" for c in transliterated)

    # 3) Nur [a-zA-Z0-9-] behalten, Rest zu `-`
    cleaned = re.sub(r"[^a-zA-Z0-9-]", "-", ascii_only)

    # 4) Mehrfache `-` collapsen
    cleaned = re.sub(r"-+", "-", cleaned)

    # 5) Führende/abschließende `-` entfernen
    cleaned = cleaned.strip("-")

    # 6) Kleinbuchstaben
    cleaned = cleaned.lower()

    # 7) Wenn leer: "untitled"
    if not cleaned:
        return "untitled"

    # 8) Auf max_len kürzen, möglichst am letzten `-` vor max_len
    if len(cleaned) > max_len:
        candidate = cleaned[:max_len]
        last_dash = candidate.rfind("-")
        if last_dash > 0:
            cleaned = candidate[:last_dash]
        else:
            cleaned = candidate
        # Nach dem Kürzen erneut trimmen, falls ein `-` am Rand entstanden ist
        cleaned = cleaned.strip("-")
        if not cleaned:
            return "untitled"

    return cleaned


def _escape_quoted(value: str) -> str:
    """Escaped Anführungszeichen und Backslashes für Quoted-YAML-Strings."""
    return value.replace("\\", "\\\\").replace('"', '\\"')


def _now_berlin_iso() -> tuple[str, str]:
    """Gibt (date_only, iso_with_offset) für Europe/Berlin zurück.

    date_only: "YYYY-MM-DD"
    iso_with_offset: "YYYY-MM-DDTHH:MM:SS+02:00" (Offset je nach Sommer-/Winterzeit)
    """
    try:
        from zoneinfo import ZoneInfo
        now = datetime.now(ZoneInfo("Europe/Berlin"))
    except Exception:
        # Fallback: lokale Systemzeit
        now = datetime.now().astimezone()

    date_only = now.strftime("%Y-%m-%d")
    offset = now.strftime("%z")  # "+0200"
    offset_formatted = f"{offset[:3]}:{offset[3:]}" if offset else "+00:00"
    iso_with_offset = now.strftime("%Y-%m-%dT%H:%M:%S") + offset_formatted
    return date_only, iso_with_offset


def write_backup(
    notebook_id: str,
    notebook_slug: str,
    source_id: str,
    fulltext: str,
    metadata: dict,
    reason: str,
    backups_root: Path,
) -> Path:
    """Schreibt ein Backup einer NotebookLM-Quelle vor dem Löschen.

    Zielpfad:
        backups_root / notebook_slug / <YYYY-MM-DD heute> / <source_id>__<ascii-slug>.md

    `backups_root` ist Pflicht-Parameter — kein impliziter Vault-Default.

    Verhalten:
    - Parent-Verzeichnisse werden angelegt (`mkdir(parents=True, exist_ok=True)`).
    - Wenn die Zieldatei bereits existiert, wird FileExistsError geworfen —
      niemals überschreiben.
    - Frontmatter wird als Template-String geschrieben, KEIN PyYAML.
    - Der Original-Titel mit Umlauten steht im Frontmatter unter
      `notebooklm-original-title` und als H1-Heading im Body. Nur der Dateiname
      bekommt einen ASCII-Slug.
    - Atomares Schreiben via tempfile + os.replace() im Zielverzeichnis.
    - Encoding: UTF-8.

    Rückgabe: der Path der geschriebenen Backup-Datei.
    """
    backups_root = Path(backups_root)
    date_only, iso_ts = _now_berlin_iso()

    # Path-Härtung gegen Traversal (Wave-D Brille-H H.1, OWASP A01/A03 Defense-
    # in-Depth): notebook_slug und source_id kommen via Caller von außen, eine
    # bösartige oder fehlerhafte API-Response mit `../..` würde den Zielpfad
    # sprengen. Wir härten beide gegen Regex `^[A-Za-z0-9._-]+$`.
    _SAFE_SEGMENT = re.compile(r"^[A-Za-z0-9._-]+$")
    if not _SAFE_SEGMENT.match(notebook_slug):
        raise ValueError(
            f"notebook_slug enthält unzulässige Zeichen "
            f"(erlaubt [A-Za-z0-9._-]): {notebook_slug!r}"
        )
    if not _SAFE_SEGMENT.match(source_id):
        raise ValueError(
            f"source_id enthält unzulässige Zeichen "
            f"(erlaubt [A-Za-z0-9._-]): {source_id!r}"
        )

    title = metadata.get("title", "")
    url = metadata.get("url", "")
    url_hash = metadata.get("url_hash", "")

    title_slug = slugify_ascii(title)
    filename = f"{source_id}__{title_slug}.md"

    target_dir = backups_root / notebook_slug / date_only
    target_dir.mkdir(parents=True, exist_ok=True)
    target_path = target_dir / filename

    if target_path.exists():
        raise FileExistsError(
            f"Backup existiert bereits und wird nicht überschrieben: {target_path}"
        )

    # Frontmatter-Template. Quoted-Strings für Felder mit möglichen Sonderzeichen.
    title_escaped = _escape_quoted(title)
    url_escaped = _escape_quoted(url)
    reason_escaped = _escape_quoted(reason)

    frontmatter = (
        "---\n"
        "type: backup\n"
        'description: "Backup einer NotebookLM-Quelle vor geplantem Delete. '
        'Vollständiger Fulltext, Metadaten, Löschgrund."\n'
        "tags:\n"
        "  - notebooklm\n"
        "  - kurator\n"
        "  - backup\n"
        "  - vor-delete\n"
        f"created: {date_only}\n"
        f"notebooklm-notebook-id: {notebook_id}\n"
        f"notebooklm-source-id: {source_id}\n"
        f'notebooklm-original-title: "{title_escaped}"\n'
        f'notebooklm-original-url: "{url_escaped}"\n'
        f"notebooklm-url-hash: {url_hash}\n"
        f'backup-reason: "{reason_escaped}"\n'
        f"backup-time: {iso_ts}\n"
        'ascii-transliteration-warning: "Dateiname enthält ASCII-Slug wegen '
        'Windows-CLI-Quirks. Original-Titel siehe notebooklm-original-title."\n'
        "---\n"
        "\n"
        f"# {title}\n"
        "\n"
        f"{fulltext}\n"
    )

    # Atomar schreiben: tempfile im Zielverzeichnis, dann os.replace()
    fd, tmp_path = tempfile.mkstemp(dir=target_dir, suffix=".tmp", prefix=".backup_")
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as fh:
            fh.write(frontmatter)
        os.replace(tmp_path, target_path)
    except Exception:
        try:
            os.unlink(tmp_path)
        except OSError:
            pass
        raise

    return target_path
