///|
/// The operation whose handler failed.
pub(all) enum FailureOrigin {
  Command(name~ : String)
  Autocomplete(name~ : String)
  Component(custom_id~ : String)
  Modal(custom_id~ : String)
  Event(kind~ : @model.EventKind)
  Service(name~ : String)
} derive(Debug)

///|
/// The underlying interaction context for a failure.
///
/// Event and service failures have no raw context. Responding through one of
/// these contexts without first checking the response phase may fail; prefer
/// `FailureCtx::respond_error()` for ordinary error responses.
pub(all) enum FailureRaw {
  RawCommand(@framework.CommandCtx)
  RawComponent(@framework.ComponentCtx)
  RawModal(@framework.ModalCtx)
}

///|
/// Context handed to the error policy when a handler fails.
pub struct FailureCtx {
  priv origin_ : FailureOrigin
  priv phase_ : ResponsePhase?
  priv raw_ : FailureRaw?
  priv warn_ : (String) -> Unit
}

///|
/// What opened the modal: a component (with its host message) or a
/// command.
pub fn FailureCtx::origin(self : FailureCtx) -> FailureOrigin {
  self.origin_
}

///|
/// How far the response lifecycle progressed before the failure, when an
/// interaction was involved.
pub fn FailureCtx::phase(self : FailureCtx) -> ResponsePhase? {
  self.phase_
}

///|
/// Access the underlying interaction context. Event and service failures
/// return `None`. Direct responses through this escape hatch may fail unless
/// the response phase is checked first; normally use `respond_error()`.
pub fn FailureCtx::raw(self : FailureCtx) -> FailureRaw? {
  self.raw_
}

///|
/// The interaction payload associated with this failure. Event and service
/// failures return `None`.
pub fn FailureCtx::interaction(self : FailureCtx) -> @model.Interaction? {
  match self.raw_ {
    Some(RawCommand(ctx)) => Some(ctx.interaction)
    Some(RawComponent(ctx)) => Some(ctx.interaction)
    Some(RawModal(ctx)) => Some(ctx.interaction)
    None => None
  }
}

///|
/// The user who invoked the failed interaction. Event and service failures
/// return `None`.
pub fn FailureCtx::user(self : FailureCtx) -> @model.User? {
  guard self.interaction() is Some(interaction) else { return None }
  match interaction.guild_member {
    Some(guild_member) =>
      match guild_member.user {
        Some(user) => Some(user)
        None => interaction.user
      }
    None => interaction.user
  }
}

///|
fn error_response_summary(
  content : String?,
  embeds : Array[@model.Embed]?,
  components : Array[@model.Component]?,
  files : Array[@dhttp.FileUpload]?,
) -> String {
  guard content is None else { return content.unwrap() }
  let kinds : Array[String] = []
  if embeds is Some(_) {
    kinds.push("embeds")
  }
  if components is Some(_) {
    kinds.push("components")
  }
  if files is Some(_) {
    kinds.push("files")
  }
  match kinds {
    [] => "empty error response"
    [kind] => "\{kind}-only error response"
    _ => "\{kinds.join("+")} error response"
  }
}

///|
/// Send an error without violating Discord's one-initial-response rule.
/// Event and service failures have no response target; those calls are sent to
/// the warning hook instead of guessing a channel.
pub async fn FailureCtx::respond_error(
  self : FailureCtx,
  content? : String,
  embeds? : Array[@model.Embed],
  components? : Array[@model.Component],
  files? : Array[@dhttp.FileUpload],
  allowed_mentions? : @model.AllowedMentions,
  ephemeral? : Bool = true,
) -> Unit {
  guard self.raw_ is Some(raw) else {
    (self.warn_)(
      "cannot respond to a non-interaction failure: \{error_response_summary(content, embeds, components, files)}",
    )
    return
  }
  match self.phase_ {
    Some(BeforeInitial) | None =>
      match raw {
        RawCommand(ctx) =>
          ctx.respond(
            content?,
            embeds?,
            components?,
            files?,
            allowed_mentions?,
            ephemeral~,
          )
        RawComponent(ctx) =>
          ctx.respond(
            content?,
            embeds?,
            components?,
            files?,
            allowed_mentions?,
            ephemeral~,
          )
        RawModal(ctx) =>
          ctx.respond(
            content?,
            embeds?,
            components?,
            files?,
            allowed_mentions?,
            ephemeral~,
          )
      }
    Some(AfterDeferred) | Some(AfterInitial) =>
      match raw {
        RawCommand(ctx) =>
          ctx.followup(
            content?,
            embeds?,
            components?,
            files?,
            allowed_mentions?,
            ephemeral~,
          )
          |> ignore
        RawComponent(ctx) =>
          ctx.followup(
            content?,
            embeds?,
            components?,
            files?,
            allowed_mentions?,
            ephemeral~,
          )
          |> ignore
        RawModal(ctx) =>
          ctx.followup(
            content?,
            embeds?,
            components?,
            files?,
            allowed_mentions?,
            ephemeral~,
          )
          |> ignore
      }
  }
}

///|
/// Policy invoked for interaction, event, and service failures.
pub type ErrorPolicy = async (FailureCtx, Error) -> Unit

///|
fn default_error_policy(warn : (String) -> Unit) -> ErrorPolicy {
  (ctx, error) => {
    match error {
      HandlerError::UserMessage(message~, ephemeral~) =>
        ctx.respond_error(content=message, ephemeral~)
      HandlerError::GuildOnly =>
        ctx.respond_error(content="This command can only be used in a server.")
      HandlerError::DmOnly =>
        ctx.respond_error(
          content="This command can only be used in a direct message.",
        )
      HandlerError::CheckFailed =>
        ctx.respond_error(content="You cannot use this command.")
      HandlerError::MissingPermission(permissions) =>
        ctx.respond_error(
          content="Missing required permission: \{Repr(permissions)}",
        )
      HandlerError::OnCooldown(retry_after_ms~) =>
        ctx.respond_error(
          content="This command is on cooldown. Try again in \{retry_after_ms} ms.",
        )
      HandlerError::InvalidArgument(message) =>
        ctx.respond_error(content="Invalid argument: \{message}")
      _ => warn("unhandled app failure (\{Repr(ctx.origin())}): \{Repr(error)}")
    }
  }
}

///|
async fn invoke_error_policy(
  policy : ErrorPolicy,
  ctx : FailureCtx,
  error : Error,
) -> Unit {
  policy(ctx, error) catch {
    policy_error if @async.is_being_cancelled() ||
      @async.is_cancellation_error(policy_error) => raise policy_error
    policy_error => (ctx.warn_)("error policy failed: \{Repr(policy_error)}")
  }
}