"""
inbox_scan.py — Inbox-Scanner für den NotebookLM-Kurator.

Zweck:
    Iteriert rekursiv über alle Text-Dateien in der Inbox-Wurzel und liefert
    pro Datei ein Dict mit Frontmatter, Body, Zeichenzahl, extrahierten URLs
    und dem bisherigen notebooklm-status. Zusätzlich setzt `update_marker`
    schonend die notebooklm-*-Frontmatter-Felder, ohne den Rest der Datei
    anzutasten.

Unterstützte Extensions: .md, .markdown, .txt, .html, .htm. Word-Dateien
(.doc, .docx) werden bewusst **nicht** unterstützt — die landen beim
manuellen Inbox-Bearbeitungs-Prozess des Nutzers, nicht beim Kurator.

Eiserne Regeln (nicht verhandelbar):
    1. Der Scanner **liest und markiert** Inbox-Dateien, **verschiebt oder
       löscht sie selbst nicht**. Löschen oder Verschieben findet am Ende
       eines Kurator-Laufs nur nach expliziter Nutzer-Entscheidung pro Datei
       im Post-Processing-Schritt statt (siehe SKILL.md).
       Dieser Helfer enthält keine Delete- oder Move-Funktion.
    2. Bestehendes Frontmatter wird gemerged, niemals überschrieben.
       `update_marker` darf ausschließlich `notebooklm-*`-Keys und das
       `updated`-Feld setzen. Alle anderen Felder bleiben bitgenau erhalten
       (Reihenfolge, Werte, Quoting soweit möglich).
    3. Bei Dateien ohne Frontmatter (typischer Fall für .txt oder .html)
       legt `update_marker` einen frischen Frontmatter-Block am Anfang der
       Datei an, ohne den bestehenden Body zu verändern.
    4. Encoding ist durchgehend UTF-8, deutsche Umlaute bleiben erhalten.
    5. Hidden Files (Präfix `.`) und Symlinks werden übersprungen.
    6. Kein PyYAML. Minimaler Parser für einfache Key-Value-Paare und
       Block-Listen reicht für die Inbox-Struktur.

Öffentliche API:
    scan_inbox(inbox_path)  — liefert list[dict] pro Inbox-Datei
    update_marker(path, marker) — merged notebooklm-*-Felder ins Frontmatter

Intern:
    _parse_frontmatter(content)    — (dict, body_string)
    _serialize_frontmatter(fm)     — YAML-Block inkl. --- Marker
"""

from __future__ import annotations

import os
import tempfile
from datetime import date
from pathlib import Path

from url_extract import extract_urls


# ---------------------------------------------------------------------------
# Unterstützte Extensions
# ---------------------------------------------------------------------------

# Reine Textformate, die der Kurator verarbeitet. Case-insensitive matching.
# Word-Formate (.doc, .docx) sind bewusst ausgeschlossen — die gehen in den
# manuellen Inbox-Workflow, nicht in den Kurator.
_SUPPORTED_EXTENSIONS = {".md", ".markdown", ".txt", ".html", ".htm"}


# ---------------------------------------------------------------------------
# Frontmatter-Parser (minimal, ohne PyYAML)
# ---------------------------------------------------------------------------

def _strip_quotes(value: str) -> str:
    """Entfernt umschließende einfache oder doppelte Anführungszeichen."""
    value = value.strip()
    if len(value) >= 2:
        if (value[0] == value[-1]) and value[0] in ("'", '"'):
            return value[1:-1]
    return value


def _parse_scalar(value: str) -> object:
    """Wandelt einen YAML-Skalar in einen Python-Wert um.

    Unterstützt: Strings (mit oder ohne Quotes), Integer, Boolean, null.
    Alles andere bleibt String.
    """
    raw = value.strip()
    if raw == "":
        return ""
    # Null
    if raw in ("null", "~"):
        return None
    # Booleans
    if raw in ("true", "True"):
        return True
    if raw in ("false", "False"):
        return False
    # Quoted Strings
    if len(raw) >= 2 and raw[0] == raw[-1] and raw[0] in ("'", '"'):
        return raw[1:-1]
    # Integer (nur reine Ziffern, optional mit Vorzeichen)
    if raw.lstrip("+-").isdigit():
        try:
            return int(raw)
        except ValueError:
            pass
    # Fallback: String
    return raw


def _parse_frontmatter(content: str) -> tuple[dict, str]:
    """Parst den YAML-Frontmatter-Block am Anfang eines Markdown-Strings.

    Unterstützt:
        - key: value (Skalar)
        - key:
            - item1
            - item2
          (Block-Liste)

    Gibt `(frontmatter_dict, body_string)` zurück.
    Wenn kein Frontmatter vorhanden ist: `({}, content)`.
    Insertion-Order wird erhalten (Python-Dict seit 3.7).
    """
    if not content.startswith("---"):
        return ({}, content)

    # Zeilenweise zerlegen, Line-Endings normalisieren
    lines = content.splitlines(keepends=False)

    # Erste Zeile muss genau "---" sein
    if not lines or lines[0].strip() != "---":
        return ({}, content)

    # Ende des Frontmatter-Blocks suchen
    end_idx = -1
    for i in range(1, len(lines)):
        if lines[i].strip() == "---":
            end_idx = i
            break

    if end_idx == -1:
        # Kein Abschluss gefunden → kein gültiges Frontmatter
        return ({}, content)

    fm_lines = lines[1:end_idx]
    body_lines = lines[end_idx + 1:]

    # Body rekonstruieren. Wenn nach dem abschließenden --- ein Leerzeile
    # folgt, bewahren wir die Struktur exakt (splitlines entfernt Trenner).
    # Wir schreiben den Body mit "\n"-Trennzeichen zusammen. Falls das
    # Original ein Trailing-Newline hatte, hängen wir es wieder an.
    body = "\n".join(body_lines)
    if content.endswith("\n") and body_lines:
        body += "\n"
    elif content.endswith("\n") and not body_lines:
        body = ""

    fm: dict = {}
    current_list_key: str | None = None

    for raw_line in fm_lines:
        # Listenelement?
        stripped = raw_line.strip()
        if not stripped:
            # Leerzeile beendet ggf. die laufende Liste nicht zwingend,
            # wir ignorieren sie einfach.
            continue

        # Kommentare überspringen
        if stripped.startswith("#"):
            continue

        # Listenelement gehört zum aktuellen Key
        if stripped.startswith("- ") or stripped == "-":
            if current_list_key is None:
                # Verwaistes Listenelement → ignorieren
                continue
            item_value = stripped[1:].strip()
            item_parsed = _parse_scalar(item_value) if item_value else ""
            fm[current_list_key].append(item_parsed)
            continue

        # Neuer Key
        if ":" in raw_line:
            # Nur am ersten Doppelpunkt splitten, damit URLs in Werten
            # (http://...) nicht zerrissen werden.
            key_part, _, value_part = raw_line.partition(":")
            key = key_part.strip()
            value_part = value_part.strip()

            if value_part == "":
                # Leerer Wert → Block-Liste oder leerer Wert folgt
                fm[key] = []
                current_list_key = key
            else:
                fm[key] = _parse_scalar(value_part)
                current_list_key = None

    return (fm, body)


def _serialize_scalar(value: object) -> str:
    """Wandelt einen Python-Wert in eine YAML-Skalar-Repräsentation um.

    Einfach gehalten: Strings mit Doppelpunkt, führendem Minus oder
    Hash werden in doppelte Anführungszeichen gesetzt, damit der Parser
    sie beim nächsten Lesen wieder korrekt erkennt. Umlaute bleiben
    unverändert (utf-8).
    """
    if value is None:
        return "null"
    if value is True:
        return "true"
    if value is False:
        return "false"
    if isinstance(value, int):
        return str(value)
    s = str(value)
    # Quoting-Bedarf prüfen
    needs_quotes = False
    if s == "":
        needs_quotes = True
    elif s[0] in ("-", "?", "|", ">", "!", "&", "*", "#", "%", "@", "`"):
        needs_quotes = True
    elif ": " in s or s.endswith(":"):
        needs_quotes = True
    elif s.strip() != s:
        needs_quotes = True
    elif s.lower() in ("null", "true", "false", "yes", "no"):
        needs_quotes = True

    if needs_quotes:
        # Doppelte Quotes, interne " escapen
        escaped = s.replace("\\", "\\\\").replace('"', '\\"')
        return f'"{escaped}"'
    return s


def _serialize_frontmatter(fm: dict) -> str:
    """Serialisiert ein Frontmatter-Dict zurück in einen YAML-Block.

    Bewahrt die Schlüssel-Reihenfolge. Listen werden im Block-Style
    geschrieben (jedes Item in eigener Zeile mit `  - `). Rückgabe
    inklusive der `---`-Marker oben und unten, abschließendes Newline.
    """
    lines: list[str] = ["---"]
    for key, value in fm.items():
        if isinstance(value, list):
            lines.append(f"{key}:")
            for item in value:
                lines.append(f"  - {_serialize_scalar(item)}")
        else:
            lines.append(f"{key}: {_serialize_scalar(value)}")
    lines.append("---")
    return "\n".join(lines) + "\n"


# ---------------------------------------------------------------------------
# Inbox-Scanner
# ---------------------------------------------------------------------------

def scan_inbox(inbox_path: Path) -> list[dict]:
    """Liest rekursiv alle unterstützten Text-Dateien unter `inbox_path`.

    Unterstützte Extensions: .md, .markdown, .txt, .html, .htm
    (Case-insensitive. Word-Formate .doc und .docx sind bewusst ausgenommen.)

    Überspringt Hidden Files (Präfix `.`) und folgt keinen Symlinks.
    Für jede Datei wird ein Dict erzeugt mit den Feldern:
        path (absoluter Pfad)
        frontmatter (dict)
        body (str)
        char_count (int, len(body))
        urls (list[dict], via url_extract.extract_urls)
        existing_status (str | None, aus frontmatter["notebooklm-status"])

    Bei leerer oder fehlender Inbox wird eine leere Liste zurückgegeben.
    """
    inbox_path = Path(inbox_path)
    results: list[dict] = []

    if not inbox_path.exists() or not inbox_path.is_dir():
        return results

    # Rekursiv ALLE Dateien durchgehen und nach Extension filtern, statt pro
    # Extension einen rglob-Aufruf zu machen. Einfacher und schneller.
    for file_path in inbox_path.rglob("*"):
        if not file_path.is_file():
            continue
        if file_path.suffix.lower() not in _SUPPORTED_EXTENSIONS:
            continue
        # Hidden Files anhand des Namens filtern (auch in Unterordnern)
        if any(part.startswith(".") for part in file_path.relative_to(inbox_path).parts):
            continue
        # Symlinks ignorieren — sowohl die Datei selbst als auch Elternpfade
        try:
            if file_path.is_symlink():
                continue
        except OSError:
            continue

        try:
            content = file_path.read_text(encoding="utf-8")
        except (OSError, UnicodeDecodeError):
            # Unleserliche Dateien überspringen, aber nicht abbrechen
            continue

        frontmatter, body = _parse_frontmatter(content)

        try:
            urls = extract_urls(body)
        except Exception:
            urls = []

        existing_status = frontmatter.get("notebooklm-status")
        if not isinstance(existing_status, str):
            existing_status = None

        results.append({
            "path": file_path.resolve(),
            "frontmatter": frontmatter,
            "body": body,
            "char_count": len(body),
            "urls": urls,
            "existing_status": existing_status,
        })

    return results


# ---------------------------------------------------------------------------
# Marker-Update (merge, kein Überschreiben)
# ---------------------------------------------------------------------------

_ALLOWED_PREFIX = "notebooklm-"


def update_marker(path: Path, marker: dict) -> None:
    """Merged die notebooklm-*-Felder aus `marker` in das Frontmatter.

    Regeln:
        - Nur Keys mit Präfix `notebooklm-` und der Key `updated` werden
          gesetzt. Alles andere aus `marker` wird ignoriert, damit keine
          fremden Felder in die Datei rutschen.
        - Bestehende Frontmatter-Felder bleiben unangetastet.
        - `updated` wird auf heute (YYYY-MM-DD) gesetzt.
        - Wenn die Datei kein Frontmatter hat, wird eines angelegt.
        - Das Schreiben erfolgt atomar via tempfile + os.replace().
        - Encoding durchgehend utf-8.
    """
    path = Path(path)
    content = path.read_text(encoding="utf-8")
    frontmatter, body = _parse_frontmatter(content)

    # Nur erlaubte Keys übernehmen
    for key, value in marker.items():
        if key.startswith(_ALLOWED_PREFIX):
            frontmatter[key] = value

    # `updated` immer auf heute setzen
    frontmatter["updated"] = date.today().isoformat()

    # Neuen Inhalt zusammenbauen
    fm_block = _serialize_frontmatter(frontmatter)

    # Sicherstellen, dass zwischen Frontmatter und Body eine Leerzeile steht,
    # wenn der Body nicht bereits damit beginnt.
    if body.startswith("\n"):
        new_content = fm_block + body
    elif body == "":
        new_content = fm_block
    else:
        new_content = fm_block + "\n" + body

    # 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=".inbox_scan_")
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as fh:
            fh.write(new_content)
        os.replace(tmp_path, path)
    except Exception:
        try:
            os.unlink(tmp_path)
        except OSError:
            pass
        raise
