///|
/// One computed backoff step.
priv struct BackoffStep {
  delay_ms : Double
  previous_ms : Double
}

///|
fn backoff_step(
  jitter : String,
  base_ms : Double,
  multiplier : Double,
  max_ms : Double,
  attempt : Int32,
  previous_ms : Double,
  rng : SplitMix64,
) -> BackoffStep raise InputError {
  // Geometric growth from the base, capped: pure = base * m^(k-1). The
  // loop grows step by step so the cap applies to every intermediate value,
  // exactly like the bounded caller-side loop this models.
  let mut pure = base_ms
  for _ in 1.. max_ms {
      pure = max_ms
    }
  }
  let pure = if pure > max_ms { max_ms } else { pure }
  let draw = rng.next_unit()
  let delay = match jitter {
    "none" => pure
    // AWS Exponential Backoff and Jitter: full sleeps anywhere in [0, pure].
    "full" => draw * pure
    // Equal jitter keeps half the exponential and jitters the rest.
    "equal" => pure / 2.0 + draw * pure / 2.0
    // Decorrelated jitter redraws between base and 3x the previous sleep.
    "decorrelated" => {
      let ceiling = if previous_ms * 3.0 > max_ms {
        max_ms
      } else {
        previous_ms * 3.0
      }
      let low = if base_ms > ceiling { ceiling } else { base_ms }
      low + draw * (ceiling - low)
    }
    _ => raise InputError("unsupported jitter: " + jitter)
  }
  let delay = if delay > max_ms { max_ms } else { delay }
  { delay_ms: delay, previous_ms: delay, }
}

///|
fn backoff_with(request : Value) -> Value raise InputError {
  fields_only(request, [
    "operation", "strategy", "base_ms", "multiplier", "max_ms", "jitter", "attempts",
    "seed",
  ])
  let strategy = optional_str(get(request, "strategy"), "exponential")
  if strategy != "exponential" && strategy != "constant" {
    raise InputError("unsupported backoff strategy: " + strategy)
  }
  if strategy == "constant" {
    raise InputError(
      "constant strategy is not implemented in 0.1.0; exponential is",
    )
  }
  let base_ms = num(required(request, "base_ms"))
  if base_ms < 1.0 || base_ms.floor() != base_ms || base_ms > 3600000.0 {
    raise InputError("base_ms must be a whole number 1..3600000")
  }
  let multiplier = optional_num(get(request, "multiplier"), 2.0)
  if multiplier <= 1.0 || multiplier >= 10.0 {
    raise InputError("multiplier must lie within (1, 10)")
  }
  let max_ms = optional_num(get(request, "max_ms"), 30000.0)
  if max_ms < base_ms || max_ms.floor() != max_ms || max_ms > 3600000.0 {
    raise InputError("max_ms must be a whole number base_ms..3600000")
  }
  let jitter = optional_str(get(request, "jitter"), "none")
  match jitter {
    "none" | "full" | "equal" | "decorrelated" => ()
    _ => raise InputError("unsupported jitter: " + jitter)
  }
  let attempts = num(required(request, "attempts"))
  if attempts < 1.0 || attempts.floor() != attempts || attempts > 64.0 {
    raise InputError("attempts must be a whole number 1..64")
  }
  let seed = optional_num(get(request, "seed"), 0.0)
  if seed < 0.0 || seed.floor() != seed || seed > 9007199254740991.0 {
    raise InputError("seed must be a whole number 0..2^53-1")
  }
  let rng = splitmix64(seed.to_uint64())
  let delays : Array[Value] = []
  let mut previous = base_ms
  let mut capped = 0
  for attempt in 1..<(attempts.to_int() + 1) {
    let step = backoff_step(
      jitter, base_ms, multiplier, max_ms, attempt, previous, rng,
    )
    if step.delay_ms >= max_ms {
      capped += 1
    }
    previous = step.previous_ms
    delays.push(Number(step.delay_ms))
  }
  record([
    ("strategy", String(strategy)),
    ("base_ms", Number(base_ms)),
    ("multiplier", Number(multiplier)),
    ("max_ms", Number(max_ms)),
    ("jitter", String(jitter)),
    ("attempts", Number(attempts)),
    ("seed", Number(seed)),
    ("delays_ms", Array(delays)),
    ("capped_attempts", Number(capped.to_double())),
  ])
}