// gRPC hedging policy (the `hedgingPolicy` of a service config's method config): unlike
// retry, hedging fires attempts *in parallel* — an attempt every `hedging_delay_millis`
// without waiting for the previous to fail — and commits the call on the first attempt
// whose status is fatal (OK, or an error outside `non_fatal`). A non-fatal failure lets
// hedging keep going. This is pure decision logic — all-backend and total — that the async
// `Channel` drives: it decides *whether another attempt may start* and *whether a finished
// attempt commits the call*; the socket layer originates the parallel attempts, waits the
// delay, and cancels the losers.

///|
/// A method's hedging policy. `max_attempts` counts every parallel attempt (gRPC caps it at
/// 5); a new attempt originates every `hedging_delay_millis` while attempts remain. A
/// finished attempt commits the whole call unless its status is one of `non_fatal`, in which
/// case hedging continues.
pub(all) struct HedgingPolicy {
  max_attempts : Int
  hedging_delay_millis : Int
  non_fatal : Array[Int]
}

///|
/// A conventional default: up to 3 parallel attempts, a fresh one every 500 ms, treating
/// only `UNAVAILABLE` as non-fatal — so a transient transport failure keeps hedging while a
/// real application error commits at once.
pub fn HedgingPolicy::default() -> HedgingPolicy {
  {
    max_attempts: 3,
    hedging_delay_millis: 500,
    non_fatal: [Status::code(Unavailable)],
  }
}

///|
/// The effective attempt cap: the configured `max_attempts`, but never above gRPC's ceiling
/// of 5.
pub fn HedgingPolicy::attempt_cap(self : HedgingPolicy) -> Int {
  if self.max_attempts > 5 {
    5
  } else {
    self.max_attempts
  }
}

///|
/// Whether another hedged attempt may originate given `attempts_started` (the number already
/// launched): only while attempts remain under the cap.
pub fn HedgingPolicy::can_start_another(
  self : HedgingPolicy,
  attempts_started : Int,
) -> Bool {
  attempts_started < self.attempt_cap()
}

///|
/// Whether a finished attempt with `status_code` commits the whole call — stopping hedging
/// and returning this result. OK commits (success), and so does any error *not* listed as
/// non-fatal (a real failure retrying won't fix). A non-fatal error does not commit; hedging
/// continues.
pub fn HedgingPolicy::should_commit(
  self : HedgingPolicy,
  status_code : Int,
) -> Bool {
  if status_code == 0 {
    return true
  }
  for code in self.non_fatal {
    if code == status_code {
      return false
    }
  }
  true
}

///|
/// The delay between originating hedged attempts. The async driver waits this long before
/// launching the next parallel attempt; `0` fires them all at once.
pub fn HedgingPolicy::delay_millis(self : HedgingPolicy) -> Int {
  if self.hedging_delay_millis < 0 {
    0
  } else {
    self.hedging_delay_millis
  }
}