///|
/// Decides whether to retry after a failed attempt, and how long to wait.
///
/// `attempt` counts the retries already made: 0 after the first attempt
/// fails. The checks run in order, and the first that applies wins:
///
/// 1. `attempt >= max_retries` stops with `Exhausted`.
/// 2. A body that cannot be sent again (`replayable` false) stops with
///    `NotReplayable`.
/// 3. A response carrying `override_header` with the value `true` (ignoring
///    case and surrounding whitespace) skips step 4; `false` stops with
///    `ServerSaidNo`; any other value is ignored.
/// 4. The outcome must be retryable for the request's method: a response
///    through the first `StatusRule` containing its status; `Protocol`
///    never; `Proxy` when `unsent_failures` is set and its status matches a
///    `StatusRule`; another unsent failure when `unsent_failures` is set;
///    any other failure through `sent_failures`. `Idempotent` methods are
///    those of a request for which `idempotent` is true.
/// 5. The wait is `server_delay_ms` when given, otherwise the response's
///    header per `retry_after.source` (an HTTP date needs `now_unix_ms`),
///    bounded by `retry_after.cap`; without one it is
///    `backoff.delay_ms(attempt, random)`.
/// 6. With `remaining_ms`, a wait that is not shorter than it stops with
///    `Budget(wait)`.
///
/// ```mbt check
/// test {
///   let rule : @retry.RetryRule = {
///     ..@retry.RetryRule::never(),
///     max_retries: 2,
///     statuses: [{ statuses: [Code(429)], methods: Any, }],
///     retry_after: {
///       source: Header,
///       cap: StopAbove(60000),
///       zero_means_backoff: false,
///     },
///   }
///   let post = @http.Request::post("/messages")
///   let headers = @http.Headers::of([("Retry-After", "3")])
///   let limited = @retry.Response(status=429, headers~)
///   assert_eq(
///     rule.decide(
///       limited,
///       post.head(),
///       attempt=0,
///       random=0.5,
///       idempotent=false,
///       replayable=true,
///     ),
///     Wait(3000),
///   )
///   assert_eq(
///     rule.decide(
///       limited,
///       post.head(),
///       attempt=0,
///       random=0.5,
///       remaining_ms=2000,
///       idempotent=false,
///       replayable=true,
///     ),
///     Stop(Budget(3000)),
///   )
/// }
/// ```
pub fn RetryRule::decide(
  self : RetryRule,
  outcome : AttemptOutcome,
  request : @http.RequestHead,
  attempt~ : Int32,
  random~ : Double,
  now_unix_ms? : Int64,
  remaining_ms? : Int64,
  server_delay_ms? : Int32,
  idempotent~ : Bool,
  replayable~ : Bool,
) -> Decision {
  if attempt >= self.max_retries {
    return Stop(Exhausted)
  }
  if !replayable {
    return Stop(NotReplayable)
  }
  let forced = match (outcome, self.override_header) {
    (Response(headers~, ..), Some(name)) =>
      match headers.get(name).map(value => value.trim().to_lower()) {
        Some("true") => true
        Some("false") => return Stop(ServerSaidNo)
        _ => false
      }
    _ => false
  }
  if !forced && self.refusal(outcome, request, idempotent) is Some(reason) {
    return Stop(reason)
  }
  let wait = match self.server_delay(outcome, server_delay_ms, now_unix_ms) {
    Ok(Some(delay)) => delay
    Ok(None) => self.backoff.delay_ms(attempt, random)
    Err(reason) => return Stop(reason)
  }
  if remaining_ms is Some(remaining) && wait.to_int64() >= remaining {
    return Stop(Budget(wait))
  }
  Wait(wait)
}

///|
// Step 4: why the outcome may not be retried, if it may not.
fn RetryRule::refusal(
  self : RetryRule,
  outcome : AttemptOutcome,
  request : @http.RequestHead,
  idempotent : Bool,
) -> StopReason? {
  match outcome {
    Response(status~, ..) =>
      match self.status_rule(status) {
        None => Some(NotRetryable)
        Some(rule) => permit(rule.methods, request, idempotent)
      }
    Failure(
      TransportError(
        kind=Invalid
        | Protocol
        | TooLarge(..)
        | Unreplayable
        | Closed
        | TooManyRedirects(..)
        | RedirectRefused,
        ..
      )
    ) => Some(NotRetryable)
    Failure(TransportError(kind=Proxy(status~), ..)) =>
      if self.unsent_failures && self.status_rule(status) is Some(_) {
        None
      } else {
        Some(NotRetryable)
      }
    Failure(error) =>
      if error.is_unsent() {
        if self.unsent_failures {
          None
        } else {
          Some(NotRetryable)
        }
      } else if self.sent_failures is Never {
        Some(NotRetryable)
      } else {
        permit(self.sent_failures, request, idempotent)
      }
  }
}

///|
fn RetryRule::status_rule(self : RetryRule, status : Int32) -> StatusRule? {
  for rule in self.statuses {
    for matcher in rule.statuses {
      if matcher.contains(status) {
        return Some(rule)
      }
    }
  }
  None
}

///|
// None when `methods` allows the request, else `MethodNotAllowed`.
fn permit(
  methods : Methods,
  request : @http.RequestHead,
  idempotent : Bool,
) -> StopReason? {
  let allowed = match methods {
    Never => false
    Any => true
    Idempotent => idempotent
    Listed(names) => {
      let verb = request.meth.to_string().to_lower()
      names.iter().any(name => name.to_string().to_lower() == verb)
    }
  }
  if allowed {
    None
  } else {
    Some(MethodNotAllowed)
  }
}

///|
// Step 5: the server's delay after `retry_after.cap`, None to use the
// backoff, or the reason to stop.
fn RetryRule::server_delay(
  self : RetryRule,
  outcome : AttemptOutcome,
  given : Int32?,
  now_unix_ms : Int64?,
) -> Result[Int32?, StopReason] {
  let delay = match (given, outcome) {
    (Some(delay), _) => Some(delay.max(0))
    (None, Response(headers~, ..)) =>
      match self.retry_after.source {
        Ignore => None
        Header =>
          match headers.get("retry-after") {
            Some(value) => parse_retry_after(value, now_unix_ms?)
            None => None
          }
        MsThenHeader => retry_after_ms(headers, now_unix_ms?)
      }
    (None, Failure(_)) => None
  }
  guard delay is Some(delay) else { return Ok(None) }
  if delay == 0 && self.retry_after.zero_means_backoff {
    return Ok(None)
  }
  match self.retry_after.cap {
    StopAbove(limit) if delay > limit => Err(RetryAfterTooLong(delay))
    BackoffAbove(limit) if delay > limit => Ok(None)
    _ => Ok(Some(delay))
  }
}