#!/usr/bin/env python3
"""Tiny durable loop harness primitives.

Loop engineering is just: pick next task, run one bounded step, write a receipt.
No dashboard required until this boring JSONL spine is proven useful.
"""

from __future__ import annotations

import argparse
import json
import os
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any, Iterable, Optional


@dataclass
class LoopTask:
    id: str
    title: str
    status: str = "todo"


@dataclass
class LoopEvent:
    run_id: str
    type: str
    data: dict[str, Any]
    timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())


@dataclass
class CostBudget:
    max_tokens: int
    used_tokens: int = 0

    def allow(self, estimate_tokens: int) -> bool:
        return self.used_tokens + estimate_tokens <= self.max_tokens

    def record(self, tokens: int) -> None:
        self.used_tokens += tokens


def load_tasks(path: str) -> list[LoopTask]:
    with open(path, "r", encoding="utf-8") as f:
        raw = json.load(f)
    return [LoopTask(**item) for item in raw]


def save_tasks(path: str, tasks: Iterable[LoopTask]) -> None:
    os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        json.dump([asdict(task) for task in tasks], f, indent=2)
        f.write("\n")


def next_task(tasks: Iterable[LoopTask]) -> Optional[LoopTask]:
    return next((task for task in tasks if task.status == "todo"), None)


def append_event(path: str, event: LoopEvent) -> None:
    os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
    with open(path, "a", encoding="utf-8") as f:
        f.write(json.dumps(asdict(event), ensure_ascii=False) + "\n")


def read_events(path: str) -> list[LoopEvent]:
    if not os.path.exists(path):
        return []
    with open(path, "r", encoding="utf-8") as f:
        return [LoopEvent(**json.loads(line)) for line in f if line.strip()]


def run_one_cycle(tasks_path: str, events_path: str, run_id: str, budget: CostBudget, estimate_tokens: int = 0) -> dict[str, Any]:
    """Claim one todo task if budget allows; callers do the actual agent work."""
    if not budget.allow(estimate_tokens):
        event = LoopEvent(run_id, "blocked", {"reason": "token_budget", "estimate_tokens": estimate_tokens, "budget": asdict(budget)})
        append_event(events_path, event)
        return {"status": "blocked", "reason": "token_budget"}

    tasks = load_tasks(tasks_path)
    task = next_task(tasks)
    if task is None:
        event = LoopEvent(run_id, "done", {"reason": "no_todo_tasks"})
        append_event(events_path, event)
        return {"status": "done"}

    task.status = "claimed"
    budget.record(estimate_tokens)
    save_tasks(tasks_path, tasks)
    event = LoopEvent(run_id, "claimed", {"task": asdict(task), "estimate_tokens": estimate_tokens, "budget": asdict(budget)})
    append_event(events_path, event)
    return {"status": "claimed", "task": asdict(task), "budget": asdict(budget)}


def main() -> int:
    parser = argparse.ArgumentParser(description="Run one bounded loop-harness cycle")
    parser.add_argument("--tasks", required=True, help="JSON task list")
    parser.add_argument("--events", required=True, help="JSONL event log")
    parser.add_argument("--run-id", required=True)
    parser.add_argument("--max-tokens", type=int, default=20_000)
    parser.add_argument("--used-tokens", type=int, default=0)
    parser.add_argument("--estimate-tokens", type=int, default=0)
    args = parser.parse_args()

    receipt = run_one_cycle(
        args.tasks,
        args.events,
        args.run_id,
        CostBudget(args.max_tokens, args.used_tokens),
        args.estimate_tokens,
    )
    print(json.dumps(receipt, indent=2))
    return 1 if receipt.get("status") == "blocked" else 0


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