import * as menu from "@zag-js/menu";
import { positionerStyle, connect } from "marko-zag";
import { cn } from "#lib/utils.ts";
import Icon from "../icon/icon.marko";
import Submenu from "./submenu.marko";
import { dropdownMenu as styles } from "./classes.ts";

export interface DropdownMenuItem {
  type?: "item" | "separator" | "label" | "checkbox" | "radio" | "sub";
  value?: string;
  label?: string;
  disabled?: boolean;
  shortcut?: string;
  /** Upstream `variant: "default" | "destructive"`, emitted as `data-variant`. */
  variant?: "default" | "destructive";
  /** Emits `data-inset` — indents the entry to align with icon-bearing siblings. */
  inset?: boolean;
  /** Checked state for `type="checkbox"` and `type="radio"` entries. */
  checked?: boolean;
  /** Groups radio items — required for `type: "radio"` entries. */
  radioGroup?: string;
  /** `type: "radio"` entries only — this entry's value within its `radioGroup`. */
  radioValue?: string;
  /**
   * `type: "sub"` entries only — the nested submenu's own item list. One
   * level deep: a `type: "sub"` entry inside a submenu's own `subEntries` is
   * not wired (same limit as menubar's submenu).
   */
  subEntries?: readonly DropdownMenuItem[];
}

// Attr-tag form of DropdownMenuItem. A SINGLE attr tag name (`item`) carries
// all five kinds (discriminated by `type`) rather than separate per-kind
// attr tags: Marko collects each attr-tag NAME into its own independent
// iterable on `input` with no shared ordering between differently-named
// siblings, so `<@item>`/`<@separator>`/`<@label>` as distinct tag names
// could not preserve call-site interleaving. One name with a `type`
// discriminant keeps document order intact by construction (same design as
// context-menu.marko). `content` lets an item or label carry a markup body;
// the plain `label` string renders as the fallback when no body is supplied.
export interface DropdownMenuItemAttrs extends DropdownMenuItem {
  content?: Marko.Body;
}

export interface Input {
  trigger: Marko.AttrTag<{ content: Marko.Body<[Record<string, unknown>]> }>;
  items?: DropdownMenuItem[];
  item?: Marko.AttrTag<DropdownMenuItemAttrs>;
  select?: (value: string) => void;
  /** Fires when a checkbox item's checked state changes; receives its value and new checked state. */
  checkedChange?: (value: string, checked: boolean) => void;
  /** Fires when a radio group's selected value changes; receives the group name and new value. */
  radioChange?: (group: string, value: string) => void;
  /**
   * Machine pass-throughs, forwarded to `@zag-js/menu` by `<zag-machine ...
   * from=input/>` regardless of whether they're declared here (see
   * `buildMachineProps` in marko-zag) — declared for type safety and DX.
   * `positioning`/`onHighlightChange` are what a caller needs to relocate the
   * popup (side/align/gutter/mobile anchor override) and mirror the
   * highlighted item for a hover-preview, the two capabilities the typeset/
   * create picker.marko files used to hand-roll a whole parallel menu for.
   */
  positioning?: menu.Props["positioning"];
  onHighlightChange?: menu.Props["onHighlightChange"];
  onOpenChange?: menu.Props["onOpenChange"];
  closeOnSelect?: menu.Props["closeOnSelect"];
  class?: string;
}

// Walk-safe replacement for a `<const/{ class: className, trigger }=input/>`
// destructure + `<const/entries=...>` derivation. This component itself is
// mounted via dynamic-tag-content (item-dropdown.marko's
// `<for|person| of=PEOPLE><@item>...` composition, mounted through marko-zag's
// portal/dynamic-tag mechanism) so its OWN walk-time reads — the `<for>`
// selector and `<if=entry.content>` branches below — fire during the
// client's first mount walk, before ANY `<const>`'s deferred/resume signal
// on this scope has ever computed a value. Same defect class as the
// nested-portal `<const/api>` race documented in
// notes/bug-marko-dynamic-tag-hydration-crash.md, but the raced const here
// is dropdown-menu.marko's OWN `entries`/`className`/`trigger`, not `api()`
// — the crash traced to THIS component's slot, not the Zag connect(). Since
// `entries` derives purely from `input` (no service/api dependency), the
// fix is simpler than the `<let>`-mirror pattern used for `api()` races: a
// module-level `static function` computes it fresh from `input` at each use
// site, so there is no scope-property read to race the walk at all — same
// idea as the `connectFresh` static-function pattern once used for `api()`
// races (removed after marko-js/marko#4062 fixed the underlying walk-order
// defect), applied here to a plain data derivation instead of a live
// connect() call.
static function menuEntries(input: Input): DropdownMenuItemAttrs[] {
  const attrItems = [...(input.item ?? [])];
  return attrItems.length > 0
    ? attrItems
    : (input.items ?? []).map((item) => ({ ...item, content: undefined as Marko.Body | undefined }));
}

<zag-machine/service=() => menu from=input
  onSelect(details: menu.SelectionDetails) {
    input.select?.(details.value);
  }
/>
<const/api=() => connect(menu, service())/>

// <zag-machine> returns the service GETTER itself, which is what each
// <Submenu> child receives so it can call `setParent` on this menu's
// service (see submenu.marko for the full wiring rationale).

<${input.trigger.content}({ ...api().getTriggerProps(), "data-slot": "dropdown-menu-trigger" })/>
<zag-portal>
  <if=api().open>
    <div ...api().getPositionerProps() style=positionerStyle data-slot="dropdown-menu-positioner">
      <div
        ...api().getContentProps()
        data-slot="dropdown-menu-content"
        class=cn(styles.content, input.class)
      >
        <for|entry, index| of=menuEntries(input)>
          <const/value=entry.value ?? `item-${index}`/>
          <if=entry.type === "separator">
            <div ...api().getSeparatorProps() data-slot="dropdown-menu-separator" class=styles.separator/>
          </if>
          <else if=entry.type === "label">
            <div data-slot="dropdown-menu-label" data-inset=entry.inset || undefined class=styles.label>
              <if=entry.content>
                <${entry.content}/>
              </if>
              <else>
                ${entry.label}
              </else>
            </div>
          </else>
          <else if=entry.type === "checkbox">
            <div
              ...api().getOptionItemProps({
                type: "checkbox",
                value,
                checked: entry.checked ?? false,
                disabled: entry.disabled,
                onCheckedChange: (checked) => input.checkedChange?.(value, checked),
              })
              data-slot="dropdown-menu-checkbox-item"
              data-inset=entry.inset || undefined
              class=styles.checkboxItem
            >
              <span ...api().getItemIndicatorProps({ value, checked: entry.checked ?? false }) data-slot="dropdown-menu-checkbox-item-indicator" class=styles.itemIndicator>
                <if=entry.checked>
                  <Icon name="Check"/>
                </if>
              </span>
              <if=entry.content>
                <${entry.content}/>
              </if>
              <else>
                ${entry.label}
              </else>
              <if=entry.shortcut>
                <span data-slot="dropdown-menu-shortcut" class=styles.shortcut>${entry.shortcut}</span>
              </if>
            </div>
          </else>
          <else if=entry.type === "radio">
            <div
              ...api().getOptionItemProps({
                type: "radio",
                value: entry.radioValue ?? value,
                checked: entry.checked ?? false,
                disabled: entry.disabled,
                onCheckedChange: () => input.radioChange?.(entry.radioGroup ?? "", entry.radioValue ?? value),
              })
              data-slot="dropdown-menu-radio-item"
              data-inset=entry.inset || undefined
              class=styles.radioItem
            >
              <span ...api().getItemIndicatorProps({ value: entry.radioValue ?? value, checked: entry.checked ?? false }) data-slot="dropdown-menu-radio-item-indicator" class=styles.itemIndicator>
                <if=entry.checked>
                  <Icon name="Circle" class=styles.radioItemIcon/>
                </if>
              </span>
              <if=entry.content>
                <${entry.content}/>
              </if>
              <else>
                ${entry.label}
              </else>
              <if=entry.shortcut>
                <span data-slot="dropdown-menu-shortcut" class=styles.shortcut>${entry.shortcut}</span>
              </if>
            </div>
          </else>
          <else if=entry.type === "sub">
            <Submenu
              entries=entry.subEntries ?? []
              label=entry.label
              disabled=entry.disabled
              inset=entry.inset
              parent=() => service()
              parentApi=() => api()
              onChildReady=(childService) => api().setChild(childService)
            >
              <@trigger|triggerProps|>
                <div
                  ...triggerProps
                  data-slot="dropdown-menu-sub-trigger"
                  data-inset=entry.inset || undefined
                  class=styles.subTrigger
                >
                  ${entry.label}
                  <Icon name="ChevronRight" class=styles.subTriggerIcon/>
                </div>
              </@trigger>
            </Submenu>
          </else>
          <else>
            <div
              ...api().getItemProps({ value, disabled: entry.disabled })
              data-slot="dropdown-menu-item"
              data-inset=entry.inset || undefined
              data-variant=entry.variant ?? "default"
              class=styles.item
            >
              <if=entry.content>
                <${entry.content}/>
              </if>
              <else>
                ${entry.label}
              </else>
              <if=entry.shortcut>
                <span data-slot="dropdown-menu-shortcut" class=styles.shortcut>${entry.shortcut}</span>
              </if>
            </div>
          </else>
        </for>
      </div>
    </div>
  </if>
</zag-portal>

<return=api>
