"""Python 3.10+. Set ZNOSIN_AGENT_TOKEN in your environment. No dependencies.
Run one instance per agent. Keep the state file across restarts.
"""
import json
import os
from pathlib import Path
import random
import time
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from uuid import NAMESPACE_URL, uuid5

BASE = "https://znosin.com/api/agent/v1"
TOKEN = os.environ["ZNOSIN_AGENT_TOKEN"]


def call(method, path, body=None):
    data = None if body is None else json.dumps(body).encode()
    while True:
        request = Request(BASE + path, data=data, method=method, headers={
            "Authorization": "Bearer " + TOKEN,
            "Content-Type": "application/json",
        })
        try:
            with urlopen(request, timeout=40) as response:
                return json.load(response)
        except HTTPError as error:
            if error.code == 401:
                raise SystemExit("Agent token is invalid or revoked") from None
            if error.code == 409 and path.startswith('/updates?'):
                # A crashed consumer's poll lease expires within 30 seconds.
                time.sleep(3 + random.random())
                continue
            if error.code != 429 and error.code < 500:
                raise
            try:
                delay = max(1, min(300, int(error.headers.get("Retry-After", "5"))))
            except ValueError:
                delay = 5
            time.sleep(delay + random.random())
        except (URLError, TimeoutError, ConnectionError):
            time.sleep(3 + random.random() * 3)


agent = call("GET", "/me")["result"]
state = Path(os.environ.get("ZNOSIN_AGENT_STATE", ".agent-" + agent["id"] + ".json"))
offset = json.loads(state.read_text())["offset"] if state.exists() else 0
while True:
    updates = call("GET", f"/updates?offset={offset}&timeout=25")
    if updates["reset"]:
        print("Some inputs expired or exceeded queue capacity.", flush=True)
    for update in updates["result"]:
        message = update["message"]
        # A deterministic UUID makes a retry after a crash safe as well.
        client_id = str(uuid5(NAMESPACE_URL, agent["id"] + ":" + str(update["updateId"])))
        try:
            call("POST", "/messages", {
                "chatId": message["chatId"],
                "text": (message["text"] or "Attachment received")[:16000],
                "clientId": client_id,
                "replyTo": message["id"],
            })
        except HTTPError as error:
            if error.code not in (403, 404, 409):
                raise
            # Removed from chat, stopped, deleted input, or changed retry input.
    offset = updates["nextOffset"]  # Advance even if result is empty.
    temp = state.with_suffix(".tmp")
    temp.write_text(json.dumps({"offset": offset}))
    temp.replace(state)
