"""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"]
# The preference belongs to this event, not to a global user/device cache.
# Old events can contain null. A developer chooses the fallback and translations.
answers = {
    "en": ("Confirmed", "Received"), "ru": ("Подтверждено", "Получено"),
    "uk": ("Підтверджено", "Отримано"), "de": ("Bestätigt", "Empfangen"),
    "fr": ("Confirmé", "Reçu"), "pl": ("Potwierdzono", "Otrzymano"),
    "zh": ("已确认", "已收到"),
}
call("PUT", "/commands", {"language": "en", "commands": [
    {"command": "start", "description": "Buttons and mini apps"},
    {"command": "menu", "description": "Reply keyboard"},
    {"command": "hide", "description": "Remove reply keyboard"},
]})
state = Path(os.environ.get("ZNOSIN_AGENT_STATE", ".agent-buttons-" + 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&callbacks=true")
    if updates["reset"]:
        print("Some inputs expired or exceeded queue capacity.", flush=True)
    for update in updates["result"]:
        try:
            if "callback" in update:
                cb = update["callback"]
                # This demo only acknowledges the click. In a real order flow,
                # authorize cb["from"]["id"] against YOUR order before changing it.
                # Use cb["id"] as a durable deduplication key for business effects.
                if cb["buttonId"] == "confirm" and cb["data"] == "demo:confirm":
                    call("POST", "/callbacks/" + cb["id"] + "/answer", {
                        "text": answers.get(cb["from"].get("language_code"), answers["en"])[0] + ", " + cb["from"]["name"], "showAlert": False,
                    })
                continue
            message = update["message"]
            command = (message.get("command") or {}).get("name", "")
            markup = None
            text = "Use /start, /menu or /hide."
            if command == "start":
                text = "Choose an action"
                markup = {"type": "inline", "rows": [
                    [{"text": "Confirm", "id": "confirm", "data": "demo:confirm"}],
                    [{"text": "Website", "url": "https://znosin.com"},
                     {"text": "Calculator", "appId": "calculator"}],
                ]}
            elif command == "menu":
                text = "Choose a reply"
                markup = {"type": "reply", "rows": [["Yes", "No"], ["Help"]], "oneTime": True}
            elif command == "hide":
                text, markup = "Keyboard removed", {"type": "remove"}
            else:
                text = answers.get(message["from"].get("language_code"), answers["en"])[1] + ": " + message["text"][:15000]
            call("POST", "/messages", {
                "chatId": message["chatId"], "text": text, "markup": markup,
                "clientId": str(uuid5(NAMESPACE_URL, agent["id"] + ":buttons:" + str(update["updateId"]))),
                "replyTo": message["id"],
            })
        except HTTPError as error:
            if error.code not in (403, 404, 409):
                raise
            # Expired callback, revoked chat access or deleted source: skip.
    offset = updates["nextOffset"]
    temp = state.with_suffix(".tmp")
    temp.write_text(json.dumps({"offset": offset}))
    temp.replace(state)
