// gRPC transparent-retry policy (the `retryPolicy` of a service config's method
// config): how many attempts a call may make, which `grpc-status` codes permit
// another attempt, and the exponential backoff between them. This is pure decision
// logic — all-backend and total — that the async `Channel` drives: it decides
// *whether* and *how long to wait*, the socket layer re-issues and sleeps.

///|
/// A method's retry policy. `max_attempts` counts the first try plus every retry
/// (gRPC caps it at 5); a call is retried only while attempts remain and its status
/// is in `retryable`. Backoff starts at `initial_backoff_millis` and grows by
/// `backoff_multiplier` each retry, capped at `max_backoff_millis`.
pub(all) struct RetryPolicy {
  max_attempts : Int
  initial_backoff_millis : Int
  max_backoff_millis : Int
  backoff_multiplier : Double
  retryable : Array[Int]
}

///|
/// A conventional default: up to 3 attempts, 100 ms initial backoff doubling to a
/// 1 s cap, retrying only `UNAVAILABLE` — the code a transient, safe-to-retry
/// transport failure carries.
pub fn RetryPolicy::default() -> RetryPolicy {
  {
    max_attempts: 3,
    initial_backoff_millis: 100,
    max_backoff_millis: 1000,
    backoff_multiplier: 2.0,
    retryable: [Status::code(Unavailable)],
  }
}

///|
/// Whether a call that has made `attempts_made` attempts (the first is 1) and got
/// `status_code` should be tried again: attempts must remain, and the status must be
/// non-OK and listed as retryable.
pub fn RetryPolicy::should_retry(
  self : RetryPolicy,
  status_code : Int,
  attempts_made : Int,
) -> Bool {
  // gRPC caps the effective attempt count at 5 regardless of the configured value.
  let cap = if self.max_attempts > 5 { 5 } else { self.max_attempts }
  if attempts_made >= cap || status_code == 0 {
    return false
  }
  for code in self.retryable {
    if code == status_code {
      return true
    }
  }
  false
}

///|
/// The backoff cap before the `retry_index`-th retry (the first retry is 1):
/// `min(initial * multiplier^(retry_index-1), max)`. gRPC sleeps a value drawn
/// uniformly from `[0, cap]`; the async driver applies that jitter, so this returns
/// the deterministic upper bound (which is also what a jitter-free driver sleeps).
pub fn RetryPolicy::backoff_millis(
  self : RetryPolicy,
  retry_index : Int,
) -> Int {
  // Before the first retry there is nothing to back off from, and the policy's own
  // starting value is the answer; the arithmetic after it is moonpool's.
  let index = if retry_index < 1 { 1 } else { retry_index }
  let schedule = @moonpool.Backoff::new(
    base=@moondate.Span::new(millis=self.initial_backoff_millis.to_int64()),
    factor=self.backoff_multiplier,
    cap=@moondate.Span::new(millis=self.max_backoff_millis.to_int64()),
    jitter=Rigid,
  )
  let ms = (schedule.ceiling(index).nanos / 1_000_000L).to_int()
  // Never sleep a negative duration, even if a policy is misconfigured with a negative
  // backoff.
  if ms < 0 {
    0
  } else {
    ms
  }
}