///|
/// The generation state shown next to session titles and task cards.
///
/// - `Generating` — a spinner while a run is in flight
/// - `Count(n)` — a white square with the number of queued/finished tasks
/// - `Done` — a brand-blue tick circle
/// - `Dot` — a small brand-blue unread dot
/// - `Pending(label)` — an orange pill while a run awaits review
/// - `Partial` — an orange info circle for partial failures
pub(all) enum SessionStatus {
  Generating
  Count(Int)
  Done
  Dot
  Pending(String)
  Partial
} derive(Eq)

///|
pub extend SessionStatus with Eq::{equal, not_equal}

///|
const SessionStatusCellStyle : String = "display:inline-flex;flex:none;align-items:center;justify-content:center;width:20px;height:20px"

///|
const SessionStatusNumStyle : String = "font-size:10px;line-height:1;font-weight:500"

///|
/// The 20px generation-status indicator of session rows, titles, and task
/// cards. `Pending` sizes to its label; every other kind is a fixed 20px
/// cell.
///
/// ```moonbit nocheck
/// session_status(kind=Generating)
/// session_status(kind=Count(5))
/// session_status(kind=Pending("Review"))
/// ```
pub fn session_status(
  kind~ : SessionStatus,
  id? : String,
  class? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  let element_attrs = @internal.sumi_attrs(attrs)
    .data_set("slot", "session-status")
    .data_set(
      "kind",
      match kind {
        Generating => "generating"
        Count(_) => "count"
        Done => "done"
        Dot => "dot"
        Pending(_) => "pending"
        Partial => "partial"
      },
    )
  let (root_style, content) : (String, @html.Html) = match kind {
    Generating =>
      (
        SessionStatusCellStyle +
        ";color:var(--sumi-primary-fg-disabled,rgba(224,245,255,0.2))",
        @primitives.spinner(size=20),
      )
    Count(n) =>
      (
        SessionStatusCellStyle +
        ";border-radius:10px;background:var(--sumi-primary-bg,#fafafa);color:var(--sumi-primary-fg,#1a1a1a)",
        @html.span(
          style=[@internal.SumiFontNum, SessionStatusNumStyle],
          n.to_string(),
        ),
      )
    Done =>
      (
        SessionStatusCellStyle + ";color:var(--sumi-brand,#009efa)",
        @primitives.icon_tick_circle_fill(size=17),
      )
    Dot =>
      (
        SessionStatusCellStyle,
        @html.span(
          style=[
            "width:8px;height:8px;border-radius:9999px;background:var(--sumi-brand,#009efa)",
          ],
          ([] : Array[@html.Html]),
        ),
      )
    Pending(label) =>
      (
        "display:inline-flex;flex:none;align-items:center;justify-content:center;padding:5px 8px;border-radius:10px;background:var(--sumi-orange-block,rgba(255,146,48,0.16));color:var(--sumi-orange,#ff9230);white-space:nowrap",
        @html.span(style=[@internal.SumiFontNum, SessionStatusNumStyle], [
          @html.text(label),
        ]),
      )
    Partial =>
      (
        SessionStatusCellStyle + ";color:var(--sumi-favorite,#ffa21e)",
        @primitives.icon_info_circle_fill(size=17),
      )
  }
  @html.span(
    style=@internal.sumi_styles([root_style], style),
    id?,
    class?,
    attrs=element_attrs,
    [content],
  )
}

///|
const SessionItemStyle : String = "display:flex;align-items:center;gap:8px;width:100%;padding:8px 10px;border-radius:8px;font-size:13px;line-height:22px;color:var(--sumi-text,#ffffff);user-select:none"

///|
const SessionItemDisabledStyle : String = "color:var(--sumi-text-disabled,rgba(255,255,255,0.2));cursor:not-allowed"

///|
const SessionItemActionStyle : String = "display:inline-flex;flex:none;align-items:center;justify-content:center;width:20px;height:20px;margin:0;padding:0;border:none;border-radius:4px;background:transparent;color:var(--sumi-text-secondary,rgba(255,255,255,0.7));cursor:pointer;appearance:none;-webkit-appearance:none;outline:none"

///|
/// One hover-revealed row action (rename / delete) of a `session_item`.
/// Clicks are stop-propagationed so they never trigger the row's own
/// `on_click`.
fn session_action(
  icon : @html.Html,
  label : String,
  cmd : @cmd.Cmd,
) -> @html.Html {
  @html.button(
    style=[
      @internal.SumiBoxSizing,
      @internal.SumiTextRendering,
      @internal.SumiTransition,
      SessionItemActionStyle,
    ],
    type_="button",
    title=label,
    attrs=@html.Attrs::build()
      .data_set("slot", "session-item-action")
      .aria_label(label)
      .on_click(fn(ev) {
        ev.stop_propagation()
        cmd
      }),
    [icon],
  )
}

///|
/// One row of a session list: a 13/22 label with an optional trailing
/// generation status (or a check mark while `active`), revealing rename /
/// delete actions on hover. The status cell hides while hovered so the
/// actions take its place.
///
/// ```moonbit nocheck
/// session_item(
///   label="Desert Mirage Expedition",
///   active=current == id,
///   status=Done,
///   on_click=emit.map(Open(id)),
///   on_edit=emit.map(Rename(id)),
///   on_delete=emit.map(Remove(id)),
/// )
/// ```
pub fn session_item(
  label~ : String,
  active? : Bool = false,
  disabled? : Bool = false,
  status? : SessionStatus,
  on_click? : @cmd.Cmd,
  on_edit? : @cmd.Cmd,
  on_delete? : @cmd.Cmd,
  edit_label? : String = "Rename",
  delete_label? : String = "Delete",
  aria_label? : String,
  id? : String,
  class? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  let element_attrs = @internal.sumi_attrs(attrs).data_set(
    "slot", "session-item",
  )
  if active {
    ignore(element_attrs.data_set("active", "true").aria_current("true"))
  }
  let has_actions = !disabled && (on_edit is Some(_) || on_delete is Some(_))
  if has_actions {
    ignore(element_attrs.data_set("actions", "true"))
  }
  if disabled {
    ignore(element_attrs.data_set("disabled", "true").aria_disabled("true"))
  }
  if aria_label is Some(label_text) {
    ignore(element_attrs.aria_label(label_text))
  }
  let status_cell : @html.Html = if active {
    @html.span(
      style=[
        "display:inline-flex;flex:none;align-items:center;justify-content:center;width:20px;height:20px;color:var(--sumi-text,#ffffff)",
      ],
      attrs=@html.Attrs::build().data_set("slot", "session-item-status"),
      [@primitives.icon_check_fill_large(size=16)],
    )
  } else {
    match status {
      Some(kind) =>
        @html.span(
          style=[
            "display:inline-flex;flex:none;align-items:center;justify-content:center;width:20px;height:20px",
          ],
          attrs=@html.Attrs::build().data_set("slot", "session-item-status"),
          [session_status(kind~)],
        )
      None => @html.nothing
    }
  }
  let actions_view : @html.Html = if has_actions {
    let buttons : Array[@html.Html] = []
    if on_edit is Some(cmd) {
      buttons.push(
        session_action(@primitives.icon_edit(size=16), edit_label, cmd),
      )
    }
    if on_delete is Some(cmd) {
      buttons.push(
        session_action(@primitives.icon_delete(size=16), delete_label, cmd),
      )
    }
    @html.span(
      style=["display:none;flex:none;align-items:center;gap:8px"],
      attrs=@html.Attrs::build().data_set("slot", "session-item-actions"),
      buttons,
    )
  } else {
    @html.nothing
  }
  @html.div(
    style=@internal.sumi_styles(
      [
        @internal.SumiBoxSizing,
        @internal.SumiFontSans,
        @internal.SumiTextRendering,
        @internal.SumiTransition,
        SessionItemStyle,
        if disabled {
          SessionItemDisabledStyle
        } else if on_click is Some(_) {
          "cursor:pointer"
        } else {
          ""
        },
      ],
      style,
    ),
    id?,
    class?,
    on_click?=if disabled { None } else { on_click },
    attrs=element_attrs,
    [
      @html.span(
        style=[
          "flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap",
        ],
        [@html.text(label)],
      ),
      status_cell,
      actions_view,
    ],
  )
}

///|
/// The hairline separator of a session list: a 0.5px rule inset 12px on
/// both sides inside a 4px-tall cell.
pub fn session_divider(
  id? : String,
  class? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  @html.div(
    style=@internal.sumi_styles(
      ["display:flex;flex:none;align-items:center;height:4px"],
      style,
    ),
    id?,
    class?,
    attrs=@internal.sumi_attrs(attrs)
      .data_set("slot", "session-divider")
      .role("separator")
      .aria_hidden("true"),
    [
      @html.span(
        style=[
          "flex:1;height:0.5px;margin:0 12px;background:var(--sumi-stroke-secondary,rgba(255,255,255,0.04))",
        ],
        ([] : Array[@html.Html]),
      ),
    ],
  )
}

///|
/// The session-list panel: a floating menu surface (`--sumi-menu`, 12px
/// radius, 4px padding) holding `session_item` rows and `session_divider`
/// separators. With `max_height` the panel scrolls with the library's thin
/// scrollbar, tinted to the design's blue.
///
/// ```moonbit nocheck
/// session_list(max_height=320, [
///   session_item(label="Neon Harbor Nights", active=true),
///   session_item(label="Alpine Sunrise Timelapse", status=Generating),
///   session_divider(),
///   session_item(label="Archived Storyboards", disabled=true),
/// ])
/// ```
pub fn session_list(
  children~ : Array[@html.Html],
  width? : Int = 280,
  max_height? : Int,
  aria_label? : String,
  id? : String,
  class? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  let element_attrs = @internal.sumi_attrs(attrs).data_set(
    "slot", "session-list",
  )
  if aria_label is Some(label_text) {
    ignore(element_attrs.aria_label(label_text))
  }
  @html.div(
    style=@internal.sumi_styles(
      [
        @internal.SumiBoxSizing,
        @internal.SumiFontSans,
        @internal.SumiTextRendering,
        "display:flex;flex-direction:column;gap:2px;width:\{width}px;padding:4px;border:0.5px solid var(--sumi-stroke-secondary,rgba(255,255,255,0.04));border-radius:12px;background:var(--sumi-menu,#262626);box-shadow:0px 8px 28px 0px var(--sumi-shadow,rgba(0,0,0,0.24));--sumi-scrollbar:rgba(204,221,255,0.12)",
        match max_height {
          Some(h) => "max-height:\{h}px;overflow-x:clip;overflow-y:auto"
          None => ""
        },
      ],
      style,
    ),
    id?,
    class?,
    attrs=element_attrs,
    children,
  )
}

///|
const SessionTitleStyle : String = "display:inline-flex;align-items:center;gap:8px;max-width:100%;margin:0;padding:4px 8px;border:none;border-radius:8px;background:transparent;font-size:14px;line-height:22px;color:var(--sumi-text,#ffffff);cursor:pointer;user-select:none;appearance:none;-webkit-appearance:none;outline:none"

///|
/// The current-session pill of a chat window header: a 14/22 label, an
/// optional generation status, and a 12px chevron, with the design's
/// ghost hover/press fills. Typically the trigger of a `session_list`
/// dropdown.
pub fn session_title(
  label~ : String,
  status? : SessionStatus,
  on_click? : @cmd.Cmd,
  disabled? : Bool = false,
  aria_label? : String,
  id? : String,
  class? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  let element_attrs = @internal.sumi_attrs(attrs).data_set(
    "slot", "session-title",
  )
  if aria_label is Some(label_text) {
    ignore(element_attrs.aria_label(label_text))
  }
  let status_view : @html.Html = match status {
    Some(kind) => session_status(kind~)
    None => @html.nothing
  }
  @html.button(
    style=@internal.sumi_styles(
      [
        @internal.SumiBoxSizing,
        @internal.SumiFontSans,
        @internal.SumiTextRendering,
        @internal.SumiTransition,
        SessionTitleStyle,
        if disabled {
          "color:var(--sumi-text-disabled,rgba(255,255,255,0.2));cursor:not-allowed"
        } else {
          ""
        },
      ],
      style,
    ),
    type_="button",
    disabled~,
    id?,
    class?,
    on_click?,
    attrs=element_attrs,
    [
      @html.span(
        style=[
          "margin:0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap",
        ],
        [@html.text(label)],
      ),
      status_view,
      @html.span(
        style=["display:inline-flex;flex:none"],
        attrs=@html.Attrs::build().aria_hidden("true"),
        [@primitives.icon_chevron_down_fill(size=12)],
      ),
    ],
  )
}

///|
/// The header row of a chat window: the current-session `session_title`
/// on the left and new-conversation / collapse icon buttons on the right
/// (gap 4).
///
/// ```moonbit nocheck
/// session_header(
///   title="Desert Mirage Expedition",
///   status=Done,
///   on_title=emit(ToggleSessionMenu),
///   on_new=emit(NewConversation),
///   on_collapse=emit(CollapsePanel),
/// )
/// ```
pub fn session_header(
  title~ : String,
  status? : SessionStatus,
  on_title? : @cmd.Cmd,
  on_new? : @cmd.Cmd,
  on_collapse? : @cmd.Cmd,
  new_label? : String = "New conversation",
  collapse_label? : String = "Collapse",
  aria_label? : String,
  id? : String,
  class? : String,
  attrs? : @html.Attrs,
  style? : Array[String] = [],
) -> @html.Html {
  let element_attrs = @internal.sumi_attrs(attrs).data_set(
    "slot", "session-header",
  )
  if aria_label is Some(label_text) {
    ignore(element_attrs.aria_label(label_text))
  }
  @html.div(
    style=@internal.sumi_styles(
      [
        @internal.SumiBoxSizing,
        @internal.SumiFontSans,
        @internal.SumiTextRendering,
        "display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%",
      ],
      style,
    ),
    id?,
    class?,
    attrs=element_attrs,
    [
      session_title(label=title, status?, on_click?=on_title),
      @html.div(style=["display:flex;flex:none;align-items:center;gap:4px"], [
        @primitives.icon_button(
          @primitives.icon_new_conversation(size=16),
          aria_label=new_label,
          on_click?=on_new,
        ),
        @primitives.icon_button(
          @primitives.icon_fullscreen_alt(size=16),
          aria_label=collapse_label,
          on_click?=on_collapse,
        ),
      ]),
    ],
  )
}