# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
"""trigger_enforcer.py — UserPromptSubmit-Hook.

Liest .skills/manifest.json (relativ zu payload.cwd), sammelt trigger_words
aller aktiven Skills und prüft den User-Prompt case-insensitive auf Treffer.
Pro getroffenem Skill wird ein Hinweis in additionalContext injiziert.

Aktive-Skills-Auflösung (Welle 2):
    1. Existiert <cwd>/.docs/sync-state.json mit Feld active_skills →
       strikt: nur dort gelistete Skills werden geprüft.
    2. Datei fehlt oder defekt → Fallback auf default_active-Approximation
       (Erstinstall-Pfad, kein erster sync.py-Lauf).

Output-Format (host-abhängig, AP2-MINI-WELLE-DUAL-OUTPUT-SPEC §5.3):
    {"action": "allow"}                                # kein Treffer (beide Hosts)
    Treffer unter Codex (turn_id im Payload): pures
    {"hookSpecificOutput": {"hookEventName": "UserPromptSubmit",
    "additionalContext": ...}} — Codex verwirft Output mit Extra-Keys.
    Treffer unter Claude Code: Dual-Objekt aus Bundle-Eval-Konvention
    (top-level "action"/"additionalContext") plus hookSpecificOutput.

Erstinstall-fest: fehlendes Manifest, Parse-Fehler oder leerer Prompt → allow,
kein Crash.
"""
import json
import sys
from pathlib import Path


def _is_active(info: dict) -> bool:
    """Fallback-Approximation: Skill ist aktiv, wenn er in mindestens einem
    Profil default_active=true hat. Greift nur, wenn sync-state.json fehlt
    oder kein active_skills-Feld enthält (Erstinstall-Pfad).
    """
    return any((info.get("default_active") or {}).values())


def _load_active_skills(cwd: Path) -> set[str] | None:
    """Liest active_skills aus <cwd>/.docs/sync-state.json.

    Rückgabewerte:
    - set[str]: Liste der aktiven Skill-Namen laut sync.py-State.
    - None:     Datei fehlt, kein active_skills-Feld, oder JSON-Parse-Fehler
                → Aufrufer soll auf _is_active()-Approximation zurückfallen.
    """
    state_path = cwd / ".docs" / "sync-state.json"
    if not state_path.exists():
        return None
    try:
        state = json.loads(state_path.read_text(encoding="utf-8"))
    except json.JSONDecodeError:
        print(
            "trigger_enforcer: sync-state.json ist kein gültiges JSON — "
            "Fallback auf default_active-Approximation.",
            file=sys.stderr,
        )
        return None
    except OSError as exc:
        # OneDrive-Sync, NTFS-File-Lock oder mid-write atomic replace können
        # PermissionError/FileNotFoundError werfen zwischen exists() und
        # read_text(). Hook darf nicht crashen — sauberer Fallback statt
        # UserPromptSubmit-Tod. Reproduzierbar auf OneDrive-Vault unter Windows.
        print(
            f"trigger_enforcer: sync-state.json nicht lesbar ({type(exc).__name__}) — "
            "Fallback auf default_active-Approximation.",
            file=sys.stderr,
        )
        return None
    # Vorwärts-Kompat-Guard: nur schema_version 1 wird vom Hook in dieser
    # Form gelesen. Wenn sync.py künftig die active_skills-Semantik ändert
    # (z.B. Profil-Qualifizierung), sollte der alte Hook NICHT raten — er
    # fällt auf die default_active-Approximation zurück, bis er selbst
    # aktualisiert wird.
    if state.get("schema_version", 1) != 1:
        return None
    active = state.get("active_skills")
    if active is None:
        return None
    return set(active)


def _decide(payload: dict) -> dict:
    prompt = payload.get("prompt") or ""
    if not prompt:
        return {"action": "allow"}

    cwd = Path(payload.get("cwd") or Path.cwd())
    manifest_path = cwd / ".skills" / "manifest.json"
    if not manifest_path.exists():
        return {"action": "allow"}

    try:
        manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    except json.JSONDecodeError:
        return {"action": "allow"}

    # Welle-2-Pfad: sync-state.json liefert die kanonische Liste aktiver Skills.
    # Fallback-Pfad: Erstinstall ohne sync-Lauf → default_active-Approximation.
    active_skills_set = _load_active_skills(cwd)

    prompt_lower = prompt.lower()
    matches: list[tuple[str, str]] = []
    for name, info in (manifest.get("skills") or {}).items():
        if active_skills_set is not None:
            # Strikt: nur Skills, die sync.py explizit aktiviert hat.
            if name not in active_skills_set:
                continue
        else:
            # Fallback: any()-Approximation über default_active-Felder.
            if not _is_active(info):
                continue
        for trigger in info.get("trigger_words") or []:
            if trigger.lower() in prompt_lower:
                matches.append((name, trigger))
                break

    if not matches:
        return {"action": "allow"}

    lines = [
        f'- `{name}` (Trigger: „{trigger}")' for name, trigger in matches
    ]
    context = (
        "Folgende Skills passen zum Prompt und sollten aktiviert werden:\n"
        + "\n".join(lines)
    )
    hook_specific = {
        "hookEventName": "UserPromptSubmit",
        "additionalContext": context,
    }
    # Host-Detection (AP2-MINI-WELLE-DUAL-OUTPUT-SPEC §5.3): Codex verwirft
    # JSON-Output mit unbekannten Top-Level-Keys komplett (live bewiesen,
    # Transkript-Probe 2026-06-10) und akzeptiert nur das pure
    # hookSpecificOutput-Objekt. Diskriminator: turn_id ist im live
    # verifizierten Codex-Payload vorhanden, in der offiziellen Claude-Code-
    # Hook-Input-Spezifikation dagegen nicht.
    if "turn_id" in payload:
        return {"hookSpecificOutput": hook_specific}
    # Claude Code / Bundle-Evals: Dual-Output — Eval-Konvention top-level
    # plus offizielle hookSpecificOutput-Signatur (CODEX-COMPATIBILITY §7).
    return {
        "action": "allow",
        "additionalContext": context,
        "hookSpecificOutput": hook_specific,
    }


def main():
    """F14: UTF-8 für stdin/stdout/stderr erzwungen (Windows-cp1252-Default).
    Trigger-Hinweise enthalten deutsche Skill-Namen und Trigger-Worte.
    stdin ebenfalls: Codex liefert den Prompt UTF-8-codiert; ohne reconfigure
    wird „für" zu „fÃ¼r" und Umlaut-Trigger matchen nicht (Live-Befund
    2026-06-10, interaktiver Codex-Test, AP2-MINI-WELLE-DUAL-OUTPUT-SPEC §5.1).
    """
    sys.stdin.reconfigure(encoding="utf-8", errors="replace")
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")
    sys.stderr.reconfigure(encoding="utf-8", errors="replace")
    try:
        payload = json.loads(sys.stdin.read())
    except json.JSONDecodeError:
        print(json.dumps({"action": "allow"}))
        return
    print(json.dumps(_decide(payload), ensure_ascii=False))


if __name__ == "__main__":
    main()
