///|
/// sttp-shared AttributeMap without Any: per-key slot plus closures.
pub struct AttributeKey[T] {
  priv id : Int32
  priv name : String
  priv slot : Ref[T?]
}

///|
let next_key : Ref[Int32] = { val: 0, }

///|
/// Creates a distinct typed key; identical names do not alias keys.
pub fn[T] AttributeKey::new(name : String) -> AttributeKey[T] {
  next_key.val += 1
  { id: next_key.val, name, slot: { val: None, }, }
}

///|
/// Returns the key's diagnostic name.
pub fn[T] AttributeKey::name(self : AttributeKey[T]) -> String {
  self.name
}

///|
/// Immutable associations of typed keys to local metadata.
pub struct Attributes {
  priv entries : Map[Int32, () -> Unit]
}

///|
/// Creates an empty attribute collection.
pub fn Attributes::empty() -> Attributes {
  { entries: Map([]), }
}

///|
/// Associates a value with a key on an independent copy.
pub fn[T] Attributes::put(
  self : Attributes,
  key : AttributeKey[T],
  value : T,
) -> Attributes {
  let m = self.entries.copy()
  m[key.id] = () => key.slot.val = Some(value)
  { entries: m, }
}

///|
/// Looks up a value with its original type, without consuming it.
pub fn[T] Attributes::get(self : Attributes, key : AttributeKey[T]) -> T? {
  match self.entries.get(key.id) {
    Some(f) => {
      f()
      let v = key.slot.val
      key.slot.val = None
      v
    }
    None => None
  }
}

///|
/// Set by server adapters.
pub let remote_addr : AttributeKey[String] = AttributeKey::new("remote_addr")

///|
/// Adapter-owned scheduling for work that may outlive the response. Invoke
/// during request handling; the adapter owns completion and shutdown policy.
pub let background : AttributeKey[(async () -> Unit) -> Unit] = AttributeKey::new(
  "background",
)

///|
/// Set on a response by its handler: a streamed response with this attribute
/// set to true is closed when the server starts to shut down, instead of
/// being drained; for endless streams such as server-sent events, whose
/// clients reconnect. The server stops pulling the body, closes it and ends
/// the response at the point the stream had reached (a chunked body with its
/// last chunk, otherwise by closing the connection), so shutdown does not
/// wait out its drain time for a stream that never ends by itself.
///
/// ```mbt check
/// test {
///   let response = @mttp_core.Response::new(200)
///   let response = {
///     ..response,
///     attributes: response.attributes.put(@mttp_core.close_on_shutdown, true),
///   }
///   assert_eq(response.attributes.get(@mttp_core.close_on_shutdown), Some(true))
/// }
/// ```
pub let close_on_shutdown : AttributeKey[Bool] = AttributeKey::new(
  "close_on_shutdown",
)

///|
/// A caller's statement that resending this request is safe even after it
/// was written (discord's REPLAY_SAFE_HEADER, without a wire header).
pub let replay_safe : AttributeKey[Bool] = AttributeKey::new("replay_safe")

///|
/// An idempotent method or an explicit replay_safe statement. A header alone
/// cannot establish the remote service's deduplication contract.
pub fn is_idempotent(req : Request) -> Bool {
  req.meth.is_idempotent() || req.attributes.get(replay_safe) == Some(true)
}