///|
/// Sliding-window log limiter for gateway commands: Discord allows 120 sends
/// per 60 seconds per connection; a few slots are reserved for heartbeats
/// (which bypass this limiter), so user commands default to 115.
pub struct CommandLimiter {
  priv timestamps : Array[Int64]
  priv limit : Int32
  priv window_ms : Int64
  priv sleeper_ : async (Int32) -> Unit
}

///|
/// Create a limiter allowing `limit` sends per sliding window (60 seconds by default).
pub fn CommandLimiter::CommandLimiter(
  limit~ : Int32 = 115,
  window_ms~ : Int32 = 60000,
  sleeper~ : async (Int32) -> Unit = ms => @async.sleep(ms),
) -> CommandLimiter {
  { timestamps: [], limit, window_ms: window_ms.to_int64(), sleeper_: sleeper, }
}

///|
/// Wait until a send slot is available within the window, then consume it.
pub async fn CommandLimiter::acquire(self : CommandLimiter) -> Unit {
  self.wait_for_slot()
  self.record()
}

///|
// Callers must serialize waiting, sending, and recording to avoid oversubscription.
async fn CommandLimiter::wait_for_slot(self : CommandLimiter) -> Unit {
  for ;; {
    let now = @clock.now_ms()
    while self.timestamps.length() > 0 &&
          now - self.timestamps[0] >= self.window_ms {
      self.timestamps.remove(0) |> ignore
    }
    if self.timestamps.length() < self.limit {
      break
    }
    let wait = self.timestamps[0] + self.window_ms - now
    (self.sleeper_)(wait.to_int() + 1)
  }
}

///|
fn CommandLimiter::record(self : CommandLimiter) -> Unit {
  self.timestamps.push(@clock.now_ms())
}