///|
/// Inputs to a retry decision. Attempt zero is the initial exchange.
pub(all) struct RetryContext {
  request : @http.RequestHead
  idempotent : Bool
  bucket : String
  attempt : Int32
  random : Double
  now_unix_ms : Int64
  remaining_ms : Int64?
  replayable : Bool
  default_decision : @retry.Decision
}

///|
/// Creates a context, with midpoint jitter and no deadline by default.
pub fn RetryContext::new(
  request : @http.RequestHead,
  idempotent~ : Bool,
  replayable~ : Bool,
  attempt~ : Int32 = 0,
  bucket~ : String = "default",
  random~ : Double = 0.5,
  now_unix_ms~ : Int64 = 0L,
  remaining_ms? : Int64,
  default_decision~ : @retry.Decision = Stop(NotRetryable),
) -> RetryContext {
  {
    request,
    idempotent,
    bucket,
    attempt,
    random,
    now_unix_ms,
    remaining_ms,
    replayable,
    default_decision,
  }
}

///|
/// Captures live request facts at the boundary before invoking a policy.
/// Later acquisition or closure of the body cannot change this snapshot.
pub fn RetryContext::capture(
  request : @http.Request,
  attempt~ : Int32 = 0,
  bucket~ : String = "default",
  random~ : Double = 0.5,
  now_unix_ms~ : Int64 = 0L,
  remaining_ms? : Int64,
  replayable? : Bool,
) -> RetryContext {
  RetryContext::new(
    request.head(),
    idempotent=@http.is_idempotent(request),
    replayable=replayable.unwrap_or_else(() => request.body.is_replayable()),
    attempt~,
    bucket~,
    random~,
    now_unix_ms~,
    remaining_ms?,
  )
}

///|
/// A typed value or transport failure offered before any stream handoff.
pub(all) enum AttemptResult[T] {
  /// A complete buffered value with actual response metadata.
  Buffered(Reply[T])
  /// A selected stream consumer that has not started.
  StreamReady(@http.ResponseHead)
  /// A raised transport failure, distinct from response error values.
  TransportFailure(@http.TransportError)
}

///|
/// Projects HTTP facts for a generic retry rule without inspecting T.
pub fn[T] AttemptResult::http_outcome(
  self : AttemptResult[T],
) -> @retry.AttemptOutcome {
  match self {
    Buffered(reply) =>
      @retry.Response(status=reply.status, headers=reply.headers)
    StreamReady(head) =>
      @retry.Response(status=head.status, headers=head.headers)
    TransportFailure(error) => @retry.Failure(error)
  }
}

///|
/// Applies pure HTTP retry data to a snapshot.
pub fn RetryPolicy::decide(
  self : RetryPolicy,
  outcome : @retry.AttemptOutcome,
  context : RetryContext,
) -> @retry.Decision {
  self.rule.decide(
    outcome,
    context.request,
    attempt=context.attempt,
    random=context.random,
    now_unix_ms=context.now_unix_ms,
    remaining_ms?=context.remaining_ms,
    replayable=context.replayable,
    idempotent=context.idempotent,
  )
}