"""
curate.py — File-Handling-Layer für den curator-Skill.

Drei Modi:

    --mode list-proposed
        Gibt alle Staging-Einträge mit status:proposed als JSON auf stdout.
        Gibt count:0 + leere proposals-Liste zurück, wenn die Staging-Datei
        nicht existiert (Graceful-Skip, kein Fehler).

    --mode accept --number N --reason "..."
        Setzt Eintrag N auf status:accepted und schreibt einen Log-Eintrag
        in curator-log.md. Der eigentliche Wiki-Edit ist nicht Teil dieses
        Skripts — er läuft über das Edit-Tool des LLM, damit der Nutzer
        jeden Diff sieht und explizit bestätigt.

    --mode reject --number N --reason "..."
        Setzt Eintrag N auf status:rejected mit Grund und loggt in
        curator-log.md.

Security by Design: Das Skript darf ausschließlich in die Staging-Datei
(paths['staging_file']) und die Log-Datei (paths['log_file']) schreiben.
Jeder Schreibversuch auf einen anderen Pfad ist verboten — der Code enthält
keine open(..., 'w')-Aufrufe außerhalb dieser beiden Pfade.

HINWEIS zum Vorschlags-Header-Pattern:
Das Regex-Muster r"^### Vorschlag #(\d+) — (.+?)$" ist an das Format
gekoppelt, das reflect.py beim Anhängen von Vorschlägen erzeugt. Beide
Skills müssen das Muster gemeinsam ändern, wenn das Format geändert wird.
Da es sich um ein deutschen Sprachstring handelt, wäre es locale-abhängig
— für eine Mehrsprachigkeit müsste es nach locale_data/ ausgelagert werden.

Pfade werden über den config_loader aus bundle.json aufgelöst.
Default-Pfade (falls keine skill.config.yaml vorhanden):
  - staging_file: {system}/Lerner Loop/reflector-staging.md
  - log_file:     {system}/Lerner Loop/curator-log.md
"""

from __future__ import annotations

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


_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

# Default-Pfade mit Layout-Platzhaltern. Single Source of Truth für sowohl
# _load_config als auch den Eval-Runner (run_evals.py importiert diese
# Konstante, damit Fixture- und Produktiv-Pfad identisch aufgelöst werden).
DEFAULT_PATHS: dict[str, str] = {
    "staging_file": "{system}/Lerner Loop/reflector-staging.md",
    "log_file": "{system}/Lerner Loop/curator-log.md",
}

# Muster an das reflector-Format gekoppelt — siehe HINWEIS oben.
PROPOSAL_HEADER_RE = re.compile(
    r"^### Vorschlag #(\d+) — (.+?)$", re.MULTILINE
)


def _load_config(vault: Path, bundle_root: Path | None = None):
    """Lädt die Skill-Konfiguration mit Default-Pfaden."""
    return load_skill_config(
        skill_dir=_SKILL_DIR,
        vault_root=vault,
        bundle_root=bundle_root,
        default_paths=DEFAULT_PATHS,
    )


def get_staging_path(vault: Path, bundle_root: Path | None = None) -> Path:
    """Gibt den aufgelösten Pfad zur Staging-Datei zurück."""
    cfg = _load_config(vault, bundle_root)
    return cfg.paths["staging_file"]


def get_log_path(vault: Path, bundle_root: Path | None = None) -> Path:
    """Gibt den aufgelösten Pfad zur Log-Datei zurück."""
    cfg = _load_config(vault, bundle_root)
    return cfg.paths["log_file"]


# ---------------------------------------------------------------------------
# Parsing-Hilfsfunktionen
# ---------------------------------------------------------------------------

def parse_proposals(text: str) -> list[dict]:
    """Zerlegt die Staging-Datei in eine Liste von Vorschlags-Blöcken."""
    headers = list(PROPOSAL_HEADER_RE.finditer(text))
    proposals: list[dict] = []
    for idx, match in enumerate(headers):
        start = match.start()
        end = headers[idx + 1].start() if idx + 1 < len(headers) else len(text)
        block = text[start:end]
        number = int(match.group(1))
        timestamp = match.group(2).strip()
        fields = {
            "number": number,
            "timestamp": timestamp,
            "type": _extract_field(block, "Typ"),
            "source": _extract_field(block, "Quelle"),
            "content": _extract_field(block, "Inhalt"),
            "target": _extract_field(block, "Vorgeschlagene Ablage"),
            "reason": _extract_field(block, "Begründung"),
            "status": _extract_field(block, "Status"),
            "block": block,
        }
        proposals.append(fields)
    return proposals


def _extract_field(block: str, label: str) -> str:
    match = re.search(
        rf"^\- \*\*{re.escape(label)}:\*\* (.+?)$",
        block,
        re.MULTILINE,
    )
    return match.group(1).strip() if match else ""


# ---------------------------------------------------------------------------
# Modi
# ---------------------------------------------------------------------------

def list_proposed(vault: Path, bundle_root: Path | None = None) -> dict:
    """Gibt alle proposed-Einträge als JSON zurück. Graceful-Skip wenn kein Staging."""
    path = get_staging_path(vault, bundle_root)
    if not path.exists():
        return {"count": 0, "proposals": [], "staging_path": str(path)}
    text = path.read_text(encoding="utf-8")
    all_proposals = parse_proposals(text)
    proposed = [
        {k: v for k, v in p.items() if k != "block"}
        for p in all_proposals
        if p["status"].lower() == "proposed"
    ]
    return {
        "count": len(proposed),
        "proposals": proposed,
        "staging_path": str(path),
    }


def update_status(
    vault: Path,
    number: int,
    new_status: str,
    reason: str,
    bundle_root: Path | None = None,
) -> dict:
    """Setzt den Status eines Vorschlags in der Staging-Datei und schreibt Log.

    Schreib-Allowlist: Nur staging_file und log_file dürfen beschrieben werden.
    Pfade kommen ausschließlich aus der Bundle-Config — keine User-Pfade als Argument.
    """
    cfg = _load_config(vault, bundle_root)
    staging = cfg.paths["staging_file"]
    log = cfg.paths["log_file"]

    if not staging.exists():
        return {"ok": False, "error": "staging file not found"}
    text = staging.read_text(encoding="utf-8")
    proposals = parse_proposals(text)
    target = next((p for p in proposals if p["number"] == number), None)
    if not target:
        return {"ok": False, "error": f"proposal #{number} not found"}
    if target["status"].lower() not in {"proposed"}:
        return {
            "ok": False,
            "error": (
                f"proposal #{number} is in status '{target['status']}', not 'proposed'"
            ),
        }

    old_block = target["block"]
    # Status-Zeile ersetzen
    new_block = re.sub(
        r"^\- \*\*Status:\*\* proposed\s*$",
        f"- **Status:** {new_status}",
        old_block,
        count=1,
        flags=re.MULTILINE,
    )
    # Grund-Zeile direkt darunter einfügen, falls nicht vorhanden
    grund_label = f"{new_status.capitalize()}-Grund"
    if f"- **{grund_label}:**" not in new_block:
        new_block = (
            new_block.rstrip() + f"\n- **{grund_label}:** {reason}\n\n"
        )
    new_text = text.replace(old_block, new_block, 1)
    # updated-Feld im Frontmatter mitziehen
    new_text = re.sub(
        r"^updated:\s*.+?$",
        f"updated: {datetime.now().strftime('%Y-%m-%d')}",
        new_text,
        count=1,
        flags=re.MULTILINE,
    )
    # Allowlist-Check: Nur staging und log dürfen beschrieben werden.
    # Dieser assert fängt Programmier-Fehler, nicht User-Input.
    assert staging == cfg.paths["staging_file"], "BUG: Schreibziel außerhalb Allowlist!"
    staging.write_text(new_text, encoding="utf-8")

    log_entry = _build_log_entry(target, new_status, reason)
    _append_log(log, log_entry)

    return {
        "ok": True,
        "number": number,
        "new_status": new_status,
        "reason": reason,
        "log_path": str(log),
    }


# ---------------------------------------------------------------------------
# Log-Hilfsfunktionen
# ---------------------------------------------------------------------------

def _build_log_entry(proposal: dict, new_status: str, reason: str) -> str:
    now = datetime.now().strftime("%Y-%m-%d %H:%M")
    return (
        f"## {now} — Vorschlag #{proposal['number']} → {new_status}\n\n"
        f"- **Typ:** {proposal['type']}\n"
        f"- **Quelle:** {proposal['source']}\n"
        f"- **Ziel:** {proposal['target']}\n"
        f"- **Inhalt:** {proposal['content']}\n"
        f"- **Entscheidung:** {new_status}\n"
        f"- **Grund:** {reason}\n\n"
    )


def _log_header() -> str:
    today = datetime.now().strftime("%Y-%m-%d")
    return (
        "---\n"
        "type: curator-log\n"
        'description: "Entscheidungs-Log des Curator-Skills. Hält Zeitstempel, '
        'Vorschlags-Nummer, Ziel, Entscheidung und Grund. Append-only."\n'
        "tags:\n"
        "  - curator\n"
        "  - log\n"
        "  - second-brain\n"
        "  - karpathy\n"
        f"created: {today}\n"
        f"updated: {today}\n"
        "---\n\n"
        "# Curator Log\n\n"
        "Jeder Eintrag dokumentiert eine Entscheidung des Curators "
        "(accepted oder rejected). Append-only, ältere Einträge werden "
        "nie verändert.\n\n"
    )


def _append_log(log_path: Path, entry: str) -> None:
    """Schreibt einen Eintrag ans Log. Erstellt die Datei beim ersten Aufruf."""
    if not log_path.exists():
        log_path.parent.mkdir(parents=True, exist_ok=True)
        log_path.write_text(_log_header() + entry, encoding="utf-8")
        return
    text = log_path.read_text(encoding="utf-8")
    text = re.sub(
        r"^updated:\s*.+?$",
        f"updated: {datetime.now().strftime('%Y-%m-%d')}",
        text,
        count=1,
        flags=re.MULTILINE,
    )
    if not text.endswith("\n"):
        text += "\n"
    text += entry
    log_path.write_text(text, encoding="utf-8")


# ---------------------------------------------------------------------------
# Einstiegspunkt
# ---------------------------------------------------------------------------

def main() -> int:
    parser = argparse.ArgumentParser(
        description="File-Handling-Layer für den curator-Skill."
    )
    parser.add_argument(
        "--mode",
        choices=["list-proposed", "accept", "reject"],
        required=True,
    )
    parser.add_argument("--vault", type=Path, default=Path.cwd())
    parser.add_argument("--bundle-root", type=Path, default=None)
    parser.add_argument("--number", type=int, default=None)
    parser.add_argument("--reason", type=str, default="")
    args = parser.parse_args()

    vault = args.vault.resolve()
    bundle_root = args.bundle_root.resolve() if args.bundle_root else None

    if args.mode == "list-proposed":
        result = list_proposed(vault, bundle_root)
        print(json.dumps(result, ensure_ascii=False, indent=2))
        return 0

    if args.mode in {"accept", "reject"}:
        if args.number is None:
            print(json.dumps({"ok": False, "error": "--number required"}))
            return 1
        if not args.reason.strip():
            print(json.dumps({"ok": False, "error": "--reason required and must not be empty"}))
            return 1
        new_status = "accepted" if args.mode == "accept" else "rejected"
        result = update_status(vault, args.number, new_status, args.reason, bundle_root)
        print(json.dumps(result, ensure_ascii=False, indent=2))
        return 0 if result.get("ok") else 1

    return 1


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