///|
/// A Discord message component dispatched by its numeric wire discriminator.
#derive(Debug)
#non_exhaustive
pub(all) enum Component {
  ActionRow(ActionRow)
  Button(Button)
  StringSelect(SelectMenu)
  TextInput(TextInput)
  UserSelect(SelectMenu)
  RoleSelect(SelectMenu)
  MentionableSelect(SelectMenu)
  ChannelSelect(SelectMenu)
  Section(Section)
  TextDisplay(TextDisplay)
  Thumbnail(Thumbnail)
  MediaGallery(MediaGallery)
  File(FileComponent)
  Separator(Separator)
  Container(Container)
  Label(Label)
  FileUpload(FileUpload)
  RadioGroup(RadioGroup)
  CheckboxGroup(CheckboxGroup)
  Checkbox(Checkbox)
  Unknown(Json)
}

///|
/// Whether any top-level component requires Discord's Components V2 message
/// flag.
///
/// ```mbt check
/// test "detect top-level Components V2 layouts" {
///   let row : @model.Component = ActionRow({ id: None, components: [], })
///   let container : @model.Component = Container({
///     id: None,
///     components: [],
///     accent_color: None,
///     spoiler: None,
///   })
///   assert_false(@model.requires_components_v2([row]))
///   assert_true(@model.requires_components_v2([row, container]))
/// }
/// ```
pub fn requires_components_v2(components : Array[Component]) -> Bool {
  components.any(component => {
    match component {
      Section(_)
      | TextDisplay(_)
      | MediaGallery(_)
      | File(_)
      | Separator(_)
      | Container(_) => true
      _ => false
    }
  })
}

///|
fn component_payload(payload : Json, typ : Int32) -> Json {
  match payload {
    Object(fields) => {
      fields["type"] = Json::number(typ.to_double())
      Json::object(fields)
    }
    value => value
  }
}

///|
/// Encodes the component with its `type` discriminator.
pub impl ToJson for Component with fn to_json(self) {
  match self {
    ActionRow(value) => component_payload(ToJson::to_json(value), 1)
    Button(value) => component_payload(ToJson::to_json(value), 2)
    StringSelect(value) => component_payload(ToJson::to_json(value), 3)
    TextInput(value) => component_payload(ToJson::to_json(value), 4)
    UserSelect(value) => component_payload(ToJson::to_json(value), 5)
    RoleSelect(value) => component_payload(ToJson::to_json(value), 6)
    MentionableSelect(value) => component_payload(ToJson::to_json(value), 7)
    ChannelSelect(value) => component_payload(ToJson::to_json(value), 8)
    Section(value) => component_payload(ToJson::to_json(value), 9)
    TextDisplay(value) => component_payload(ToJson::to_json(value), 10)
    Thumbnail(value) => component_payload(ToJson::to_json(value), 11)
    MediaGallery(value) => component_payload(ToJson::to_json(value), 12)
    File(value) => component_payload(ToJson::to_json(value), 13)
    Separator(value) => component_payload(ToJson::to_json(value), 14)
    Container(value) => component_payload(ToJson::to_json(value), 17)
    Label(value) => component_payload(ToJson::to_json(value), 18)
    FileUpload(value) => component_payload(ToJson::to_json(value), 19)
    RadioGroup(value) => component_payload(ToJson::to_json(value), 21)
    CheckboxGroup(value) => component_payload(ToJson::to_json(value), 22)
    Checkbox(value) => component_payload(ToJson::to_json(value), 23)
    Unknown(value) => value
  }
}

///|
/// Decodes a component by its `type` discriminator.
pub impl @json.FromJson for Component with fn from_json(json, path) {
  guard json is Object(fields) else {
    raise JsonDecodeError((path, "expected a component (object)"))
  }
  match fields.get("type") {
    Some(Number(value, ..)) =>
      match value.to_int() {
        1 => ActionRow(@json.from_json(json, path~))
        2 => Button(@json.from_json(json, path~))
        3 => StringSelect(@json.from_json(json, path~))
        4 => TextInput(@json.from_json(json, path~))
        5 => UserSelect(@json.from_json(json, path~))
        6 => RoleSelect(@json.from_json(json, path~))
        7 => MentionableSelect(@json.from_json(json, path~))
        8 => ChannelSelect(@json.from_json(json, path~))
        9 => Section(@json.from_json(json, path~))
        10 => TextDisplay(@json.from_json(json, path~))
        11 => Thumbnail(@json.from_json(json, path~))
        12 => MediaGallery(@json.from_json(json, path~))
        13 => File(@json.from_json(json, path~))
        14 => Separator(@json.from_json(json, path~))
        17 => Container(@json.from_json(json, path~))
        18 => Label(@json.from_json(json, path~))
        19 => FileUpload(@json.from_json(json, path~))
        21 => RadioGroup(@json.from_json(json, path~))
        22 => CheckboxGroup(@json.from_json(json, path~))
        23 => Checkbox(@json.from_json(json, path~))
        _ => Unknown(json)
      }
    _ => Unknown(json)
  }
}

///|
/// A layout row containing nested interactive components.
#derive(ToJson, FromJson, Debug)
pub(all) struct ActionRow {
  id : Int32?
  components : Array[Component]
}

///|
/// A clickable button component.
#derive(ToJson, FromJson, Debug)
pub(all) struct Button {
  id : Int32?
  style : ButtonStyle
  label : String?
  emoji : Emoji?
  custom_id : String?
  sku_id : SkuId?
  url : String?
  disabled : Bool?
}

///|
/// The visual and behavioral style of a button.
#derive(Eq, Debug)
#non_exhaustive
pub(all) enum ButtonStyle {
  Primary
  Secondary
  Success
  Danger
  Link
  Premium
  Unknown(Int32)
}

///|
/// The Discord integer value of this `ButtonStyle`.
pub fn ButtonStyle::to_int(self : ButtonStyle) -> Int32 {
  match self {
    Primary => 1
    Secondary => 2
    Success => 3
    Danger => 4
    Link => 5
    Premium => 6
    Unknown(value) => value
  }
}

///|
/// The `ButtonStyle` for its Discord integer value; unknown values become `Unknown(n)`.
pub fn ButtonStyle::from_int(value : Int32) -> ButtonStyle {
  match value {
    1 => Primary
    2 => Secondary
    3 => Success
    4 => Danger
    5 => Link
    6 => Premium
    value => Unknown(value)
  }
}

///|
/// Encodes the `ButtonStyle` as its Discord integer value.
pub impl ToJson for ButtonStyle with fn to_json(self) {
  Json::number(self.to_int().to_double())
}

///|
/// Decodes the `ButtonStyle` from a JSON number; unknown values become `Unknown(n)`.
pub impl @json.FromJson for ButtonStyle with fn from_json(json, path) {
  match json {
    Number(value, ..) => ButtonStyle::from_int(value.to_int())
    _ => raise JsonDecodeError((path, "expected a button style (number)"))
  }
}

///|
/// A text or auto-populated select menu component.
#derive(ToJson, FromJson, Debug)
pub(all) struct SelectMenu {
  id : Int32?
  custom_id : String
  placeholder : String?
  min_values : Int32?
  max_values : Int32?
  disabled : Bool?
  required : Bool?
  options : Array[SelectOption]?
  channel_types : Array[ChannelType]?
  default_values : Array[SelectDefaultValue]?
  values : Array[String]?
}

///|
/// A selectable option in a string select menu.
#derive(ToJson, FromJson, Debug)
pub(all) struct SelectOption {
  label : String
  value : String
  description : String?
  emoji : Emoji?
  default : Bool?
}

///|
/// A preselected entity in an auto-populated select menu.
#derive(ToJson(fields(typ(rename="type"))), FromJson(fields(typ(rename="type"))), Debug)
pub(all) struct SelectDefaultValue {
  id : GenericId
  typ : String
}

///|
/// A section containing text components and a button or thumbnail accessory.
#derive(ToJson, FromJson, Debug)
pub(all) struct Section {
  id : Int32?
  components : Array[Component]
  accessory : Component
}

///|
/// A markdown text block displayed in a Components v2 layout.
#derive(ToJson, FromJson, Debug)
pub(all) struct TextDisplay {
  id : Int32?
  content : String
}

///|
/// A thumbnail accessory rendered from an unfurled media item.
#derive(ToJson, FromJson, Debug)
pub(all) struct Thumbnail {
  id : Int32?
  media : UnfurledMediaItem
  description : Nullable[String]?
  spoiler : Bool?
}

///|
/// A gallery containing one to ten media items.
#derive(ToJson, FromJson, Debug)
pub(all) struct MediaGallery {
  id : Int32?
  items : Array[MediaGalleryItem]
}

///|
/// One item in a media gallery.
#derive(ToJson, FromJson, Debug)
pub(all) struct MediaGalleryItem {
  media : UnfurledMediaItem
  description : Nullable[String]?
  spoiler : Bool?
}

///|
/// A file attachment rendered as a Components v2 file component.
#derive(ToJson, FromJson, Debug)
pub(all) struct FileComponent {
  id : Int32?
  file : UnfurledMediaItem
  spoiler : Bool?
  name : String?
  size : Int32?
}

///|
/// A visual separator between Components v2 content blocks.
#derive(ToJson, FromJson, Debug)
pub(all) struct Separator {
  id : Int32?
  divider : Bool?
  spacing : Int32?
}

///|
/// A Components v2 container with an optional accent color.
#derive(ToJson, FromJson, Debug)
pub(all) struct Container {
  id : Int32?
  components : Array[Component]
  accent_color : Nullable[Int32]?
  spoiler : Bool?
}

///|
/// A modal label wrapping one interactive component.
#derive(ToJson, FromJson, Debug)
pub(all) struct Label {
  id : Int32?
  // Required when sending a modal; omitted in modal-submit payloads.
  label : String?
  description : String?
  component : Component
}

///|
/// A modal file upload component.
#derive(ToJson, FromJson, Debug)
pub(all) struct FileUpload {
  id : Int32?
  custom_id : String
  min_values : Int32?
  max_values : Int32?
  required : Bool?
  values : Array[String]?
  file_types : Array[FileTypeFilter]?
}

///|
/// A modal radio group accepting one option.
#derive(ToJson, FromJson, Debug)
pub(all) struct RadioGroup {
  id : Int32?
  custom_id : String
  options : Array[RadioOption]
  required : Bool?
  value : String?
}

///|
/// A selectable option shared by radio and checkbox groups.
#derive(ToJson, FromJson, Debug)
pub(all) struct RadioOption {
  value : String
  label : String
  description : String?
  default : Bool?
}

///|
/// A modal checkbox group accepting zero or more options.
#derive(ToJson, FromJson, Debug)
pub(all) struct CheckboxGroup {
  id : Int32?
  custom_id : String
  options : Array[RadioOption]
  min_values : Int32?
  max_values : Int32?
  required : Bool?
  values : Array[String]?
}

///|
/// A single modal checkbox.
#derive(ToJson, FromJson, Debug)
pub(all) struct Checkbox {
  id : Int32?
  custom_id : String
  default : Bool?
  value : Bool?
}

///|
/// Media metadata used by thumbnail, gallery, and file components.
#derive(ToJson, FromJson, Debug)
pub(all) struct UnfurledMediaItem {
  url : String
  proxy_url : String?
  height : Nullable[Int32]?
  width : Nullable[Int32]?
  placeholder : String?
  placeholder_version : Int32?
  content_type : String?
  flags : Int32?
  /// Discord's content scanning result, once the media has been scanned. The
  /// scan can finish after the message is sent, so it may first appear in a
  /// Message Update.
  content_scan_metadata : ContentScanMetadata?
  attachment_id : String?
}

///|
/// The result of Discord's content scanning for an unfurled media item.
#derive(ToJson, FromJson, Debug)
pub(all) struct ContentScanMetadata {
  flags : ContentScanFlags
}

///|
/// Bit flags recording what Discord's content scanning found in a media item.
#derive(Eq)
pub(all) struct ContentScanFlags(UInt64)

///|
/// No flags set.
pub fn ContentScanFlags::none() -> ContentScanFlags {
  ContentScanFlags(0)
}

///|
/// Flags from a raw bit field; unknown bits are kept.
pub fn ContentScanFlags::from_bits(bits : UInt64) -> ContentScanFlags {
  ContentScanFlags(bits)
}

///|
/// The raw bit field.
pub fn ContentScanFlags::bits(self : ContentScanFlags) -> UInt64 {
  self.0
}

///|
/// Whether every flag in `other` is set.
pub fn ContentScanFlags::contains(
  self : ContentScanFlags,
  other : ContentScanFlags,
) -> Bool {
  (self.0 & other.0) == other.0
}

///|
/// Combines flags: every flag set in either operand is set.
pub impl BitOr for ContentScanFlags with fn lor(a, b) {
  ContentScanFlags(a.0 | b.0)
}

///|
/// Keeps the flags set in both operands.
pub impl BitAnd for ContentScanFlags with fn land(a, b) {
  ContentScanFlags(a.0 & b.0)
}

///|
/// Debug output is the raw bit field.
pub impl Debug for ContentScanFlags with fn to_repr(self) {
  Repr(self.0)
}

///|
/// Encodes the flags as a JSON number.
pub impl ToJson for ContentScanFlags with fn to_json(self) {
  Json::number(self.0.to_double(), repr=self.0.to_string())
}

///|
/// Decodes the flags from a JSON number; unknown bits are kept.
pub impl @json.FromJson for ContentScanFlags with fn from_json(json, path) {
  match json {
    Number(_, repr=Some(repr)) =>
      ContentScanFlags(
        @string.parse_uint64(repr.to_string()) catch {
          _ =>
            raise JsonDecodeError(
              (path, "expected content scan flags (number), got \{repr}"),
            )
        },
      )
    Number(value, repr=None) => ContentScanFlags(value.to_uint64())
    _ => raise JsonDecodeError((path, "expected content scan flags (number)"))
  }
}

///|
/// The media was flagged as explicit content.
pub fn ContentScanFlags::is_explicit_content() -> ContentScanFlags {
  ContentScanFlags(1UL << 0)
}

///|
/// The media was flagged as gore.
pub fn ContentScanFlags::is_gore_content() -> ContentScanFlags {
  ContentScanFlags(1UL << 1)
}

///|
/// The media was flagged as self-harm content.
pub fn ContentScanFlags::is_self_harm_content() -> ContentScanFlags {
  ContentScanFlags(1UL << 2)
}

///|
/// A free-form text input component used in a modal.
#derive(ToJson, FromJson, Debug)
pub(all) struct TextInput {
  id : Int32?
  custom_id : String
  // Required when sending a modal; omitted in modal-submit payloads.
  style : TextInputStyle?
  label : String?
  min_length : Int32?
  max_length : Int32?
  required : Bool?
  value : String?
  placeholder : String?
}

///|
/// The single-line or multi-line style of a text input.
#derive(Eq, Debug)
#non_exhaustive
pub(all) enum TextInputStyle {
  Short
  Paragraph
  Unknown(Int32)
}

///|
/// The Discord integer value of this `TextInputStyle`.
pub fn TextInputStyle::to_int(self : TextInputStyle) -> Int32 {
  match self {
    Short => 1
    Paragraph => 2
    Unknown(value) => value
  }
}

///|
/// The `TextInputStyle` for its Discord integer value; unknown values become `Unknown(n)`.
pub fn TextInputStyle::from_int(value : Int32) -> TextInputStyle {
  match value {
    1 => Short
    2 => Paragraph
    value => Unknown(value)
  }
}

///|
/// Encodes the `TextInputStyle` as its Discord integer value.
pub impl ToJson for TextInputStyle with fn to_json(self) {
  Json::number(self.to_int().to_double())
}

///|
/// Decodes the `TextInputStyle` from a JSON number; unknown values become `Unknown(n)`.
pub impl @json.FromJson for TextInputStyle with fn from_json(json, path) {
  match json {
    Number(value, ..) => TextInputStyle::from_int(value.to_int())
    _ => raise JsonDecodeError((path, "expected a text input style (number)"))
  }
}