#!/usr/bin/env bash
# Place a call so that a retry can never place it twice.
# One Idempotency-Key per intended call: created once, stored with your own record, reused on every retry.
# Requires bash, curl and jq. Runs in test mode only: nothing is dialled or charged.
#
#   export RELATIONSAI_API_KEY="rai_test_..."
#   bash idempotent-retry.sh
set -euo pipefail

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

BODY_FILE="$(mktemp)" HDR_FILE="$(mktemp)"
trap 'rm -f "$BODY_FILE" "$HDR_FILE"' EXIT

new_key() { uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid; }
header() { { grep -i "^$1:" "$HDR_FILE" || true; } | tail -n 1 | cut -d' ' -f2- | tr -d '\r'; }
error_line() { jq -r --arg s "$1" '"\($s) \(.error.code) (request \(.error.request_id)): \(.error.message)"' "$BODY_FILE" >&2; }
# Exponential backoff with full jitter: a random whole number of seconds between 0 and min(30, 2^attempt).
backoff() { local cap=$(( 2 ** $1 )); if (( cap > 30 )); then cap=30; fi; echo $(( RANDOM % (cap + 1) )); }

# get PATH [curl -G data...]: prints the response body.
get() {
  local path="$1" status; shift
  status="$(curl -sS -G "$BASE$path" "$@" -H "Authorization: Bearer $RELATIONSAI_API_KEY" \
    --max-time 30 -o "$BODY_FILE" -w '%{http_code}')"
  if (( status >= 400 )); then error_line "$status"; return 1; fi
  cat "$BODY_FILE"
}

# find_call TO SINCE ORDER_ID: prints the call placed for this order, or nothing.
find_call() {
  get /calls --data-urlencode "to=$1" --data-urlencode "created_after=$2" --data-urlencode "limit=100" |
    jq -c --arg order "$3" 'first(.data[] | select(.metadata.order_id == $order)) // empty'
}

# post_with_retry PATH JSON IDEMPOTENCY_KEY [FIND_EXISTING_COMMAND]: every attempt sends the SAME key, so a stored
# result is replayed instead of running again. A 500 internal_error is NOT stored: a same-key retry would run the
# request again, so after one the command is asked first whether the write already happened.
# The response body is left in $BODY_FILE and its headers in $HDR_FILE.
post_with_retry() {
  local path="$1" body="$2" key="$3" find_existing="${4:-}" max=5 attempt status retry delay existing code
  for (( attempt = 1; ; attempt++ )); do
    if ! status="$(curl -sS -X POST "$BASE$path" -H "Authorization: Bearer $RELATIONSAI_API_KEY" \
        -H "Idempotency-Key: $key" -H "Content-Type: application/json" --data "$body" \
        --max-time 30 -D "$HDR_FILE" -o "$BODY_FILE" -w '%{http_code}')"; then
      # No response (timeout, connection reset): the request may or may not have run. Retry with the same key.
      if (( attempt == max )); then echo "No response after $attempt attempts. Check state before sending again." >&2; return 1; fi
      sleep "$(backoff "$attempt")"
      continue
    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 502): like no response, it may or may not have run.
      if (( attempt == max )); then echo "$status without an API error body after $attempt attempts. Check state before sending again." >&2; return 1; fi
      sleep "$(backoff "$attempt")"
      continue
    fi
    code="$(jq -r '.error.code' "$BODY_FILE")"
    if [[ "$code" == "internal_error" && -n "$find_existing" ]]; then
      existing="$($find_existing)"
      if [[ -n "$existing" ]]; then printf '%s\n' "$existing" >"$BODY_FILE"; : >"$HDR_FILE"; return 0; fi
      retry="yes"
    fi
    if [[ "$retry" == "no" ]] || (( attempt == max )); then
      if jq -e .error "$BODY_FILE" >/dev/null 2>&1; then error_line "$status"; else echo "$status $code after $attempt attempts" >&2; fi
      return 1
    fi
    # retry "after": wait Retry-After seconds. retry "yes": back off exponentially. Same key either way.
    if [[ "$retry" == "after" ]]; then delay="$(header Retry-After)"; delay="${delay:-1}"; else delay="$(backoff "$attempt")"; fi
    echo "$code: retrying in $delay s with the same Idempotency-Key"
    sleep "$delay"
  done
}

agent_id="${RELATIONSAI_AGENT_ID:-$(get /agents --data-urlencode "limit=100" | jq -r 'first(.data[] | select(.sample) | .id) // empty')}"
if [[ -z "$agent_id" ]]; then echo "No Sample agent found. Use the id of one of your own agents." >&2; exit 1; fi

# Your own record, for example an order that needs a reminder call. Create the key once and store it
# with the record before the first attempt, so a restarted worker retries with the same key.
order_id="ord_1001" phone="+12025550101" order_key="$(new_key)"
payload="$(jq -n --arg agent "$agent_id" --arg to "$phone" --arg order "$order_id" \
  '{agent_id: $agent, to: $to, metadata: {order_id: $order}}')"
started_at="$(date -u +%Y-%m-%dT%H:%M:%S.000Z)"
find_existing() { find_call "$phone" "$started_at" "$order_id"; }

if ! post_with_retry /calls "$payload" "$order_key" find_existing; then
  if [[ "$(jq -r '.error.code // empty' "$BODY_FILE" 2>/dev/null)" == "idempotency_outcome_unknown" ]]; then
    # Look for the call before deciding to send it again with a NEW key.
    existing="$(find_existing | jq -r '.id // empty')"
    if [[ -n "$existing" ]]; then echo "The call exists: $existing. Do not send it again." >&2
    else echo "No call found: send it again with a NEW key." >&2; fi
  fi
  exit 1
fi
first_id="$(jq -r '.id' "$BODY_FILE")"
echo "Placed $first_id, status $(jq -r '.status' "$BODY_FILE"), replayed: $(header Idempotent-Replayed | grep -q true && echo true || echo false)"

# A retry after a lost response: the same request with the same key returns the original call.
post_with_retry /calls "$payload" "$order_key" find_existing
again_id="$(jq -r '.id' "$BODY_FILE")"
echo "Retried: $again_id, replayed: $(header Idempotent-Replayed | grep -q true && echo true || echo false)"
if [[ "$again_id" != "$first_id" ]]; then echo "A retry with the same key must return the same call." >&2; exit 1; fi
echo "Same call returned: no second call was placed."
