#!/usr/bin/env python3
"""One request helper that respects rate limits and retries only what is safe to retry.

  retry "after" (429, 503): wait Retry-After seconds, plus a little jitter.
  retry "yes" (500), no response at all, or a body that is not an API error (for example a proxy's
  HTML 502): exponential backoff with full jitter, 1 s doubling up to 30 s, at most 5 attempts.
  retry "no": never retried; fix the request or the configuration.
Writes (POST, PATCH, DELETE) are sent without an Idempotency-Key, so they are retried only when the answer
says nothing was done (429 or 503). After no response, a 500 or any other uncertain answer, the helper stops:
check whether the write happened (GET the resource) before sending it again.
Requires Python 3.9+ and requests. Read-only demo: works with a test or a live key.

    export RELATIONSAI_API_KEY="rai_test_..."
    python3 rate-limit-backoff.py
"""
import os
import random
import sys
import time

import requests

api_key = os.environ.get("RELATIONSAI_API_KEY", "")
base_url = os.environ.get("RELATIONSAI_API_BASE", "https://voice-api.relationsai.com/v1").rstrip("/")
session = requests.Session()
session.headers["Authorization"] = f"Bearer {api_key}"

MAX_ATTEMPTS = 5
MAX_WAIT = 60  # seconds. Longer waits (such as the daily call limit) go back to the caller.
pause_until = 0.0  # Set when a response says the window is used up.


def backoff_seconds(attempt):
    return random.uniform(0, min(30, 2 ** attempt))


def request(method, path, body=None, params=None):
    global pause_until
    is_write = method != "GET"
    uncertain = RuntimeError(f"{method} {path}: no clear answer; check whether it happened before sending it again")
    for attempt in range(1, MAX_ATTEMPTS + 1):
        if time.monotonic() < pause_until:
            time.sleep(pause_until - time.monotonic())
        try:
            r = session.request(method, f"{base_url}{path}", json=body, params=params, timeout=30)
        except requests.RequestException:
            if is_write:
                raise uncertain  # it may or may not have run
            if attempt == MAX_ATTEMPTS:
                raise
            time.sleep(backoff_seconds(attempt))
            continue

        # Slow down before the limit is hit: no requests left in this window means wait for the reset.
        if r.headers.get("RateLimit-Remaining") == "0":
            pause_until = time.monotonic() + float(r.headers.get("RateLimit-Reset", "1"))
        try:
            data = r.json()
        except ValueError:
            data = None
        if r.ok and data is not None:
            return data, r.headers
        if not isinstance(data, dict) or "error" not in data:
            # Not an API answer (for example a proxy's HTML error page): treat it like no response.
            if is_write:
                raise uncertain
            if attempt == MAX_ATTEMPTS:
                raise RuntimeError(f"{r.status_code} without an API error body after {attempt} attempts")
            time.sleep(backoff_seconds(attempt))
            continue

        e = data["error"]
        failure = RuntimeError(f"{r.status_code} {e['code']} (request {e['request_id']}): {e['message']}")
        if e["retry"] == "no" or attempt == MAX_ATTEMPTS:
            raise failure
        if is_write and r.status_code not in (429, 503):
            raise failure  # only 429 and 503 mean nothing was done
        wait = backoff_seconds(attempt)
        if e["retry"] == "after":
            wait = float(r.headers.get("Retry-After", "1")) + random.uniform(0, 1)
            if wait > MAX_WAIT:
                raise failure  # for example daily_call_limit_reached: retry after the reset
        print(f"{e['code']}: waiting {wait:.0f} s before attempt {attempt + 1}")
        time.sleep(wait)


def main():
    if not api_key.startswith("rai_"):
        raise RuntimeError("Set RELATIONSAI_API_KEY to an API key (rai_test_... or rai_live_...).")
    # Demo: read the workspace limits, then show the rate-limit headers of a few requests.
    workspace, _ = request("GET", "/workspace")
    for name, limit in workspace["limits"]["rate_limits"].items():
        print(f"{name}: {limit['per_key_per_minute'] or '-'} per key, {limit['per_workspace_per_minute'] or '-'} per workspace, per minute")
    for i in range(1, 4):
        _, headers = request("GET", "/agents", params={"limit": 1})
        print(f"Request {i}: RateLimit-Remaining {headers['RateLimit-Remaining']} of {headers['RateLimit-Limit']}, resets in {headers['RateLimit-Reset']} s")


if __name__ == "__main__":
    try:
        main()
    except (RuntimeError, requests.RequestException) as err:
        sys.exit(f"Error: {err}")
