///|
/// Kumo-compatible control sizes supported by Button and Input.
pub(all) enum ControlSize {
  Xs
  Sm
  Base
  Lg
}

///|
/// The Kumo Button variants supported by this MoonBit implementation.
pub(all) enum ButtonVariant {
  Primary
  Secondary
  Ghost
  Destructive
  SecondaryDestructive
  Outline
}

///|
/// Input appearance. An error message selects `Error` automatically when the
/// caller leaves this value unspecified.
pub(all) enum InputVariant {
  Default
  Error
}

///|
/// The Kumo Text variants supported by this MoonBit implementation.
pub(all) enum TextVariant {
  Heading
  Body
  Secondary
  Success
  Error
  Mono
  MonoSecondary
}

///|
/// Render a Kumo Button as safe HTML. `attrs` adds ordinary HTML attributes
/// such as `id`, `aria-label`, or `data-*` values.
pub fn button(
  children : Html,
  variant? : ButtonVariant = Secondary,
  size? : ControlSize = Base,
  disabled? : Bool = false,
  button_type? : String = "button",
  class_name? : String = "",
  attrs? : Array[Attribute] = [],
) -> Html {
  let class_key = "\{button_variant_name(variant)}|\{size_name(size)}|\{bool_name(disabled)}"
  let classes = append_classes(
    append_classes(generated_button_classes(class_key), emphasis_class(variant)),
    class_name,
  )
  let attributes : Array[Attribute] = [
    text_attribute("data-kumo-component", "Button"),
    text_attribute("class", classes),
  ]
  if disabled {
    attributes.push(boolean_attribute("disabled"))
  }
  attributes.push(text_attribute("type", button_type))
  let final_attributes = merge_attributes(attributes, attrs)
  let content = raw_element("span", [text_attribute("class", "contents")], [
    children,
  ])
  let button_children = match variant {
    Primary | Destructive => {
      let decoration = raw_element(
        "span",
        [
          text_attribute("aria-hidden", "true"),
          text_attribute(
            "class", "absolute inset-0 rounded-[inherit] bg-linear-to-b from-(--kumo-button-emphasis-gradient-start) to-(--kumo-button-emphasis-gradient-end) shadow-[inset_0_1px_0_0_var(--kumo-button-emphasis-bg)] group-hover:from-(--kumo-button-emphasis-bg)",
          ),
        ],
        [],
      )
      let content_wrap = raw_element(
        "span",
        [text_attribute("class", "relative flex items-center gap-1.5")],
        [content],
      )
      [decoration, content_wrap]
    }
    _ => [content]
  }
  raw_element("button", final_attributes, button_children)
}

///|
/// Render a Kumo Input. Labels, descriptions, and errors are semantic nodes;
/// the field wrapper links them to the control with stable ids.
pub fn input(
  id~ : String,
  size? : ControlSize = Base,
  variant? : InputVariant,
  label? : Html,
  description? : Html,
  error? : Html,
  placeholder? : String,
  disabled? : Bool = false,
  required? : Bool,
  class_name? : String = "",
  attrs? : Array[Attribute] = [],
) -> Html {
  let resolved_variant = match variant {
    Some(value) => input_variant_name(value)
    None =>
      match error {
        Some(_) => "error"
        None => "default"
      }
  }
  let class_key = "\{resolved_variant}|\{size_name(size)}"
  let classes = append_classes(generated_input_classes(class_key), class_name)
  let input_id = match find_attribute(attrs, "id") {
    Some(value) => value
    None => id
  }
  let has_field = label is Some(_) || description is Some(_) || error is Some(_)
  let initial_value = match find_attribute(attrs, "value") {
    Some(value) => value
    None => ""
  }
  let input_attributes : Array[Attribute] = [text_attribute("id", input_id)]
  match label {
    Some(_) =>
      input_attributes.push(
        text_attribute("aria-labelledby", "\{input_id}-label"),
      )
    None => ()
  }
  input_attributes.push(text_attribute("class", classes))
  if disabled {
    input_attributes.push(text_attribute("data-disabled", ""))
    input_attributes.push(boolean_attribute("disabled"))
  }
  if has_field && initial_value != "" {
    input_attributes.push(text_attribute("data-filled", ""))
  }
  match required {
    Some(true) => input_attributes.push(boolean_attribute("required"))
    _ => ()
  }
  if has_field {
    let description_id = "\{input_id}-description"
    let error_id = "\{input_id}-error"
    // Kumo's Field gives the error message precedence over helper text. Keep
    // aria-describedby aligned with the node that is actually rendered.
    let described_by = match (description, error) {
      (_, Some(_)) => error_id
      (Some(_), None) => description_id
      (None, None) => ""
    }
    if described_by != "" {
      input_attributes.push(text_attribute("aria-describedby", described_by))
    }
  }
  match placeholder {
    Some(value) => input_attributes.push(text_attribute("placeholder", value))
    None => ()
  }
  let final_input_attributes = merge_attributes(input_attributes, attrs)
  let control = raw_element("input", final_input_attributes, [])
  if !has_field {
    return control
  }
  let field_children : Array[Html] = []
  match label {
    Some(label_node) => {
      let label_content : Array[Html] = [label_node]
      match required {
        Some(false) =>
          label_content.push(
            raw_element(
              "span",
              [text_attribute("class", "font-normal text-kumo-subtle")],
              [text("(optional)")],
            ),
          )
        _ => ()
      }
      let label_span = raw_element(
        "span",
        [text_attribute("class", "inline-flex items-center gap-1")],
        label_content,
      )
      field_children.push(
        raw_element(
          "label",
          initial_state_attributes(
            [
              text_attribute("id", "\{input_id}-label"),
              text_attribute("for", input_id),
              text_attribute(
                "class", "m-0 text-base font-medium text-kumo-default select-none",
              ),
            ],
            initial_value,
          ),
          [label_span],
        ),
      )
    }
    None => ()
  }
  field_children.push(control)
  match (error, description) {
    (Some(error_node), _) =>
      field_children.push(
        raw_element(
          "div",
          initial_state_attributes(
            [
              text_attribute("id", "\{input_id}-error"),
              text_attribute(
                "class", "text-sm leading-snug text-kumo-danger col-span-full",
              ),
            ],
            initial_value,
          ),
          [error_node],
        ),
      )
    (None, Some(description_node)) =>
      field_children.push(
        raw_element(
          "p",
          initial_state_attributes(
            [
              text_attribute("id", "\{input_id}-description"),
              text_attribute(
                "class", "text-sm leading-snug text-kumo-subtle col-span-full",
              ),
            ],
            initial_value,
          ),
          [description_node],
        ),
      )
    (None, None) => ()
  }
  raw_element(
    "div",
    initial_state_attributes(
      [
        text_attribute(
          "class", "yk-kumo-input-field grid gap-2 has-[input[type=checkbox]]:grid-cols-[auto_1fr] has-[input[type=checkbox]]:items-center has-[[role=switch]]:grid-cols-[auto_1fr] has-[[role=switch]]:items-center",
        ),
      ],
      initial_value,
    ),
    field_children,
  )
}

///|
fn initial_state_attributes(
  attributes : Array[Attribute],
  initial_value : String,
) -> Array[Attribute] {
  if initial_value != "" {
    attributes.push(text_attribute("data-filled", ""))
  }
  attributes
}

///|
/// Render a Kumo Text element with an optional semantic tag override.
pub fn text_component(
  children : Html,
  variant? : TextVariant = Body,
  size? : ControlSize = Base,
  as_element? : String,
  bold? : Bool = false,
  truncate? : Bool = false,
  class_name? : String = "",
  attrs? : Array[Attribute] = [],
) -> Html {
  let tag = match as_element {
    Some(value) => value
    None => default_text_tag(variant)
  }
  let class_key = "\{text_variant_name(variant)}|\{size_name(size)}"
  let classes = generated_text_classes(class_key)
  let with_bold = if is_copy_variant(variant) && bold {
    append_classes(classes, "font-medium")
  } else {
    classes
  }
  let with_truncate = if truncate {
    append_classes(with_bold, "min-w-0 truncate")
  } else {
    with_bold
  }
  let attributes = merge_attributes(
    [text_attribute("class", append_classes(with_truncate, class_name))],
    attrs,
  )
  raw_element(tag, attributes, [children])
}

///|
/// Render a simple or layered Kumo LayerCard container.
pub fn layer_card(
  children : Html,
  layered? : Bool = false,
  class_name? : String = "",
  attrs? : Array[Attribute] = [],
) -> Html {
  let variant = if layered { "layered" } else { "simple" }
  let classes = append_classes(generated_layer_card_class(variant), class_name)
  let attributes = merge_attributes([text_attribute("class", classes)], attrs)
  raw_element("div", attributes, [children])
}

///|
/// Render the secondary heading section used inside a layered LayerCard.
pub fn layer_card_secondary(
  children : Html,
  class_name? : String = "",
  attrs? : Array[Attribute] = [],
) -> Html {
  layer_card_section(children, "secondary", class_name, attrs)
}

///|
/// Render the primary content section used inside a layered LayerCard.
pub fn layer_card_primary(
  children : Html,
  class_name? : String = "",
  attrs? : Array[Attribute] = [],
) -> Html {
  layer_card_section(children, "primary", class_name, attrs)
}

///|
fn layer_card_section(
  children : Html,
  section : String,
  class_name : String,
  attrs : Array[Attribute],
) -> Html {
  let classes = append_classes(generated_layer_card_class(section), class_name)
  let attributes = merge_attributes([text_attribute("class", classes)], attrs)
  raw_element("div", attributes, [children])
}

///|
fn size_name(size : ControlSize) -> String {
  match size {
    Xs => "xs"
    Sm => "sm"
    Base => "base"
    Lg => "lg"
  }
}

///|
fn button_variant_name(variant : ButtonVariant) -> String {
  match variant {
    Primary => "primary"
    Secondary => "secondary"
    Ghost => "ghost"
    Destructive => "destructive"
    SecondaryDestructive => "secondary-destructive"
    Outline => "outline"
  }
}

///|
fn input_variant_name(variant : InputVariant) -> String {
  match variant {
    Default => "default"
    Error => "error"
  }
}

///|
fn text_variant_name(variant : TextVariant) -> String {
  match variant {
    Heading => "heading"
    Body => "body"
    Secondary => "secondary"
    Success => "success"
    Error => "error"
    Mono => "mono"
    MonoSecondary => "mono-secondary"
  }
}

///|
fn default_text_tag(variant : TextVariant) -> String {
  match variant {
    Heading | Mono | MonoSecondary => "span"
    _ => "p"
  }
}

///|
fn is_copy_variant(variant : TextVariant) -> Bool {
  match variant {
    Body | Secondary | Success | Error => true
    _ => false
  }
}

///|
fn emphasis_class(variant : ButtonVariant) -> String {
  match variant {
    Primary => "yk-kumo-button-emphasis-primary"
    Destructive => "yk-kumo-button-emphasis-destructive"
    _ => ""
  }
}

///|
fn bool_name(value : Bool) -> String {
  if value {
    "true"
  } else {
    "false"
  }
}

///|
fn append_classes(base : String, extra : String) -> String {
  if extra == "" {
    base
  } else if base == "" {
    extra
  } else {
    "\{base} \{extra}"
  }
}

///|
fn find_attribute(attributes : Array[Attribute], name : String) -> String? {
  for attribute in attributes {
    if attribute.name == name {
      return attribute.value
    }
  }
  None
}

///|
fn merge_attributes(
  base : Array[Attribute],
  extra : Array[Attribute],
) -> Array[Attribute] {
  let merged : Array[Attribute] = []
  for attribute in base {
    merged.push(attribute)
  }
  for attribute in extra {
    let mut found_index : Int? = None
    for index in 0..
        if attribute.name == "class" {
          match (merged[index].value, attribute.value) {
            (Some(base_value), Some(extra_value)) =>
              merged[index] = text_attribute(
                "class",
                append_classes(base_value, extra_value),
              )
            _ => merged[index] = attribute
          }
        } else {
          merged[index] = attribute
        }
      None => merged.push(attribute)
    }
  }
  merged
}