///|
/// Failures distinguished by transport, HTTP status, decoding, or configuration.
pub(all) suberror SdkError {
  Transport(@http.HttpError)
  Status(status~ : Int, headers~ : @http.Headers, body~ : Bytes)
  RateLimited(retry_after_ms~ : Int?, headers~ : @http.Headers, body~ : Bytes)
  Decode(message~ : String, body~ : Bytes)
  Config(String)
} derive(Debug)

///|
/// Formats the classified failure for debugging.
pub extend SdkError with @debug.Debug::{to_repr}

///|
/// Returns successful responses unchanged and classifies all other statuses.
///
/// ```mbt check
/// test {
///   let response : @http.Response = {
///     status: 204,
///     headers: @http.Headers::new(),
///     body: b"",
///   }
///   assert_eq(@runtime.classify(response), response)
///   let error = @runtime.Transport(@http.Timeout(500))
///   assert_eq(error.status(), None)
///   assert_true(error.is_retryable())
/// }
/// ```
pub fn classify(
  response : @http.Response,
  now_unix_ms? : Int64,
) -> @http.Response raise SdkError {
  if response.status >= 200 && response.status <= 299 {
    response
  } else if response.status == 429 {
    raise RateLimited(
      retry_after_ms=retry_after_ms(response.headers, now_unix_ms?),
      headers=response.headers,
      body=response.body,
    )
  } else {
    raise Status(
      status=response.status,
      headers=response.headers,
      body=response.body,
    )
  }
}

///|
/// Returns an HTTP status only for failures with an HTTP status.
pub fn SdkError::status(self : SdkError) -> Int? {
  match self {
    Status(status~, ..) => Some(status)
    RateLimited(..) => Some(429)
    _ => None
  }
}

///|
/// Reports transient failures; request idempotency remains the caller's concern.
pub fn SdkError::is_retryable(self : SdkError) -> Bool {
  match self {
    Transport(Connect(_) | Timeout(_)) | RateLimited(..) => true
    Status(status=408 | 409 | 500 | 502 | 503 | 504, ..) => true
    _ => false
  }
}