import * as cascadeSelect from "@zag-js/cascade-select";
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 { cascadeSelect as styles } from "./classes.ts";

export interface CascadeSelectItem {
  value: string;
  label: string;
  disabled?: boolean;
  children?: CascadeSelectItem[];
}

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

// `items` builds the machine's tree `collection`; `value` is the plain
// string[][] path-list the caller works with (each selected leaf is a full
// value path, e.g. ["asia", "india", "mumbai"]).
//
// Dual API: `<@option>` attr tags are a flat, Marko-idiomatic convenience
// for the common non-nested case; `items=` remains the way to describe a
// real tree (attr tags don't recurse into children per the compound-tag-dx
// report). If `<@option>` tags are present they win over `items`.
export type Input = Omit<MachineInput<"div", cascadeSelect.Props<CascadeSelectItem>>, "collection"> & {
  items?: CascadeSelectItem[];
  option?: Marko.AttrTag<CascadeSelectOptionAttrs>;
  valueChange?: (value: string[][]) => void;
  placeholder?: string;
  label?: string;
};

<const/optionTags=[...(input.option ?? [])]/>
// Annotated as CascadeSelectItem[] (not left to inference): a flat
// `<@option>` tag is just an item without `children`, so both arms are
// CascadeSelectItem[]. Without this the ternary infers a union whose
// attr-tag arm has no `children`, which the tree walkers below read.
<const/items: CascadeSelectItem[]=(optionTags.length > 0 ? optionTags.map((option) => ({ value: option.value, label: option.label, disabled: option.disabled })) : (input.items ?? []))/>
<const/{ placeholder, label, class: className }=input/>

<const/buildCollection=(list: CascadeSelectItem[]) =>
  cascadeSelect.collection({
    rootNode: { value: "", label: "", children: list },
    nodeToValue: (node: CascadeSelectItem) => node.value,
    nodeToString: (node: CascadeSelectItem) => node.label,
    nodeToChildren: (node: CascadeSelectItem) => node.children ?? [],
    isNodeDisabled: (node: CascadeSelectItem) => !!node.disabled,
  })
/>

// `collection` is derived state (rebuilt whenever `items` 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.
<zag/api=() => cascadeSelect from=input props=(picked) => ({ ...picked, collection: buildCollection(items) })/>

<const/nativeAttrs=() =>
  stripOwnProps(
    cascadeSelect.splitProps(input)[1],
    "class",
    "valueChange",
    "items",
    "option",
    "placeholder",
    "label",
  )
>

// `api().valueAsString` derives from the machine's internal `selectedItems`
// context, which the @zag-js/cascade-select machine (verified through
// 1.43.0) only populates inside the `set.value` action — i.e. on
// interactive selection. It is NOT recomputed from `value`/`defaultValue`
// when the `value` bindable seeds at machine init, so on first mount with
// an initial value, `valueAsString` is empty even though `api().value`
// itself is correctly seeded. We rebuild the label ourselves from
// `api().value` (proven correct at init) walked against `items`, mirroring
// the machine's own single-mode formatter (path labels joined by " / ").
<const/pathLabel=(path: string[]) => {
  const labels: string[] = [];
  let siblings = items;
  for (const segmentValue of path) {
    const node = siblings.find((candidate) => candidate.value === segmentValue);
    if (!node) break;
    labels.push(node.label);
    siblings = node.children ?? [];
  }
  return labels.join(" / ");
}>
<const/triggerLabel=() => api().value.map(pathLabel).join(", ")>

// The panels along the currently highlighted path: the root list, plus one
// list per ancestor of the highlighted item that has children. Each panel
// shows the children of the node at that depth. `indexPath`/`valuePath` are
// the REAL tree coordinates of the panel's parent node (empty for the root
// panel) — the machine resolves nodes from the collection by indexPath, so
// these must match the actual tree structure, not panel-local positions.
<const/panels=() => {
  // `V` in `cascadeSelect.Api<T, V>` defaults to `@zag-js/collection`'s
  // `TreeNode` (= `any`), never inferred as `CascadeSelectItem` — same in
  // the old three-tag wiring, since nothing in either shape passes an
  // explicit type argument. Annotate here rather than widen `<zag>`'s api
  // getter itself.
  const highlightedItems = api().highlightedItems as CascadeSelectItem[];
  type Panel = { indexPath: number[]; valuePath: string[]; children: CascadeSelectItem[] };
  // Tracked separately from the array so the compiler knows a parent always
  // exists: `result[result.length - 1]` is `Panel | undefined` under
  // noUncheckedIndexedAccess even though the list is seeded non-empty here.
  let parent: Panel = { indexPath: [], valuePath: [], children: items };
  const result: Panel[] = [parent];
  let siblings = items;
  for (const node of highlightedItems) {
    const childItems = node.children ?? [];
    const nodeIndex = siblings.indexOf(node);
    if (childItems.length > 0 && nodeIndex !== -1) {
      parent = {
        indexPath: [...parent.indexPath, nodeIndex],
        valuePath: [...parent.valuePath, node.value],
        children: childItems,
      };
      result.push(parent);
      siblings = childItems;
    }
  }
  return result;
}>
// Value paths ("/"-joined) of the child-lists actually rendered right now —
// one per panel beyond the root, i.e. one per highlighted-chain node with
// children. Zag's getItemProps emits aria-controls/aria-owns pointing at the
// child list id whenever an item HAS children, but the component only mounts
// that list while the item sits on the highlighted chain, so the idrefs
// dangle (and fail aria-valid-attr-value) for every non-highlighted branch.
// Items not in this set get their aria-controls/aria-owns dropped below.
<const/renderedChildLists=() => new Set(panels().slice(1).map((p) => p.valuePath.join("/")))>

<const/labelProps=() => {
  const props = api().getLabelProps() as Record<string, unknown>;
  delete props["for"];
  delete props["htmlFor"];
  return props;
}/>

<div ...nativeAttrs() ...api().getRootProps() data-slot="cascade-select" class=cn(className)>
  <span ...labelProps() data-slot="cascade-select-label" class=(label ? styles.label : styles.labelSrOnly)>${label || placeholder || "Select an option"}</span>
  <div ...api().getControlProps() data-slot="cascade-select-control">
    <button type="button" ...api().getTriggerProps() aria-haspopup="tree" aria-labelledby=(labelProps().id as string) data-slot="cascade-select-trigger" class=styles.trigger>
      <span data-slot="cascade-select-value">${triggerLabel() || placeholder}</span>
      <Icon ...api().getIndicatorProps() name="ChevronDown" class=styles.indicatorIcon/>
    </button>
  </div>
  <const/hiddenInputProps=() => {
    const props = api().getHiddenInputProps() as Record<string, unknown>;
    delete props["aria-labelledby"];
    delete props["ariaLabelledBy"];
    return props;
  }/>
  <input type="hidden" ...hiddenInputProps() data-slot="cascade-select-hidden-input" hidden/>
  <zag-portal>
    <if=api().open>
      <div ...api().getPositionerProps() data-slot="cascade-select-positioner" style=positionerStyle>
        <div
          ...api().getContentProps()
          role="tree"
          // The @zag-js machine seeds highlightedValue as [[]] on open, so
          // connect emits aria-activedescendant pointing at a synthesized
          // "item:" id that no element carries (aria-valid-attr-value).
          // Only forward the idref when the highlight chain reaches an
          // actual item.
          aria-activedescendant=(api().highlightedValue?.flat().length ? api().getContentProps()["aria-activedescendant"] : undefined)
          data-slot="cascade-select-content"
          class=styles.content
        >
          <for|panel| of=panels()>
            <div
              ...api().getListProps({ item: { value: "", label: "", children: panel.children }, indexPath: panel.indexPath, value: panel.valuePath })
              aria-level=undefined
              data-slot="cascade-select-list"
              class=styles.list
            >
              <for|entry, index| of=panel.children>
                <const/itemProps={ item: entry, indexPath: [...panel.indexPath, index], value: [...panel.valuePath, entry.value] }/>
                <const/hasRenderedChildList=renderedChildLists().has(itemProps.value.join("/"))/>
                <div
                  ...api().getItemProps(itemProps)
                  aria-controls=(hasRenderedChildList ? api().getItemProps(itemProps)["aria-controls"] : undefined)
                  aria-owns=(hasRenderedChildList ? api().getItemProps(itemProps)["aria-owns"] : undefined)
                  data-slot="cascade-select-item"
                  class=styles.item
                >
                  <span ...api().getItemTextProps(itemProps) data-slot="cascade-select-item-text">${entry.label}</span>
                  <if=(entry.children && entry.children.length > 0)>
                    <Icon name="ChevronRight" class=styles.itemChevron/>
                  </if>
                  <else>
                    <span ...api().getItemIndicatorProps(itemProps) data-slot="cascade-select-item-indicator" class=styles.itemIndicator>
                      <Icon name="Check" class=styles.itemIndicatorIcon/>
                    </span>
                  </else>
                </div>
              </for>
            </div>
          </for>
        </div>
      </div>
    </if>
  </zag-portal>
</div>

<return=api>
