///|
/// Execution strategy for a typed application command.
pub(all) enum InteractionHandler[A] {
  Immediate(async (ImmediateCtx, A) -> CommandReply)
  Deferred(ephemeral~ : Bool, async (DeferredCtx, A) -> Unit)
  Raw(async (@framework.CommandCtx) -> Unit)
}

///|
priv struct SlashRoute {
  path : Array[String]
  dispatch : async (
    AppCtx,
    ComponentWaiter?,
    (String) -> Unit,
    @framework.CommandCtx,
  ) -> Unit
}

///|
priv struct SuggestRoute {
  path : Array[String]
  option_name : String
  handler : @interaction.SuggestHandler
}

///|
/// A type-erased slash subcommand or subcommand group.
pub struct SlashChild {
  priv definition_ : @model.CommandOption
  priv routes_ : Array[SlashRoute]
  priv node_paths_ : Array[Array[String]]
  priv nested_group_paths_ : Array[Array[String]]
  priv suggest_routes_ : Array[SuggestRoute]
  priv is_group_ : Bool
}

///|
/// An application-command definition with a context-aware decoder.
pub struct Command[A] {
  priv definition : @interaction.CommandSpec
  priv decode : (@framework.CommandCtx) -> A raise
  priv handler : InteractionHandler[A]
  priv routes : Array[SlashRoute]?
  priv node_paths : Array[Array[String]]
  priv nested_group_paths : Array[Array[String]]
  priv suggest_routes : Array[SuggestRoute]
  priv checks : Array[CommandCheck]
  priv mut cooldown : CooldownConfig?
}

///|
fn[A] suggest_routes(
  path : Array[String],
  args : @interaction.Args[A],
) -> Array[SuggestRoute] {
  args
  .suggestions()
  .map(handler => { path: path.copy(), option_name: handler.name(), handler, })
}

///|
/// Decode slash arguments and translate reader errors into a stable handler
/// error understood by the App error policy.
fn[A] decode_command_args(
  args : @interaction.Args[A],
  options : @interaction.CommandOptions,
) -> A raise HandlerError {
  args.decode(options) catch {
    source => raise InvalidArgument("\{Repr(source)}")
  }
}

///|
/// Define a typed slash command.
///
/// ```mbt check
/// test "inspect a slash-command registration spec" {
///   let command = @app.slash(
///     name="echo",
///     description="Echo text",
///     args=@interaction.Args::of(
///       @interaction.arg_string(name="text", description="Text to echo"),
///     ),
///     handler=Raw(_ => ()),
///   )
///   json_inspect(command.spec().to_json(), content={
///     "name": "echo",
///     "description": "Echo text",
///     "options": [
///       {
///         "type": 3,
///         "name": "text",
///         "description": "Text to echo",
///         "required": true,
///       },
///     ],
///   })
/// }
/// ```
pub fn[A] slash(
  name~ : String,
  description~ : String,
  args~ : @interaction.Args[A],
  handler~ : InteractionHandler[A],
  default_member_permissions? : @model.Permissions,
  nsfw? : Bool,
  integration_types? : Array[@model.ApplicationIntegrationType],
  contexts? : Array[@model.InteractionContextType],
  name_localizations? : Map[String, String],
  description_localizations? : Map[String, String],
) -> Command[A] {
  {
    definition: @interaction.CommandSpec::slash(
      name,
      description,
      options=args.definitions(),
      default_member_permissions?,
      nsfw?,
      integration_types?,
      contexts?,
      name_localizations?,
      description_localizations?,
    ),
    decode: raw => decode_command_args(args, raw.options),
    handler,
    routes: None,
    node_paths: [],
    nested_group_paths: [],
    suggest_routes: suggest_routes([], args),
    checks: [],
    cooldown: None,
  }
}

///|
fn target_key(raw : @framework.CommandCtx) -> String raise HandlerError {
  guard raw.data.target_id is Some(target_id) else {
    raise InvalidArgument("context command is missing target_id")
  }
  target_id.to_string()
}

///|
fn decode_target_user(
  raw : @framework.CommandCtx,
) -> @framework.TargetUser raise HandlerError {
  let key = target_key(raw)
  guard raw.data.resolved is Some(_) else {
    raise InvalidArgument("user target is missing resolved data")
  }
  guard raw.target_user() is Some(target) else {
    raise InvalidArgument("resolved user target \{key} was not found")
  }
  target
}

///|
fn decode_target_message(
  raw : @framework.CommandCtx,
) -> @model.Message raise HandlerError {
  let key = target_key(raw)
  guard raw.data.resolved is Some(_) else {
    raise InvalidArgument("message target is missing resolved data")
  }
  guard raw.target_message() is Some(message) else {
    raise InvalidArgument("resolved message target \{key} was not found")
  }
  message
}

///|
/// Define a USER context-menu command.
///
/// ```mbt check
/// test "inspect context-menu command kinds" {
///   let user = @app.user_command(name="Inspect user", handler=Raw(_ => ()))
///   let message = @app.message_command(
///     name="Inspect message",
///     handler=Raw(_ => ()),
///   )
///   json_inspect(user.spec().to_json(), content={
///     "name": "Inspect user",
///     "description": "",
///     "type": 2,
///   })
///   json_inspect(message.spec().to_json(), content={
///     "name": "Inspect message",
///     "description": "",
///     "type": 3,
///   })
/// }
/// ```
pub fn user_command(
  name~ : String,
  handler~ : InteractionHandler[@framework.TargetUser],
  default_member_permissions? : @model.Permissions,
  nsfw? : Bool,
  integration_types? : Array[@model.ApplicationIntegrationType],
  contexts? : Array[@model.InteractionContextType],
  name_localizations? : Map[String, String],
) -> Command[@framework.TargetUser] {
  {
    definition: @interaction.CommandSpec::user(
      name,
      default_member_permissions?,
      nsfw?,
      integration_types?,
      contexts?,
      name_localizations?,
    ),
    decode: raw => decode_target_user(raw),
    handler,
    routes: None,
    node_paths: [],
    nested_group_paths: [],
    suggest_routes: [],
    checks: [],
    cooldown: None,
  }
}

///|
/// Define a MESSAGE context-menu command.
pub fn message_command(
  name~ : String,
  handler~ : InteractionHandler[@model.Message],
  default_member_permissions? : @model.Permissions,
  nsfw? : Bool,
  integration_types? : Array[@model.ApplicationIntegrationType],
  contexts? : Array[@model.InteractionContextType],
  name_localizations? : Map[String, String],
) -> Command[@model.Message] {
  {
    definition: @interaction.CommandSpec::message(
      name,
      default_member_permissions?,
      nsfw?,
      integration_types?,
      contexts?,
      name_localizations?,
    ),
    decode: raw => decode_target_message(raw),
    handler,
    routes: None,
    node_paths: [],
    nested_group_paths: [],
    suggest_routes: [],
    checks: [],
    cooldown: None,
  }
}

///|
fn prefixed_path(prefix : String, path : Array[String]) -> Array[String] {
  let result = [prefix]
  for part in path {
    result.push(part)
  }
  result
}

///|
fn path_key(path : Array[String]) -> String {
  path.join("\u{1f}")
}

///|
/// Define a typed slash subcommand.
pub fn[A] subcommand(
  name~ : String,
  description~ : String,
  args~ : @interaction.Args[A],
  handler~ : InteractionHandler[A],
  name_localizations? : Map[String, String],
  description_localizations? : Map[String, String],
) -> SlashChild {
  let decode = (raw : @framework.CommandCtx) => {
    decode_command_args(args, raw.options)
  }
  {
    definition_: @interaction.sub_command(
      name,
      description,
      options=args.definitions(),
      name_localizations?,
      description_localizations?,
    ),
    routes_: [
      {
        path: [name],
        dispatch: (bot, waiter, warn, raw) => {
          dispatch_command_handler(
            raw.data.name,
            raw => decode(raw),
            handler,
            bot,
            waiter,
            warn,
            raw,
          )
        },
      },
    ],
    node_paths_: [[name]],
    nested_group_paths_: [],
    suggest_routes_: suggest_routes([name], args),
    is_group_: false,
  }
}

///|
/// Define a slash subcommand group. Nested groups are retained for validation
/// and rejected by `App::validate`, never by panic or abort here.
pub fn subcommand_group(
  name~ : String,
  description~ : String,
  children~ : Array[SlashChild],
  name_localizations? : Map[String, String],
  description_localizations? : Map[String, String],
) -> SlashChild {
  let definitions = children.map(child => child.definition_)
  let routes = []
  let node_paths = [[name]]
  let nested_group_paths = []
  let suggest_routes = []
  for child in children {
    for route in child.routes_ {
      routes.push({ ..route, path: prefixed_path(name, route.path), })
    }
    for path in child.node_paths_ {
      node_paths.push(prefixed_path(name, path))
    }
    if child.is_group_ {
      nested_group_paths.push([name, child.definition_.name])
    }
    for path in child.nested_group_paths_ {
      nested_group_paths.push(prefixed_path(name, path))
    }
    for route in child.suggest_routes_ {
      suggest_routes.push({ ..route, path: prefixed_path(name, route.path), })
    }
  }
  {
    definition_: @interaction.sub_command_group(
      name,
      description,
      definitions,
      name_localizations?,
      description_localizations?,
    ),
    routes_: routes,
    node_paths_: node_paths,
    nested_group_paths_: nested_group_paths,
    suggest_routes_: suggest_routes,
    is_group_: true,
  }
}

///|
/// Define a slash command whose children dispatch by submitted option path.
pub fn slash_group(
  name~ : String,
  description~ : String,
  children~ : Array[SlashChild],
  default_member_permissions? : @model.Permissions,
  nsfw? : Bool,
  integration_types? : Array[@model.ApplicationIntegrationType],
  contexts? : Array[@model.InteractionContextType],
  name_localizations? : Map[String, String],
  description_localizations? : Map[String, String],
) -> Command[Unit] {
  let routes = []
  let node_paths = []
  let nested_group_paths = []
  let suggest_routes = []
  for child in children {
    routes.append(child.routes_)
    node_paths.append(child.node_paths_)
    nested_group_paths.append(child.nested_group_paths_)
    suggest_routes.append(child.suggest_routes_)
  }
  {
    definition: @interaction.CommandSpec::slash(
      name,
      description,
      options=children.map(child => child.definition_),
      default_member_permissions?,
      nsfw?,
      integration_types?,
      contexts?,
      name_localizations?,
      description_localizations?,
    ),
    decode: _ => (),
    handler: Raw(_ => ()),
    routes: Some(routes),
    node_paths,
    nested_group_paths,
    suggest_routes,
    checks: [],
    cooldown: None,
  }
}

///|
/// Add a pre-execution check. Checks run in registration order before argument
/// decoding. Returning false is equivalent to `HandlerError::CheckFailed`.
pub fn[A] Command::check(self : Command[A], check : CommandCheck) -> Command[A] {
  self.checks.push(check)
  self
}

///|
/// Apply a fixed-window cooldown to this command. The window starts when all
/// checks pass, before argument decoding and handler execution.
pub fn[A] Command::cooldown(
  self : Command[A],
  seconds~ : Int,
  bucket? : CooldownBucket = User,
) -> Command[A] {
  self.cooldown = Some({ seconds, bucket, })
  self
}

///|
/// The command registration specification.
pub fn[A] Command::spec(self : Command[A]) -> @interaction.CommandSpec {
  self.definition
}

///|
fn[A] raise_failure(
  command : String,
  phase : ResponsePhase,
  source : Error,
) -> A raise {
  if @async.is_being_cancelled() || @async.is_cancellation_error(source) {
    raise source
  }
  raise Failure(command~, phase~, source~)
}

///|
async fn[A] dispatch_command_handler(
  command : String,
  decode : (@framework.CommandCtx) -> A raise,
  handler : InteractionHandler[A],
  bot : AppCtx,
  waiter : ComponentWaiter?,
  warn : (String) -> Unit,
  raw : @framework.CommandCtx,
) -> Unit {
  let value = decode(raw) catch {
    source => raise_failure(command, BeforeInitial, source)
  }
  match handler {
    Immediate(run) => {
      let response = run({ raw_: raw, bot_: bot, }, value) catch {
        source => raise_failure(command, BeforeInitial, source)
      }
      response.send(raw) catch {
        source => raise_failure(command, BeforeInitial, source)
      }
    }
    Deferred(ephemeral~, run) => {
      raw.defer_response(ephemeral~) catch {
        source => raise_failure(command, BeforeInitial, source)
      }
      run({ raw_: raw, bot_: bot, waiter_: waiter, warn_: warn, }, value) catch {
        source => raise_failure(command, AfterDeferred, source)
      }
    }
    Raw(run) =>
      run(raw) catch {
        source => raise_failure(command, BeforeInitial, source)
      }
  }
}

///|
async fn[A] Command::dispatch_typed(
  self : Command[A],
  bot : AppCtx,
  waiter : ComponentWaiter?,
  warn : (String) -> Unit,
  raw : @framework.CommandCtx,
) -> Unit {
  match self.routes {
    None =>
      dispatch_command_handler(
        self.definition.name,
        self.decode,
        self.handler,
        bot,
        waiter,
        warn,
        raw,
      )
    Some(routes) => {
      let path = raw.options.path()
      let key = path_key(path)
      for route in routes {
        if path_key(route.path) == key {
          (route.dispatch)(bot, waiter, warn, raw)
          return
        }
      }
      raise_failure(
        self.definition.name,
        BeforeInitial,
        HandlerError::InvalidArgument(
          "unknown subcommand path: \{if path.is_empty() { "" } else { path.join("/") }}",
        ),
      )
    }
  }
}

///|
async fn dispatch_suggest(
  routes : Array[SuggestRoute],
  warn : (String) -> Unit,
  raw : @framework.AutocompleteCtx,
) -> Unit {
  let path = raw.options.path()
  guard raw.options.focused_input() is Some(input) else {
    raise HandlerError::InvalidArgument(
      "autocomplete payload has no focused option",
    )
  }
  for route in routes {
    if path_key(route.path) == path_key(path) &&
      route.option_name == input.name() {
      let ctx = @interaction.SuggestCtx::{
        interaction: raw.interaction,
        options: raw.options,
        guild_id: raw.interaction.guild_id,
        locale: raw.interaction.locale,
      }
      let choices = route.handler.suggest(ctx, input)
      let choices = if choices.length() > 25 {
        let count = choices.length()
        let command_path = [raw.data.name, ..path].join("/")
        warn(
          "autocomplete choices truncated for command path \{command_path}, option \{input.name()}: \{count} choices exceeds the limit of 25",
        )
        choices[:25].to_owned()
      } else {
        choices
      }
      raw.suggest(choices)
      return
    }
  }
  let display_path = if path.is_empty() { "" } else { path.join("/") }
  raise HandlerError::InvalidArgument(
    "no autocomplete route for \{display_path}:\{input.name()}",
  )
}

///|
/// A type-erased command consumed by an App executor.
pub struct RegisteredCommand {
  priv name_ : String
  priv definition : @interaction.CommandSpec
  priv node_paths_ : Array[Array[String]]
  priv nested_group_paths_ : Array[Array[String]]
  priv suggest_routes_ : Array[SuggestRoute]
  priv checks_ : Array[CommandCheck]
  priv cooldown_ : CooldownConfig?
  priv cooldowns_ : Map[String, Int64]
  priv mut cooldown_cleanup_after_ : Int64
  priv dispatch : async (
    AppCtx,
    ComponentWaiter?,
    (String) -> Unit,
    @framework.CommandCtx,
  ) -> Unit
}

///|
/// The top-level command name used for routing.
pub fn RegisteredCommand::name(self : RegisteredCommand) -> String {
  self.name_
}

///|
/// Erase the decoded argument type while retaining registration and dispatch.
pub fn[A] Command::erase(self : Command[A]) -> RegisteredCommand {
  {
    name_: self.definition.name,
    definition: self.definition,
    node_paths_: self.node_paths,
    nested_group_paths_: self.nested_group_paths,
    suggest_routes_: self.suggest_routes,
    checks_: self.checks,
    cooldown_: self.cooldown,
    cooldowns_: Map([]),
    cooldown_cleanup_after_: 0L,
    dispatch: (bot, waiter, warn, raw) => {
      self.dispatch_typed(bot, waiter, warn, raw)
    },
  }
}