#!/usr/bin/env python3
"""
Exportiert einen Claude-Code-Chatverlauf als lesbares Markdown.

Claude Code legt Sitzungen unter ~/.claude/projects/<pfad-slug>/*.jsonl ab.
Diese Dateien werden schnell sehr gross, weil gelesene PDFs und Bilder als
base64 mitgeschrieben werden. Dieses Skript entfernt genau diese Blobs und
gibt nur den Gespraechstext aus.

Der Sitzungspfad wird aus dem aktuellen Arbeitsverzeichnis abgeleitet -
das Skript funktioniert dadurch auf jedem Rechner ohne Anpassung.

Aufruf:
    python tools/export-chat.py                   # Gespraechstext
    python tools/export-chat.py --tools           # zusaetzlich Werkzeugaufrufe
    python tools/export-chat.py --thinking        # zusaetzlich Denkschritte
    python tools/export-chat.py --alle            # alles
    python tools/export-chat.py --liste           # nur Sitzungen auflisten
    python tools/export-chat.py -o verlauf.md     # Zieldatei bestimmen
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from datetime import datetime
from pathlib import Path

# Blocktypen, die reine Binaerdaten transportieren
BLOB_TYPEN = {"image", "document"}

# Rauschen aus dem Nachrichtentext entfernen
MUSTER_RAUSCHEN = [
    re.compile(r"<system-reminder>.*?</system-reminder>", re.S),
    re.compile(r"<command-name>.*?</command-name>", re.S),
    re.compile(r"<command-message>.*?</command-message>", re.S),
    re.compile(r"<local-command-stdout>.*?</local-command-stdout>", re.S),
    re.compile(r"<task-notification>.*?</task-notification>", re.S),
]

# Ueberschrift ausserhalb von Codebloecken
MUSTER_UEBERSCHRIFT = re.compile(r"^(#{1,6})(\s)")


def projekt_slug(pfad: Path) -> str:
    """C:\\Users\\x\\projekt  ->  C--Users-x-projekt"""
    return re.sub(r"[:\\/]", "-", str(pfad))


def sitzungen_finden(basis: Path) -> list[Path]:
    if not basis.is_dir():
        return []
    return sorted(basis.glob("*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True)


def saeubern(text: str) -> str:
    for muster in MUSTER_RAUSCHEN:
        text = muster.sub("", text)
    return text.strip()


def absenken(text: str, stufen: int = 2) -> str:
    """Ueberschriften im Nachrichtentext tiefer legen.

    Sprecherwechsel stehen auf Ebene 2. Ohne Absenkung wuerde eine
    Zwischenueberschrift aus einer Antwort wie ein eigener Beitrag aussehen.
    Codebloecke bleiben unangetastet.
    """
    zeilen = []
    im_block = False
    for zeile in text.splitlines():
        if zeile.lstrip().startswith("```"):
            im_block = not im_block
            zeilen.append(zeile)
            continue
        if not im_block:
            treffer = MUSTER_UEBERSCHRIFT.match(zeile)
            if treffer:
                tiefe = min(len(treffer.group(1)) + stufen, 6)
                zeile = MUSTER_UEBERSCHRIFT.sub("#" * tiefe + r"\2", zeile, count=1)
        zeilen.append(zeile)
    return "\n".join(zeilen)


def zeit(roh: str | None) -> str:
    if not roh:
        return ""
    try:
        return datetime.fromisoformat(roh.replace("Z", "+00:00")).strftime("%H:%M")
    except ValueError:
        return ""


def werkzeug_zeile(block: dict) -> str:
    """Werkzeugaufruf auf eine knappe Zeile eindampfen."""
    name = block.get("name", "?")
    eingabe = block.get("input") or {}
    if not isinstance(eingabe, dict):
        return f"**{name}**"

    for schluessel in ("description", "file_path", "pattern", "command", "query", "prompt"):
        wert = eingabe.get(schluessel)
        if isinstance(wert, str) and wert.strip():
            kurz = " ".join(wert.split())
            if len(kurz) > 100:
                kurz = kurz[:100] + " ..."
            return f"**{name}** — {kurz}"
    return f"**{name}**"


def bloecke_lesen(inhalt, opt) -> list[str]:
    """Einen content-Wert in Markdown-Absaetze uebersetzen."""
    if isinstance(inhalt, str):
        sauber = saeubern(inhalt)
        return [absenken(sauber)] if sauber else []

    if not isinstance(inhalt, list):
        return []

    teile: list[str] = []
    for block in inhalt:
        if not isinstance(block, dict):
            continue
        typ = block.get("type")

        if typ == "text":
            sauber = saeubern(block.get("text", ""))
            if sauber:
                teile.append(absenken(sauber))

        elif typ == "thinking" and opt.thinking:
            sauber = saeubern(block.get("thinking", ""))
            if sauber:
                eingerueckt = "\n".join("> " + z for z in sauber.splitlines())
                teile.append(f"> *Denkschritt*\n>\n{eingerueckt}")

        elif typ == "tool_use" and opt.tools:
            teile.append(f"`{werkzeug_zeile(block)}`")

        elif typ == "tool_result" and opt.ergebnisse:
            roh = block.get("content")
            if isinstance(roh, list):
                roh = " ".join(
                    b.get("text", "") for b in roh
                    if isinstance(b, dict) and b.get("type") == "text"
                )
            if isinstance(roh, str) and roh.strip():
                kurz = roh.strip()
                if len(kurz) > opt.grenze:
                    kurz = kurz[: opt.grenze] + f"\n... [{len(roh) - opt.grenze} Zeichen gekuerzt]"
                teile.append(f"```\n{kurz}\n```")

        elif typ in BLOB_TYPEN:
            quelle = block.get("source") or {}
            art = quelle.get("media_type", typ)
            teile.append(f"*[Anhang entfernt: {art}]*")

    return teile


def exportieren(datei: Path, opt) -> tuple[str, dict]:
    zeilen: list[str] = []
    zahlen = {"saetze": 0, "beitraege": 0, "blobs": 0, "uebersprungen": 0, "injektionen": 0}
    letzte_rolle = None
    von = bis = None

    with datei.open(encoding="utf-8") as fh:
        for roh in fh:
            roh = roh.strip()
            if not roh:
                continue
            zahlen["saetze"] += 1
            try:
                satz = json.loads(roh)
            except json.JSONDecodeError:
                zahlen["uebersprungen"] += 1
                continue

            if satz.get("type") == "attachment":
                zahlen["blobs"] += 1
                continue
            if satz.get("type") not in ("user", "assistant"):
                continue

            nachricht = satz.get("message")
            if not isinstance(nachricht, dict):
                continue

            # Echte Benutzereingaben stehen immer als String im Satz.
            # Listen unter role=user sind Werkzeugausgaben, Skill-Inhalte
            # oder System-Einwuerfe - die gehoeren nicht in den Verlauf.
            if nachricht.get("role") == "user" and not isinstance(nachricht.get("content"), str):
                if not opt.ergebnisse:
                    zahlen["injektionen"] += 1
                    continue

            zeitstempel = satz.get("timestamp")
            if zeitstempel:
                von = von or zeitstempel
                bis = zeitstempel

            teile = bloecke_lesen(nachricht.get("content"), opt)
            if not teile:
                continue

            rolle = nachricht.get("role")
            titel = "Benutzer" if rolle == "user" else "Claude"

            # Aufeinanderfolgende Beitraege derselben Rolle zusammenfassen
            if rolle != letzte_rolle:
                uhr = zeit(zeitstempel)
                zeilen.append(f"\n## {titel}" + (f" · {uhr}" if uhr else ""))
                letzte_rolle = rolle
                zahlen["beitraege"] += 1

            zeilen.append("\n" + "\n\n".join(teile))

    kopf = [
        "# Chatverlauf",
        "",
        f"Sitzung `{datei.stem}`  ",
        f"Zeitraum {zeit(von)} bis {zeit(bis)} · {zahlen['beitraege']} Beiträge "
        f"aus {zahlen['saetze']} Sätzen",
        "",
        "Anhänge und Binärdaten sind entfernt. Erzeugt mit `tools/export-chat.py`.",
        "",
        "---",
    ]
    return "\n".join(kopf + zeilen) + "\n", zahlen


def main() -> int:
    p = argparse.ArgumentParser(description="Claude-Code-Chatverlauf als Markdown exportieren.")
    p.add_argument("-o", "--out", type=Path, help="Zieldatei (Vorgabe: chatverlauf.md)")
    p.add_argument("--tools", action="store_true", help="Werkzeugaufrufe als Kurzzeile aufnehmen")
    p.add_argument("--thinking", action="store_true", help="Denkschritte aufnehmen")
    p.add_argument("--ergebnisse", action="store_true", help="Werkzeugausgaben aufnehmen (gekuerzt)")
    p.add_argument("--alle", action="store_true", help="Werkzeuge, Denkschritte und Ergebnisse")
    p.add_argument("--grenze", type=int, default=800, help="Maximale Zeichen je Werkzeugausgabe")
    p.add_argument("--sitzung", help="Sitzungs-ID statt der neuesten")
    p.add_argument("--liste", action="store_true", help="Sitzungen auflisten und beenden")
    opt = p.parse_args()

    if opt.alle:
        opt.tools = opt.thinking = opt.ergebnisse = True

    basis = Path.home() / ".claude" / "projects" / projekt_slug(Path.cwd())
    sitzungen = sitzungen_finden(basis)

    if not sitzungen:
        print(f"Keine Sitzung gefunden unter:\n  {basis}", file=sys.stderr)
        print("\nDas Skript muss im Projektordner laufen, zu dem die Sitzung gehoert.",
              file=sys.stderr)
        return 1

    if opt.liste:
        print(f"Sitzungen in {basis}:\n")
        for s in sitzungen:
            mb = s.stat().st_size / 1024 / 1024
            wann = datetime.fromtimestamp(s.stat().st_mtime).strftime("%Y-%m-%d %H:%M")
            print(f"  {s.stem}   {mb:8.1f} MB   {wann}")
        return 0

    quelle = sitzungen[0]
    if opt.sitzung:
        treffer = [s for s in sitzungen if s.stem.startswith(opt.sitzung)]
        if not treffer:
            print(f"Sitzung '{opt.sitzung}' nicht gefunden. --liste zeigt alle.", file=sys.stderr)
            return 1
        quelle = treffer[0]

    ziel = opt.out or Path("chatverlauf.md")
    text, zahlen = exportieren(quelle, opt)
    ziel.write_text(text, encoding="utf-8")

    vorher = quelle.stat().st_size / 1024 / 1024
    nachher = ziel.stat().st_size / 1024 / 1024
    faktor = f"{vorher / nachher:.0f}" if nachher > 0.01 else "?"

    print(f"Quelle   {quelle.name}  ({vorher:.1f} MB)")
    print(f"Ziel     {ziel}  ({nachher:.2f} MB)")
    print(f"Faktor   {faktor}x kleiner · {zahlen['blobs']} Anhänge entfernt")
    print(f"Inhalt   {zahlen['beitraege']} Beiträge aus {zahlen['saetze']} Sätzen")
    return 0


if __name__ == "__main__":
    sys.exit(main())
