///|
/// Initial response returned by an immediate component handler.
pub(all) enum ComponentReply {
  UpdateMessage(MessageUpdate)
  Message(InitialResponse)
  ShowModal(ModalHandle)
}

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

///|
fn[A] ComponentHandler::ack_plan(self : ComponentHandler[A]) -> AckPlan {
  match self {
    DeferredUpdate(_) => DeferUpdate
    DeferredMessage(ephemeral~, _) => DeferMessage(ephemeral~)
    Immediate(_) | Raw(_) => Respond
  }
}

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

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

///|
/// Reply by editing the message that hosts the component. Omitted fields keep
/// their current value; `clear_content` removes the text, and
/// `keep_attachments` lists the existing attachments to retain alongside new
/// `files` (see `MessageUpdate`).
pub fn ComponentReply::update_message(
  content? : String,
  clear_content~ : Bool = false,
  embeds? : Array[@model.Embed],
  components? : Array[@model.Component],
  allowed_mentions? : @model.AllowedMentions,
  files? : Array[@dhttp.FileUpload],
  keep_attachments? : Array[@model.AttachmentRequest],
  poll? : @model.PollCreateRequest,
) -> ComponentReply {
  UpdateMessage(
    MessageUpdate(
      content?,
      clear_content~,
      embeds?,
      components?,
      allowed_mentions?,
      files?,
      keep_attachments?,
      poll?,
    ),
  )
}

///|
/// 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,
  poll? : @model.PollCreateRequest,
  ephemeral~ : Bool = false,
) -> ComponentReply {
  Message(
    InitialResponse::message(
      content?,
      embeds?,
      components?,
      files?,
      allowed_mentions?,
      poll?,
      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_,
    poll?=self.poll_,
    ephemeral=self.ephemeral_,
  )
}

///|
async fn ComponentReply::send(
  self : ComponentReply,
  ctx : @framework.ComponentCtx,
) -> Unit {
  match self {
    UpdateMessage(update) => update.send_component(ctx)
    Message(response) => response.send_component(ctx)
    ShowModal(handle) => handle.send_component(ctx)
  }
}

///|
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 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()
}

///|
/// Milliseconds left to return the initial response, or `None` when the
/// executor set no deadline.
pub fn ComponentImmediateCtx::remaining_ms(
  self : ComponentImmediateCtx,
) -> Int32? {
  self.raw_.remaining_ms()
}

///|
/// Escape hatch to the underlying context. Its response methods bypass this
/// handler's discipline of returning its initial response: a callback sent
/// through it makes the returned reply fail with
/// `ResponseGateError::AlreadyResponded`, which reaches the error policy.
/// Prefer the read-only accessors on this context. Each use raises an
/// `alert_unchecked` warning; once the bypass is deliberate, silence it with
/// `warnings = "-alert_unchecked"` in that package's `moon.pkg`.
#alert(unchecked, "bypasses the initial-response discipline; a reply sent through it makes the handler's returned reply fail with AlreadyResponded")
pub fn ComponentImmediateCtx::unchecked_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 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.
/// Failure handling still follows the gate's real response state.
pub fn ComponentDeferredCtx::raw(
  self : ComponentDeferredCtx,
) -> @framework.ComponentCtx {
  self.raw_
}

///|
/// Fetch the original response.
pub async fn ComponentDeferredCtx::original(
  self : ComponentDeferredCtx,
) -> @model.Message {
  self.raw_.original_response()
}

///|
/// Edit the original response after deferring. For `DeferredUpdate`
/// handlers this edits the component's host message. New `files` replace
/// the existing attachments unless `keep_attachments` lists the ones to retain;
/// `poll` can be added only here, while editing the deferred response.
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],
  keep_attachments? : Array[@model.AttachmentRequest],
  poll? : @model.PollCreateRequest,
) -> @model.Message {
  self.raw_.edit_response(
    content?,
    clear_content~,
    embeds?,
    components?,
    allowed_mentions?,
    files?,
    keep_attachments?,
    poll?,
  )
}

///|
/// Delete the original response.
pub async fn ComponentDeferredCtx::delete_original(
  self : ComponentDeferredCtx,
) -> Unit {
  self.raw_.delete_response()
}

///|
/// 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,
  poll? : @model.PollCreateRequest,
  ephemeral~ : Bool = false,
) -> @model.Message {
  self.raw_.followup(
    content?,
    embeds?,
    components?,
    files?,
    allowed_mentions?,
    poll?,
    ephemeral~,
  )
}

///|
/// Fetch a followup message sent for this interaction.
pub async fn ComponentDeferredCtx::get_followup(
  self : ComponentDeferredCtx,
  message_id : @model.MessageId,
) -> @model.Message {
  self.raw_.get_followup(message_id)
}

///|
/// Edit a followup message sent for this interaction. New `files` replace
/// its attachments unless `keep_attachments` lists the ones to retain.
pub async fn ComponentDeferredCtx::edit_followup(
  self : ComponentDeferredCtx,
  message_id : @model.MessageId,
  content? : String,
  clear_content~ : Bool = false,
  embeds? : Array[@model.Embed],
  components? : Array[@model.Component],
  allowed_mentions? : @model.AllowedMentions,
  files? : Array[@dhttp.FileUpload],
  keep_attachments? : Array[@model.AttachmentRequest],
) -> @model.Message {
  self.raw_.edit_followup(
    message_id,
    content?,
    clear_content~,
    embeds?,
    components?,
    allowed_mentions?,
    files?,
    keep_attachments?,
  )
}

///|
/// Delete a followup message sent for this interaction.
pub async fn ComponentDeferredCtx::delete_followup(
  self : ComponentDeferredCtx,
  message_id : @model.MessageId,
) -> Unit {
  self.raw_.delete_followup(message_id)
}

///|
/// Wait for the next click on the component `route.custom_id(state)`
/// identifies — build the button with the same route and state. By default
/// only the invoking user may satisfy the wait; pass `from=Anyone` for a
/// public flow or `from=User(id)` for someone else. Returns `None` on
/// timeout, or at once without a gateway connection. Raises `CustomIdError`
/// when the state does not encode.
///
/// The returned context has not been answered, and the waited interaction does
/// not pass through admission or the automatic defer: respond to it (for
/// example `update_message` or `defer_update`) within three seconds.
pub async fn[A] ComponentDeferredCtx::wait_for_component(
  self : ComponentDeferredCtx,
  route : ComponentRoute[A],
  state : A,
  from~ : WaitFrom = Invoker,
  timeout_ms? : Int32,
) -> @framework.ComponentCtx? {
  wait_for_route(
    self.waiter_,
    self.warn_,
    route,
    state,
    from.user_filter(self.user().id),
    timeout_ms,
  )
}

///|
async fn[A] ComponentRoute::dispatch_typed(
  self : ComponentRoute[A],
  handler : ComponentHandler[A],
  bot : AppCtx,
  waiter : ComponentWaiter?,
  warn : (AppWarning) -> Unit,
  raw : @framework.ComponentCtx,
) -> Unit {
  match handler {
    Immediate(run) => {
      let value = self.decode(raw.data.custom_id)
      let response = run({ raw_: raw, bot_: bot, }, value)
      response.send(raw)
    }
    DeferredUpdate(run) => {
      let value = self.decode(raw.data.custom_id)
      ensure_acknowledged(component_admission(raw), DeferUpdate)
      run({ raw_: raw, bot_: bot, waiter_: waiter, warn_: warn, }, value)
    }
    DeferredMessage(ephemeral~, run) => {
      let value = self.decode(raw.data.custom_id)
      ensure_acknowledged(component_admission(raw), DeferMessage(ephemeral~))
      run({ raw_: raw, bot_: bot, waiter_: waiter, warn_: warn, }, value)
    }
    Raw(run) => run(raw)
  }
}

///|
priv struct RegisteredComponent {
  pattern : AppRoutePattern
  ack : AckPlan
  dispatch : async (
    AppCtx,
    ComponentWaiter?,
    (AppWarning) -> Unit,
    @framework.ComponentCtx,
  ) -> Unit
}

///|
fn[A] ComponentRoute::erase(
  self : ComponentRoute[A],
  handler : ComponentHandler[A],
) -> RegisteredComponent {
  {
    pattern: Id(self.id_),
    ack: handler.ack_plan(),
    dispatch: (bot, waiter, warn, raw) => {
      self.dispatch_typed(handler, bot, waiter, warn, raw)
    },
  }
}