#!/usr/bin/env python3
"""Bounded durable state for agent-driven CARBON validation loops."""

from __future__ import annotations

import argparse
import json
import os
import secrets
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


SCHEMA = "carbon.auto/v1"


def now() -> str:
    return datetime.now(timezone.utc).isoformat()


def write_json(path: Path, value: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as handle:
            json.dump(value, handle, indent=2)
            handle.write("\n")
        os.replace(tmp_name, path)
    finally:
        if os.path.exists(tmp_name):
            os.unlink(tmp_name)


def loop_dir(value: str) -> Path:
    return Path(value).expanduser().resolve()


def load(directory: Path) -> dict[str, Any]:
    path = directory / "loop.json"
    if not path.is_file():
        raise SystemExit(f"Loop not found: {path}")
    data = json.loads(path.read_text(encoding="utf-8"))
    if data.get("schema") != SCHEMA:
        raise SystemExit(f"Unsupported loop schema: {data.get('schema')}")
    return data


def save(directory: Path, data: dict[str, Any]) -> None:
    data["updatedAt"] = now()
    write_json(directory / "loop.json", data)


def command_init(args: argparse.Namespace) -> None:
    root = Path(args.root).expanduser().resolve()
    loop_id = args.id or f"auto-{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}-{secrets.token_hex(3)}"
    directory = root / ".carbon" / "auto" / loop_id
    if directory.exists():
        raise SystemExit(f"Loop already exists: {directory}")
    created = now()
    state = {
        "schema": SCHEMA,
        "id": loop_id,
        "target": str(args.target).strip(),
        "scope": str(args.scope).strip(),
        "status": "ready",
        "createdAt": created,
        "updatedAt": created,
        "limits": {
            "maxCycles": args.max_cycles,
            "maxMinutes": args.max_minutes,
            "stopAfterCleanCycles": args.stop_after_clean,
        },
        "cycles": [],
        "consecutiveCleanCycles": 0,
        "mutationPolicy": "propose-only; each fix requires scoped user approval",
        "stopReason": "",
    }
    save(directory, state)
    print(directory)


def command_start(args: argparse.Namespace) -> None:
    directory = loop_dir(args.loop_dir)
    state = load(directory)
    if state["status"] not in {"ready", "paused"}:
        raise SystemExit(f"Cannot start loop from status {state['status']}")
    state["status"] = "running"
    state.setdefault("startedAt", now())
    save(directory, state)
    print(directory / "loop.json")


def command_cycle(args: argparse.Namespace) -> None:
    directory = loop_dir(args.loop_dir)
    state = load(directory)
    if state["status"] != "running":
        raise SystemExit("Start the loop before recording a cycle.")
    cycle_no = len(state["cycles"]) + 1
    findings = max(0, args.finding_count)
    state["cycles"].append({
        "cycle": cycle_no,
        "outcome": args.outcome,
        "findingCount": findings,
        "runDir": str(args.run_dir or "").strip(),
        "summary": str(args.summary or "").strip()[:4000],
        "completedAt": now(),
    })
    started = datetime.fromisoformat(state["startedAt"])
    elapsed_minutes = max(0.0, (datetime.now(timezone.utc) - started).total_seconds() / 60)
    state["elapsedMinutes"] = round(elapsed_minutes, 2)
    clean = args.outcome == "passed" and findings == 0
    state["consecutiveCleanCycles"] = state["consecutiveCleanCycles"] + 1 if clean else 0
    if elapsed_minutes >= state["limits"]["maxMinutes"]:
        state["status"] = "completed"
        state["stopReason"] = "time-limit"
    elif cycle_no >= state["limits"]["maxCycles"]:
        state["status"] = "completed"
        state["stopReason"] = "max-cycles"
    elif state["consecutiveCleanCycles"] >= state["limits"]["stopAfterCleanCycles"]:
        state["status"] = "completed"
        state["stopReason"] = "clean-cycle-threshold"
    elif args.outcome == "blocked":
        state["status"] = "paused"
        state["stopReason"] = "blocked"
    save(directory, state)
    print(json.dumps({
        "cycle": cycle_no,
        "status": state["status"],
        "stopReason": state["stopReason"],
        "next": "Propose any fix and obtain scoped approval before mutation." if findings else "Run the next risk-ranked CARBON Quick pass.",
    }))


def command_stop(args: argparse.Namespace) -> None:
    directory = loop_dir(args.loop_dir)
    state = load(directory)
    state["status"] = "stopped"
    state["stopReason"] = str(args.reason).strip()[:1000]
    state["stoppedAt"] = now()
    save(directory, state)
    print(directory / "loop.json")


def command_status(args: argparse.Namespace) -> None:
    print(json.dumps(load(loop_dir(args.loop_dir)), indent=2))


def parser() -> argparse.ArgumentParser:
    root = argparse.ArgumentParser(description=__doc__)
    sub = root.add_subparsers(dest="command", required=True)
    init = sub.add_parser("init")
    init.add_argument("--root", required=True)
    init.add_argument("--target", required=True)
    init.add_argument("--scope", default="current change and adjacent behavior")
    init.add_argument("--id")
    init.add_argument("--max-cycles", type=int, default=5, choices=range(1, 51))
    init.add_argument("--max-minutes", type=int, default=60, choices=range(1, 1441))
    init.add_argument("--stop-after-clean", type=int, default=2, choices=range(1, 11))
    init.set_defaults(func=command_init)

    start = sub.add_parser("start")
    start.add_argument("--loop-dir", required=True)
    start.set_defaults(func=command_start)

    cycle = sub.add_parser("cycle")
    cycle.add_argument("--loop-dir", required=True)
    cycle.add_argument("--outcome", required=True, choices=("passed", "failed", "blocked", "partial"))
    cycle.add_argument("--finding-count", type=int, default=0)
    cycle.add_argument("--run-dir", default="")
    cycle.add_argument("--summary", default="")
    cycle.set_defaults(func=command_cycle)

    stop = sub.add_parser("stop")
    stop.add_argument("--loop-dir", required=True)
    stop.add_argument("--reason", required=True)
    stop.set_defaults(func=command_stop)

    status = sub.add_parser("status")
    status.add_argument("--loop-dir", required=True)
    status.set_defaults(func=command_status)
    return root


if __name__ == "__main__":
    arguments = parser().parse_args()
    arguments.func(arguments)
