///|
/// Shared skeleton for option builders: every field a builder does not set
/// stays `None` so the registration payload omits it.
fn base_option(
  typ : @model.CommandOptionType,
  name : String,
  description : String,
  name_localizations? : Map[String, String],
  description_localizations? : Map[String, String],
) -> @model.CommandOption {
  {
    typ,
    name,
    name_localizations: name_localizations.map(value => Value(value)),
    description,
    description_localizations: description_localizations.map(value => {
      Value(value)
    }),
    required: None,
    choices: None,
    options: None,
    channel_types: None,
    min_value: None,
    max_value: None,
    min_length: None,
    max_length: None,
    autocomplete: None,
  }
}

///|
/// Describe a STRING option.
pub fn string_option(
  name : String,
  description : String,
  required? : Bool,
  choices? : Array[@model.CommandOptionChoice],
  min_length? : Int,
  max_length? : Int,
  autocomplete? : Bool,
  name_localizations? : Map[String, String],
  description_localizations? : Map[String, String],
) -> @model.CommandOption {
  {
    ..base_option(
      String,
      name,
      description,
      name_localizations?,
      description_localizations?,
    ),
    required,
    choices,
    min_length,
    max_length,
    autocomplete,
  }
}

///|
/// Describe an INTEGER option.
pub fn integer_option(
  name : String,
  description : String,
  required? : Bool,
  choices? : Array[@model.CommandOptionChoice],
  min_value? : Int64,
  max_value? : Int64,
  autocomplete? : Bool,
  name_localizations? : Map[String, String],
  description_localizations? : Map[String, String],
) -> @model.CommandOption {
  let min_value = min_value.map(value => Json::number(value.to_double()))
  let max_value = max_value.map(value => Json::number(value.to_double()))
  {
    ..base_option(
      Integer,
      name,
      description,
      name_localizations?,
      description_localizations?,
    ),
    required,
    choices,
    min_value,
    max_value,
    autocomplete,
  }
}

///|
/// Describe a NUMBER option.
pub fn number_option(
  name : String,
  description : String,
  required? : Bool,
  choices? : Array[@model.CommandOptionChoice],
  min_value? : Double,
  max_value? : Double,
  autocomplete? : Bool,
  name_localizations? : Map[String, String],
  description_localizations? : Map[String, String],
) -> @model.CommandOption {
  let min_value = min_value.map(value => Json::number(value))
  let max_value = max_value.map(value => Json::number(value))
  {
    ..base_option(
      Number,
      name,
      description,
      name_localizations?,
      description_localizations?,
    ),
    required,
    choices,
    min_value,
    max_value,
    autocomplete,
  }
}

///|
/// Describe a BOOLEAN option.
pub fn boolean_option(
  name : String,
  description : String,
  required? : Bool,
  name_localizations? : Map[String, String],
  description_localizations? : Map[String, String],
) -> @model.CommandOption {
  {
    ..base_option(
      Boolean,
      name,
      description,
      name_localizations?,
      description_localizations?,
    ),
    required,
  }
}

///|
/// Describe a USER option.
pub fn user_option(
  name : String,
  description : String,
  required? : Bool,
  name_localizations? : Map[String, String],
  description_localizations? : Map[String, String],
) -> @model.CommandOption {
  {
    ..base_option(
      User,
      name,
      description,
      name_localizations?,
      description_localizations?,
    ),
    required,
  }
}

///|
/// Describe a CHANNEL option.
pub fn channel_option(
  name : String,
  description : String,
  required? : Bool,
  channel_types? : Array[@model.ChannelType],
  name_localizations? : Map[String, String],
  description_localizations? : Map[String, String],
) -> @model.CommandOption {
  {
    ..base_option(
      Channel,
      name,
      description,
      name_localizations?,
      description_localizations?,
    ),
    required,
    channel_types,
  }
}

///|
/// Describe a ROLE option.
pub fn role_option(
  name : String,
  description : String,
  required? : Bool,
  name_localizations? : Map[String, String],
  description_localizations? : Map[String, String],
) -> @model.CommandOption {
  {
    ..base_option(
      Role,
      name,
      description,
      name_localizations?,
      description_localizations?,
    ),
    required,
  }
}

///|
/// Describe a MENTIONABLE option.
pub fn mentionable_option(
  name : String,
  description : String,
  required? : Bool,
  name_localizations? : Map[String, String],
  description_localizations? : Map[String, String],
) -> @model.CommandOption {
  {
    ..base_option(
      Mentionable,
      name,
      description,
      name_localizations?,
      description_localizations?,
    ),
    required,
  }
}

///|
/// Describe an ATTACHMENT option.
pub fn attachment_option(
  name : String,
  description : String,
  required? : Bool,
  name_localizations? : Map[String, String],
  description_localizations? : Map[String, String],
) -> @model.CommandOption {
  {
    ..base_option(
      Attachment,
      name,
      description,
      name_localizations?,
      description_localizations?,
    ),
    required,
  }
}

///|
/// Describe a SUB_COMMAND option.
pub fn sub_command(
  name : String,
  description : String,
  options? : Array[@model.CommandOption] = [],
  name_localizations? : Map[String, String],
  description_localizations? : Map[String, String],
) -> @model.CommandOption {
  let options = if options.length() > 0 { Some(options) } else { None }
  {
    ..base_option(
      SubCommand,
      name,
      description,
      name_localizations?,
      description_localizations?,
    ),
    options,
  }
}

///|
/// Describe a SUB_COMMAND_GROUP option.
pub fn sub_command_group(
  name : String,
  description : String,
  sub_commands : Array[@model.CommandOption],
  name_localizations? : Map[String, String],
  description_localizations? : Map[String, String],
) -> @model.CommandOption {
  let options = if sub_commands.length() > 0 {
    Some(sub_commands)
  } else {
    None
  }
  {
    ..base_option(
      SubCommandGroup,
      name,
      description,
      name_localizations?,
      description_localizations?,
    ),
    options,
  }
}

///|
/// Describe a string-valued option choice.
pub fn string_choice(
  name : String,
  value : String,
  name_localizations? : Map[String, String],
) -> @model.CommandOptionChoice {
  {
    name,
    name_localizations: name_localizations.map(value => Value(value)),
    value: Json::string(value),
  }
}

///|
/// Describe an integer-valued option choice.
pub fn int_choice(
  name : String,
  value : Int64,
  name_localizations? : Map[String, String],
) -> @model.CommandOptionChoice {
  {
    name,
    name_localizations: name_localizations.map(value => Value(value)),
    value: Json::number(value.to_double()),
  }
}

///|
/// Describe a number-valued option choice.
pub fn number_choice(
  name : String,
  value : Double,
  name_localizations? : Map[String, String],
) -> @model.CommandOptionChoice {
  {
    name,
    name_localizations: name_localizations.map(value => Value(value)),
    value: Json::number(value),
  }
}

///|
/// Build a message component row.
pub fn action_row(components : Array[@model.Component]) -> @model.Component {
  ActionRow({ id: None, components, })
}

///|
/// Build an interactive button.
pub fn button(
  custom_id~ : String,
  label? : String,
  style? : @model.ButtonStyle = Primary,
  emoji? : @model.Emoji,
  disabled? : Bool,
) -> @model.Component {
  Button({
    id: None,
    style,
    label,
    emoji,
    custom_id: Some(custom_id),
    sku_id: None,
    url: None,
    disabled,
  })
}

///|
/// Build a link button.
pub fn link_button(
  url~ : String,
  label? : String,
  emoji? : @model.Emoji,
  disabled? : Bool,
) -> @model.Component {
  Button({
    id: None,
    style: Link,
    label,
    emoji,
    custom_id: None,
    sku_id: None,
    url: Some(url),
    disabled,
  })
}

///|
/// Build an option for a string select menu.
pub fn select_option(
  label~ : String,
  value~ : String,
  description? : String,
  emoji? : @model.Emoji,
  default? : Bool,
) -> @model.SelectOption {
  { label, value, description, emoji, default, }
}

///|
/// Build a string select menu.
pub fn string_select(
  custom_id~ : String,
  options~ : Array[@model.SelectOption],
  placeholder? : String,
  min_values? : Int,
  max_values? : Int,
  disabled? : Bool,
) -> @model.Component {
  StringSelect({
    id: None,
    custom_id,
    placeholder,
    min_values,
    max_values,
    disabled,
    options: Some(options),
    channel_types: None,
    default_values: None,
    values: None,
  })
}

///|
fn auto_select(
  typ : Int,
  custom_id : String,
  placeholder : String?,
  min_values : Int?,
  max_values : Int?,
  disabled : Bool?,
  channel_types : Array[@model.ChannelType]?,
) -> @model.Component {
  let menu = @model.SelectMenu::{
    id: None,
    custom_id,
    placeholder,
    min_values,
    max_values,
    disabled,
    options: None,
    channel_types,
    default_values: None,
    values: None,
  }
  match typ {
    5 => UserSelect(menu)
    6 => RoleSelect(menu)
    7 => MentionableSelect(menu)
    _ => ChannelSelect(menu)
  }
}

///|
/// Build a user select menu; the chosen users arrive in
/// `resolved` and via the component context's `selected_users`.
pub fn user_select(
  custom_id~ : String,
  placeholder? : String,
  min_values? : Int,
  max_values? : Int,
  disabled? : Bool,
) -> @model.Component {
  auto_select(5, custom_id, placeholder, min_values, max_values, disabled, None)
}

///|
/// Build a role select menu; the chosen roles arrive via the component
/// context's `selected_roles`.
pub fn role_select(
  custom_id~ : String,
  placeholder? : String,
  min_values? : Int,
  max_values? : Int,
  disabled? : Bool,
) -> @model.Component {
  auto_select(6, custom_id, placeholder, min_values, max_values, disabled, None)
}

///|
/// Build a mentionable select menu accepting both users and roles.
pub fn mentionable_select(
  custom_id~ : String,
  placeholder? : String,
  min_values? : Int,
  max_values? : Int,
  disabled? : Bool,
) -> @model.Component {
  auto_select(7, custom_id, placeholder, min_values, max_values, disabled, None)
}

///|
/// Build a channel select menu, optionally restricted to `channel_types`;
/// the chosen channels arrive via the component context's
/// `selected_channels`.
pub fn channel_select(
  custom_id~ : String,
  placeholder? : String,
  min_values? : Int,
  max_values? : Int,
  disabled? : Bool,
  channel_types? : Array[@model.ChannelType],
) -> @model.Component {
  auto_select(
    8, custom_id, placeholder, min_values, max_values, disabled, channel_types,
  )
}

///|
/// Build a text display block (components v2): markdown `content` rendered
/// in the message body. Reply and send layers set the required
/// `IS_COMPONENTS_V2` message flag automatically.
pub fn text_display(content : String) -> @model.Component {
  TextDisplay({ id: None, content, })
}

///|
/// Build a section (components v2): up to three text displays laid out next
/// to an `accessory` (a thumbnail or button). Reply and send layers set the
/// required `IS_COMPONENTS_V2` message flag automatically.
pub fn section(
  components~ : Array[@model.Component],
  accessory~ : @model.Component,
) -> @model.Component {
  Section({ id: None, components, accessory, })
}

///|
/// Build a thumbnail accessory (components v2) from an image `url`
/// (`https://...` or `attachment://`).
pub fn thumbnail(
  url~ : String,
  description? : String,
  spoiler? : Bool,
) -> @model.Component {
  Thumbnail({
    id: None,
    media: {
      url,
      proxy_url: None,
      height: None,
      width: None,
      placeholder: None,
      placeholder_version: None,
      content_type: None,
      flags: None,
      attachment_id: None,
    },
    description,
    spoiler,
  })
}

///|
/// Build a container (components v2): groups child components in a rounded
/// box with an optional `accent_color` (`0xRRGGBB`) stripe, similar to an
/// embed. Reply and send layers set the required `IS_COMPONENTS_V2` message
/// flag automatically.
pub fn container(
  components~ : Array[@model.Component],
  accent_color? : Int,
  spoiler? : Bool,
) -> @model.Component {
  Container({
    id: None,
    components,
    accent_color: accent_color.map(value => Value(value)),
    spoiler,
  })
}

///|
/// Build a separator (components v2): vertical padding between components,
/// with an optional visible `divider` line and `spacing` size (1 = small,
/// 2 = large). Reply and send layers set the required `IS_COMPONENTS_V2`
/// message flag automatically.
pub fn separator(divider? : Bool, spacing? : Int) -> @model.Component {
  Separator({ id: None, divider, spacing, })
}

///|
/// Build one media gallery entry from an image or video `url`
/// (`https://...` or `attachment://`), for use with
/// `media_gallery`.
pub fn media_item(
  url~ : String,
  description? : String,
  spoiler? : Bool,
) -> @model.MediaGalleryItem {
  {
    media: {
      url,
      proxy_url: None,
      height: None,
      width: None,
      placeholder: None,
      placeholder_version: None,
      content_type: None,
      flags: None,
      attachment_id: None,
    },
    description,
    spoiler,
  }
}

///|
/// Build a media gallery (components v2): a grid of 1–10 media items. Reply
/// and send layers set the required `IS_COMPONENTS_V2` message flag
/// automatically.
pub fn media_gallery(
  items : Array[@model.MediaGalleryItem],
) -> @model.Component {
  MediaGallery({ id: None, items, })
}

///|
/// Wrap a modal component with a `label` and optional `description`. Modals
/// require every input to be wrapped in a label; the typed `ModalField`
/// builders do this automatically.
pub fn label(
  label~ : String,
  component~ : @model.Component,
  description? : String,
) -> @model.Component {
  Label({ id: None, label: Some(label), description, component, })
}

///|
/// Build a modal text input. `style` picks single-line `Short` (default) or
/// multi-line `Paragraph`; `value` prefills the field. Wrap it with `label`
/// before placing it in a modal (the typed `ModalField` builders handle
/// this).
pub fn text_input(
  custom_id~ : String,
  style? : @model.TextInputStyle = Short,
  placeholder? : String,
  min_length? : Int,
  max_length? : Int,
  required? : Bool,
  value? : String,
) -> @model.Component {
  TextInput({
    id: None,
    custom_id,
    style: Some(style),
    label: None,
    min_length,
    max_length,
    required,
    value,
    placeholder,
  })
}