#!/usr/bin/env bash
# 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 bash, curl and jq. Read-only demo: works with a test or a live key.
#
#   export RELATIONSAI_API_KEY="rai_test_..."
#   bash rate-limit-backoff.sh
set -euo pipefail

BASE="${RELATIONSAI_API_BASE:-https://voice-api.relationsai.com/v1}"
BASE="${BASE%/}"
if [[ "${RELATIONSAI_API_KEY:-}" != rai_* ]]; then
  echo "Set RELATIONSAI_API_KEY to an API key (rai_test_... or rai_live_...)." >&2
  exit 1
fi

BODY_FILE="$(mktemp)" HDR_FILE="$(mktemp)"
trap 'rm -f "$BODY_FILE" "$HDR_FILE"' EXIT
MAX_ATTEMPTS=5
MAX_WAIT=60 # seconds. Longer waits (such as the daily call limit) go back to the caller.
PAUSE_UNTIL=0 # Set when a response says the window is used up.

header() { { grep -i "^$1:" "$HDR_FILE" || true; } | tail -n 1 | cut -d' ' -f2- | tr -d '\r'; }
backoff() { local cap=$(( 2 ** $1 )); if (( cap > 30 )); then cap=30; fi; echo $(( RANDOM % (cap + 1) )); }

# request METHOD PATH [JSON]: the body is left in $BODY_FILE and the headers in $HDR_FILE.
request() {
  local method="$1" path="$2" body="${3:-}" attempt status retry delay now remaining
  local args=(-sS -X "$method" "$BASE$path" -H "Authorization: Bearer $RELATIONSAI_API_KEY"
    --max-time 30 -D "$HDR_FILE" -o "$BODY_FILE" -w '%{http_code}')
  if [[ -n "$body" ]]; then args+=(-H "Content-Type: application/json" --data "$body"); fi

  for (( attempt = 1; ; attempt++ )); do
    now="$(date +%s)"
    if (( now < PAUSE_UNTIL )); then sleep $(( PAUSE_UNTIL - now )); fi
    if ! status="$(curl "${args[@]}")"; then
      if [[ "$method" != GET ]]; then echo "$method $path: no clear answer; check whether it happened before sending it again." >&2; return 1; fi # it may or may not have run
      if (( attempt == MAX_ATTEMPTS )); then echo "No response after $attempt attempts." >&2; return 1; fi
      sleep "$(backoff "$attempt")"
      continue
    fi
    # Slow down before the limit is hit: no requests left in this window means wait for the reset.
    remaining="$(header RateLimit-Remaining)"
    if [[ "$remaining" == "0" ]]; then delay="$(header RateLimit-Reset)"; PAUSE_UNTIL=$(( $(date +%s) + ${delay:-1} )); fi
    if (( status < 400 )) && jq -e . "$BODY_FILE" >/dev/null 2>&1; then return 0; fi
    if ! retry="$(jq -er '.error.retry' "$BODY_FILE" 2>/dev/null)"; then
      # Not an API answer (for example a proxy's HTML error page): treat it like no response.
      if [[ "$method" != GET ]]; then echo "$method $path: no clear answer; check whether it happened before sending it again." >&2; return 1; fi
      if (( attempt == MAX_ATTEMPTS )); then echo "$status without an API error body after $attempt attempts." >&2; return 1; fi
      sleep "$(backoff "$attempt")"
      continue
    fi
    delay="$(backoff "$attempt")"
    if [[ "$retry" == "after" ]]; then delay="$(header Retry-After)"; delay=$(( ${delay:-1} + RANDOM % 2 )); fi
    if [[ "$method" != GET && "$status" != 429 && "$status" != 503 ]]; then retry="no"; fi # only 429 and 503 mean nothing was done
    if [[ "$retry" == "no" ]] || (( attempt == MAX_ATTEMPTS || delay > MAX_WAIT )); then
      jq -r --arg s "$status" '"\($s) \(.error.code) (request \(.error.request_id)): \(.error.message)"' "$BODY_FILE" >&2
      return 1 # a long Retry-After (for example daily_call_limit_reached) goes back to the caller
    fi
    echo "$(jq -r '.error.code' "$BODY_FILE"): waiting $delay s before attempt $(( attempt + 1 ))"
    sleep "$delay"
  done
}

# Demo: read the workspace limits, then show the rate-limit headers of a few requests.
request GET /workspace
jq -r '.limits.rate_limits | to_entries[] |
  "\(.key): \(.value.per_key_per_minute // "-") per key, \(.value.per_workspace_per_minute // "-") per workspace, per minute"' "$BODY_FILE"
for i in 1 2 3; do
  request GET "/agents?limit=1"
  echo "Request $i: RateLimit-Remaining $(header RateLimit-Remaining) of $(header RateLimit-Limit), resets in $(header RateLimit-Reset) s"
done
