///|
/// One admission decision at one instant. `retry_after` is the exact number
/// of seconds until a request of the same weight could be admitted; it is
/// null when the decision is to allow.
priv struct Admission {
  allowed : Bool
  retry_after : Double?
  tokens : Double
}

///|
/// A token bucket: capacity tokens, refilled continuously at `rate` tokens
/// per second. Time is passed in, never read, so every decision is a pure
/// function of (state, now, weight) and replays deterministically.
priv struct TokenBucket {
  rate : Double
  capacity : Double
  mut tokens : Double
  mut last : Double
}

///|
fn token_bucket(rate : Double, capacity : Double) -> TokenBucket {
  // Full at construction, which is the standard initial state.
  { rate, capacity, tokens: capacity, last: 0.0, }
}

///|
fn TokenBucket::check(
  self : TokenBucket,
  now : Double,
  weight : Double,
) -> Admission {
  let elapsed = now - self.last
  if elapsed > 0.0 {
    let refilled = self.tokens + elapsed * self.rate
    self.tokens = if refilled > self.capacity {
      self.capacity
    } else {
      refilled
    }
    self.last = now
  }
  if weight <= self.tokens {
    self.tokens = self.tokens - weight
    { allowed: true, retry_after: None, tokens: self.tokens, }
  } else {
    let deficit = weight - self.tokens
    {
      allowed: false,
      retry_after: Some(deficit / self.rate),
      tokens: self.tokens,
    }
  }
}