import * as combobox from "@zag-js/combobox";
import type { MachineInput } from "marko-zag";
import { positionerStyle } from "marko-zag";
import Icon from "../icon/icon.marko";
import { cn } from "#lib/utils.ts";
import { stripOwnProps } from "#lib/native-attrs.ts";
import { combobox as styles } from "./classes.ts";

export interface ComboboxItem {
  value: string;
  label: string;
  disabled?: boolean;
}

// Optional grouping — same display-only shape as select.marko's `groups`
// (zag has no native group concept; see combobox.props.mjs). Mirrors
// upstream's `ComboboxGroup`/`ComboboxLabel` slots.
export interface ComboboxGroupData {
  label: string;
  items: ComboboxItem[];
}

// `items`/`value` are narrowed vs. the raw machine Props: `items` drives the
// generated `collection`, and `value` is typed as the plain string[] the
// caller works with (the machine's own `value` type matches already).
export type Input = Omit<MachineInput<"div", combobox.Props>, "collection"> & {
  items: ComboboxItem[];
  groups?: ComboboxGroupData[];
  valueChange?: (value: string[]) => void;
  inputValueChange?: (inputValue: string) => void;
  highlightChange?: (highlightedValue: string | null) => void;
  /** Show the clear ("x") button when a value is selected. */
  showClear?: boolean;
};

<const/{ items, groups, showClear, class: className }=input/>

<let/filtered=items/>

<const/buildCollection=(list: ComboboxItem[]) =>
  combobox.collection({
    items: list,
    itemToValue: (item: ComboboxItem) => item.value,
    itemToString: (item: ComboboxItem) => item.label,
    isItemDisabled: (item: ComboboxItem) => !!item.disabled,
  })
/>

// `collection` is derived state (rebuilt whenever `filtered` changes), not a
// plain input field — it holds methods and cannot cross the tag-input
// boundary. Layer it on in `props=`, which receives the picked props.
// `onInputValueChange`/`onValueChange` are overridden (not left to the
// default sugar rule) because they also drive the local `filtered` list.
<zag/api=() => combobox from=input props=(picked) => ({ ...picked, collection: buildCollection(filtered) })
  onInputValueChange(details: combobox.InputValueChangeDetails) {
    input.onInputValueChange?.(details);
    input.inputValueChange?.(details.inputValue);
    // Only narrow the list while the user is typing. Zag also fires this
    // with reason "item-select" (writing the chosen label into the input),
    // which would otherwise re-filter the list down to the selection and
    // leave the reopened dropdown showing a single item.
    if (details.reason !== "input-change") {
      filtered = items;
      return;
    }
    const query = details.inputValue.trim().toLowerCase();
    filtered = query === "" ? items : items.filter((item) => item.label.toLowerCase().includes(query));
  }
  onValueChange(details: combobox.ValueChangeDetails) {
    input.onValueChange?.(details);
    input.valueChange?.(details.value);
    filtered = items;
  }
  onHighlightChange(details: combobox.HighlightChangeDetails) {
    input.onHighlightChange?.(details);
    input.highlightChange?.(details.highlightedValue);
  }
/>

<const/nativeAttrs=() =>
  stripOwnProps(
    combobox.splitProps(input)[1],
    "class",
    "items",
    "groups",
    "valueChange",
    "inputValueChange",
    "highlightChange",
    "showClear",
  )
>

// `filteredGroups`: group shape re-derived from `filtered` so typing narrows
// each group's items the same way the flat list narrows (group membership by
// value lookup against the original `groups` prop).
<const/filteredGroups=() => {
  if (!groups) return null;
  const filteredValues = new Set(filtered.map((item) => item.value));
  return groups
    .map((group) => ({ label: group.label, items: group.items.filter((item) => filteredValues.has(item.value)) }))
    .filter((group) => group.items.length > 0);
}>

<div ...nativeAttrs() ...api().getRootProps() data-slot="combobox" class=cn(className)>
  <span data-slot="combobox-value" class=styles.value>${api().valueAsString}</span>
  <div
    ...api().getControlProps()
    data-slot="combobox-control"
    class=styles.control
  >
    <if=(input.multiple && api().hasSelectedItems)>
      <div data-slot="combobox-chips" class=styles.chips>
        // `V` in `combobox.Api<T, V>` has no default and can't be inferred
        // through `<zag>`'s api getter (same in the old three-tag wiring,
        // since nothing there passed an explicit type argument either) — cast
        // rather than widen `<zag>`'s api getter itself.
        <for|item: ComboboxItem| of=(api().selectedItems as ComboboxItem[])>
          <span data-slot="combobox-chip" class=styles.chip>
            ${item.label}
            <button
              type="button"
              data-slot="combobox-chip-remove"
              class=styles.chipRemove
              disabled=input.disabled
              aria-label=`Remove ${item.label}`
              on-click() { api().clearValue(item.value); }
            >
              <Icon name="X" class=styles.chipRemoveIcon/>
            </button>
          </span>
        </for>
        <input type="text"
          ...api().getInputProps()
          data-slot="combobox-chip-input"
          class=styles.chipInput
        />
      </div>
    </if>
    <else>
      <input type="text"
        ...api().getInputProps()
        data-slot="combobox-input"
        class=styles.input
      />
    </else>
    <if=(showClear && api().hasSelectedItems)>
      <button type="button"
        ...api().getClearTriggerProps()
        data-slot="combobox-clear"
        class=styles.clear
      >
        <Icon name="X" class=styles.clearIcon/>
      </button>
    </if>
    <button type="button"
      ...api().getTriggerProps()
      data-slot="combobox-trigger"
      class=styles.trigger
    >
      <Icon name="ChevronDown" class=styles.triggerIcon/>
    </button>
  </div>
  <zag-portal>
    <if=api().open>
      <div ...api().getPositionerProps() style=positionerStyle data-slot="combobox-positioner" class=styles.positioner>
        <div
          ...api().getContentProps()
          data-slot="combobox-content"
          class=styles.content
        >
          // composite defaults to TRUE in the @zag-js machine, so the
          // shipping shape is: content role=listbox, this list role-less.
          // An aria-labelledby on a role-less div inside the listbox reads
          // as a disallowed named child (aria-required-children) — strip
          // it there. Only an explicit composite={false} renders the list
          // as the listbox, where Zag's aria-labelledby legitimately
          // names it.
          <div
            ...api().getListProps()
            aria-labelledby=(input.composite === false ? api().getListProps()["aria-labelledby"] : undefined)
            data-slot="combobox-list"
            class=styles.list
          >
            <if=filteredGroups()>
              <for|group, groupIndex| of=filteredGroups()>
                <div ...api().getItemGroupProps({ id: `group-${groupIndex}` }) data-slot="combobox-group" class=styles.group>
                  <div ...api().getItemGroupLabelProps({ htmlFor: `group-${groupIndex}` }) data-slot="combobox-label" class=styles.label>${group.label}</div>
                  <div role="presentation" data-slot="combobox-collection">
                    <for|item| of=group.items>
                      <div
                        ...api().getItemProps({ item })
                        data-slot="combobox-item"
                        class=styles.item
                      >
                        <span ...api().getItemTextProps({ item }) data-slot="combobox-item-text">${item.label}</span>
                        <span ...api().getItemIndicatorProps({ item }) data-slot="combobox-item-indicator" class=styles.itemIndicator>
                          <Icon name="Check" class=styles.itemIndicatorIcon/>
                        </span>
                      </div>
                    </for>
                  </div>
                </div>
                <if=(groupIndex < (filteredGroups()?.length ?? 0) - 1)>
                  <div data-slot="combobox-separator" class=styles.separator></div>
                </if>
              </for>
            </if>
            <else>
              <div role="presentation" data-slot="combobox-collection">
                <for|item| of=filtered>
                  <div
                    ...api().getItemProps({ item })
                    data-slot="combobox-item"
                    class=styles.item
                  >
                    <span ...api().getItemTextProps({ item }) data-slot="combobox-item-text">${item.label}</span>
                    <span ...api().getItemIndicatorProps({ item }) data-slot="combobox-item-indicator" class=styles.itemIndicator>
                      <Icon name="Check" class=styles.itemIndicatorIcon/>
                    </span>
                  </div>
                </for>
              </div>
            </else>
          </div>
          <if=(filtered.length === 0)>
            <div data-slot="combobox-empty" class=styles.empty>No results found.</div>
          </if>
        </div>
      </div>
    </if>
  </zag-portal>
</div>

<return=api>
