"""
write_log.py — Schreibt einen Session-Log als Markdown.

Wird von den Claude-Code-Hooks PreCompact und SessionEnd aufgerufen, kann
aber auch manuell laufen. Liest einen JSON-Payload über stdin (oder via
--stdin-file), extrahiert Topic und Verlauf aus dem Transcript, schreibt
eine Markdown-Datei mit Frontmatter.

Design-Prinzipien:
- Immer exit 0, damit der Hook Claude Code nicht blockiert.
- Fehler landen als HTML-Kommentar im Body, nicht als Exception.
- Bei leerem oder kaputtem stdin wird trotzdem eine Datei geschrieben,
  mit klarem Marker "Kein Session-Inhalt verfügbar", damit ein Mensch
  beim Nachlesen sofort sieht, was passiert ist.
- Keine Halluzinationen: unbekannte Felder bleiben leer oder tragen
  den dokumentierten Fallback-Wert.
- Pfade kommen aus skill.config.yaml (via config_loader). Fehlt die Datei,
  greift der Default-Output-Pfad {logbuch}/Z Session Logs/{YYYY-MM}/{DD}.

Aufruf:
    python write_log.py --trigger pre-compact
    python write_log.py --trigger session-end
    python write_log.py --trigger session-end --stdin-file test.json
    python write_log.py --trigger session-end --output-dir /tmp/test
"""

from __future__ import annotations

import argparse
import importlib
import importlib.util
import json
import re
import sys
from datetime import datetime
from pathlib import Path

# _lib liegt eine Ebene über diesem Skript im .skills/-Baum.
# sys.path-Setup damit der Loader ohne Installation gefunden wird.
_SKILL_DIR = Path(__file__).resolve().parent
_SKILLS_ROOT = _SKILL_DIR.parent
if str(_SKILLS_ROOT) not in sys.path:
    sys.path.insert(0, str(_SKILLS_ROOT))

from _lib.config_loader import load_skill_config  # noqa: E402


def _load_locale(locale_name: str):
    """Lädt das Locale-Modul aus locale_data/. Fällt auf 'en' zurück."""
    try:
        # locale_data-Paket liegt direkt im Skill-Verzeichnis
        skill_dir_str = str(_SKILL_DIR)
        if skill_dir_str not in sys.path:
            sys.path.insert(0, skill_dir_str)
        pkg_init = _SKILL_DIR / "locale_data" / "__init__.py"
        spec = importlib.util.spec_from_file_location("locale_data", pkg_init)
        mod = importlib.util.module_from_spec(spec)
        # Submodule de/en müssen registriert sein bevor loader ausführt
        sys.modules.setdefault("locale_data", mod)
        spec.loader.exec_module(mod)
        sys.modules["locale_data"] = mod
        return mod.get_locale(locale_name)
    except Exception:
        # Letzter Fallback: leere Stopwords
        class _Empty:
            STOPWORDS: set = set()
            UMLAUT_MAP: dict = {}
        return _Empty()


# ---------------------------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------------------------

def slugify(text: str, umlaut_map: dict, stopwords: set, max_words: int = 4) -> str:
    """Slug-Bildung: Umlaute normalisieren, Stopwörter raus,
    erste N sinntragende Wörter mit Bindestrich verbinden.

    Stopwörter werden vor dem Vergleich ebenfalls normalisiert, damit
    Einträge mit echten Umlauten (z.B. 'für') nach der Normalisierung
    korrekt gefiltert werden.
    """
    if not text:
        return ""
    for orig, repl in umlaut_map.items():
        text = text.replace(orig, repl)
    text = text.lower()
    text = re.sub(r"[^a-z0-9\s-]", " ", text)
    # Stopwörter ebenfalls normalisieren (Umlaute in Stopword-Set auflösen)
    normalized_stops: set[str] = set()
    for sw in stopwords:
        normalized = sw.lower()
        for orig, repl in umlaut_map.items():
            normalized = normalized.replace(orig, repl)
        normalized_stops.add(normalized)
    words = [w for w in text.split() if w and w not in normalized_stops and len(w) > 1]
    if not words:
        return ""
    return "-".join(words[:max_words])[:60].strip("-")


def read_stdin_payload(stdin_file: Path | None) -> dict:
    """Liest JSON-Payload von stdin oder aus Datei. Gibt bei Fehler
    ein Dict mit {"_error": "..."} zurück, nie eine Exception."""
    try:
        if stdin_file:
            raw = stdin_file.read_text(encoding="utf-8", errors="replace")
        elif not sys.stdin.isatty():
            raw = sys.stdin.read()
        else:
            return {"_empty": True}
        raw = raw.strip()
        if not raw:
            return {"_empty": True}
        return json.loads(raw)
    except Exception as exc:  # noqa: BLE001
        return {"_error": f"stdin parse error: {exc}"}


def read_transcript(path_str: str | None) -> list[dict]:
    """Liest eine JSONL-Transcript-Datei. Gibt leere Liste bei
    fehlendem Pfad oder Lesefehler zurück."""
    if not path_str:
        return []
    path = Path(path_str)
    if not path.exists():
        return []
    entries: list[dict] = []
    try:
        for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
            line = line.strip()
            if not line:
                continue
            try:
                entries.append(json.loads(line))
            except json.JSONDecodeError:
                continue
    except Exception:  # noqa: BLE001
        return []
    return entries


def extract_text_content(content) -> str:
    """Extrahiert Text aus einem Claude-Content-Feld, das String
    oder Liste von Content-Blöcken sein kann."""
    if isinstance(content, str):
        return content
    if isinstance(content, list):
        parts = []
        for block in content:
            if not isinstance(block, dict):
                continue
            if block.get("type") == "text":
                parts.append(block.get("text", ""))
        return "\n".join(parts)
    return ""


def first_user_message(entries: list[dict]) -> str:
    """Findet die erste User-Message im Transcript."""
    for entry in entries:
        if entry.get("type") != "user":
            continue
        msg = entry.get("message", {})
        if msg.get("role") != "user":
            continue
        text = extract_text_content(msg.get("content", ""))
        text = re.sub(r"<system-reminder>.*?</system-reminder>", "", text, flags=re.DOTALL)
        text = re.sub(r"<[^>]+>", "", text)
        text = text.strip()
        if text:
            return text
    return ""


def summarize_exchanges(entries: list[dict], max_items: int = 20) -> list[str]:
    """Baut eine Liste von Kurzfassungen der letzten Exchanges."""
    bullets: list[str] = []
    for entry in entries:
        etype = entry.get("type")
        msg = entry.get("message", {})
        if etype == "user":
            text = extract_text_content(msg.get("content", ""))
            text = re.sub(r"<system-reminder>.*?</system-reminder>", "", text, flags=re.DOTALL)
            text = re.sub(r"<[^>]+>", "", text).strip()
            if text:
                excerpt = text.replace("\n", " ")[:140]
                bullets.append(f"- **User:** {excerpt}")
        elif etype == "assistant":
            content = msg.get("content", [])
            if isinstance(content, list):
                for block in content:
                    if not isinstance(block, dict):
                        continue
                    if block.get("type") == "text":
                        text = block.get("text", "").replace("\n", " ").strip()
                        if text:
                            bullets.append(f"- **Claude:** {text[:140]}")
                    elif block.get("type") == "tool_use":
                        tool = block.get("name", "unknown")
                        bullets.append(f"- **Tool:** {tool}")
    if len(bullets) > max_items:
        bullets = bullets[-max_items:]
    return bullets


def extract_artifacts(entries: list[dict]) -> list[str]:
    """Sammelt alle Dateipfade aus Write-/Edit-Tool-Aufrufen."""
    paths: list[str] = []
    for entry in entries:
        if entry.get("type") != "assistant":
            continue
        msg = entry.get("message", {})
        content = msg.get("content", [])
        if not isinstance(content, list):
            continue
        for block in content:
            if not isinstance(block, dict) or block.get("type") != "tool_use":
                continue
            name = block.get("name", "")
            if name not in {"Write", "Edit", "NotebookEdit"}:
                continue
            inp = block.get("input", {})
            if isinstance(inp, dict):
                p = inp.get("file_path") or inp.get("notebook_path")
                if p:
                    paths.append(p)
    seen: set[str] = set()
    unique: list[str] = []
    for p in paths:
        if p not in seen:
            seen.add(p)
            unique.append(p)
    return unique


def build_body(
    payload: dict,
    trigger: str,
    umlaut_map: dict,
    stopwords: set,
) -> tuple[str, str, str]:
    """Baut den Markdown-Body und gibt (topic_slug, description, body) zurück."""
    error = payload.get("_error")
    empty = payload.get("_empty")

    if empty or error:
        note = "Kein Session-Inhalt verfügbar (stdin leer oder nicht lesbar)."
        body = (
            "## Topic\n"
            f"{note}\n\n"
            "## Verlauf\n"
            "Kein Session-Inhalt verfügbar.\n\n"
            "## Artefakte\n"
            "Kein Session-Inhalt verfügbar.\n"
        )
        if error:
            body += f"\n<!-- {error} -->\n"
        description = f"Session-Log ohne Payload ({trigger})."
        return "", description, body

    transcript_path = payload.get("transcript_path")
    entries = read_transcript(transcript_path)

    first_msg = first_user_message(entries)
    topic_slug = slugify(first_msg, umlaut_map, stopwords)

    if first_msg:
        topic_text = first_msg.replace("\n", " ").strip()
        if len(topic_text) > 200:
            topic_text = topic_text[:197] + "..."
    else:
        topic_text = "Keine User-Message im Transcript gefunden."

    exchanges = summarize_exchanges(entries)
    artifacts = extract_artifacts(entries)

    body_parts = ["## Topic", topic_text, "", "## Verlauf"]
    if exchanges:
        body_parts.extend(exchanges)
    else:
        body_parts.append("Kein Verlauf aus dem Transcript extrahierbar.")
    body_parts.append("")
    body_parts.append("## Artefakte")
    if artifacts:
        for p in artifacts:
            body_parts.append(f"- `{p}`")
    else:
        body_parts.append("Keine Datei-Änderungen erkannt.")
    body_parts.append("")

    body = "\n".join(body_parts)

    if first_msg:
        desc_source = topic_text if len(topic_text) <= 140 else topic_text[:137] + "..."
        description = f"Session-Log ({trigger}): {desc_source}"
    else:
        description = f"Session-Log ({trigger}) ohne extrahierbare User-Message."

    return topic_slug, description, body


def build_frontmatter(
    description: str,
    trigger: str,
    session_id: str,
    message_count: int,
    extra_tags: list[str] | None = None,
    iso9001_marker: bool = False,
) -> str:
    today = datetime.now().strftime("%Y-%m-%d")
    status = "autocompact" if trigger == "pre-compact" else "complete"
    safe_desc = description.replace('"', '\\"').replace("\n", " ")
    if len(safe_desc) > 300:
        safe_desc = safe_desc[:297] + "..."

    base_tags = ["session-log", "raw-layer"]
    all_tags = base_tags + [t for t in (extra_tags or []) if t not in base_tags]
    tags_yaml = "\n".join(f"  - {t}" for t in all_tags)

    lines = [
        "---",
        "type: session-log",
        f'description: "{safe_desc}"',
        "tags:",
        tags_yaml,
        f"created: {today}",
        f"updated: {today}",
        f"trigger: {trigger}",
        f"status: {status}",
        "processed: false",
        f"session-id: {session_id}",
        f"message-count: {message_count}",
    ]

    if iso9001_marker:
        lines += [
            'projekt: ""',
            "dauer_minuten: 0",
            "angefasste_dateien_anzahl: 0",
            "neue_funktionale_dateien_anzahl: 0",
            "nc_count: 0",
            "kvp_count: 0",
        ]

    lines.append("---")
    lines.append("")
    return "\n".join(lines) + "\n"


def resolve_output_path(output_dir: Path, topic_slug: str) -> Path:
    now = datetime.now().strftime("%Y-%m-%d-%H%M")
    slug = topic_slug or "ohne-thema"
    base = f"session-{now}-{slug}.md"
    candidate = output_dir / base
    counter = 2
    while candidate.exists():
        candidate = output_dir / f"session-{now}-{slug}-{counter}.md"
        counter += 1
    return candidate


def _resolve_time_placeholders(path_str: str) -> str:
    """Ersetzt {YYYY-MM}, {DD}, {HHMM} in einem Pfad-String durch aktuelle Zeit."""
    now = datetime.now()
    return (
        path_str
        .replace("{YYYY-MM}", now.strftime("%Y-%m"))
        .replace("{DD}", now.strftime("%d"))
        .replace("{HHMM}", now.strftime("%H%M"))
    )


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--trigger",
        choices=["pre-compact", "session-end"],
        required=True,
    )
    parser.add_argument("--vault", type=Path, default=Path.cwd())
    parser.add_argument("--output-dir", type=Path, default=None)
    parser.add_argument("--stdin-file", type=Path, default=None)
    args = parser.parse_args()

    try:
        vault = args.vault.resolve()

        # Config laden (fehlt skill.config.yaml → Defaults greifen über default_paths).
        # Die Layout-Placeholder werden vom Loader gegen bundle.json.vault_layout
        # aufgelöst — externe User mit angepasstem vault_layout profitieren so auch
        # ohne eigene skill.config.yaml von ihrer Ordner-Konvention.
        config = load_skill_config(
            skill_dir=_SKILL_DIR,
            vault_root=vault,
            bundle_root=None,
            default_paths={
                "output_dir": "{logbuch}/Z Session Logs/{YYYY-MM}/{DD}",
            },
        )

        # Locale bestimmen
        locale = _load_locale(config.locale)
        umlaut_map = getattr(locale, "UMLAUT_MAP", {})
        stopwords = getattr(locale, "STOPWORDS", set())

        # Output-Verzeichnis bestimmen (CLI > Config-/Default-Pfad aus Loader).
        # Der Loader hat unseren skill-eigenen Default ("{logbuch}/Z Session Logs/{YYYY-MM}/{DD}")
        # bereits gegen bundle.json.vault_layout aufgelöst — der Branch unten muss daher
        # nicht mehr selbst auf "05 Logbuch" fallen.
        if args.output_dir:
            output_dir = args.output_dir.resolve()
        else:
            raw_str = str(config.paths["output_dir"])
            output_dir = Path(_resolve_time_placeholders(raw_str))
            if not output_dir.is_absolute():
                output_dir = vault / output_dir
        output_dir.mkdir(parents=True, exist_ok=True)

        # Tagesabschluss-Branch (vor dem normalen Log)
        if config.features.get("tagesabschluss_modus", False):
            submodule_path = _SKILL_DIR / "_optional" / "tagesabschluss.py"
            if submodule_path.exists():
                try:
                    spec = importlib.util.spec_from_file_location(
                        "_optional.tagesabschluss", submodule_path
                    )
                    mod = importlib.util.module_from_spec(spec)
                    spec.loader.exec_module(mod)
                    mod.run(config, vault_root=vault)
                except Exception as exc:  # noqa: BLE001
                    print(
                        f"session-log: Tagesabschluss-Submodul Fehler: {exc}",
                        file=sys.stderr,
                    )
                    # Kein Abbruch — weiter mit normalem Log
            else:
                print(
                    "session-log: tagesabschluss_modus aktiv, aber "
                    "_optional/tagesabschluss.py fehlt. "
                    "Kopiere _optional/tagesabschluss.example.py nach "
                    "_optional/tagesabschluss.py und implementiere deine Logik.",
                    file=sys.stderr,
                )

        payload = read_stdin_payload(args.stdin_file)
        topic_slug, description, body = build_body(payload, args.trigger, umlaut_map, stopwords)

        session_id = str(payload.get("session_id", "")) if isinstance(payload, dict) else ""
        transcript_path = payload.get("transcript_path") if isinstance(payload, dict) else None
        entries = read_transcript(transcript_path) if transcript_path else []
        message_count = len(entries)

        extra_tags = config.frontmatter.get("extra_tags", [])
        if not isinstance(extra_tags, list):
            extra_tags = []
        iso9001_marker = bool(config.features.get("iso9001_marker", False))

        frontmatter = build_frontmatter(
            description,
            args.trigger,
            session_id,
            message_count,
            extra_tags=extra_tags,
            iso9001_marker=iso9001_marker,
        )
        output_path = resolve_output_path(output_dir, topic_slug)
        output_path.write_text(frontmatter + body, encoding="utf-8")

        print(str(output_path))
        return 0
    except Exception as exc:  # noqa: BLE001
        print(f"session-log write_log.py error: {exc}", file=sys.stderr)
        return 0


if __name__ == "__main__":
    raise SystemExit(main())
