///|
/// Read-only context passed to command checks before argument decoding and
/// handler execution.
pub struct CheckCtx {
  priv raw_ : @framework.CommandCtx
  priv bot_ : AppCtx
}

///|
/// The app-level services: the REST client and application id.
pub fn CheckCtx::app(self : CheckCtx) -> AppCtx {
  self.bot_
}

///|
/// Guild or DM invocation scope, carrying the invoking member or user.
pub fn CheckCtx::scope(self : CheckCtx) -> @framework.InvocationScope {
  self.raw_.scope()
}

///|
/// The validated guild invocation. In a DM this raises
/// `HandlerError::GuildOnly`, which the error policy renders normally.
pub fn CheckCtx::guild_scope(
  self : CheckCtx,
) -> @framework.GuildInvocation raise HandlerError {
  require_guild(self.raw_.guild_scope())
}

///|
/// The invoking user.
pub fn CheckCtx::user(self : CheckCtx) -> @model.User {
  self.raw_.user()
}

///|
/// The full interaction payload.
pub fn CheckCtx::interaction(self : CheckCtx) -> @model.Interaction {
  self.raw_.interaction
}

///|
/// The guild the interaction was invoked in, or `None` outside guilds. Use
/// `guild_scope()` for flows that require a guild; this accessor is for
/// maybe-guild flows where DMs are valid.
pub fn CheckCtx::guild_id(self : CheckCtx) -> @model.GuildId? {
  self.raw_.interaction.guild_id
}

///|
/// A command guard. Returning false produces `HandlerError::CheckFailed`; raise
/// a more specific `HandlerError` for an expected denial. Any other error
/// reaches the error policy unchanged.
pub type CommandCheck = async (CheckCtx) -> Bool

///|
/// Require a guild invocation.
pub fn guild_only() -> CommandCheck {
  ctx => {
    match ctx.scope() {
      Guild(_) => true
      Dm(_) => raise GuildOnly
    }
  }
}

///|
/// Require a direct-message invocation.
pub fn dm_only() -> CommandCheck {
  ctx => {
    match ctx.scope() {
      Dm(_) => true
      Guild(_) => raise DmOnly
    }
  }
}

///|
/// Require effective guild permissions supplied on the interaction member.
/// Discord does not provide member permissions for DM invocations.
pub fn required_permissions(required : @model.Permissions) -> CommandCheck {
  ctx => {
    let guild = ctx.guild_scope()
    guard guild.member.permissions is Some(actual) &&
      (
        actual.contains(@model.Permissions::administrator()) ||
        actual.contains(required)
      ) else {
      raise MissingPermission(required)
    }
    true
  }
}

///|
/// Identity used to share a fixed-window command cooldown.
pub(all) enum CooldownBucket {
  User
  Guild
  Global
} derive(Debug, Eq)

///|
priv struct CooldownConfig {
  seconds : Int32
  bucket : CooldownBucket
}

///|
fn cooldown_key(
  config : CooldownConfig,
  ctx : CheckCtx,
) -> String raise HandlerError {
  match config.bucket {
    User => "user:\{ctx.user().id}"
    Guild =>
      match ctx.guild_id() {
        Some(guild_id) => "guild:\{guild_id}"
        None => raise GuildOnly
      }
    Global => "global"
  }
}

///|

///|
/// Whether a command with a cooldown runs while the cooldown store cannot
/// answer.
pub(all) enum CooldownOutage {
  /// Run it without a cooldown: cooldowns damp abuse, so an outage should not
  /// take every command offline.
  Allow
  /// Refuse it as if its cooldown were active for its whole window.
  Refuse
} derive(Debug, Eq)

///|
/// The App's cooldown store, its outage policy, and whether the store is
/// currently failing, so an outage is reported once rather than per command.
/// `store` is `None` until the first `attach` when no store was supplied.
priv struct Cooldowns {
  mut store : &@cooldown.CooldownStore?
  outage : CooldownOutage
  mut down : Bool
}

///|
/// The store an executor's commands use: the supplied one, or else the App's
/// own in-memory store, made at the first `attach` on that executor's clock
/// so its windows follow the time everything else there reads.
fn Cooldowns::bind(
  self : Cooldowns,
  clock : &@hclock.Clock,
) -> &@cooldown.CooldownStore {
  match self.store {
    Some(store) => store
    None => {
      let store = @cooldown.InMemoryCooldownStore(now=() => clock.now_unix_ms())
      self.store = Some(store)
      store
    }
  }
}

///|
/// Record whether the store answered. Returns true when this starts an outage.
fn Cooldowns::observe(self : Cooldowns, answered~ : Bool) -> Bool {
  let starts = !answered && !self.down
  self.down = !answered
  starts
}

///|
async fn RegisteredCommand::run_checks_and_cooldown(
  self : RegisteredCommand,
  bot : AppCtx,
  raw : @framework.CommandCtx,
  store : &@cooldown.CooldownStore,
  cooldowns : Cooldowns,
  warn : (AppWarning) -> Unit,
) -> Unit {
  let ctx = CheckCtx::{ raw_: raw, bot_: bot, }
  for check in self.checks_ {
    if !check(ctx) {
      raise CheckFailed
    }
  }
  guard self.cooldown_ is Some(config) else { return }
  let key = self.key() + "|" + cooldown_key(config, ctx)
  let duration_ms = config.seconds.to_int64() * 1000L
  let decision = store.try_acquire(key, window_ms=duration_ms)
  if cooldowns.observe(answered=!(decision is Unavailable(_))) &&
    decision is Unavailable(reason~) {
    warn(CooldownUnavailable(reason~, outage=cooldowns.outage))
  }
  match decision {
    Acquired => ()
    Active(retry_after_ms~) => raise OnCooldown(retry_after_ms~)
    Unavailable(_) =>
      if cooldowns.outage is Refuse {
        raise OnCooldown(retry_after_ms=duration_ms)
      }
  }
}