#!/usr/bin/env python3
"""Хуки Чекана — механическое принуждение правил.

Три принципа, от которых нельзя отступать:

1. **Отказ вместо просьбы.** Правило, записанное только прозой, — это надежда.
   Здесь запреты возвращают `deny`, и обойти их уговорами нельзя.
2. **Fail-open.** Любая непредвиденная ошибка внутри хука обязана пропустить вызов.
   Инструмент, ломающий сессию из-за собственного бага, хуже отсутствующего.
3. **Молчание, когда нечего сказать.** Хук не комментирует нормальную работу.

Оркестратор отличается от исполнителя по полю `agent_id`: оно приходит только из
субагента. Его отсутствие означает главную сессию.
"""

from __future__ import annotations

import json
import subprocess
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

ALLOW_PREFIXES = (".chekan",)
MEMORY_FILES = {"CLAUDE.md", "AGENTS.md"}


def emit_allow() -> None:
    sys.exit(0)


def emit_deny(event: str, reason: str) -> None:
    print(
        json.dumps(
            {
                "hookSpecificOutput": {
                    "hookEventName": event,
                    "permissionDecision": "deny",
                    "permissionDecisionReason": reason,
                }
            },
            ensure_ascii=False,
        )
    )
    sys.exit(0)


def emit_context(event: str, text: str) -> None:
    print(
        json.dumps(
            {"hookSpecificOutput": {"hookEventName": event, "additionalContext": text}},
            ensure_ascii=False,
        )
    )
    sys.exit(0)


def active_run(cwd: Path):
    """Возвращает (Run, state) или (None, None). Никогда не бросает."""
    try:
        from chekan.store import Run, find_root

        root = find_root(cwd)
        slug = Run.active_slug(root)
        if not slug:
            return None, None
        run = Run(root, slug)
        state = json.loads(run.state_path.read_text(encoding="utf-8"))
        return run, state
    except Exception:
        return None, None


# --- запрет на правку кода оркестратором ---


def guard_write(payload: dict) -> None:
    if payload.get("agent_id"):
        emit_allow()  # это исполнитель, ему как раз положено писать код

    cwd = Path(payload.get("cwd") or ".")
    run, state = active_run(cwd)
    if run is None or state is None:
        emit_allow()
    if state.get("phase") != "build":
        emit_allow()

    target = payload.get("tool_input", {}).get("file_path") or ""
    if not target:
        emit_allow()

    path = Path(target)
    try:
        rel = path.resolve().relative_to(run.root)
    except (ValueError, OSError):
        emit_allow()

    if rel.parts and rel.parts[0] in ALLOW_PREFIXES:
        emit_allow()
    if rel.name in MEMORY_FILES:
        emit_allow()

    emit_deny(
        "PreToolUse",
        f"Чекан: идёт фаза сборки, а вы оркестратор — код проекта пишут субагенты.\n"
        f"Отклонена правка {rel}.\n\n"
        f"Ваш контекст единственный, который живёт весь прогон: прочитанный сейчас дифф "
        f"останется в нём до конца. Запустите исполнителя со свежим контекстом на таск, "
        f"а сами оставайтесь в .chekan/ и git.\n\n"
        f"Если правка действительно ваша (файл памяти, состояние) — она разрешена "
        f"автоматически. Если нужно выйти из режима, закройте прогон: chekan report",
    )


# --- страж коммита ---


def guard_bash(payload: dict) -> None:
    command = payload.get("tool_input", {}).get("command") or ""
    if "git commit" not in command:
        emit_allow()

    cwd = Path(payload.get("cwd") or ".")
    run, state = active_run(cwd)
    if run is None or state is None:
        emit_allow()

    problems: list[str] = []

    staged = subprocess.run(
        ["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
        cwd=run.root,
        capture_output=True,
        text=True,
        timeout=20,
    )
    files = [run.root / f for f in staged.stdout.split("\n") if f.strip()]
    if files:
        from chekan.secrets import scan_files

        findings = scan_files(files, run.root)
        problems += [f"секрет в индексе: {f}" for f in findings[:5]]

    try:
        from chekan.gates import build_invariant

        problems += build_invariant(run)
    except Exception:
        pass

    if problems:
        emit_deny(
            "PreToolUse",
            "Чекан: коммит остановлен.\n  · " + "\n  · ".join(problems) + "\n\n"
            "Секреты выньте в .env и добавьте имена в .env.example. Таски без "
            "доказательства закройте через `chekan run` и `chekan task done`.",
        )
    emit_allow()


# --- подсказки ---


def on_session_start(payload: dict) -> None:
    cwd = Path(payload.get("cwd") or ".")
    run, state = active_run(cwd)
    if run is None or state is None:
        emit_allow()
    if state.get("finished_at"):
        emit_allow()

    tasks = state.get("tasks", [])
    done = sum(1 for t in tasks if t.get("status") == "done")
    tail = f", задач закрыто {done} из {len(tasks)}" if tasks else ""
    line = (
        f"Чекан: идёт прогон «{state.get('title')}», фаза {state.get('phase')}{tail}. "
        "Продолжить: `chekan next`."
    )

    # Панель поднимается сама и открывается в браузере — но только если сервера ещё
    # не было. Иначе каждая новая сессия плодила бы вкладки.
    try:
        from chekan import dash

        url, started = dash.serve(run)
        line += f" Панель: {url}"
        if started and dash.open_in_browser(url):
            line += " (открыта в браузере)"
    except Exception:
        pass  # панель не обязана мешать старту сессии

    emit_context("SessionStart", line)


def on_pre_compact(payload: dict) -> None:
    cwd = Path(payload.get("cwd") or ".")
    run, state = active_run(cwd)
    if run is None:
        emit_allow()
    emit_context(
        "PreCompact",
        "Чекан: состояние прогона лежит на диске в .chekan/ — после сжатия "
        "перечитайте его через `chekan next`, а не восстанавливайте по памяти. "
        "Бриф, манифест и журнал доказательств никуда не делись.",
    )


HANDLERS = {
    "Write": guard_write,
    "Edit": guard_write,
    "NotebookEdit": guard_write,
    "Bash": guard_bash,
}


def main() -> None:
    try:
        payload = json.loads(sys.stdin.read() or "{}")
    except Exception:
        emit_allow()

    event = payload.get("hook_event_name", "")
    try:
        if event == "PreToolUse":
            handler = HANDLERS.get(payload.get("tool_name", ""))
            if handler:
                handler(payload)
        elif event == "SessionStart":
            on_session_start(payload)
        elif event == "PreCompact":
            on_pre_compact(payload)
    except SystemExit:
        raise
    except Exception:
        pass  # fail-open: баг хука не имеет права ломать сессию
    emit_allow()


if __name__ == "__main__":
    main()
