# /// script
# requires-python = ">=3.11"
# dependencies = ["pyyaml"]
# ///
"""scan_old_vault.py — Read-only-Scanner für alte Vaults.

Scannt einen bestehenden Vault und gibt eine JSON-Liste der Migrations-Kandidaten
aus. Schreibt niemals in den alten Vault zurück.

CLI-Verwendung:
    python scan_old_vault.py --old-vault /pfad/zum/alten/vault
    python scan_old_vault.py --old-vault /pfad --output kandidaten.json

Nur die fünf Whitelist-Verzeichnisse werden gescannt:
    00 Kontext/, 02 Projekte/, 03 Bereiche/, 04 Ressourcen/, 08 Systemsteuerung/
"""
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

import yaml

# Harte Pfad-Whitelist — verzeichnis-relativ zum --old-vault-Argument.
# Keine Wildcards, kein Glob außerhalb dieser Liste.
ALLOWED_TOP_DIRS: list[str] = [
    "00 Kontext",
    "02 Projekte",
    "03 Bereiche",
    "04 Ressourcen",
    "08 Systemsteuerung",
]

# Frontmatter-Typen, die automatisch aufgenommen werden (unabhängig vom Verzeichnis)
INCLUDED_TYPES: set[str] = {"kontext", "prinzip", "routine"}

# Kategorie-Mapping: Dateiname-Pattern → Kategorie
# Für Dateien unter 00 Kontext/ gilt feinere Zuordnung.
_CONTEXT_CATEGORY_MAP: dict[str, str] = {
    "glossar":      "glossar",
    "schreibstil":  "schreibstil",
    "branding":     "branding",
}


def _parse_frontmatter(text: str) -> dict:
    """Extrahiert das YAML-Frontmatter aus einem Markdown-Text.

    Gibt ein leeres Dict zurück, wenn kein Frontmatter gefunden wird oder
    das YAML-Parsing fehlschlägt. Niemals ein Exception nach außen.
    """
    if not text.startswith("---"):
        return {}
    end = text.find("---", 3)
    if end < 0:
        return {}
    yaml_str = text[3:end].strip()
    try:
        data = yaml.safe_load(yaml_str)
        return data if isinstance(data, dict) else {}
    except yaml.YAMLError:
        return {}


def _determine_category(md_path: Path, old_vault: Path, fm: dict) -> str:
    """Bestimmt die Kategorie eines Kandidaten anhand von Pfad und Frontmatter."""
    try:
        rel = md_path.relative_to(old_vault)
    except ValueError:
        return "kontext-sonstiges"

    parts = rel.parts
    if not parts:
        return "kontext-sonstiges"

    top = parts[0]

    if top == "00 Kontext":
        # Feinere Unterscheidung anhand des Dateinamens (lowercase)
        stem_lower = md_path.stem.lower()
        for key, cat in _CONTEXT_CATEGORY_MAP.items():
            if key in stem_lower:
                return cat
        return "kontext-sonstiges"

    if top == "02 Projekte":
        return "projekt"

    if top == "03 Bereiche":
        return "bereich"

    if top == "04 Ressourcen":
        return "ressource"

    if top == "08 Systemsteuerung":
        fm_type = fm.get("type", "")
        if fm_type == "prinzip":
            return "prinzip"
        if fm_type == "routine":
            return "routine"
        # Ohne Frontmatter-Typ: anhand von Pfad-Teil schätzen
        path_str = str(rel).lower()
        if "prinzip" in path_str:
            return "prinzip"
        if "routine" in path_str:
            return "routine"
        return "kontext-sonstiges"

    return "kontext-sonstiges"


def _should_include(md_path: Path, old_vault: Path, fm: dict) -> bool:
    """Entscheidet, ob eine Datei in die Kandidatenliste aufgenommen wird.

    Aufnahme-Kriterien (OR-Verknüpfung):
    1. Frontmatter type ∈ INCLUDED_TYPES
    2. Datei liegt direkt unter 00 Kontext/ (beliebige Tiefe)
    3. Frontmatter status=aktiv in 02 Projekte/ (Projekte)
    4. Datei liegt unter 03 Bereiche/ oder 04 Ressourcen/ (Verzeichnis-Scan)
    """
    try:
        rel = md_path.relative_to(old_vault)
    except ValueError:
        return False

    parts = rel.parts
    if not parts:
        return False

    top = parts[0]

    # Kriterium 1: Frontmatter-Typ in INCLUDED_TYPES
    if fm.get("type") in INCLUDED_TYPES:
        return True

    # Kriterium 2: Unter 00 Kontext/
    if top == "00 Kontext":
        return True

    # Kriterium 3: Aktive Projekte unter 02 Projekte/
    if top == "02 Projekte":
        return fm.get("status") == "aktiv"

    # Kriterium 4: Bereiche und Ressourcen
    if top in ("03 Bereiche", "04 Ressourcen"):
        return True

    return False


def scan_vault(
    old_vault: Path,
    include_size_limit: bool = False,
) -> list[dict]:
    """Scannt den alten Vault und gibt eine Liste von Kandidaten-Dicts zurück.

    Schreibt NIEMALS in den alten Vault. Nutzt ausschließlich Path.read_text(encoding='utf-8').

    Parameter:
        old_vault:          Absoluter Pfad zum alten Vault-Verzeichnis.
        include_size_limit: Wenn True, werden Dateien > 100 KB ausgeschlossen.

    Gibt:
        Liste von Dicts mit keys: path, type, description, category, size_bytes.
        Leere Liste, wenn keine Kandidaten gefunden.
    """
    SIZE_LIMIT_BYTES = 100 * 1024  # 100 KB

    candidates: list[dict] = []

    for top_dir_name in ALLOWED_TOP_DIRS:
        top_dir = old_vault / top_dir_name
        if not top_dir.is_dir():
            continue
        for md_path in sorted(top_dir.rglob("*.md")):
            if not md_path.is_file():
                continue

            # Größenprüfung
            size_bytes = md_path.stat().st_size
            if include_size_limit and size_bytes > SIZE_LIMIT_BYTES:
                continue

            # Frontmatter lesen — niemals write/append
            try:
                text = md_path.read_text(encoding="utf-8")
            except (OSError, UnicodeDecodeError):
                text = ""

            fm = _parse_frontmatter(text)

            if not _should_include(md_path, old_vault, fm):
                continue

            category = _determine_category(md_path, old_vault, fm)

            candidates.append({
                "path": str(md_path),
                "type": fm.get("type"),
                "description": fm.get("description", ""),
                "category": category,
                "size_bytes": size_bytes,
            })

    return candidates


def main() -> None:
    """Einstiegspunkt für den CLI-Aufruf."""
    # UTF-8-Hardening: stdout und stderr immer UTF-8, Fehler werden ersetzt
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")
    sys.stderr.reconfigure(encoding="utf-8", errors="replace")

    parser = argparse.ArgumentParser(
        description="Read-only-Scanner für alte Vaults. Gibt JSON-Kandidatenliste aus.",
    )
    parser.add_argument(
        "--old-vault",
        required=True,
        metavar="PFAD",
        help="Pfad zum alten Vault-Verzeichnis (Pflicht).",
    )
    parser.add_argument(
        "--output",
        metavar="DATEI",
        default=None,
        help="Ausgabedatei für das JSON (Default: stdout).",
    )
    args = parser.parse_args()

    old_vault = Path(args.old_vault)
    if not old_vault.exists() or not old_vault.is_dir():
        print(
            f"Fehler: --old-vault '{old_vault}' ist kein gültiges Verzeichnis.",
            file=sys.stderr,
        )
        sys.exit(2)

    candidates = scan_vault(old_vault)
    output_json = json.dumps(candidates, ensure_ascii=False, indent=2)

    if args.output:
        Path(args.output).write_text(output_json, encoding="utf-8")
    else:
        print(output_json)


if __name__ == "__main__":
    main()
