///|
/// Build the `data` object of a message-style interaction response.
fn message_response_data(
  content : String?,
  embeds : Array[@model.Embed]?,
  components : Array[@model.Component]?,
  files : Array[@dhttp.FileUpload]?,
  allowed_mentions : @model.AllowedMentions?,
  ephemeral : Bool,
) -> Json raise @dhttp.DiscordHttpError {
  let flags = @dhttp.message_flags_with_components_v2(
    if ephemeral {
      Some(@model.MessageFlags::ephemeral())
    } else {
      None
    },
    content,
    embeds,
    components,
  )
  let builder = @model.ObjBuilder()
    .opt("content", content)
    .opt("embeds", embeds)
    .opt("components", components)
    .opt("allowed_mentions", allowed_mentions)
  if files is Some(fs) && fs.length() > 0 {
    builder.field("attachments", @dhttp.attachments_json(fs)) |> ignore
  }
  builder.opt("flags", flags) |> ignore
  builder.build()
}

///|
async fn respond_message(
  gate : ResponseGate,
  typ : @model.InteractionResponseType,
  content : String?,
  embeds : Array[@model.Embed]?,
  components : Array[@model.Component]?,
  files : Array[@dhttp.FileUpload]?,
  allowed_mentions : @model.AllowedMentions?,
  ephemeral : Bool,
) -> Unit {
  gate.send(
    {
      typ,
      data: Some(
        message_response_data(
          content, embeds, components, files, allowed_mentions, ephemeral,
        ),
      ),
    },
    files?,
  )
}

///|
async fn respond_modal(
  gate : ResponseGate,
  custom_id : String,
  title : String,
  components : Array[@model.Component],
) -> Unit {
  gate.send({
    typ: Modal,
    data: Some(
      @model.ObjBuilder()
      .field("custom_id", custom_id)
      .field("title", title)
      .field("components", components)
      .build(),
    ),
  })
}

///|
async fn interaction_original_response(
  client : @dhttp.Client,
  application_id : @model.ApplicationId,
  token : String,
) -> @model.Message {
  client.get_original_response(application_id, token)
}

///|
async fn delete_interaction_response(
  client : @dhttp.Client,
  application_id : @model.ApplicationId,
  token : String,
) -> Unit {
  client.delete_original_response(application_id, token)
}

///|
async fn interaction_followup(
  client : @dhttp.Client,
  application_id : @model.ApplicationId,
  token : String,
  message_id : @model.MessageId,
) -> @model.Message {
  client.get_followup(application_id, token, message_id)
}

///|
async fn edit_interaction_followup(
  client : @dhttp.Client,
  application_id : @model.ApplicationId,
  token : String,
  message_id : @model.MessageId,
  content : String?,
  clear_content : Bool,
  embeds : Array[@model.Embed]?,
  components : Array[@model.Component]?,
  allowed_mentions : @model.AllowedMentions?,
  files : Array[@dhttp.FileUpload]?,
  keep_attachments : Array[@model.AttachmentRequest]?,
) -> @model.Message {
  client.edit_followup(
    application_id,
    token,
    message_id,
    content?,
    clear_content~,
    embeds?,
    components?,
    allowed_mentions?,
    files?,
    keep_attachments?,
  )
}

///|
async fn delete_interaction_followup(
  client : @dhttp.Client,
  application_id : @model.ApplicationId,
  token : String,
  message_id : @model.MessageId,
) -> Unit {
  client.delete_followup(application_id, token, message_id)
}

///|
/// Proof that an interaction was invoked in a guild: the guild id and the
/// invoking member, both validated when the context was built.
pub(all) struct GuildInvocation {
  guild_id : @model.GuildId
  member : @model.GuildMember
} derive(Debug)

///|
/// The user who invoked this interaction.
pub fn GuildInvocation::user(self : GuildInvocation) -> @model.User {
  match self.member.user {
    Some(user) => user
    None => abort("validated guild invocation has no member user")
  }
}

///|
/// The context in which an interaction was invoked.
pub(all) enum InvocationScope {
  Guild(GuildInvocation)
  Dm(@model.User)
} derive(Debug)

///|
/// The interaction that opened a submitted modal.
pub(all) enum ModalOrigin {
  FromComponent(@model.Message)
  FromCommand
} derive(Debug)

///|
/// A malformed interaction that cannot satisfy the guarantees exposed by a
/// typed handler context.
pub(all) suberror InteractionContextError {
  MissingInvoker
  MissingGuildMemberUser
  MissingGuildId
  MissingComponentMessage
} derive(Debug)

///|
/// The resolved target of a USER context-menu command.
pub(all) struct TargetUser {
  user : @model.User
  member : @model.GuildMember?
}

///|
/// Prefer the guild member when both `member` and `user` are present: the
/// documented contract is exactly-one, but an interpretable payload should
/// never make the interaction unanswerable.
fn interaction_scope(
  interaction : @model.Interaction,
) -> InvocationScope raise InteractionContextError {
  match (interaction.guild_member, interaction.user) {
    (Some(guild_member), _) => {
      guard guild_member.user is Some(_) else { raise MissingGuildMemberUser }
      guard interaction.guild_id is Some(guild_id) else { raise MissingGuildId }
      Guild({ guild_id, member: guild_member, })
    }
    (None, Some(user)) => Dm(user)
    (None, None) => raise MissingInvoker
  }
}

///|
fn InvocationScope::invoking_user(self : InvocationScope) -> @model.User {
  match self {
    Guild(guild) => guild.user()
    Dm(user) => user
  }
}

///|
fn component_message(
  interaction : @model.Interaction,
) -> @model.Message raise InteractionContextError {
  match interaction.message {
    Some(message) => message
    None => raise MissingComponentMessage
  }
}

///|
fn modal_origin(interaction : @model.Interaction) -> ModalOrigin {
  match interaction.message {
    Some(message) => FromComponent(message)
    None => FromCommand
  }
}

///|
/// Context handed to a slash / context-menu command handler.
pub(all) struct CommandCtx {
  client : @dhttp.Client
  application_id : @model.ApplicationId
  interaction : @model.Interaction
  data : @model.CommandData
  options : @interaction.CommandOptions
  priv scope_ : InvocationScope
  priv gate : ResponseGate
}

///|
/// Whether the command was invoked in a guild or a DM.
pub fn CommandCtx::scope(self : CommandCtx) -> InvocationScope {
  self.scope_
}

///|
/// The validated guild invocation, or `None` when invoked in a DM.
pub fn CommandCtx::guild_scope(self : CommandCtx) -> GuildInvocation? {
  match self.scope_ {
    Guild(guild) => Some(guild)
    Dm(_) => None
  }
}

///|
/// The invoking user.
pub fn CommandCtx::user(self : CommandCtx) -> @model.User {
  self.scope_.invoking_user()
}

///|
/// The resolved target of a MESSAGE context-menu command, or `None` when this
/// is not a message command or its target is unavailable.
pub fn CommandCtx::target_message(self : CommandCtx) -> @model.Message? {
  guard self.data.typ is Message &&
    self.data.target_id is Some(target_id) &&
    self.data.resolved is Some(resolved) &&
    resolved.messages is Some(messages) else {
    return None
  }
  messages.get(target_id.to_string())
}

///|
/// The resolved target of a USER context-menu command, or `None` when this is
/// not a user command or its target is unavailable.
pub fn CommandCtx::target_user(self : CommandCtx) -> TargetUser? {
  guard self.data.typ is User &&
    self.data.target_id is Some(target_id) &&
    self.data.resolved is Some(resolved) &&
    resolved.users is Some(users) &&
    users.get(target_id.to_string()) is Some(user) else {
    return None
  }
  let target_member = match resolved.members {
    Some(members) => members.get(target_id.to_string())
    None => None
  }
  Some({ user, member: target_member, })
}

///|
/// Decode the submitted options into a typed `CommandModel`.
pub fn[T : @interaction.CommandModel] CommandCtx::model(
  self : CommandCtx,
) -> T raise {
  T::from_options(self.options)
}

///|
/// Send the initial "channel message with source" response.
pub async fn CommandCtx::respond(
  self : CommandCtx,
  content? : String,
  embeds? : Array[@model.Embed],
  components? : Array[@model.Component],
  files? : Array[@dhttp.FileUpload],
  allowed_mentions? : @model.AllowedMentions,
  ephemeral? : Bool = false,
) -> Unit {
  respond_message(
    self.gate,
    ChannelMessageWithSource,
    content,
    embeds,
    components,
    files,
    allowed_mentions,
    ephemeral,
  )
}

///|
/// Acknowledge now and reply later via `edit_response` / `followup`.
pub async fn CommandCtx::defer_response(
  self : CommandCtx,
  ephemeral? : Bool = false,
) -> Unit {
  respond_message(
    self.gate,
    DeferredChannelMessageWithSource,
    None,
    None,
    None,
    None,
    None,
    ephemeral,
  )
}

///|
/// Display a modal as the initial command response.
pub async fn CommandCtx::show_modal(
  self : CommandCtx,
  custom_id~ : String,
  title~ : String,
  components~ : Array[@model.Component],
) -> Unit {
  respond_modal(self.gate, custom_id, title, components)
}

///|
/// Fetch the original response to this command interaction.
pub async fn CommandCtx::original_response(self : CommandCtx) -> @model.Message {
  interaction_original_response(
    self.client,
    self.application_id,
    self.interaction.token,
  )
}

///|
/// Delete the original response to this command interaction.
pub async fn CommandCtx::delete_response(self : CommandCtx) -> Unit {
  delete_interaction_response(
    self.client,
    self.application_id,
    self.interaction.token,
  )
}

///|
/// Edit the original response (typically after `defer`).
pub async fn CommandCtx::edit_response(
  self : CommandCtx,
  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.client.edit_original_response(
    self.application_id,
    self.interaction.token,
    content?,
    clear_content~,
    embeds?,
    components?,
    allowed_mentions?,
    files?,
    keep_attachments?,
    poll?,
  )
}

///|
/// Send a followup message after the initial response.
pub async fn CommandCtx::followup(
  self : CommandCtx,
  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.client.create_followup(
    self.application_id,
    self.interaction.token,
    content?,
    embeds?,
    components?,
    files?,
    allowed_mentions?,
    poll?,
    flags=if ephemeral {
      @model.MessageFlags::ephemeral()
    } else {
      @model.MessageFlags::from_bits(0)
    },
  )
}

///|
/// Fetch a followup message for this command interaction.
pub async fn CommandCtx::get_followup(
  self : CommandCtx,
  message_id : @model.MessageId,
) -> @model.Message {
  interaction_followup(
    self.client,
    self.application_id,
    self.interaction.token,
    message_id,
  )
}

///|
/// Edit a followup message for this command interaction.
pub async fn CommandCtx::edit_followup(
  self : CommandCtx,
  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 {
  edit_interaction_followup(
    self.client,
    self.application_id,
    self.interaction.token,
    message_id,
    content,
    clear_content,
    embeds,
    components,
    allowed_mentions,
    files,
    keep_attachments,
  )
}

///|
/// Delete a followup message for this command interaction.
pub async fn CommandCtx::delete_followup(
  self : CommandCtx,
  message_id : @model.MessageId,
) -> Unit {
  delete_interaction_followup(
    self.client,
    self.application_id,
    self.interaction.token,
    message_id,
  )
}

///|
/// Context handed to a message-component (button / select) handler.
pub(all) struct ComponentCtx {
  client : @dhttp.Client
  application_id : @model.ApplicationId
  interaction : @model.Interaction
  data : @model.ComponentData
  priv scope_ : InvocationScope
  priv message_ : @model.Message
  priv gate : ResponseGate
}

///|
/// Whether the component was invoked in a guild or a DM.
pub fn ComponentCtx::scope(self : ComponentCtx) -> InvocationScope {
  self.scope_
}

///|
/// The validated guild invocation, or `None` when invoked in a DM.
pub fn ComponentCtx::guild_scope(self : ComponentCtx) -> GuildInvocation? {
  match self.scope_ {
    Guild(guild) => Some(guild)
    Dm(_) => None
  }
}

///|
/// The invoking user.
pub fn ComponentCtx::user(self : ComponentCtx) -> @model.User {
  self.scope_.invoking_user()
}

///|
/// The message to which the component was attached.
pub fn ComponentCtx::message(self : ComponentCtx) -> @model.Message {
  self.message_
}

///|
/// Send a new "channel message with source" response.
pub async fn ComponentCtx::respond(
  self : ComponentCtx,
  content? : String,
  embeds? : Array[@model.Embed],
  components? : Array[@model.Component],
  files? : Array[@dhttp.FileUpload],
  allowed_mentions? : @model.AllowedMentions,
  ephemeral? : Bool = false,
) -> Unit {
  respond_message(
    self.gate,
    ChannelMessageWithSource,
    content,
    embeds,
    components,
    files,
    allowed_mentions,
    ephemeral,
  )
}

///|
/// Edit the message the component is attached to.
pub async fn ComponentCtx::update_message(
  self : ComponentCtx,
  content? : String,
  embeds? : Array[@model.Embed],
  components? : Array[@model.Component],
  allowed_mentions? : @model.AllowedMentions,
) -> Unit {
  respond_message(
    self.gate,
    UpdateMessage,
    content,
    embeds,
    components,
    None,
    allowed_mentions,
    false,
  )
}

///|
/// Display a modal as the initial component response.
pub async fn ComponentCtx::show_modal(
  self : ComponentCtx,
  custom_id~ : String,
  title~ : String,
  components~ : Array[@model.Component],
) -> Unit {
  respond_modal(self.gate, custom_id, title, components)
}

///|
/// Acknowledge now and send a message later through webhook methods.
pub async fn ComponentCtx::defer_response(
  self : ComponentCtx,
  ephemeral? : Bool = false,
) -> Unit {
  respond_message(
    self.gate,
    DeferredChannelMessageWithSource,
    None,
    None,
    None,
    None,
    None,
    ephemeral,
  )
}

///|
/// Acknowledge without any visible change (edit later if needed).
pub async fn ComponentCtx::defer_update(self : ComponentCtx) -> Unit {
  self.gate.send({ typ: DeferredUpdateMessage, data: None, })
}

///|
/// Fetch the original response to this component interaction.
pub async fn ComponentCtx::original_response(
  self : ComponentCtx,
) -> @model.Message {
  interaction_original_response(
    self.client,
    self.application_id,
    self.interaction.token,
  )
}

///|
/// Delete the original response to this component interaction.
pub async fn ComponentCtx::delete_response(self : ComponentCtx) -> Unit {
  delete_interaction_response(
    self.client,
    self.application_id,
    self.interaction.token,
  )
}

///|
/// Edit the original interaction response after deferring.
pub async fn ComponentCtx::edit_response(
  self : ComponentCtx,
  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.client.edit_original_response(
    self.application_id,
    self.interaction.token,
    content?,
    clear_content~,
    embeds?,
    components?,
    allowed_mentions?,
    files?,
    keep_attachments?,
    poll?,
  )
}

///|
/// Send a component-interaction followup message.
pub async fn ComponentCtx::followup(
  self : ComponentCtx,
  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.client.create_followup(
    self.application_id,
    self.interaction.token,
    content?,
    embeds?,
    components?,
    files?,
    allowed_mentions?,
    poll?,
    flags=if ephemeral {
      @model.MessageFlags::ephemeral()
    } else {
      @model.MessageFlags::from_bits(0)
    },
  )
}

///|
/// Fetch a followup message for this component interaction.
pub async fn ComponentCtx::get_followup(
  self : ComponentCtx,
  message_id : @model.MessageId,
) -> @model.Message {
  interaction_followup(
    self.client,
    self.application_id,
    self.interaction.token,
    message_id,
  )
}

///|
/// Edit a followup message for this component interaction.
pub async fn ComponentCtx::edit_followup(
  self : ComponentCtx,
  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 {
  edit_interaction_followup(
    self.client,
    self.application_id,
    self.interaction.token,
    message_id,
    content,
    clear_content,
    embeds,
    components,
    allowed_mentions,
    files,
    keep_attachments,
  )
}

///|
/// Delete a followup message for this component interaction.
pub async fn ComponentCtx::delete_followup(
  self : ComponentCtx,
  message_id : @model.MessageId,
) -> Unit {
  delete_interaction_followup(
    self.client,
    self.application_id,
    self.interaction.token,
    message_id,
  )
}

///|
/// Context handed to a modal-submit handler.
pub(all) struct ModalCtx {
  client : @dhttp.Client
  application_id : @model.ApplicationId
  interaction : @model.Interaction
  data : @model.ModalData
  priv scope_ : InvocationScope
  priv origin_ : ModalOrigin
  priv gate : ResponseGate
}

///|
/// Whether the modal was submitted in a guild or a DM.
pub fn ModalCtx::scope(self : ModalCtx) -> InvocationScope {
  self.scope_
}

///|
/// The validated guild invocation, or `None` when invoked in a DM.
pub fn ModalCtx::guild_scope(self : ModalCtx) -> GuildInvocation? {
  match self.scope_ {
    Guild(guild) => Some(guild)
    Dm(_) => None
  }
}

///|
/// The invoking user.
pub fn ModalCtx::user(self : ModalCtx) -> @model.User {
  self.scope_.invoking_user()
}

///|
/// Whether the modal was opened from a component or from a command.
pub fn ModalCtx::origin(self : ModalCtx) -> ModalOrigin {
  self.origin_
}

///|
/// The submitted value of the text input with the given custom id.
pub fn ModalCtx::text_value(self : ModalCtx, custom_id : String) -> String? {
  fn walk(components : Array[@model.Component]) -> String? {
    for component in components {
      match component {
        ActionRow(row) =>
          if walk(row.components) is Some(value) {
            return Some(value)
          }
        Label(label) =>
          if walk([label.component]) is Some(value) {
            return Some(value)
          }
        TextInput(input) =>
          if input.custom_id == custom_id {
            return input.value
          }
        _ => ()
      }
    }
    None
  }

  walk(self.data.components)
}

///|
/// Send a "channel message with source" response to the modal.
pub async fn ModalCtx::respond(
  self : ModalCtx,
  content? : String,
  embeds? : Array[@model.Embed],
  components? : Array[@model.Component],
  files? : Array[@dhttp.FileUpload],
  allowed_mentions? : @model.AllowedMentions,
  ephemeral? : Bool = false,
) -> Unit {
  respond_message(
    self.gate,
    ChannelMessageWithSource,
    content,
    embeds,
    components,
    files,
    allowed_mentions,
    ephemeral,
  )
}

///|
/// Acknowledge now and reply later via a followup.
pub async fn ModalCtx::defer_response(
  self : ModalCtx,
  ephemeral? : Bool = false,
) -> Unit {
  respond_message(
    self.gate,
    DeferredChannelMessageWithSource,
    None,
    None,
    None,
    None,
    None,
    ephemeral,
  )
}

///|
/// Fetch the original response to this modal interaction.
pub async fn ModalCtx::original_response(self : ModalCtx) -> @model.Message {
  interaction_original_response(
    self.client,
    self.application_id,
    self.interaction.token,
  )
}

///|
/// Delete the original response to this modal interaction.
pub async fn ModalCtx::delete_response(self : ModalCtx) -> Unit {
  delete_interaction_response(
    self.client,
    self.application_id,
    self.interaction.token,
  )
}

///|
/// Edit the original modal interaction response after deferring.
pub async fn ModalCtx::edit_response(
  self : ModalCtx,
  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.client.edit_original_response(
    self.application_id,
    self.interaction.token,
    content?,
    clear_content~,
    embeds?,
    components?,
    allowed_mentions?,
    files?,
    keep_attachments?,
    poll?,
  )
}

///|
/// Send a modal-interaction followup message.
pub async fn ModalCtx::followup(
  self : ModalCtx,
  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.client.create_followup(
    self.application_id,
    self.interaction.token,
    content?,
    embeds?,
    components?,
    files?,
    allowed_mentions?,
    poll?,
    flags=if ephemeral {
      @model.MessageFlags::ephemeral()
    } else {
      @model.MessageFlags::from_bits(0)
    },
  )
}

///|
/// Fetch a followup message for this modal interaction.
pub async fn ModalCtx::get_followup(
  self : ModalCtx,
  message_id : @model.MessageId,
) -> @model.Message {
  interaction_followup(
    self.client,
    self.application_id,
    self.interaction.token,
    message_id,
  )
}

///|
/// Edit a followup message for this modal interaction.
pub async fn ModalCtx::edit_followup(
  self : ModalCtx,
  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 {
  edit_interaction_followup(
    self.client,
    self.application_id,
    self.interaction.token,
    message_id,
    content,
    clear_content,
    embeds,
    components,
    allowed_mentions,
    files,
    keep_attachments,
  )
}

///|
/// Delete a followup message for this modal interaction.
pub async fn ModalCtx::delete_followup(
  self : ModalCtx,
  message_id : @model.MessageId,
) -> Unit {
  delete_interaction_followup(
    self.client,
    self.application_id,
    self.interaction.token,
    message_id,
  )
}

///|
/// Context handed to an autocomplete handler.
pub(all) struct AutocompleteCtx {
  client : @dhttp.Client
  application_id : @model.ApplicationId
  interaction : @model.Interaction
  data : @model.CommandData
  options : @interaction.CommandOptions
  priv gate : ResponseGate
}

///|
/// Send autocomplete suggestions. Choices beyond the first 25 are truncated.
pub async fn AutocompleteCtx::suggest(
  self : AutocompleteCtx,
  choices : Array[@model.CommandOptionChoice],
) -> Unit {
  let choices = if choices.length() > 25 {
    choices[:25].to_owned()
  } else {
    choices
  }
  self.gate.send({
    typ: Autocomplete,
    data: Some(@model.ObjBuilder().field("choices", choices).build()),
  })
}