"""
index_io.py — Lese-/Schreib-Helper für den URL-Hash-Index des NotebookLM Kurators.

Atomares Schreiben via tempfile + os.replace() (Windows-tolerant).
Nur stdlib, keine externen Abhängigkeiten.
"""

import json
import os
import tempfile
from pathlib import Path


_DEFAULT_SCHEMA_VERSION = 1
_DEFAULT_DESCRIPTION = (
    "URL-Hash-Index des NotebookLM Kurators. "
    "Mapping url_hash -> Eintrag. Wird vor jedem source add konsultiert "
    "und nach jedem erfolgreichen Add geschrieben."
)


def load_index(path: Path) -> dict:
    """Liest url-hashes.json und gibt das entries-Dict zurück.

    Bei nicht-existenter Datei: return {}.
    Bei kaputtem JSON: raise ValueError mit klarer Meldung.
    """
    if not path.exists():
        return {}

    try:
        raw = path.read_text(encoding="utf-8")
        data = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise ValueError(
            f"Index-Datei enthält ungültiges JSON ({path}): {exc}"
        ) from exc

    # Provenance-Eskalation (Wave-D Brille F-F2-1 + Memory
    # feedback_provenance_json_null_eskalation): JSON-null/Array/String
    # liefern non-dict-Top-Level. `.get()` würde dann AttributeError statt
    # ValueError werfen — Status-Lüge gegenüber Doku.
    if not isinstance(data, dict):
        raise ValueError(
            f"Index-Datei hat keinen Dict-Top-Level ({path}): "
            f"erwartet Dict, bekam {type(data).__name__}"
        )

    entries = data.get("entries", {})
    if not isinstance(entries, dict):
        raise ValueError(
            f"Index-Datei hat kein gültiges 'entries'-Dict ({path})"
        )
    return entries


def save_index(path: Path, entries: dict) -> None:
    """Schreibt entries atomar in die url-hashes.json.

    schema_version und description werden aus der bestehenden Datei übernommen
    (oder auf Defaults gesetzt, wenn Datei nicht existiert).
    Atomar: tempfile im selben Verzeichnis + os.replace().
    """
    # Bestehende Metadaten lesen, falls vorhanden
    schema_version = _DEFAULT_SCHEMA_VERSION
    description = _DEFAULT_DESCRIPTION

    if path.exists():
        try:
            existing = json.loads(path.read_text(encoding="utf-8"))
            schema_version = existing.get("schema_version", _DEFAULT_SCHEMA_VERSION)
            description = existing.get("description", _DEFAULT_DESCRIPTION)
        except (json.JSONDecodeError, OSError):
            # Datei lesbar? Defaults behalten, damit wir nicht abstürzen
            pass

    payload = {
        "schema_version": schema_version,
        "description": description,
        "entries": entries,
    }

    serialized = json.dumps(payload, ensure_ascii=False, indent=2)

    # Atomar schreiben: temp-Datei im selben Verzeichnis, dann os.replace()
    dir_ = path.parent
    dir_.mkdir(parents=True, exist_ok=True)

    fd, tmp_path = tempfile.mkstemp(dir=dir_, suffix=".tmp", prefix=".index_io_")
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as fh:
            fh.write(serialized)
        os.replace(tmp_path, path)
    except Exception:
        # Aufräumen bei Fehler
        try:
            os.unlink(tmp_path)
        except OSError:
            pass
        raise


def lookup(entries: dict, url_hash: str) -> dict | None:
    """Gibt den Eintrag für url_hash zurück oder None."""
    return entries.get(url_hash)


def record(entries: dict, url_hash: str, payload: dict) -> None:
    """Schreibt einen neuen Eintrag ins entries-Dict (IN-PLACE, nicht auf Disk).

    Weigert sich zu überschreiben: raise ValueError wenn url_hash schon vorhanden,
    mit Meldung die die existierende source_id nennt.
    """
    if url_hash in entries:
        existing_source_id = entries[url_hash].get("source_id", "<unbekannt>")
        raise ValueError(
            f"Hash '{url_hash}' ist bereits registriert "
            f"(source_id: {existing_source_id}). Kein Überschreiben."
        )
    entries[url_hash] = payload
