"""One copy only: a cron job, worker or scheduler runs as exactly one copy everywhere.
Python 3.8+, standard library only. Set CASCADE_API_KEY (and CASCADE_API for a local server).

The running copy holds job/<name> and renews every ttl/3 (one metered decision each).
If it dies, the hold lapses after ttl_ms and the next copy that starts takes over.
"""
import os, socket, threading

from once_only import cascade, keep_held


def singleton(name, run, agent=None, ttl_ms=15000):
    """run(lost_event) -> result; stop when lost_event is set. Returns {"ran": False, "behind"} if another copy runs."""
    agent = agent or f"{socket.gethostname()}:{os.getpid()}"
    cell = "job/" + name
    c = cascade("/claim", {"cell": cell, "agent": agent, "ttl_ms": ttl_ms})
    if not c.get("ok"):
        return {"ran": False, "behind": c.get("behind")}
    lost = threading.Event()
    stop = keep_held(cell, agent, ttl_ms, lost)
    try:
        return {"ran": True, "result": run(lost), "receipt": c["entry_hash"]}
    finally:
        stop()
        try:
            cascade("/release", {"cell": cell, "agent": agent})
        except Exception:
            pass
