///|
/// One typed input in a modal form.
pub struct ModalField[A] {
  priv definition_ : @model.Component
  priv decode_ : (Array[@model.Component]) -> A raise HandlerError
  priv present_ : (Array[@model.Component]) -> Bool
}

///|
/// A composable collection of typed modal inputs.
pub struct ModalFields[A] {
  priv definitions_ : Array[@model.Component]
  priv decode_ : (Array[@model.Component]) -> A raise HandlerError
}

///|
/// A typed modal definition. The field decoder is retained for the matching
/// `on_modal` registration while `show` erases it into a `ModalHandle`.
pub struct Modal[A] {
  priv custom_id_ : String
  priv title_ : String
  priv fields_ : ModalFields[A]
}

///|
/// A type-erased modal response ready to be returned by a command or component
/// handler.
pub struct ModalHandle {
  priv custom_id_ : String
  priv title_ : String
  priv components_ : Array[@model.Component]
}

///|
/// Invalid per-show text-input prefill configuration.
pub(all) suberror ModalPrefillError {
  UnknownCustomId(custom_id~ : String)
  NonTextInput(custom_id~ : String)
} derive(Debug)

///|
fn find_modal_component(
  components : Array[@model.Component],
  custom_id : String,
) -> @model.Component? {
  for component in components {
    match component {
      ActionRow(row) =>
        if find_modal_component(row.components, custom_id) is Some(found) {
          return Some(found)
        }
      Label(label) =>
        if find_modal_component([label.component], custom_id) is Some(found) {
          return Some(found)
        }
      TextInput(input) if input.custom_id == custom_id => return Some(component)
      StringSelect(select) if select.custom_id == custom_id =>
        return Some(component)
      _ => ()
    }
  }
  None
}

///|
fn modal_component_present(
  components : Array[@model.Component],
  custom_id : String,
) -> Bool {
  match find_modal_component(components, custom_id) {
    Some(TextInput(input)) => input.value is Some(value) && !value.is_empty()
    Some(StringSelect(select)) =>
      select.values is Some(values) && !values.is_empty()
    _ => false
  }
}

///|
fn set_modal_field_optional(component : @model.Component) -> @model.Component {
  match component {
    Label(label) =>
      Label({ ..label, component: set_modal_field_optional(label.component), })
    TextInput(input) => TextInput({ ..input, required: Some(false), })
    StringSelect(select) => StringSelect({ ..select, min_values: Some(0), })
    other => other
  }
}

///|
fn modal_text_input_ids(components : Array[@model.Component]) -> Array[String] {
  let result = []
  for component in components {
    match component {
      ActionRow(row) => result.append(modal_text_input_ids(row.components))
      Label(label) => result.append(modal_text_input_ids([label.component]))
      TextInput(input) => result.push(input.custom_id)
      _ => ()
    }
  }
  result
}

///|
fn prefill_modal_component(
  component : @model.Component,
  values : Map[String, String],
) -> @model.Component {
  match component {
    ActionRow(row) =>
      ActionRow({
        ..row,
        components: row.components.map(component => {
          prefill_modal_component(component, values)
        }),
      })
    Label(label) =>
      Label({
        ..label,
        component: prefill_modal_component(label.component, values),
      })
    TextInput(input) =>
      match values.get(input.custom_id) {
        Some(value) => TextInput({ ..input, value: Some(value), })
        None => component
      }
    _ => component
  }
}

///|
fn prefill_modal_components(
  components : Array[@model.Component],
  values : Map[String, String],
) -> Array[@model.Component] raise ModalPrefillError {
  for custom_id, _ in values {
    match find_modal_component(components, custom_id) {
      Some(TextInput(_)) => ()
      Some(_) => raise NonTextInput(custom_id~)
      None => raise UnknownCustomId(custom_id~)
    }
  }
  components.map(component => prefill_modal_component(component, values))
}

///|
/// Define a modern label-wrapped text input. `value` is its static prefill;
/// `Modal::show(values=...)` can override it for one response.
pub fn text_field(
  custom_id~ : String,
  label~ : String,
  style? : @model.TextInputStyle = Short,
  description? : String,
  placeholder? : String,
  min_length? : Int,
  max_length? : Int,
  value? : String,
) -> ModalField[String] {
  let input = @interaction.text_input(
    custom_id~,
    style~,
    placeholder?,
    min_length?,
    max_length?,
    required=true,
    value?,
  )
  {
    definition_: @interaction.label(label~, component=input, description?),
    present_: components => modal_component_present(components, custom_id),
    decode_: components => {
      match find_modal_component(components, custom_id) {
        Some(TextInput({ value: Some(value), .. })) if !value.is_empty() =>
          value
        _ => raise InvalidArgument("missing modal field: \{custom_id}")
      }
    },
  }
}

///|
/// Define a modern label-wrapped string select.
pub fn select_field(
  custom_id~ : String,
  label~ : String,
  options~ : Array[@model.SelectOption],
  description? : String,
  placeholder? : String,
  min_values? : Int,
  max_values? : Int,
) -> ModalField[Array[String]] {
  let select = @interaction.string_select(
    custom_id~,
    options~,
    placeholder?,
    min_values?,
    max_values?,
  )
  {
    definition_: @interaction.label(label~, component=select, description?),
    present_: components => modal_component_present(components, custom_id),
    decode_: components => {
      match find_modal_component(components, custom_id) {
        Some(StringSelect({ values: Some(values), .. })) if !values.is_empty() =>
          values
        _ => raise InvalidArgument("missing modal field: \{custom_id}")
      }
    },
  }
}

///|
/// Make a modal field optional. Missing and empty submissions decode to None.
pub fn[A] ModalField::optional(self : ModalField[A]) -> ModalField[A?] {
  {
    definition_: set_modal_field_optional(self.definition_),
    present_: self.present_,
    decode_: components => {
      if (self.present_)(components) {
        Some((self.decode_)(components))
      } else {
        None
      }
    },
  }
}

///|
/// Use a default when a modal field is missing or empty.
pub fn[A] ModalField::with_default(
  self : ModalField[A],
  value : A,
) -> ModalField[A] {
  {
    definition_: set_modal_field_optional(self.definition_),
    present_: self.present_,
    decode_: components => {
      if (self.present_)(components) {
        (self.decode_)(components)
      } else {
        value
      }
    },
  }
}

///|
/// Validate a decoded field. Returning a message rejects the submission.
pub fn[A] ModalField::validate(
  self : ModalField[A],
  check : (A) -> String?,
) -> ModalField[A] {
  {
    definition_: self.definition_,
    present_: self.present_,
    decode_: components => {
      let value = (self.decode_)(components)
      match check(value) {
        Some(message) => raise InvalidArgument(message)
        None => value
      }
    },
  }
}

///|
/// Transform a decoded modal field without changing its emitted component.
pub fn[A, B] ModalField::map(
  self : ModalField[A],
  f : (A) -> B,
) -> ModalField[B] {
  {
    definition_: self.definition_,
    present_: self.present_,
    decode_: components => f((self.decode_)(components)),
  }
}

///|
/// Lift a single field into a `ModalFields`; combine further with `zip`
/// and `map`, or use the `mapN` helpers directly.
pub fn[A] ModalFields::of(field : ModalField[A]) -> ModalFields[A] {
  { definitions_: [field.definition_], decode_: field.decode_, }
}

///|
/// Transform the decoded value without changing the emitted
/// components.
pub fn[A, B] ModalFields::map(
  self : ModalFields[A],
  f : (A) -> B,
) -> ModalFields[B] {
  {
    definitions_: self.definitions_,
    decode_: components => f((self.decode_)(components)),
  }
}

///|
/// Concatenate two field sets and pair their decoded values.
pub fn[A, B] ModalFields::zip(
  self : ModalFields[A],
  other : ModalFields[B],
) -> ModalFields[(A, B)] {
  let definitions = self.definitions_.copy()
  definitions.append(other.definitions_)
  {
    definitions_: definitions,
    decode_: components => {
      ((self.decode_)(components), (other.decode_)(components))
    },
  }
}

///|
/// Build a modal's field set from one field, mapping its decoded
/// value into the handler's argument type.
pub fn[A, Z] ModalFields::map1(
  a : ModalField[A],
  f : (A) -> Z,
) -> ModalFields[Z] {
  ModalFields::of(a).map(f)
}

///|
/// Build a modal's field set from two fields, combining their
/// decoded values with the final function argument.
pub fn[A, B, Z] ModalFields::map2(
  a : ModalField[A],
  b : ModalField[B],
  f : (A, B) -> Z,
) -> ModalFields[Z] {
  ModalFields::of(a).zip(ModalFields::of(b)).map(pair => f(pair.0, pair.1))
}

///|
/// Build a modal's field set from three fields, combining their
/// decoded values with the final function argument.
pub fn[A, B, C, Z] ModalFields::map3(
  a : ModalField[A],
  b : ModalField[B],
  c : ModalField[C],
  f : (A, B, C) -> Z,
) -> ModalFields[Z] {
  ModalFields::map2(a, b, (a, b) => (a, b))
  .zip(ModalFields::of(c))
  .map(value => f(value.0.0, value.0.1, value.1))
}

///|
/// Build a modal's field set from four fields, combining their
/// decoded values with the final function argument.
pub fn[A, B, C, D, Z] ModalFields::map4(
  a : ModalField[A],
  b : ModalField[B],
  c : ModalField[C],
  d : ModalField[D],
  f : (A, B, C, D) -> Z,
) -> ModalFields[Z] {
  ModalFields::map3(a, b, c, (a, b, c) => (a, b, c))
  .zip(ModalFields::of(d))
  .map(value => f(value.0.0, value.0.1, value.0.2, value.1))
}

///|
/// Build a modal's field set from five fields, combining their
/// decoded values with the final function argument.
pub fn[A, B, C, D, E, Z] ModalFields::map5(
  a : ModalField[A],
  b : ModalField[B],
  c : ModalField[C],
  d : ModalField[D],
  e : ModalField[E],
  f : (A, B, C, D, E) -> Z,
) -> ModalFields[Z] {
  ModalFields::map4(a, b, c, d, (a, b, c, d) => (a, b, c, d))
  .zip(ModalFields::of(e))
  .map(value => f(value.0.0, value.0.1, value.0.2, value.0.3, value.1))
}

///|
/// Define a typed modal.
///
/// ```mbt check
/// test "construct and validate a typed modal" {
///   let feedback = @app.modal(
///     custom_id="feedback",
///     title="Feedback",
///     fields=@app.ModalFields::of(
///       @app.text_field(custom_id="details", label="Details").optional(),
///     ),
///   )
///   let app = @app.App(sync=Disabled)
///   app.on_modal(feedback, Raw(_ => ()))
///   app.validate()
///   let _ : @app.ModalHandle = feedback.show(state="draft")
/// }
/// ```
pub fn[A] modal(
  custom_id~ : String,
  title~ : String,
  fields~ : ModalFields[A],
) -> Modal[A] {
  { custom_id_: custom_id, title_: title, fields_: fields, }
}

///|
/// Prepare a modal response, optionally appending opaque state after `:` and
/// overriding text-input defaults by `custom_id` for this response only.
/// Raises `ModalPrefillError` when an override key is unknown or does not name
/// a text input.
pub fn[A] Modal::show(
  self : Modal[A],
  state? : String,
  values? : Map[String, String],
) -> ModalHandle raise ModalPrefillError {
  let components = match values {
    Some(overrides) =>
      prefill_modal_components(self.fields_.definitions_, overrides)
    None => self.fields_.definitions_.copy()
  }
  {
    custom_id_: match state {
      Some(value) => "\{self.custom_id_}:\{value}"
      None => self.custom_id_
    },
    title_: self.title_,
    components_: components,
  }
}

///|
fn[A] Modal::decode(
  self : Modal[A],
  components : Array[@model.Component],
) -> A raise HandlerError {
  (self.fields_.decode_)(components)
}

///|
async fn ModalHandle::send_command(
  self : ModalHandle,
  ctx : @framework.CommandCtx,
) -> Unit {
  ctx.show_modal(
    custom_id=self.custom_id_,
    title=self.title_,
    components=self.components_,
  )
}

///|
async fn ModalHandle::send_component(
  self : ModalHandle,
  ctx : @framework.ComponentCtx,
) -> Unit {
  ctx.show_modal(
    custom_id=self.custom_id_,
    title=self.title_,
    components=self.components_,
  )
}

///|
/// Execution strategy for a typed modal submission.
pub(all) enum ModalSubmitHandler[A] {
  Immediate(async (ModalImmediateCtx, A) -> InitialResponse)
  Deferred(ephemeral~ : Bool, async (ModalDeferredCtx, A) -> Unit)
  Raw(async (@framework.ModalCtx) -> Unit)
}

///|
/// Read-only modal-submit context for handlers returning an initial message.
pub struct ModalImmediateCtx {
  priv raw_ : @framework.ModalCtx
  priv bot_ : AppCtx
  priv state_ : String?
}

///|
/// Modal-submit context available after a deferred response.
pub struct ModalDeferredCtx {
  priv raw_ : @framework.ModalCtx
  priv bot_ : AppCtx
  priv state_ : String?
}

///|
fn modal_state(base : String, custom_id : String) -> String? raise HandlerError {
  if custom_id == base {
    return None
  }
  let prefix = "\{base}:"
  guard custom_id.has_prefix(prefix) else {
    raise InvalidArgument("modal custom_id does not match \{base}")
  }
  Some(custom_id[prefix.length():].to_owned())
}

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

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

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

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

///|
/// What opened the modal: a component (with its host message) or a
/// command.
pub fn ModalImmediateCtx::origin(
  self : ModalImmediateCtx,
) -> @framework.ModalOrigin {
  self.raw_.origin()
}

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

///|
/// The opaque state string passed to `Modal::show`, if any.
pub fn ModalImmediateCtx::state(self : ModalImmediateCtx) -> String? {
  self.state_
}

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

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

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

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

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

///|
/// What opened the modal: a component (with its host message) or a
/// command.
pub fn ModalDeferredCtx::origin(
  self : ModalDeferredCtx,
) -> @framework.ModalOrigin {
  self.raw_.origin()
}

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

///|
/// The opaque state string passed to `Modal::show`, if any.
pub fn ModalDeferredCtx::state(self : ModalDeferredCtx) -> String? {
  self.state_
}

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

///|
/// Edit the original response after deferring.
pub async fn ModalDeferredCtx::edit_original(
  self : ModalDeferredCtx,
  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 ModalDeferredCtx::followup(
  self : ModalDeferredCtx,
  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~,
  )
}

///|
async fn InitialResponse::send_modal(
  self : InitialResponse,
  ctx : @framework.ModalCtx,
) -> 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[A] dispatch_modal_handler(
  modal : Modal[A],
  handler : ModalSubmitHandler[A],
  bot : AppCtx,
  raw : @framework.ModalCtx,
) -> Unit {
  let route = raw.data.custom_id
  let state = modal_state(modal.custom_id_, route) catch {
    source => raise_failure(route, BeforeInitial, source)
  }
  match handler {
    Raw(run) =>
      run(raw) catch {
        source => raise_failure(route, BeforeInitial, source)
      }
    Immediate(run) => {
      let value = modal.decode(raw.data.components) catch {
        source => raise_failure(route, BeforeInitial, source)
      }
      let response = run({ raw_: raw, bot_: bot, state_: state, }, value) catch {
        source => raise_failure(route, BeforeInitial, source)
      }
      response.send_modal(raw) catch {
        source => raise_failure(route, BeforeInitial, source)
      }
    }
    Deferred(ephemeral~, run) => {
      let value = modal.decode(raw.data.components) catch {
        source => raise_failure(route, BeforeInitial, source)
      }
      raw.defer_response(ephemeral~) catch {
        source => raise_failure(route, BeforeInitial, source)
      }
      run({ raw_: raw, bot_: bot, state_: state, }, value) catch {
        source => raise_failure(route, AfterDeferred, source)
      }
    }
  }
}