///|
/// Conservative defaults for transient HTTP responses, not transport failures.
pub fn retryable_status(status : Int) -> Bool {
  match status {
    408 | 429 | 500 | 502 | 503 | 504 => true
    _ => false
  }
}

///|
/// HTTP methods are case-sensitive. Body replayability is checked separately.
pub fn idempotent_method(http_method : String) -> Bool {
  match http_method {
    "GET" | "HEAD" | "PUT" | "DELETE" | "OPTIONS" | "TRACE" => true
    _ => false
  }
}

///|
/// attempt=0 is the first retry. Returns None for invalid input/settings.
/// Growth saturates before multiplication, including near Int64::MAX.
pub fn backoff_ceiling(settings : Settings, attempt : Int) -> Int64? {
  if !valid_settings(settings) || attempt < 0 || attempt > 1000 {
    return None
  }
  let mut delay = settings.base_delay_ms
  for i = 0; i < attempt; i = i + 1 {
    if delay >= settings.max_delay_ms - delay {
      return Some(settings.max_delay_ms)
    }
    delay = delay * 2L
  }
  Some(delay)
}

///|
/// Apply caller-provided full-jitter sample in 0..1_000_000, inclusive.
/// Splitting quotient/remainder avoids overflow without floating point.
pub fn jitter_delay(ceiling_ms : Int64, sample_ppm : Int) -> Int64? {
  if ceiling_ms < 0L || sample_ppm < 0 || sample_ppm > 1000000 {
    return None
  }
  let n = sample_ppm.to_int64()
  Some(ceiling_ms / 1000000L * n + ceiling_ms % 1000000L * n / 1000000L)
}