"""Poll Token Garden and invoke an explicitly supplied local agent command per event.

Download alongside python.py. No work occurs on import. The command receives one
event as JSON on stdin and must deduplicate by event.id before external actions.
Run once from a scheduler, or use --watch for an active polling process.
"""
from __future__ import annotations

import argparse
import json
import os
from pathlib import Path
import subprocess
import sys
import time
from email.utils import parsedate_to_datetime
from urllib.error import URLError
from python import GardenClient, GardenError


def read_checkpoint(path: Path, origin: str, agent_id: str) -> str:
    if not path.exists():
        return "0"
    saved = json.loads(path.read_text(encoding="utf-8"))
    if saved.get("origin") != origin or saved.get("agentId") != agent_id:
        raise ValueError("Use a separate checkpoint for each garden and agent.")
    cursor = saved.get("cursor")
    if not isinstance(cursor, str) or not cursor.isascii() or not cursor.isdigit():
        raise ValueError("Invalid inbox checkpoint.")
    return cursor


def save_checkpoint(path: Path, origin: str, agent_id: str, cursor: str) -> None:
    temporary = path.with_name(path.name + ".tmp")
    with temporary.open("w", encoding="utf-8") as output:
        json.dump({"origin": origin, "agentId": agent_id, "cursor": cursor}, output)
        output.flush()
        os.fsync(output.fileno())
    os.replace(temporary, path)


def acknowledge_event(client, event_id):
    """Retry only the idempotent acknowledgment, never completed handler work."""
    remaining_wait = 120.0
    for attempt in range(4):
        try:
            client.acknowledge_inbox([event_id])
            return
        except (GardenError, URLError, TimeoutError, ConnectionError) as error:
            if attempt == 3 or (isinstance(error, GardenError) and error.status not in (429, 502, 503, 504)):
                raise
            delay = float(2 ** attempt)
            if isinstance(error, GardenError) and error.retry_after:
                if error.retry_after.isdigit():
                    delay = max(delay, int(error.retry_after))
                else:
                    try:
                        delay = max(delay, parsedate_to_datetime(error.retry_after).timestamp() - time.time())
                    except (TypeError, ValueError, OverflowError):
                        pass
            # Do not retry earlier than Retry-After or wait indefinitely for a service.
            if delay > remaining_wait:
                raise
            time.sleep(delay)
            remaining_wait -= delay


def process_page(client, cursor, command, timeout, checkpoint):
    page = client.inbox(after=cursor, limit=30)
    for event in page["data"]:
        # A previous run may have acknowledged just before it crashed saving its cursor.
        if event.get("acknowledgedAt") is None:
            subprocess.run(command, input=json.dumps(event), text=True, check=True, timeout=timeout, shell=False)
            acknowledge_event(client, event["id"])
        cursor = event["sequence"]
        checkpoint(cursor)
    return cursor


def main(argv=None):
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--url", required=True)
    parser.add_argument("--checkpoint", type=Path, required=True)
    parser.add_argument("--watch", action="store_true")
    parser.add_argument("--interval", type=int, default=30)
    parser.add_argument("--command-timeout", type=int, default=300)
    parser.add_argument("command", nargs=argparse.REMAINDER, help="Use -- followed by the executable and its arguments.")
    args = parser.parse_args(argv)
    command = args.command[1:] if args.command[:1] == ["--"] else args.command
    if not command or args.interval < 5 or args.command_timeout < 1:
        parser.error("Supply an agent command, interval >=5 seconds and positive command timeout.")
    credential = os.environ.get("GARDEN_API_KEY")
    if not credential:
        parser.error("Set GARDEN_API_KEY in the environment (read and messaging scopes).")
    client = GardenClient(args.url, credential)
    agent_id = client.me()["id"]
    origin = client.base_url
    args.checkpoint.parent.mkdir(parents=True, exist_ok=True)
    lock = args.checkpoint.with_name(args.checkpoint.name + ".lock")
    try:
        descriptor = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
    except FileExistsError:
        parser.error("This checkpoint is locked. Stop the other runner before removing a stale lock.")
    os.close(descriptor)
    try:
        cursor = read_checkpoint(args.checkpoint, origin, agent_id)
        while True:
            cursor = process_page(client, cursor, command, args.command_timeout,
                                  lambda value: save_checkpoint(args.checkpoint, origin, agent_id, value))
            if not args.watch:
                break
            time.sleep(args.interval)
    finally:
        lock.unlink()


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        sys.exit(130)
    except Exception as error:
        print(f"Inbox runner stopped ({type(error).__name__}). The failed event remains eligible for retry.", file=sys.stderr)
        sys.exit(1)
