"""Offline loop demonstration: chooser is a test double, NOT a connected LLM.

Run from the lab directory: python reference/agent_loop.py --scenario approved
The tool executor keeps its boundaries when chooser is replaced by an LLM adapter.
"""
import argparse
from copy import deepcopy
import json
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
sys.path.insert(0, str(Path(__file__).resolve().parent))
from ticket_write import ApprovalRejected, CloseIntent, Ledger, Ticket, digest_args
from ticket_write.ledger import WriteTimeout
from closer import VerifiedCloser
from expiry import ExpiringAuthority

CASE = "CASE-3188"
OP = "op_close_3188"
NOTE = "重复件，关闭"


def example_chooser(observations):
    """Deterministic stand-in for structured model output. No hidden tool execution."""
    if not observations:
        return {"tool": "get_case", "ticket_id": CASE}
    if observations[-1]["tool"] == "get_case":
        return {"tool": "search_similar", "ticket_id": CASE}
    return {"tool": "close_case", "ticket_id": CASE, "note": NOTE}


def run(scenario="approved", chooser=None, max_steps=4):
    ledger = Ledger([Ticket(CASE, "OPEN", 7)])
    ledger.plan_timeout_after_commit(OP)
    authority = ExpiringAuthority(b"local-demo-secret-only", clock=lambda: 100)
    # A trusted test harness stands in for human approval, outside model output.
    approval = None if scenario == "missing" else authority.issue(
        operation_id=OP, args_digest=digest_args(CASE, NOTE),
        expires_at=99 if scenario == "expired" else 200,
    )
    if chooser is None:
        chooser = (lambda observations: {"tool": "get_case", "ticket_id": CASE}) \
            if scenario == "loop" else example_chooser
    observations, trace = [], []
    closer = VerifiedCloser(ledger, authority)

    def finish(status, reason):
        return {"status": status, "reason": reason, "writes": ledger.committed_writes,
                "attempt_ids": ledger.attempt_ids, "lookup_calls": ledger.lookup_calls,
                "trace": trace}

    for step in range(1, max_steps + 1):
        action = chooser(deepcopy(observations))
        entry = {"step": step, "observation": list(observations), "decision": action}
        trace.append(entry)
        if not isinstance(action, dict):
            entry["result"] = "invalid structured action"
            return finish("HANDOFF", "invalid action")
        tool = action.get("tool")
        allowed_keys = ({"tool", "reason"} if tool == "stop" else
                        {"tool", "ticket_id", "note"} if tool == "close_case" else {"tool", "ticket_id"})
        if tool not in {"get_case", "search_similar", "close_case", "stop"} or set(action) != allowed_keys:
            entry["result"] = "tool or fields not allowed"
            return finish("HANDOFF", "untrusted tool name or extra/missing fields")
        if tool == "stop":
            if not isinstance(action["reason"], str) or not action["reason"].strip():
                entry["result"] = "stop requires a reason"
                return finish("HANDOFF", "invalid stop action")
            entry["result"] = "no write requested; task success has not been asserted"
            return finish("STOPPED", action["reason"])
        if action["ticket_id"] != CASE or (tool == "close_case" and not isinstance(action["note"], str)):
            entry["result"] = "invalid arguments"
            return finish("HANDOFF", "invalid arguments")
        if tool == "get_case":
            result = {"status": ledger.read(CASE).status, "version": ledger.read(CASE).version}
        elif tool == "search_similar":
            result = {"same_event": True, "similar_case": "CASE-3000",
                      "subject_status": "WITHDRAWN" if scenario == "conflict" else "OPEN"}
        else:
            states = [o["result"] for o in observations if o["tool"] == "get_case"]
            matches = [o["result"] for o in observations if o["tool"] == "search_similar"]
            if not states or not matches or not matches[-1]["same_event"]:
                entry["result"] = "missing evidence"
                return finish("HANDOFF", "read state and corroborate the duplicate first")
            if states[-1]["status"] != matches[-1]["subject_status"]:
                entry["result"] = "conflicting observations"
                return finish("HANDOFF", "resolve source/version conflict before closing")
            if approval is None:
                entry["result"] = "approval required"
                return finish("WAIT_APPROVAL", "show this exact action to the approver")
            try:
                receipt = closer.close(CASE, CloseIntent(OP, action["note"]), approval)
            except ApprovalRejected as exc:
                entry["result"] = str(exc)
                return finish("WAIT_APPROVAL", "approval invalid for this action now")
            except (ValueError, WriteTimeout) as exc:
                entry["result"] = str(exc)
                return finish("HANDOFF", "state changed or outcome remains unknown")
            entry["result"] = {"receipt_id": receipt.receipt_id, "operation_id": receipt.operation_id}
            return finish("DONE", "receipt found; no more decisions or writes")
        entry["result"] = result
        observations.append({"tool": tool, "result": result})
    return finish("BUDGET_STOP", "decision budget reached; preserve observations for a person")


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--scenario", choices=["approved", "missing", "conflict", "expired", "loop"], default="approved")
    args = parser.parse_args()
    print(json.dumps(run(args.scenario), ensure_ascii=False, indent=2))
