import * as treeView from "@zag-js/tree-view";
import { mergeProps, type MachineInput } from "marko-zag";
import { cn } from "#lib/utils.ts";
import { stripOwnProps } from "#lib/native-attrs.ts";
import { treeView as styles } from "./classes.ts";

export interface TreeViewItem {
  id: string;
  label: string;
  children?: TreeViewItem[];
  disabled?: boolean;
}

// MachineInput = native <div> attributes ∩ the machine's full Props, with
// `id` made optional (generated below, overridable). `collection` is
// derived from `items` below rather than accepted directly, so it is
// omitted here. `expandedValueChange`/`selectedValueChange` are additive
// sugar enabling Marko's two-way bind shorthand; Zag's own
// `onExpandedChange`/`onSelectionChange` also work and receive the details
// object. Both are wired by explicit overrides on <zag>: the default rule
// would derive `expandedChange`/`selectionChange` from those callback
// names, which is not what this component exposes, so it emits nothing.
export type Input = Omit<MachineInput<"div", treeView.Props>, "collection"> & {
  items: TreeViewItem[];
  expandedValueChange?: (value: string[]) => void;
  selectedValueChange?: (value: string[]) => void;
};

<id/uid/>

<const/buildCollection=(list: TreeViewItem[]) => treeView.collection({
  rootNode: { id: "__root__", label: "", children: list } as TreeViewItem,
  nodeToValue: (node: TreeViewItem) => node.id,
  nodeToString: (node: TreeViewItem) => node.label,
  nodeToChildren: (node: TreeViewItem) => node.children ?? [],
  isNodeDisabled: (node: TreeViewItem) => !!node.disabled,
})/>

// `collection` is derived from `items` on every read (rather than picked
// off `input`) and must stay inside `props=`: the built TreeCollection holds
// functions and is not serializable as a bare tag-input value.
<zag/api=() => treeView from=input props=(picked) => ({ ...picked, collection: buildCollection(input.items) })
  id=uid
  onExpandedChange(details: treeView.ExpandedChangeDetails) {
    input.onExpandedChange?.(details);
    input.expandedValueChange?.(details.expandedValue);
  }
  onSelectionChange(details: treeView.SelectionChangeDetails) {
    input.onSelectionChange?.(details);
    input.selectedValueChange?.(details.selectedValue);
  }
/>

// Whatever is neither machine-owned (splitProps) nor component-owned
// (class, items, expandedValueChange, selectedValueChange) passes through
// to the root element: aria-*, data-*...
<const/nativeAttrs=() =>
  stripOwnProps(
    treeView.splitProps(input)[1],
    "class",
    "items",
    "expandedValueChange",
    "selectedValueChange",
  )
>

<define/ChevronIcon>
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="24"
    height="24"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    stroke-width="2"
    stroke-linecap="round"
    stroke-linejoin="round"
    class=styles.chevron
  >
    <path d="m9 18 6-6-6-6"/>
  </svg>
</define>

<define/TreeNode|{ node, indexPath }: { node: TreeViewItem; indexPath: number[] }|>
  <const/nodeProps={ node, indexPath }/>
  <const/isBranch=(!!node.children && node.children.length > 0)/>
  <if=isBranch>
    <li ...api().getBranchProps(nodeProps) data-slot="tree-view-branch">
      <div
        ...api().getBranchControlProps(nodeProps)
        data-slot="tree-view-branch-control"
        class=styles.branchControl
        style={ "padding-left": `calc(0.5rem + var(--depth) * 1rem)` }
      >
        // Indicator, not trigger: a nested role="button" inside the
        // role="treeitem" branch control is invalid (axe: nested-interactive,
        // button-name). The branch control itself toggles expansion on click
        // and keyboard; the chevron is purely visual (aria-hidden via Zag).
        <span
          ...api().getBranchIndicatorProps(nodeProps)
          data-slot="tree-view-branch-indicator"
          class=styles.branchIndicator
        >
          <ChevronIcon/>
        </span>
        <span data-slot="tree-view-branch-text" class=styles.branchText>${node.label}</span>
      </div>
      <ul ...api().getBranchContentProps(nodeProps) data-slot="tree-view-branch-content" class=styles.branchContent>
        <for|childNode, childIndex| of=node.children>
          <TreeNode node=childNode indexPath=[...indexPath, childIndex]/>
        </for>
      </ul>
    </li>
  </if>
  <else>
    <li
      ...mergeProps(api().getItemProps(nodeProps), {
        style: { "padding-left": `calc(0.5rem + var(--depth) * 1rem)` },
      })
      data-slot="tree-view-item"
      class=styles.item
    >
      <span data-slot="tree-view-item-text" class=styles.itemText>${node.label}</span>
    </li>
  </else>
</define>

<div ...nativeAttrs() ...api().getRootProps() data-slot="tree-view" class=cn(styles.root, input.class)>
  <ul ...api().getTreeProps() data-slot="tree-view-tree" class=styles.tree>
    <for|node, index| of=input.items>
      <TreeNode node=node indexPath=[index]/>
    </for>
  </ul>
</div>

// Parents can receive the api getter: <TreeView/treeViewApi/> then
// `treeViewApi().selectedValue` — it stays live across machine updates.
<return=api>
