///|
/// Initial response returned by an immediate component handler.
pub(all) enum ComponentReply {
  UpdateMessage(
    content~ : String?,
    embeds~ : Array[@model.Embed]?,
    components~ : Array[@model.Component]?,
    allowed_mentions~ : @model.AllowedMentions?
  )
  Message(InitialResponse)
  ShowModal(ModalHandle)
}

///|
/// Execution strategy for a component route.
pub(all) enum ComponentHandler {
  Immediate(async (ComponentImmediateCtx) -> ComponentReply)
  DeferredUpdate(async (ComponentDeferredCtx) -> Unit)
  DeferredMessage(ephemeral~ : Bool, async (ComponentDeferredCtx) -> Unit)
  Raw(async (@framework.ComponentCtx) -> Unit)
}

///|
/// Read-only component context for handlers returning their initial callback.
pub struct ComponentImmediateCtx {
  priv raw_ : @framework.ComponentCtx
  priv bot_ : AppCtx
  priv prefix_ : String
}

///|
/// Component context available after a deferred callback.
pub struct ComponentDeferredCtx {
  priv raw_ : @framework.ComponentCtx
  priv bot_ : AppCtx
  priv prefix_ : String
  priv waiter_ : ComponentWaiter?
  priv warn_ : (String) -> Unit
}

///|
/// Reply by editing the message that hosts the component.
pub fn ComponentReply::update_message(
  content? : String,
  embeds? : Array[@model.Embed],
  components? : Array[@model.Component],
  allowed_mentions? : @model.AllowedMentions,
) -> ComponentReply {
  UpdateMessage(content~, embeds~, components~, allowed_mentions~)
}

///|
/// Reply with a new message; set `ephemeral=true` to show it only to
/// the invoking user.
pub fn ComponentReply::message(
  content? : String,
  embeds? : Array[@model.Embed],
  components? : Array[@model.Component],
  files? : Array[@dhttp.FileUpload],
  allowed_mentions? : @model.AllowedMentions,
  ephemeral? : Bool = false,
) -> ComponentReply {
  Message(
    InitialResponse::message(
      content?,
      embeds?,
      components?,
      files?,
      allowed_mentions?,
      ephemeral~,
    ),
  )
}

///|
async fn InitialResponse::send_component(
  self : InitialResponse,
  ctx : @framework.ComponentCtx,
) -> Unit {
  ctx.respond(
    content?=self.content_,
    embeds?=self.embeds_,
    components?=self.components_,
    files?=self.files_,
    allowed_mentions?=self.allowed_mentions_,
    ephemeral=self.ephemeral_,
  )
}

///|
async fn ComponentReply::send(
  self : ComponentReply,
  ctx : @framework.ComponentCtx,
) -> Unit {
  match self {
    UpdateMessage(content~, embeds~, components~, allowed_mentions~) =>
      ctx.update_message(content?, embeds?, components?, allowed_mentions?)
    Message(response) => response.send_component(ctx)
    ShowModal(handle) => handle.send_component(ctx)
  }
}

///|
fn component_suffix(custom_id : String, prefix : String) -> String {
  if !custom_id.has_prefix(prefix) {
    return custom_id
  }
  custom_id[prefix.length():].to_owned()
}

///|
fn component_values(raw : @framework.ComponentCtx) -> Array[String] {
  raw.data.values.map(values => values.copy()).unwrap_or([])
}

///|
fn selected_users(raw : @framework.ComponentCtx) -> Array[@model.User] {
  let result = []
  guard raw.data.resolved is Some(resolved) && resolved.users is Some(users) else {
    return result
  }
  for id in component_values(raw) {
    if users.get(id) is Some(user) {
      result.push(user)
    }
  }
  result
}

///|
fn selected_roles(raw : @framework.ComponentCtx) -> Array[@model.Role] {
  let result = []
  guard raw.data.resolved is Some(resolved) && resolved.roles is Some(roles) else {
    return result
  }
  for id in component_values(raw) {
    if roles.get(id) is Some(role) {
      result.push(role)
    }
  }
  result
}

///|
fn selected_channels(raw : @framework.ComponentCtx) -> Array[@model.Channel] {
  let result = []
  guard raw.data.resolved is Some(resolved) &&
    resolved.channels is Some(channels) else {
    return result
  }
  for id in component_values(raw) {
    if channels.get(id) is Some(channel) {
      result.push(channel)
    }
  }
  result
}

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

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

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

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

///|
/// The full interaction payload.
pub fn ComponentImmediateCtx::interaction(
  self : ComponentImmediateCtx,
) -> @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 ComponentImmediateCtx::guild_id(
  self : ComponentImmediateCtx,
) -> @model.GuildId? {
  self.raw_.interaction.guild_id
}

///|
/// The full custom id of the component that fired.
pub fn ComponentImmediateCtx::custom_id(self : ComponentImmediateCtx) -> String {
  self.raw_.data.custom_id
}

///|
/// The custom id with this route's registered prefix stripped (the full
/// id when it does not start with the prefix).
pub fn ComponentImmediateCtx::suffix(self : ComponentImmediateCtx) -> String {
  component_suffix(self.raw_.data.custom_id, self.prefix_)
}

///|
/// The raw string values selected in a select menu (empty for buttons).
pub fn ComponentImmediateCtx::values(
  self : ComponentImmediateCtx,
) -> Array[String] {
  component_values(self.raw_)
}

///|
/// Resolved user objects for a user or mentionable select.
pub fn ComponentImmediateCtx::selected_users(
  self : ComponentImmediateCtx,
) -> Array[@model.User] {
  selected_users(self.raw_)
}

///|
/// Resolved role objects for a role or mentionable select.
pub fn ComponentImmediateCtx::selected_roles(
  self : ComponentImmediateCtx,
) -> Array[@model.Role] {
  selected_roles(self.raw_)
}

///|
/// Resolved partial channel objects for a channel select.
pub fn ComponentImmediateCtx::selected_channels(
  self : ComponentImmediateCtx,
) -> Array[@model.Channel] {
  selected_channels(self.raw_)
}

///|
/// The message hosting the component.
pub fn ComponentImmediateCtx::message(
  self : ComponentImmediateCtx,
) -> @model.Message {
  self.raw_.message()
}

///|
/// Escape hatch for advanced inspection. Calling response methods on the
/// raw value opts out of this wrapper's response discipline.
pub fn ComponentImmediateCtx::raw(
  self : ComponentImmediateCtx,
) -> @framework.ComponentCtx {
  self.raw_
}

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

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

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

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

///|
/// The full interaction payload.
pub fn ComponentDeferredCtx::interaction(
  self : ComponentDeferredCtx,
) -> @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 ComponentDeferredCtx::guild_id(
  self : ComponentDeferredCtx,
) -> @model.GuildId? {
  self.raw_.interaction.guild_id
}

///|
/// The full custom id of the component that fired.
pub fn ComponentDeferredCtx::custom_id(self : ComponentDeferredCtx) -> String {
  self.raw_.data.custom_id
}

///|
/// The custom id with this route's registered prefix stripped (the full
/// id when it does not start with the prefix).
pub fn ComponentDeferredCtx::suffix(self : ComponentDeferredCtx) -> String {
  component_suffix(self.raw_.data.custom_id, self.prefix_)
}

///|
/// The raw string values selected in a select menu (empty for buttons).
pub fn ComponentDeferredCtx::values(
  self : ComponentDeferredCtx,
) -> Array[String] {
  component_values(self.raw_)
}

///|
/// Resolved user objects for a user or mentionable select.
pub fn ComponentDeferredCtx::selected_users(
  self : ComponentDeferredCtx,
) -> Array[@model.User] {
  selected_users(self.raw_)
}

///|
/// Resolved role objects for a role or mentionable select.
pub fn ComponentDeferredCtx::selected_roles(
  self : ComponentDeferredCtx,
) -> Array[@model.Role] {
  selected_roles(self.raw_)
}

///|
/// Resolved partial channel objects for a channel select.
pub fn ComponentDeferredCtx::selected_channels(
  self : ComponentDeferredCtx,
) -> Array[@model.Channel] {
  selected_channels(self.raw_)
}

///|
/// The message hosting the component.
pub fn ComponentDeferredCtx::message(
  self : ComponentDeferredCtx,
) -> @model.Message {
  self.raw_.message()
}

///|
/// Escape hatch for advanced inspection. Calling response methods on the
/// raw value opts out of this wrapper's response discipline.
pub fn ComponentDeferredCtx::raw(
  self : ComponentDeferredCtx,
) -> @framework.ComponentCtx {
  self.raw_
}

///|
/// Edit the original response after deferring. For `DeferredUpdate`
/// handlers this edits the component's host message.
pub async fn ComponentDeferredCtx::edit_original(
  self : ComponentDeferredCtx,
  content? : String,
  clear_content? : Bool = false,
  embeds? : Array[@model.Embed],
  components? : Array[@model.Component],
  allowed_mentions? : @model.AllowedMentions,
  files? : Array[@dhttp.FileUpload],
) -> @model.Message {
  self.raw_.edit_response(
    content?,
    clear_content~,
    embeds?,
    components?,
    allowed_mentions?,
    files?,
  )
}

///|
/// Send a followup message after the initial deferred response.
pub async fn ComponentDeferredCtx::followup(
  self : ComponentDeferredCtx,
  content? : String,
  embeds? : Array[@model.Embed],
  components? : Array[@model.Component],
  files? : Array[@dhttp.FileUpload],
  allowed_mentions? : @model.AllowedMentions,
  ephemeral? : Bool = false,
) -> @model.Message {
  self.raw_.followup(
    content?,
    embeds?,
    components?,
    files?,
    allowed_mentions?,
    ephemeral~,
  )
}

///|
/// Wait for the next component interaction with an exact custom id.
/// Returns `None` on timeout, or immediately when running without a
/// gateway connection.
pub async fn ComponentDeferredCtx::wait_for_component(
  self : ComponentDeferredCtx,
  custom_id~ : String,
  timeout_ms? : Int,
) -> @framework.ComponentCtx? {
  match self.waiter_ {
    Some(waiter) => waiter(custom_id, timeout_ms)
    None => {
      (self.warn_)(
        "component waiting is unavailable without a gateway connection",
      )
      None
    }
  }
}

///|
async fn dispatch_component_handler(
  prefix : String,
  handler : ComponentHandler,
  bot : AppCtx,
  waiter : ComponentWaiter?,
  warn : (String) -> Unit,
  raw : @framework.ComponentCtx,
) -> Unit {
  let route = raw.data.custom_id
  match handler {
    Immediate(run) => {
      let response = run({ raw_: raw, bot_: bot, prefix_: prefix, }) catch {
        source => raise_failure(route, BeforeInitial, source)
      }
      response.send(raw) catch {
        source => raise_failure(route, BeforeInitial, source)
      }
    }
    DeferredUpdate(run) => {
      raw.defer_update() catch {
        source => raise_failure(route, BeforeInitial, source)
      }
      run({
        raw_: raw,
        bot_: bot,
        prefix_: prefix,
        waiter_: waiter,
        warn_: warn,
      }) catch {
        source => raise_failure(route, AfterDeferred, source)
      }
    }
    DeferredMessage(ephemeral~, run) => {
      raw.defer_response(ephemeral~) catch {
        source => raise_failure(route, BeforeInitial, source)
      }
      run({
        raw_: raw,
        bot_: bot,
        prefix_: prefix,
        waiter_: waiter,
        warn_: warn,
      }) catch {
        source => raise_failure(route, AfterDeferred, source)
      }
    }
    Raw(run) =>
      run(raw) catch {
        source => raise_failure(route, BeforeInitial, source)
      }
  }
}