///|
/// Wraps a transport so that every request carries a deadline.
///
/// The library never performs I/O itself, so it cannot enforce a timeout on
/// its own: cancelling a socket read is a property of the HTTP stack, not of
/// the request. This wrapper makes the promise explicit instead. It stamps
/// `x-chroma-timeout-ms` on each request and hands it to the inner transport,
/// so a real transport can honour the value; a transport that ignores the
/// header still works, it just loses the guarantee. The bundled async
/// transport does honour it (see `AsyncHttpTransport`).
///
/// ```text
/// let client = ChromaClient::new(TimeoutTransport::new(http_stack, 5000))
/// ```
pub(all) struct TimeoutTransport[T] {
  inner : T
  timeout_ms : Int32
}

///|
/// Wraps `inner`, limiting each request to `timeout_ms` milliseconds.
///
/// A non-positive value means "no deadline", in which case only the header
/// stamping is skipped and requests pass through untouched.
pub fn[T] TimeoutTransport::new(
  inner : T,
  timeout_ms : Int32,
) -> TimeoutTransport[T] {
  { inner, timeout_ms, }
}

///|
/// Builds a timeout transport from a client's own configuration, so
/// `Config::timeout_ms` gets a consumer.
pub fn[T] TimeoutTransport::from_config(
  inner : T,
  config : Config,
) -> TimeoutTransport[T] {
  { inner, timeout_ms: config.timeout_ms, }
}

///|
/// The header a transport reads to learn its deadline.
pub const TIMEOUT_HEADER : String = "x-chroma-timeout-ms"

///|
pub impl[T : Transport] Transport for TimeoutTransport[T] with fn send(
  self,
  request,
) {
  if self.timeout_ms <= 0 {
    return self.inner.send(request)
  }
  let headers = request.headers.copy()
  headers[TIMEOUT_HEADER] = self.timeout_ms.to_string()
  self.inner.send({ ..request, headers, })
}

///|
pub extend TimeoutTransport with Transport::{send}

///|
/// The deadline a request asks for, if it carries one.
///
/// Both the header and an explicit value are read by `AsyncTransport`, so the
/// parsing lives here next to the header's definition rather than in every
/// transport. A malformed value is treated as "no deadline": the header is an
/// internal contract, and a garbage one means the wrapper was misused rather
/// than that the caller wants a zero deadline.
pub fn request_timeout_ms(request : Request) -> Int32? {
  match request.headers.get(TIMEOUT_HEADER) {
    Some(value) => parse_delay_ms(value)
    None => None
  }
}

///|
/// The wait a `RetryTransport` asks for before a re-send, if any. Same rules
/// as `request_timeout_ms`.
pub fn request_retry_delay_ms(request : Request) -> Int32? {
  match request.headers.get(RETRY_DELAY_HEADER) {
    Some(value) => parse_delay_ms(value)
    None => None
  }
}

///|
/// Reads a non-negative millisecond count; `None` when the value is not one.
fn parse_delay_ms(value : String) -> Int32? {
  let ms = @string.parse_int(value) catch { _ => return None }
  if ms > 0 {
    Some(ms)
  } else {
    None
  }
}

///|
/// Strips the two client-internal headers from a request.
///
/// They are addressed to the transport, not to Chroma, and a server is free to
/// reject an unknown header (some proxies answer 400). Every real transport
/// sends the stripped copy.
pub fn without_internal_headers(request : Request) -> Request {
  let headers = request.headers.copy()
  ignore(headers.remove(TIMEOUT_HEADER))
  ignore(headers.remove(RETRY_DELAY_HEADER))
  { ..request, headers, }
}