///|
/// The backoff strategy used between retries.
pub(all) enum Backoff {
  /// Wait a constant `delay_ms` between attempts.
  Constant(delay_ms~ : Int)
  /// Exponential backoff: `base_ms * 2^attempt`, capped at `max_ms`.
  Exponential(base_ms~ : Int, max_ms~ : Int)
  /// Exponential backoff with deterministic jitter (a fraction of the delay
  /// derived from the attempt number, to avoid a true RNG dependency).
  ExponentialJitter(base_ms~ : Int, max_ms~ : Int)
} derive(Eq, Debug)

///|
/// A retry policy: how many times to retry and how long to wait between tries.
pub(all) struct RetryPolicy {
  max_retries : Int
  backoff : Backoff
  /// Whether to honor a server-provided `Retry-After` value when present.
  respect_retry_after : Bool
}

///|
/// A sensible default policy: 3 retries with exponential backoff from 500ms,
/// capped at 8s, honoring `Retry-After`.
pub fn RetryPolicy::default() -> RetryPolicy {
  {
    max_retries: 3,
    backoff: Exponential(base_ms=500, max_ms=8000),
    respect_retry_after: true,
  }
}

///|
/// Construct a custom retry policy.
pub fn RetryPolicy::new(
  max_retries : Int,
  backoff : Backoff,
  respect_retry_after? : Bool = true,
) -> RetryPolicy {
  { max_retries, backoff, respect_retry_after }
}

///|
/// Compute the delay in milliseconds before the given zero-based `attempt`
/// (0 = the wait before the first retry).
pub fn RetryPolicy::delay_for(self : RetryPolicy, attempt : Int) -> Int {
  match self.backoff {
    Constant(delay_ms~) => delay_ms
    Exponential(base_ms~, max_ms~) => exp_delay(base_ms, max_ms, attempt)
    ExponentialJitter(base_ms~, max_ms~) => {
      let base = exp_delay(base_ms, max_ms, attempt)
      // Deterministic jitter: subtract up to ~25% based on the attempt index.
      let jitter = base / 4 * (attempt % 3) / 3
      base - jitter
    }
  }
}

///|
/// Exponential delay `base * 2^attempt`, clamped to `max`, guarding overflow.
fn exp_delay(base_ms : Int, max_ms : Int, attempt : Int) -> Int {
  let mut delay = base_ms
  let mut i = 0
  while i < attempt {
    delay = delay * 2
    if delay >= max_ms {
      return max_ms
    }
    i = i + 1
  }
  if delay > max_ms {
    max_ms
  } else {
    delay
  }
}

///|
/// Parse a `Retry-After` header value into milliseconds.
///
/// Supports the delta-seconds form (e.g. `"5"` → 5000ms). The HTTP-date form
/// is not supported and yields `None`.
pub fn parse_retry_after(value : String) -> Int? {
  let secs = @string.parse_int(value) catch { _ => return None }
  if secs >= 0 {
    Some(secs * 1000)
  } else {
    None
  }
}

///|
/// Whether an error is retryable under this policy.
pub fn is_retryable_error(err : LLMError) -> Bool {
  match err {
    Transport(_) => true
    ApiError(code~, ..) => code == 429 || code >= 500
    _ => false
  }
}

///|
/// Perform a chat completion with retries governed by an explicit policy.
///
/// On a retryable `ApiError`, if the policy honors `Retry-After` and the
/// error message contains a parseable hint, that delay is preferred over the
/// computed backoff.
pub async fn Client::chat_with_policy(
  self : Client,
  request : ChatRequest,
  policy : RetryPolicy,
) -> ChatResponse raise LLMError {
  let mut attempt = 0
  for ;; {
    let result = Ok(self.chat(request)) catch { err => Err(err) }
    match result {
      Ok(resp) => return resp
      Err(err) => {
        if attempt >= policy.max_retries || !is_retryable_error(err) {
          raise err
        }
        let delay = policy.delay_for(attempt)
        @async.sleep(delay) catch {
          e => raise Transport("interrupted during backoff: " + e.to_string())
        }
        attempt = attempt + 1
      }
    }
  }
}