///|
/// Retry limits, backoff, Retry-After cap, and non-idempotent policy.
pub(all) struct RetryPolicy {
  max_retries : Int
  backoff : Backoff
  max_retry_after_ms : Int
  retry_non_idempotent : Bool
} derive(Eq, Debug)

///|
/// Equality for retry policy values.
pub extend RetryPolicy with Eq::{equal, not_equal}

///|
/// Debug representation of retry policy values.
pub extend RetryPolicy with @debug.Debug::{to_repr}

///|
/// Uses two retries, default backoff, a 60-second server-delay cap, and only
/// retries non-idempotent requests when the failure proves they were not run.
pub fn RetryPolicy::default() -> RetryPolicy {
  {
    max_retries: 2,
    backoff: Backoff::default(),
    max_retry_after_ms: 60000,
    retry_non_idempotent: false,
  }
}

///|
/// Disables retries while retaining the other default settings.
pub fn RetryPolicy::none() -> RetryPolicy {
  { ..RetryPolicy::default(), max_retries: 0, }
}

///|
/// Reports whether the method is idempotent or an idempotency key is present.
pub fn is_idempotent(request : @http.Request) -> Bool {
  if request.headers.contains("idempotency-key") {
    return true
  }
  match request.http_method.to_lower() {
    "get" | "head" | "put" | "delete" | "options" | "trace" => true
    _ => false
  }
}

///|
/// Purely decides whether to retry and, if so, how long to wait.
pub fn RetryPolicy::next_delay_ms(
  self : RetryPolicy,
  error : SdkError,
  request : @http.Request,
  attempt : Int,
  random : Double,
) -> Int? {
  if attempt >= self.max_retries || !error.is_retryable() {
    return None
  }
  let processed_is_known_false = match error {
    RateLimited(..) | Transport(Connect(_)) => true
    _ => false
  }
  if !is_idempotent(request) &&
    !self.retry_non_idempotent &&
    !processed_is_known_false {
    return None
  }
  let server_delay = match error {
    RateLimited(retry_after_ms=Some(ms), ..) => Some(ms)
    Status(headers~, ..) => retry_after_ms(headers)
    _ => None
  }
  match server_delay {
    Some(ms) if ms > self.max_retry_after_ms => None
    Some(ms) => Some(ms)
    None => Some(self.backoff.delay_ms(attempt, random))
  }
}