#!/usr/bin/env python
"""PRD Plugin autonomous run-until-done Stop hook for Claude Code.

Wired to Stop in .claude/settings.json. When the configured autonomy tier is
`autonomous`, this keeps the agent working toward the active goal instead of
stopping for pointless checkpoints — the deterministic, plugin-managed cousin of
the native /goal command (which is per-session and judged from the transcript;
this reads real state).

It blocks the stop (returns `{"decision":"block","reason":...}`) only when ALL of:
  - automation.autonomy_level == "autonomous", and
  - automation.autonomous_run_until_done is true (default true), and
  - there is an active goal: a TRK-* tracking record whose status is not closed,
    and
  - no pause has been requested (.prd_plugin/local/autonomy-pause absent), and
  - the per-session continue count is under automation.autonomous_continue_cap.

Otherwise it allows the stop. It NEVER forces continuation past the consent
floor: when the agent genuinely needs the user (push/merge/publish/secrets, or a
real blocker), it writes `.prd_plugin/local/autonomy-pause` and the guard lets it
stop (then clears the marker). Any error allows the stop — it must never trap the
session in a loop.
"""

import json
import sys
from pathlib import Path

CLOSED_STATUSES = {"complete", "completed", "done", "closed", "cancelled",
                   "canceled", "superseded", "abandoned", "resolved",
                   # deferred/parked are explicit owner decisions to stop —
                   # the guard must never demand work on a parked goal
                   "deferred", "parked", "finished"}
GOAL_TYPES = {"goal", "active_work"}
DEFAULT_CAP = 25
PAUSE_REL = ".prd_plugin/local/autonomy-pause"


def _read_json(path):
    return json.loads(Path(path).read_text(encoding="utf-8-sig"))


def _automation(root):
    cfg = _read_json(Path(root) / ".prd_plugin" / "config.json")
    return cfg.get("automation", {}) if isinstance(cfg, dict) else {}


def active_goal(root):
    """Most-recently-updated open TRK-* goal/active_work record id, or None.

    Preferring the newest open goal avoids latching onto a stale record that an
    earlier session forgot to close.
    """
    data = _read_json(Path(root) / ".prd_plugin" / "state" / "tracking.json")
    open_goals = [
        rec for rec in data.get("records", [])
        if isinstance(rec, dict) and rec.get("type") in GOAL_TYPES
        and str(rec.get("status", "")).lower() not in CLOSED_STATUSES
    ]
    if not open_goals:
        return None
    open_goals.sort(key=lambda r: (str(r.get("updated_at") or ""),
                                   str(r.get("created_at") or ""),
                                   str(r.get("id") or "")))
    return open_goals[-1].get("id")


def is_paused(root):
    return (Path(root) / PAUSE_REL).is_file()


def clear_pause(root):
    try:
        (Path(root) / PAUSE_REL).unlink()
    except OSError:
        pass


def _stop(reason):
    return {"action": "stop", "reason": reason, "goal": None}


def decide(root, continue_count):
    """Pure decision: continue or stop. Any failure resolves to stop."""
    try:
        auto = _automation(root)
    except Exception:
        return _stop("could not read config")
    if auto.get("autonomy_level") != "autonomous":
        return _stop("autonomy tier is not autonomous")
    if not auto.get("autonomous_run_until_done", True):
        return _stop("autonomous_run_until_done is off")
    if is_paused(root):
        return _stop("autonomy-pause requested — allowing stop for the consent floor / a blocker")
    try:
        goal = active_goal(root)
    except Exception:
        return _stop("could not read tracking state")
    if not goal:
        return _stop("no active goal — nothing left to continue")
    try:
        cap = int(auto.get("autonomous_continue_cap", DEFAULT_CAP))
    except (TypeError, ValueError):
        cap = DEFAULT_CAP
    if continue_count >= cap:
        return _stop(f"autonomous continue cap reached ({cap}) — allowing stop")
    reason = (
        f"Autonomous run-until-done: goal {goal} is still active. Continue with the "
        "next incomplete step toward it — do not stop to ask 'shall I proceed?'. "
        "If you genuinely need the user (consent floor: push/merge/publish/secrets, "
        "or a real blocker), create the file .prd_plugin/local/autonomy-pause with a "
        "one-line reason and then stop; the guard will let you. Otherwise keep going."
    )
    return {"action": "continue", "reason": reason, "goal": goal}


# --- per-session continue counter (runtime, gitignored) -------------------

def _counter_path(root):
    return Path(root) / ".prd_plugin" / "local" / "stop-guard-state.json"


def _load_counter(root):
    p = _counter_path(root)
    if p.is_file():
        try:
            return _read_json(p)
        except Exception:
            return {}
    return {}


def _save_counter(root, data):
    p = _counter_path(root)
    try:
        import os
        p.parent.mkdir(parents=True, exist_ok=True)
        tmp = p.with_suffix(p.suffix + ".tmp")
        tmp.write_text(json.dumps(data), encoding="utf-8")
        os.replace(tmp, p)
    except OSError:
        pass


def main():
    try:
        raw = sys.stdin.read()
        payload = json.loads(raw) if raw.strip() else {}
    except Exception:
        payload = {}
    if not isinstance(payload, dict):
        payload = {}

    root = payload.get("cwd") or str(Path.cwd())
    session = payload.get("session_id") or "default"

    # Cumulative per-session count of forced continuations. Counting only while
    # stop_hook_active was true reset the clock on every natural stop, so the cap
    # never bit; count every forced continuation and reset only on an allow-stop.
    counters = _load_counter(root)
    count = int(counters.get(session, 0))

    d = decide(root, count)

    if d["action"] == "continue":
        counters[session] = count + 1
        _save_counter(root, counters)
        print(json.dumps({"decision": "block", "reason": d["reason"]}))
        return 0

    # allow stop: reset this session's counter and clear any pause marker
    if session in counters:
        counters.pop(session, None)
        _save_counter(root, counters)
    if is_paused(root):
        clear_pause(root)
    # silent allow (no output) — nothing to inject
    return 0


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