///|
/// Selects which gateway resources an in-memory cache retains.
///
/// Guilds, channels, roles, members, users, and voice states are enabled by
/// default. Presences and messages are opt-in because they are high-churn and
/// can dominate memory use on larger bots.
pub struct CacheResources {
  priv guilds_ : Bool
  priv channels_ : Bool
  priv roles_ : Bool
  priv members_ : Bool
  priv users_ : Bool
  priv voice_states_ : Bool
  priv presences_ : Bool
  priv messages_ : Bool
}

///|
/// Select the retained resources individually; unspecified flags keep
/// their defaults (all on except presences and messages).
pub fn CacheResources::CacheResources(
  guilds? : Bool = true,
  channels? : Bool = true,
  roles? : Bool = true,
  members? : Bool = true,
  users? : Bool = true,
  voice_states? : Bool = true,
  presences? : Bool = false,
  messages? : Bool = false,
) -> CacheResources {
  {
    guilds_: guilds,
    channels_: channels,
    roles_: roles,
    members_: members,
    users_: users,
    voice_states_: voice_states,
    presences_: presences,
    messages_: messages,
  }
}

///|
/// Retain every resource, including high-churn presences and messages.
pub fn CacheResources::all() -> CacheResources {
  CacheResources(presences=true, messages=true)
}

///|
/// Retain nothing; enable resources from this baseline for a minimal
/// cache.
pub fn CacheResources::none() -> CacheResources {
  CacheResources(
    guilds=false,
    channels=false,
    roles=false,
    members=false,
    users=false,
    voice_states=false,
  )
}

///|
/// The default selection (see `CacheResources`).
pub fn CacheResources::default() -> CacheResources {
  CacheResources()
}

///|
/// Bounds one cached resource by entry count, lifetime, or both.
///
/// Negative limits clamp to zero. A `max_entries` of zero retains nothing,
/// while a `ttl_ms` of zero makes entries expire immediately. TTL deadlines
/// are fixed when an entry is written and are never extended by reads; expiry
/// is enforced lazily without background timers.
pub struct CacheLimit {
  priv max_entries_ : Int?
  priv ttl_ms_ : Int64?
}

///|
/// Create an optional entry-count and TTL limit for one resource.
pub fn CacheLimit::CacheLimit(
  max_entries? : Int,
  ttl_ms? : Int64,
) -> CacheLimit {
  {
    max_entries_: max_entries.map(value => value.max(0)),
    ttl_ms_: ttl_ms.map(value => value.max(0L)),
  }
}

///|
/// Per-resource bounds for an in-memory cache.
///
/// Guild, channel, and user limits are global. Role, member, voice-state, and
/// presence limits apply independently per guild; message limits apply
/// independently per channel. The unavailable-guild flags and the
/// channel-to-guild index are small bookkeeping state and remain unbounded.
/// TTLs use fixed, non-sliding deadlines set at write time and are enforced
/// lazily without background timers.
pub struct CacheLimits {
  priv guilds_ : CacheLimit
  priv channels_ : CacheLimit
  priv users_ : CacheLimit
  priv roles_ : CacheLimit
  priv members_ : CacheLimit
  priv voice_states_ : CacheLimit
  priv presences_ : CacheLimit
  priv messages_ : CacheLimit
}

///|
/// Configure limits for each resource. Messages retain at most 100 entries
/// per channel by default; other resources are unbounded by default.
pub fn CacheLimits::CacheLimits(
  guilds? : CacheLimit = CacheLimit(),
  channels? : CacheLimit = CacheLimit(),
  users? : CacheLimit = CacheLimit(),
  roles? : CacheLimit = CacheLimit(),
  members? : CacheLimit = CacheLimit(),
  voice_states? : CacheLimit = CacheLimit(),
  presences? : CacheLimit = CacheLimit(),
  messages? : CacheLimit = CacheLimit(max_entries=100),
) -> CacheLimits {
  {
    guilds_: guilds,
    channels_: channels,
    users_: users,
    roles_: roles,
    members_: members,
    voice_states_: voice_states,
    presences_: presences,
    messages_: messages,
  }
}

///|
/// The default per-resource limits (see `CacheLimits`).
pub fn CacheLimits::default() -> CacheLimits {
  CacheLimits()
}

///|
priv struct Timed[V] {
  value : V
  deadline : Int64?
}

///|
priv struct TimedMap[K, V] {
  entries_ : Map[K, Timed[V]]
  limit_ : CacheLimit
  now_ : () -> Int64
}

///|
fn[K : Hash + Eq, V] TimedMap::new(
  limit : CacheLimit,
  now : () -> Int64,
) -> TimedMap[K, V] {
  { entries_: Map([]), limit_: limit, now_: now, }
}

///|
/// Deadlines are non-decreasing in insertion order (`put` appends with a
/// fresh deadline; `replace_existing` keeps the original), so removing the
/// expired insertion-order prefix removes every expired entry.
fn[K : Hash + Eq, V] TimedMap::sweep(self : TimedMap[K, V]) -> Unit {
  while self.entries_.iter().head() is Some((expired_key, timed)) &&
        timed.deadline is Some(deadline) &&
        deadline <= (self.now_)() {
    self.entries_.remove(expired_key) |> ignore
  }
}

///|
fn[K : Hash + Eq, V] TimedMap::put(
  self : TimedMap[K, V],
  key : K,
  value : V,
) -> Unit {
  self.sweep()
  guard self.limit_.max_entries_ != Some(0) else { return }
  self.entries_.remove(key) |> ignore
  self.entries_[key] = {
    value,
    deadline: self.limit_.ttl_ms_.map(ttl => (self.now_)() + ttl),
  }
  if self.limit_.max_entries_ is Some(max_entries) {
    while self.entries_.length() > max_entries {
      let (oldest_key, _) = self.entries_.iter().head().unwrap()
      self.entries_.remove(oldest_key) |> ignore
    }
  }
}

///|
fn[K : Hash + Eq, V] TimedMap::replace_existing(
  self : TimedMap[K, V],
  key : K,
  value : V,
) -> Unit {
  guard self.entries_.get(key) is Some(timed) else { return }
  if timed.deadline is Some(deadline) && deadline <= (self.now_)() {
    self.entries_.remove(key) |> ignore
    return
  }
  self.entries_[key] = { value, deadline: timed.deadline, }
}

///|
fn[K : Hash + Eq, V] TimedMap::get(self : TimedMap[K, V], key : K) -> V? {
  guard self.entries_.get(key) is Some(timed) else { return None }
  if timed.deadline is Some(deadline) && deadline <= (self.now_)() {
    self.entries_.remove(key) |> ignore
    return None
  }
  Some(timed.value)
}

///|
fn[K : Hash + Eq, V] TimedMap::remove(self : TimedMap[K, V], key : K) -> Unit {
  self.entries_.remove(key) |> ignore
}

///|
fn[K : Hash + Eq, V] TimedMap::values(self : TimedMap[K, V]) -> Array[V] {
  self.sweep()
  [
    for timed in self.entries_.values() => timed.value
  ]
}

///|
fn[K : Hash + Eq, V] TimedMap::entries(self : TimedMap[K, V]) -> Array[(K, V)] {
  self.sweep()
  [
    for key, timed in self.entries_ => (key, timed.value)
  ]
}

///|
fn[K : Hash + Eq, K2 : Hash + Eq, V] bucket(
  outer : Map[K, TimedMap[K2, V]],
  key : K,
  limit : CacheLimit,
  now : () -> Int64,
) -> TimedMap[K2, V] {
  match outer.get(key) {
    Some(value) => value
    None => {
      let value = TimedMap::new(limit, now)
      outer[key] = value
      value
    }
  }
}

///|
fn[T] prefer_update(value : T?, previous : T?) -> T? {
  match value {
    Some(_) => value
    None => previous
  }
}

///|
fn is_thread(channel : @model.Channel) -> Bool {
  match channel.typ {
    AnnouncementThread | PublicThread | PrivateThread => true
    _ => false
  }
}

///|
/// A gateway-driven, resource-selective in-memory cache.
///
/// `update` is the only mutation entry point. Entity getters return the model
/// value retained by the cache; callers should treat it as immutable. List
/// getters always allocate a new outer array so callers cannot mutate the
/// cache's collections.
pub struct InMemoryCache {
  priv resources_ : CacheResources
  priv limits_ : CacheLimits
  priv now_ : () -> Int64
  priv guilds_ : TimedMap[@model.GuildId, @model.Guild]
  priv unavailable_guilds_ : Map[@model.GuildId, Bool]
  priv channels_ : TimedMap[@model.ChannelId, @model.Channel]
  priv channel_guilds_ : Map[@model.ChannelId, @model.GuildId]
  priv roles_ : Map[@model.GuildId, TimedMap[@model.RoleId, @model.Role]]
  priv members_ : Map[
    @model.GuildId,
    TimedMap[@model.UserId, @model.GuildMember],
  ]
  priv users_ : TimedMap[@model.UserId, @model.User]
  priv voice_states_ : Map[
    @model.GuildId,
    TimedMap[@model.UserId, @model.VoiceState],
  ]
  priv presences_ : Map[
    @model.GuildId,
    TimedMap[@model.UserId, @model.PresenceUpdateEvent],
  ]
  priv messages_ : Map[
    @model.ChannelId,
    TimedMap[@model.MessageId, @model.Message],
  ]
}

///|
/// Create an empty cache retaining the selected `resources` under the
/// per-resource `limits`. Feed it gateway events via `Bot::attach_cache` (or
/// call `update` manually). TTL deadlines are fixed at write time and expiry
/// is enforced lazily without background timers.
pub fn InMemoryCache::InMemoryCache(
  resources~ : CacheResources,
  limits? : CacheLimits = CacheLimits(),
  now? : () -> Int64 = @clock.now_ms,
) -> InMemoryCache {
  {
    resources_: resources,
    limits_: limits,
    now_: now,
    guilds_: TimedMap::new(limits.guilds_, now),
    unavailable_guilds_: Map([]),
    channels_: TimedMap::new(limits.channels_, now),
    channel_guilds_: Map([]),
    roles_: Map([]),
    members_: Map([]),
    users_: TimedMap::new(limits.users_, now),
    voice_states_: Map([]),
    presences_: Map([]),
    messages_: Map([]),
  }
}

///|
fn InMemoryCache::store_user(self : InMemoryCache, user : @model.User) -> Unit {
  if self.resources_.users_ {
    self.users_.put(user.id, user)
  }
}

///|
fn InMemoryCache::store_member(
  self : InMemoryCache,
  guild_id : @model.GuildId,
  cached_member : @model.GuildMember,
) -> Unit {
  if cached_member.user is Some(user) {
    self.store_user(user)
    if self.resources_.members_ {
      bucket(self.members_, guild_id, self.limits_.members_, self.now_).put(
        user.id,
        cached_member,
      )
    }
  }
}

///|
fn InMemoryCache::store_roles(
  self : InMemoryCache,
  guild_id : @model.GuildId,
  roles : Array[@model.Role],
) -> Unit {
  if self.resources_.roles_ {
    let cached = TimedMap::new(self.limits_.roles_, self.now_)
    for role in roles {
      cached.put(role.id, role)
    }
    self.roles_[guild_id] = cached
  }
}

///|
fn InMemoryCache::store_channel(
  self : InMemoryCache,
  channel : @model.Channel,
  guild_id? : @model.GuildId,
) -> Unit {
  if prefer_update(
      channel.guild_id,
      prefer_update(guild_id, self.channel_guilds_.get(channel.id)),
    )
    is Some(resolved_guild_id) {
    self.channel_guilds_[channel.id] = resolved_guild_id
  }
  if self.resources_.channels_ {
    self.channels_.put(channel.id, channel)
  }
}

///|
fn InMemoryCache::store_presence(
  self : InMemoryCache,
  presence : @model.PresenceUpdateEvent,
  guild_id? : @model.GuildId,
) -> Unit {
  guard self.resources_.presences_ else { return }
  guard presence.user is Some(user) && user.id is Some(user_id) else { return }
  guard prefer_update(presence.guild_id, guild_id) is Some(resolved_guild_id) else {
    return
  }
  let guild_presences = bucket(
    self.presences_,
    resolved_guild_id,
    self.limits_.presences_,
    self.now_,
  )
  let previous = guild_presences.get(user_id)
  guild_presences.put(user_id, {
    user: prefer_update(presence.user, previous.bind(value => value.user)),
    guild_id: Some(resolved_guild_id),
    status: prefer_update(presence.status, previous.bind(value => value.status)),
    activities: prefer_update(
      presence.activities,
      previous.bind(value => value.activities),
    ),
    client_status: prefer_update(
      presence.client_status,
      previous.bind(value => value.client_status),
    ),
  })
}

///|
fn InMemoryCache::store_voice_state(
  self : InMemoryCache,
  voice_state : @model.VoiceState,
  guild_id? : @model.GuildId,
) -> Unit {
  if voice_state.guild_member is Some(cached_member) {
    guard prefer_update(voice_state.guild_id, guild_id) is Some(member_guild_id) else {
      return
    }
    self.store_member(member_guild_id, cached_member)
  }
  guard self.resources_.voice_states_ else { return }
  guard prefer_update(voice_state.guild_id, guild_id) is Some(resolved_guild_id) else {
    return
  }
  match voice_state.channel_id {
    Null =>
      if self.voice_states_.get(resolved_guild_id) is Some(states) {
        states.remove(voice_state.user_id)
      }
    Value(_) =>
      bucket(
        self.voice_states_,
        resolved_guild_id,
        self.limits_.voice_states_,
        self.now_,
      ).put(voice_state.user_id, voice_state)
  }
}

///|
fn InMemoryCache::store_message(
  self : InMemoryCache,
  message : @model.Message,
  guild_id : @model.GuildId?,
) -> Unit {
  self.store_user(message.author)
  if guild_id is Some(resolved_guild_id) {
    self.channel_guilds_[message.channel_id] = resolved_guild_id
  }
  guard self.resources_.messages_ else { return }
  bucket(self.messages_, message.channel_id, self.limits_.messages_, self.now_).put(
    message.id,
    message,
  )
}

///|
/// Replace a retained message without changing its eviction position.
/// A cache miss is intentionally left unfilled: an update does not establish
/// that an older message is among the channel's most recent messages.
fn InMemoryCache::update_message(
  self : InMemoryCache,
  message : @model.Message,
  guild_id : @model.GuildId?,
) -> Unit {
  self.store_user(message.author)
  if guild_id is Some(resolved_guild_id) {
    self.channel_guilds_[message.channel_id] = resolved_guild_id
  }
  guard self.resources_.messages_ else { return }
  guard self.messages_.get(message.channel_id) is Some(channel_messages) else {
    return
  }
  channel_messages.replace_existing(message.id, message)
}

///|
fn InMemoryCache::delete_message(
  self : InMemoryCache,
  channel_id : @model.ChannelId,
  message_id : @model.MessageId,
) -> Unit {
  guard self.resources_.messages_ else { return }
  if self.messages_.get(channel_id) is Some(messages) {
    messages.remove(message_id)
  }
}

///|
fn InMemoryCache::remove_channel(
  self : InMemoryCache,
  channel_id : @model.ChannelId,
) -> Unit {
  self.channels_.remove(channel_id)
  self.channel_guilds_.remove(channel_id) |> ignore
  self.messages_.remove(channel_id) |> ignore
}

///|
fn InMemoryCache::remove_guild(
  self : InMemoryCache,
  guild_id : @model.GuildId,
) -> Unit {
  self.guilds_.remove(guild_id)
  self.unavailable_guilds_.remove(guild_id) |> ignore
  self.roles_.remove(guild_id) |> ignore
  self.members_.remove(guild_id) |> ignore
  self.voice_states_.remove(guild_id) |> ignore
  self.presences_.remove(guild_id) |> ignore
  let removed_channels : Array[@model.ChannelId] = []
  for channel_id, channel_guild_id in self.channel_guilds_ {
    if channel_guild_id == guild_id {
      removed_channels.push(channel_id)
    }
  }
  for channel_id in removed_channels {
    self.remove_channel(channel_id)
  }
}

///|
fn InMemoryCache::seed_available_guild(
  self : InMemoryCache,
  available : @model.AvailableGuildCreateEvent,
) -> Unit {
  let guild_id = available.guild.id
  self.unavailable_guilds_.remove(guild_id) |> ignore
  if self.resources_.guilds_ {
    self.guilds_.put(guild_id, available.guild)
  }
  self.store_roles(guild_id, available.guild.roles)
  if self.resources_.members_ {
    self.members_[guild_id] = TimedMap::new(self.limits_.members_, self.now_)
  }
  for cached_member in available.members {
    self.store_member(guild_id, cached_member)
  }
  for channel in available.channels {
    self.store_channel(channel, guild_id~)
  }
  for thread in available.threads {
    self.store_channel(thread, guild_id~)
  }
  if self.resources_.voice_states_ {
    self.voice_states_[guild_id] = TimedMap::new(
      self.limits_.voice_states_,
      self.now_,
    )
  }
  for voice_state in available.voice_states {
    self.store_voice_state(voice_state, guild_id~)
  }
  if self.resources_.presences_ {
    self.presences_[guild_id] = TimedMap::new(
      self.limits_.presences_,
      self.now_,
    )
  }
  for presence in available.presences {
    self.store_presence(presence, guild_id~)
  }
}

///|
fn InMemoryCache::update_member(
  self : InMemoryCache,
  update : @model.GuildMemberUpdateEvent,
) -> Unit {
  self.store_user(update.user)
  guard self.resources_.members_ else { return }
  let previous = self.member(update.guild_id, update.user.id)
  let cached_member = @model.GuildMember::{
    user: Some(update.user),
    nick: prefer_update(update.nick, previous.bind(value => value.nick)),
    avatar: Some(update.avatar),
    banner: previous.bind(value => value.banner),
    roles: update.roles,
    joined_at: update.joined_at,
    premium_since: prefer_update(
      update.premium_since,
      previous.bind(value => value.premium_since),
    ),
    deaf: prefer_update(update.deaf, previous.bind(value => value.deaf)),
    mute: prefer_update(update.mute, previous.bind(value => value.mute)),
    flags: prefer_update(update.flags, previous.bind(value => value.flags)),
    pending: prefer_update(
      update.pending,
      previous.bind(value => value.pending),
    ),
    permissions: previous.bind(value => value.permissions),
    communication_disabled_until: prefer_update(
      update.communication_disabled_until,
      previous.bind(value => value.communication_disabled_until),
    ),
    avatar_decoration_data: prefer_update(
      update.avatar_decoration_data,
      previous.bind(value => value.avatar_decoration_data),
    ),
    collectibles: previous.bind(value => value.collectibles),
  }
  self.store_member(update.guild_id, cached_member)
}

///|
fn InMemoryCache::sync_threads(
  self : InMemoryCache,
  sync : @model.ThreadListSyncEvent,
) -> Unit {
  guard self.resources_.channels_ else { return }
  let removed : Array[@model.ChannelId] = []
  for entry in self.channels_.entries() {
    let (channel_id, channel) = entry
    guard self.channel_guilds_.get(channel_id) == Some(sync.guild_id) &&
      is_thread(channel) else {
      continue
    }
    let should_remove = match sync.channel_ids {
      None => true
      Some(parent_ids) =>
        match channel.parent_id {
          Some(Value(parent_id)) => parent_ids.contains(parent_id)
          _ => false
        }
    }
    if should_remove {
      removed.push(channel_id)
    }
  }
  for channel_id in removed {
    self.remove_channel(channel_id)
  }
  for thread in sync.threads {
    self.store_channel(thread, guild_id=sync.guild_id)
  }
}

///|
/// Applies one decoded gateway event. Unsupported events are intentionally
/// ignored for forward compatibility.
pub fn InMemoryCache::update(
  self : InMemoryCache,
  event : @model.Event,
) -> Unit {
  match event {
    GuildCreate(Available(available)) => self.seed_available_guild(available)
    GuildCreate(Unavailable(guild)) =>
      if self.resources_.guilds_ {
        self.unavailable_guilds_[guild.id] = true
      }
    GuildUpdate(guild) => {
      self.unavailable_guilds_.remove(guild.id) |> ignore
      if self.resources_.guilds_ {
        self.guilds_.put(guild.id, guild)
      }
      self.store_roles(guild.id, guild.roles)
    }
    GuildDelete(guild) =>
      if guild.unavailable {
        if self.resources_.guilds_ {
          self.unavailable_guilds_[guild.id] = true
        }
      } else {
        self.remove_guild(guild.id)
      }
    ChannelCreate(channel) | ChannelUpdate(channel) | ThreadUpdate(channel) =>
      self.store_channel(channel)
    ChannelDelete(channel) => self.remove_channel(channel.id)
    ThreadCreate(create) => self.store_channel(create.channel)
    ThreadDelete(delete) => self.remove_channel(delete.id)
    ThreadListSync(sync) => self.sync_threads(sync)
    GuildRoleCreate(event) | GuildRoleUpdate(event) => {
      guard self.resources_.roles_ else { return }
      bucket(self.roles_, event.guild_id, self.limits_.roles_, self.now_).put(
        event.role.id,
        event.role,
      )
    }
    GuildRoleDelete(event) =>
      if self.roles_.get(event.guild_id) is Some(roles) {
        roles.remove(event.role_id)
      }
    GuildMemberAdd(event) =>
      self.store_member(event.guild_id, event.guild_member)
    GuildMemberRemove(event) => {
      self.store_user(event.user)
      if self.members_.get(event.guild_id) is Some(members) {
        members.remove(event.user.id)
      }
      if self.voice_states_.get(event.guild_id) is Some(states) {
        states.remove(event.user.id)
      }
      if self.presences_.get(event.guild_id) is Some(presences) {
        presences.remove(event.user.id)
      }
    }
    GuildMemberUpdate(event) => self.update_member(event)
    GuildMembersChunk(event) => {
      for cached_member in event.members {
        self.store_member(event.guild_id, cached_member)
      }
      if event.presences is Some(presences) {
        for presence in presences {
          self.store_presence(presence, guild_id=event.guild_id)
        }
      }
    }
    // A ban does not imply that Discord sent GUILD_MEMBER_REMOVE. Keep member
    // state until that authoritative event arrives.
    GuildBanAdd(event) | GuildBanRemove(event) => self.store_user(event.user)
    Ready(ready) => {
      self.store_user(ready.user)
      if self.resources_.guilds_ {
        for guild in ready.guilds {
          if guild.unavailable {
            self.unavailable_guilds_[guild.id] = true
          } else {
            self.unavailable_guilds_.remove(guild.id) |> ignore
          }
        }
      }
    }
    UserUpdate(user) => self.store_user(user)
    VoiceStateUpdate(voice_state) => self.store_voice_state(voice_state)
    PresenceUpdate(presence) => self.store_presence(presence)
    MessageCreate(event) => self.store_message(event.message, event.guild_id)
    MessageUpdate(event) => self.update_message(event.message, event.guild_id)
    MessageDelete(event) => self.delete_message(event.channel_id, event.id)
    MessageDeleteBulk(event) =>
      for id in event.ids {
        self.delete_message(event.channel_id, id)
      }
    _ => ()
  }
}

///|
/// The cached guild, if retained.
pub fn InMemoryCache::guild(
  self : InMemoryCache,
  id : @model.GuildId,
) -> @model.Guild? {
  self.guilds_.get(id)
}

///|
/// All cached guilds.
pub fn InMemoryCache::guilds(self : InMemoryCache) -> Array[@model.Guild] {
  self.guilds_.values()
}

///|
/// Whether the guild is currently marked unavailable by a `GUILD_DELETE`
/// outage notice.
pub fn InMemoryCache::is_guild_unavailable(
  self : InMemoryCache,
  id : @model.GuildId,
) -> Bool {
  self.unavailable_guilds_.get(id) == Some(true)
}

///|
/// Ids of guilds currently marked unavailable.
pub fn InMemoryCache::unavailable_guilds(
  self : InMemoryCache,
) -> Array[@model.GuildId] {
  self.unavailable_guilds_.keys().to_array()
}

///|
/// The cached channel or thread, if retained.
pub fn InMemoryCache::channel(
  self : InMemoryCache,
  id : @model.ChannelId,
) -> @model.Channel? {
  self.channels_.get(id)
}

///|
/// All cached channels and threads.
pub fn InMemoryCache::channels(self : InMemoryCache) -> Array[@model.Channel] {
  self.channels_.values()
}

///|
/// The cached role, if retained.
pub fn InMemoryCache::role(
  self : InMemoryCache,
  guild_id : @model.GuildId,
  role_id : @model.RoleId,
) -> @model.Role? {
  self.roles_.get(guild_id).bind(roles => roles.get(role_id))
}

///|
/// All cached roles of a guild.
pub fn InMemoryCache::roles(
  self : InMemoryCache,
  guild_id : @model.GuildId,
) -> Array[@model.Role] {
  self.roles_.get(guild_id).map(roles => roles.values()).unwrap_or([])
}

///|
/// The cached guild member, if retained.
pub fn InMemoryCache::member(
  self : InMemoryCache,
  guild_id : @model.GuildId,
  user_id : @model.UserId,
) -> @model.GuildMember? {
  self.members_.get(guild_id).bind(members => members.get(user_id))
}

///|
/// All cached members of a guild (only members seen in events unless
/// the privileged `GUILD_MEMBERS` intent fills the cache).
pub fn InMemoryCache::members(
  self : InMemoryCache,
  guild_id : @model.GuildId,
) -> Array[@model.GuildMember] {
  self.members_.get(guild_id).map(members => members.values()).unwrap_or([])
}

///|
/// The cached user, if retained.
pub fn InMemoryCache::user(
  self : InMemoryCache,
  id : @model.UserId,
) -> @model.User? {
  self.users_.get(id)
}

///|
/// All cached users.
pub fn InMemoryCache::users(self : InMemoryCache) -> Array[@model.User] {
  self.users_.values()
}

///|
/// The cached voice state of a user in a guild, if any.
pub fn InMemoryCache::voice_state(
  self : InMemoryCache,
  guild_id : @model.GuildId,
  user_id : @model.UserId,
) -> @model.VoiceState? {
  self.voice_states_.get(guild_id).bind(states => states.get(user_id))
}

///|
/// All cached voice states of a guild.
pub fn InMemoryCache::voice_states(
  self : InMemoryCache,
  guild_id : @model.GuildId,
) -> Array[@model.VoiceState] {
  self.voice_states_.get(guild_id).map(states => states.values()).unwrap_or([])
}

///|
/// The cached presence of a user in a guild, if presences are retained.
pub fn InMemoryCache::presence(
  self : InMemoryCache,
  guild_id : @model.GuildId,
  user_id : @model.UserId,
) -> @model.PresenceUpdateEvent? {
  self.presences_.get(guild_id).bind(presences => presences.get(user_id))
}

///|
/// All cached presences of a guild.
pub fn InMemoryCache::presences(
  self : InMemoryCache,
  guild_id : @model.GuildId,
) -> Array[@model.PresenceUpdateEvent] {
  self.presences_
  .get(guild_id)
  .map(presences => presences.values())
  .unwrap_or([])
}

///|
/// The cached message, if still retained.
pub fn InMemoryCache::message(
  self : InMemoryCache,
  channel_id : @model.ChannelId,
  message_id : @model.MessageId,
) -> @model.Message? {
  self.messages_.get(channel_id).bind(messages => messages.get(message_id))
}

///|
/// Cached messages of a channel, oldest first, capped by the messages limit.
pub fn InMemoryCache::messages(
  self : InMemoryCache,
  channel_id : @model.ChannelId,
) -> Array[@model.Message] {
  self.messages_.get(channel_id).map(m => m.values()).unwrap_or([])
}

///|
/// Computes guild-level permissions from the cached guild, member, and roles.
pub fn InMemoryCache::permissions(
  self : InMemoryCache,
  guild_id : @model.GuildId,
  user_id : @model.UserId,
) -> @model.Permissions? {
  guard self.resources_.guilds_ &&
    self.resources_.members_ &&
    self.resources_.roles_ else {
    return None
  }
  guard self.guild(guild_id) is Some(guild) &&
    self.member(guild_id, user_id) is Some(cached_member) &&
    self.roles_.get(guild_id) is Some(roles) &&
    roles.get(guild_id.cast()) is Some(everyone) else {
    return None
  }
  let role_permissions : Array[@model.Permissions] = []
  for role_id in cached_member.roles {
    guard roles.get(role_id) is Some(role) else {
      // Returning a partial permission set would be unsafe: a missing cached
      // role may grant ADMINISTRATOR or another permission.
      return None
    }
    role_permissions.push(role.permissions)
  }
  Some(
    @util.base_permissions(
      is_owner=guild.owner_id == user_id,
      everyone_permissions=everyone.permissions,
      role_permissions~,
    ),
  )
}

///|
/// Computes channel permissions using the cached base resources and channel
/// overwrites. Returns `None` when any required cache resource is disabled or
/// absent.
pub fn InMemoryCache::permissions_in(
  self : InMemoryCache,
  guild_id : @model.GuildId,
  channel_id : @model.ChannelId,
  user_id : @model.UserId,
) -> @model.Permissions? {
  guard self.resources_.channels_ else { return None }
  guard self.permissions(guild_id, user_id) is Some(base) &&
    self.member(guild_id, user_id) is Some(cached_member) &&
    self.channel(channel_id) is Some(channel) &&
    self.channel_guilds_.get(channel_id) == Some(guild_id) else {
    return None
  }
  Some(
    @util.channel_permissions(
      base~,
      guild_id~,
      member_id=user_id,
      role_ids=cached_member.roles,
      overwrites=channel.permission_overwrites.unwrap_or([]),
    ),
  )
}