///|
/// Gateway-only services passed to event and service handlers after READY.
pub struct GatewayCtx {
  priv app_ : @app.AppCtx
  priv mut ready_ : @model.Ready
  priv intents_ : @model.Intents
  priv shard_ : @gateway.Shard
  priv shards_ : Array[@gateway.Shard]
  priv cache_ : @cache.InMemoryCache?
  priv framework_ : @framework.Framework
  priv collector_ : EventCollector
  priv group_ : @async.TaskGroup[Unit]
  priv voice_connections_ : Map[
    @model.Id[@model.GuildMarker],
    @voice.VoiceConnection,
  ]
  priv voice_join_gates_ : Map[@model.Id[@model.GuildMarker], @async.Semaphore]
  priv shutdown_requested_ : Ref[Bool]
}

///|
/// The transport-neutral application context for REST access.
pub fn GatewayCtx::app(self : GatewayCtx) -> @app.AppCtx {
  self.app_
}

///|
/// The latest READY payload for this gateway session.
pub fn GatewayCtx::ready(self : GatewayCtx) -> @model.Ready {
  self.ready_
}

///|
/// The latest heartbeat round-trip time for this context's shard. Unlike
/// `AppCtx::latency_ms()`, this is the exact shard value rather than the mean
/// across shards with measured latency.
pub fn GatewayCtx::latency_ms(self : GatewayCtx) -> Int64? {
  self.shard_.latency_ms()
}

///|
/// The cache attached with `Bot::attach_cache`, or `None` when no cache is
/// attached.
pub fn GatewayCtx::cache(self : GatewayCtx) -> @cache.InMemoryCache? {
  self.cache_
}

///|
/// Access the underlying gateway shard.
pub fn GatewayCtx::shard_raw(self : GatewayCtx) -> @gateway.Shard {
  self.shard_
}

///|
/// Access the underlying interaction framework.
pub fn GatewayCtx::framework_raw(self : GatewayCtx) -> @framework.Framework {
  self.framework_
}

///|
/// Bind a channel id to the gateway REST client.
pub fn GatewayCtx::channel_ref(
  self : GatewayCtx,
  channel_id : @model.ChannelId,
) -> @dhttp.ChannelRef {
  self.app_.http().channel_ref(channel_id)
}

///|
/// Resolve a channel with a cache-first lookup, including cached threads.
/// Cache misses (including a disabled channels resource) fall back to REST.
/// This favors speed; use `channel_ref(id).fetch()` when the latest channel
/// state is required. A REST result is deliberately not written back because
/// gateway events are the cache's only mutation entry point.
///
/// `MessageCreateEvent.channel_type` is commonly absent, so resolving the
/// channel makes thread detection straightforward:
///
/// ```mbt nocheck
/// let channel = ctx.resolve_channel(event.message.channel_id)
///
/// let is_thread = match channel.typ {
///   AnnouncementThread | PublicThread | PrivateThread => true
///   _ => false
/// }
/// ```
pub async fn GatewayCtx::resolve_channel(
  self : GatewayCtx,
  channel_id : @model.ChannelId,
) -> @model.Channel raise @dhttp.DiscordHttpError {
  if self.cache_ is Some(cache) {
    if cache.channel(channel_id) is Some(channel) {
      return channel
    }
  }
  self.channel_ref(channel_id).fetch()
}

///|
/// Bind a guild id to the gateway REST client.
pub fn GatewayCtx::guild_ref(
  self : GatewayCtx,
  guild_id : @model.GuildId,
) -> @dhttp.GuildRef {
  self.app_.http().guild_ref(guild_id)
}

///|
/// Bind a guild/user id pair to the gateway REST client.
pub fn GatewayCtx::member_ref(
  self : GatewayCtx,
  guild_id : @model.GuildId,
  user_id : @model.UserId,
) -> @dhttp.MemberRef {
  self.app_.http().member_ref(guild_id, user_id)
}

///|
/// Bind a user id to the gateway REST client.
pub fn GatewayCtx::user_ref(
  self : GatewayCtx,
  user_id : @model.UserId,
) -> @dhttp.UserRef {
  self.app_.http().user_ref(user_id)
}

///|
/// Bind a message-create event's message to the gateway REST client.
pub fn GatewayCtx::message_ref(
  self : GatewayCtx,
  event : @model.MessageCreateEvent,
) -> @dhttp.MessageRef {
  self.app_.http().ref_of_message(event.message)
}

///|
/// Bind a message model to the gateway REST client.
pub fn GatewayCtx::ref_of_message(
  self : GatewayCtx,
  message : @model.Message,
) -> @dhttp.MessageRef {
  self.app_.http().ref_of_message(message)
}

///|
/// Wait for the next component interaction with an exact custom id.
pub async fn GatewayCtx::wait_for_component(
  self : GatewayCtx,
  custom_id~ : String,
  timeout_ms? : Int,
) -> @framework.ComponentCtx? {
  self.framework_.wait_for_component(custom_id~, timeout_ms?)
}

///|
/// Wait for the next gateway event of `event_type` that satisfies `predicate`.
/// The event remains visible to other collectors and registered handlers.
/// Returns `None` on timeout. The caller must enable an intent capable of
/// delivering the requested event when it is not already subscribed.
pub async fn[T] GatewayCtx::wait_for(
  self : GatewayCtx,
  event_type : EventType[T],
  predicate : (T) -> Bool,
  timeout_ms? : Int,
) -> T? {
  self.collector_.wait_for(event_type, predicate, timeout_ms?)
}

///|
/// Request a graceful stop. Closing every managed shard wakes the run loop;
/// the task group then waits for already-started handlers before returning.
pub async fn GatewayCtx::shutdown(self : GatewayCtx) -> Unit {
  self.shutdown_requested_.val = true
  close_shards(self.shards_)
}