#!/usr/bin/env python3
"""Durable event-driven Jev queue for the 2026-11-07 five-App experiment.

Only one Jev run is ever submitted by this process.  It waits for the prior
run to reach a terminal state before creating the next request, and records
every decision in scheduler-state.json.  It never retries a run and never
changes a run after submission.
"""
from __future__ import annotations

import fcntl
import json
import os
import time
import urllib.request
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
STATE_PATH = ROOT / "admission" / "scheduler-state.json"
LOCK_PATH = ROOT / "admission" / "scheduler.lock"
API = "http://127.0.0.1:8000"
PROJECT = "guitaboo-5app-1rep-baseline-comparison-20261107"
TERMINAL = {"completed", "invalid", "interrupted", "startup_failed", "cancelled", "reused"}

# ODK is already queued by the initial submission.  The remaining entries are
# templates copied from the cancelled, pre-created Jev requests; no old
# idempotency key is reused.
FIRST = "run_b4871490b9e84bbcb1a9b3464ca596fd"
TEMPLATES = [
    ("wikipedia", "run_e2ec41a9bbeb4392be74bbc647c70fca"),
    ("scarletnotes", "run_c6b3ac0504354073bb8b664b60a4ddbd",),
    ("omninotes", "run_03a3021d78324925aff96071a9ef4f93"),
    ("activitydiary", "run_95ba6d15213c4b8e94673fe9af556c91"),
]


def api(path, payload=None):
    data = None if payload is None else json.dumps(payload, ensure_ascii=False).encode()
    req = urllib.request.Request(API + path, data=data, headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.loads(r.read())


def save(state):
    tmp = STATE_PATH.with_suffix(".tmp")
    tmp.write_text(json.dumps(state, ensure_ascii=False, indent=2) + "\n")
    os.replace(tmp, STATE_PATH)


def template_payload(config, app, ordinal):
    fields = [
        "project", "app", "rep", "tool", "condition", "apk_path", "apk_sha256",
        "device_id", "seed", "budget_seconds", "provider_id", "code", "parameters",
        "prompt_config", "initial_state", "evaluation_protocol", "command",
    ]
    payload = {k: config[k] for k in fields if k in config}
    payload["app"] = app
    payload["idempotency_key"] = (
        f"guitaboo-5app-fixab-serial-jev-{app}-rep2-20261107-{ordinal}"
    )
    payload["rerun_of"] = None
    payload["reuse_from"] = None
    return payload


def main():
    LOCK_PATH.parent.mkdir(parents=True, exist_ok=True)
    with LOCK_PATH.open("a+") as lock:
        fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
        state = json.loads(STATE_PATH.read_text()) if STATE_PATH.exists() else {
            "project": PROJECT, "first_run": FIRST, "next_index": 0,
            "submitted": [], "events": [], "halted": False,
        }
        while not state.get("halted") and state["next_index"] < len(TEMPLATES):
            prior = state["submitted"][-1]["run_id"] if state["submitted"] else FIRST
            detail = api("/api/runs/" + prior)
            state["events"].append({"at": time.time(), "observed": prior, "state": detail.get("state")})

            # Once ODK Jev has actually acquired the device, release the
            # deferred ODK StartLux run into the same device queue.  It then
            # follows Jev after cleanup without delaying Jev admission.
            if (prior == FIRST and detail.get("state") in
                    {"admission", "environment_setup", "starting", "exploring", "collecting", "validating"}
                    and not state.get("deferred_startlux_run_id")):
                deferred = json.loads((ROOT / "admission" / "odk-startlux-deferred-config.json").read_text())
                payload = template_payload(deferred["config"], "odkcollect", 1)
                payload["idempotency_key"] = "guitaboo-5app-fixab-serial-startlux-odkcollect-rep2-20261107"
                created = api("/api/runs", payload)
                state["deferred_startlux_run_id"] = created["id"]
                state["events"].append({"at": time.time(), "deferred_startlux_submitted": created["id"],
                                        "after_jev": prior})
                save(state)
            if detail.get("state") not in TERMINAL:
                save(state)
                time.sleep(15)
                continue

            app, template_id = TEMPLATES[state["next_index"]]
            existing = api("/api/runs/" + template_id)
            payload = template_payload(existing["config"], app, state["next_index"] + 2)
            created = api("/api/runs", payload)
            entry = {"app": app, "run_id": created["id"], "template": template_id,
                     "created_at": time.time(), "prior_run": prior}
            state["submitted"].append(entry)
            state["next_index"] += 1
            state["events"].append({"at": time.time(), "submitted": entry})
            save(state)
        state["complete"] = state["next_index"] >= len(TEMPLATES)
        save(state)


if __name__ == "__main__":
    main()
