///|
pub(all) enum BackoffStrategy {
  Fixed(Int)
  Exponential(Int, Int)
  Linear(Int, Int, Int)
  Sequence(Array[Int])
} derive(Eq, Debug)

///|
pub(all) struct RetryPolicy {
  max_attempts : Int
  max_elapsed_ms : Int
  backoff : BackoffStrategy
  retryable_codes : Array[String]
} derive(Eq, Debug)

///|
pub(all) struct RetrySnapshot {
  attempt : Int
  elapsed_ms : Int
  next_delay_ms : Int
  can_retry : Bool
  stop_reason : String
} derive(Eq, Debug)

///|
pub fn fixed_backoff(delay_ms : Int) -> BackoffStrategy {
  Fixed(clamp_non_negative(delay_ms))
}

///|
pub fn exponential_backoff(
  base_delay_ms : Int,
  max_delay_ms : Int,
) -> BackoffStrategy {
  let base = clamp_non_negative(base_delay_ms)
  Exponential(base, max_int(base, max_delay_ms))
}

///|
pub fn linear_backoff(
  initial_delay_ms : Int,
  step_ms : Int,
  max_delay_ms : Int,
) -> BackoffStrategy {
  let initial = clamp_non_negative(initial_delay_ms)
  let step = clamp_non_negative(step_ms)
  Linear(initial, step, max_int(initial, max_delay_ms))
}

///|
pub fn sequence_backoff(delays_ms : Array[Int]) -> BackoffStrategy {
  let normalized : Array[Int] = []
  for delay in delays_ms {
    normalized.push(clamp_non_negative(delay))
  }
  Sequence(normalized)
}

///|
pub fn default_retry_policy() -> RetryPolicy {
  {
    max_attempts: 3,
    max_elapsed_ms: 5000,
    backoff: exponential_backoff(100, 1000),
    retryable_codes: [],
  }
}

///|
pub fn retry_policy(
  max_attempts : Int,
  max_elapsed_ms : Int,
  backoff : BackoffStrategy,
  retryable_codes : Array[String],
) -> RetryPolicy {
  {
    max_attempts: clamp_at_least_one(max_attempts),
    max_elapsed_ms: clamp_non_negative(max_elapsed_ms),
    backoff,
    retryable_codes: retryable_codes.copy(),
  }
}

///|
pub fn retry_delay(strategy : BackoffStrategy, attempt : Int) -> Int {
  let safe_attempt = clamp_at_least_one(attempt)
  match strategy {
    Fixed(delay) => delay
    Exponential(base, cap) => exponential_delay(base, cap, safe_attempt - 1)
    Linear(initial, step, cap) =>
      min_int(cap, initial + step * (safe_attempt - 1))
    Sequence(delays) => sequence_delay(delays, safe_attempt - 1)
  }
}

///|
pub fn retry_snapshot(
  policy : RetryPolicy,
  failure : AttemptFailure,
  attempt : Int,
  elapsed_ms : Int,
) -> RetrySnapshot {
  let delay = retry_delay(policy.backoff, attempt)
  let attempts_left = attempt < policy.max_attempts
  let within_time = policy.max_elapsed_ms == 0 ||
    elapsed_ms + delay <= policy.max_elapsed_ms
  let code_allowed = policy.retryable_codes.length() == 0 ||
    array_contains(policy.retryable_codes, failure.code)
  if !failure.retryable {
    {
      attempt,
      elapsed_ms,
      next_delay_ms: delay,
      can_retry: false,
      stop_reason: "failure is permanent",
    }
  } else if !code_allowed {
    {
      attempt,
      elapsed_ms,
      next_delay_ms: delay,
      can_retry: false,
      stop_reason: "failure code is not allowed",
    }
  } else if !attempts_left {
    {
      attempt,
      elapsed_ms,
      next_delay_ms: delay,
      can_retry: false,
      stop_reason: "attempt limit reached",
    }
  } else if !within_time {
    {
      attempt,
      elapsed_ms,
      next_delay_ms: delay,
      can_retry: false,
      stop_reason: "elapsed time limit reached",
    }
  } else {
    {
      attempt,
      elapsed_ms,
      next_delay_ms: delay,
      can_retry: true,
      stop_reason: "",
    }
  }
}

///|
pub fn should_retry(
  policy : RetryPolicy,
  failure : AttemptFailure,
  attempt : Int,
  elapsed_ms : Int,
) -> Bool {
  retry_snapshot(policy, failure, attempt, elapsed_ms).can_retry
}

///|
pub fn total_retry_delay(policy : RetryPolicy) -> Int {
  total_retry_delay_from(policy, 1, 0)
}

///|
fn total_retry_delay_from(
  policy : RetryPolicy,
  attempt : Int,
  total : Int,
) -> Int {
  if attempt >= policy.max_attempts {
    total
  } else {
    let delay = retry_delay(policy.backoff, attempt)
    if policy.max_elapsed_ms > 0 && total + delay > policy.max_elapsed_ms {
      total
    } else {
      total_retry_delay_from(policy, attempt + 1, total + delay)
    }
  }
}

///|
fn exponential_delay(base : Int, cap : Int, steps : Int) -> Int {
  if steps <= 0 {
    min_int(base, cap)
  } else {
    exponential_delay(min_int(cap, base * 2), cap, steps - 1)
  }
}

///|
fn sequence_delay(delays : Array[Int], index : Int) -> Int {
  if delays.length() == 0 {
    0
  } else if index < delays.length() {
    delays[index]
  } else {
    delays[delays.length() - 1]
  }
}