// Navigation menu backed by @zag-js/navigation-menu.
// Authoring rules: notes/component-authoring.md
import * as navigationMenu from "@zag-js/navigation-menu";
import type { MachineInput } from "marko-zag";
import { navigationMenuTriggerStyle } from "./variants.ts";
import { cn } from "#lib/utils.ts";
import { stripOwnProps } from "#lib/native-attrs.ts";
import { navigationMenu as styles } from "./classes.ts";

// A link rendered directly in the menu bar, with no content panel.
export interface NavigationMenuLinkItem {
  type?: "link";
  value: string;
  label: string;
  href: string;
  current?: boolean;
  disabled?: boolean;
}

// A link rendered inside a content panel. `featured` renders the tall,
// full-height card used as the lead item in rich panels.
export interface NavigationMenuPanelLink {
  title: string;
  description?: string;
  href: string;
  current?: boolean;
  featured?: boolean;
}

// A trigger that opens a content panel in the shared viewport. `links` are
// rendered and wired by the component; the `content` body adds extra markup.
export interface NavigationMenuMenuItem {
  type: "menu";
  value: string;
  label: string;
  disabled?: boolean;
  links?: NavigationMenuPanelLink[];
}

// Passed to the `content` body so callers can add arbitrary markup to a panel
// (headings, promos, footers). Branch on `value` to render per item. Links must
// come from the item's `links` array so the machine can wire them — anything
// rendered here is plain, unwired markup.
export interface NavigationMenuContentContext {
  value: string;
}

export type NavigationMenuItem = NavigationMenuLinkItem | NavigationMenuMenuItem;

// Attr-tag entry kinds. A SINGLE attr tag name (`entry`) carries both kinds
// (discriminated by `type`, defaulting to "link") rather than separate
// `link`/`menu` attr tags: Marko collects each attr-tag NAME into its own
// independent iterable on `input` with no shared ordering between
// differently-named siblings, so distinct tag names could not preserve
// call-site interleaving. One name with a `type` discriminant keeps document
// order intact by construction (same design as dropdown-menu.marko and
// context-menu.marko).
export interface NavigationMenuLinkAttrs {
  /** Discriminant; omit (or "link") for a link rendered directly in the bar. */
  type?: "link";
  value: string;
  label: string;
  href: string;
  current?: boolean;
  disabled?: boolean;
}

// Menu kind: opens a content panel in the shared viewport. Unlike the
// items=/links[] path, this does NOT drive the featured/plain link grid — its
// `content` body renders directly into the panel, same as the items= path's
// `content` render-prop fallback does when an item has no `links`.
export interface NavigationMenuMenuAttrs {
  type: "menu";
  value: string;
  label: string;
  disabled?: boolean;
  content?: Marko.Body;
}

export type NavigationMenuEntryAttrs = NavigationMenuLinkAttrs | NavigationMenuMenuAttrs;

export type Own = {
  items?: NavigationMenuItem[];
  /** Alignment of the shared viewport relative to the active trigger. */
  align?: "start" | "center" | "end";
  /**
   * Extra markup appended to every open panel — headings, promos, footers.
   * Receives the item's value; branch on it to render per item. Links belong in
   * the item's `links` array so the machine can wire them.
   */
  content?: Marko.Body<[NavigationMenuContentContext]>;
  valueChange?: (value: string) => void;
  /**
   * Compound attr-tag API (primary, Marko-idiomatic form). One tag name
   * carries both kinds — `<@entry .../>` is a bar link (the default kind) and
   * `<@entry type="menu">` opens a content panel in the shared viewport (its
   * body renders directly inside the panel) — so entries render in exactly
   * the order written: links may sit before, between, or after menus. When
   * any `<@entry>` tags are supplied they replace `items=` entirely for this
   * render.
   */
  entry?: Marko.AttrTag<NavigationMenuEntryAttrs>;
};

export type Input = MachineInput<"nav", navigationMenu.Props & Own>;

<zag/api=() => navigationMenu from=input/>

// Machine-owned props go to the machine; component-owned props are stripped so
// they never stringify onto the <nav>.
<const/nativeAttrs=() =>
  stripOwnProps(
    navigationMenu.splitProps(input)[1],
    "class",
    "items",
    "align",
    "content",
    "valueChange",
    "entry",
  )
>

// Normalize whichever source was supplied into one items array so the render
// path below never branches. Attr tags win if present (compound is primary
// per the compound-tag-dx report). The single `<@entry>` tag name preserves
// true document order by construction, so links and menus render exactly as
// interleaved at the call site.
<const/entryTags=[...(input.entry ?? [])]/>
<const/items=(entryTags.length > 0 ? entryTags.map((tag): NavigationMenuItem => (tag.type === "menu" ? { type: "menu", value: tag.value, label: tag.label, disabled: tag.disabled } : { type: "link", value: tag.value, label: tag.label, href: tag.href, current: tag.current, disabled: tag.disabled })) : (input.items ?? []))/>

// Attr-tag `<@entry type="menu">` content bodies, keyed by value — looked up
// when rendering the panel so the attr-tag path renders its content body
// directly instead of the items=/links[]-driven featured/plain grid.
<const/menuContentByValue=new Map(entryTags.filter((tag): tag is NavigationMenuMenuAttrs => tag.type === "menu").map((tag) => [tag.value, tag.content] as const))>

<const/menuItems=() => items.filter((item) => item.type === "menu") as NavigationMenuMenuItem[]>

// Render-prop context for a content panel. Keep it plain data: anything closing
// over the machine service is unserializable across the body boundary.
<const/contentContext=(value: string) => ({ value })>

<const/featuredLinks=(item: NavigationMenuMenuItem) => (item.links ?? []).filter((link) => link.featured)>
<const/plainLinks=(item: NavigationMenuMenuItem) => (item.links ?? []).filter((link) => !link.featured)>
<const/hasFeatured=(item: NavigationMenuMenuItem) => featuredLinks(item).length !== 0>

<nav
  ...nativeAttrs()
  ...api().getRootProps()
  data-slot="navigation-menu"
  class=cn(styles.base, input.class)
>
  <ul ...api().getListProps() data-slot="navigation-menu-list" class=styles.list>
    <for|item| of=items>
      <li ...api().getItemProps({ value: item.value, disabled: item.disabled }) data-slot="navigation-menu-item" class=styles.item>
        <if=item.type === "menu">
          <button type="button"
            ...api().getTriggerProps({ value: item.value, disabled: item.disabled })
            data-slot="navigation-menu-trigger"
            class=cn(navigationMenuTriggerStyle(), styles.triggerGroup)
          >
            ${item.label}
            {" "}
            <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"
              data-slot="navigation-menu-trigger-icon"
              class=styles.triggerIcon
              aria-hidden="true"
            >
              <path d="m6 9 6 6 6-6"/>
            </svg>
          </button>
          // Keeps Tab order coherent when focus moves between trigger and
          // panel. Zag marks the proxy aria-hidden, but it is also tabbable
          // (that is its whole job), which is an aria-hidden-focus
          // violation: an aria-hidden element must not sit in the tab
          // order. The span is empty and visually clipped either way, so
          // dropping aria-hidden changes nothing for sighted users and only
          // removes a dead stop for screen readers.
          <span ...api().getTriggerProxyProps({ value: item.value, disabled: item.disabled }) aria-hidden=undefined/>
        </if>
        <else>
          <a
            ...api().getLinkProps({ value: item.value, current: item.current })
            href=item.href
            data-slot="navigation-menu-link"
            class=styles.link
          >
            ${item.label}
          </a>
        </else>
      </li>
    </for>
  </ul>

  // Caret that slides to sit under the active trigger. The machine positions it
  // through --trigger-x/--trigger-width on the root, so those vars drive real UI.
  // Lives OUTSIDE the <ul>: a bare <div> is not valid list content
  // (html-validate element-permitted-content); absolutely positioned, so DOM
  // order relative to the list is cosmetic.
  <div
    ...api().getIndicatorProps()
    data-slot="navigation-menu-indicator"
    class=styles.indicator
  >
    <div
      ...api().getArrowProps()
      data-slot="navigation-menu-indicator-arrow"
      class=styles.indicatorArrow
    />
  </div>

  // Shared animated viewport: every panel renders inside it, and the machine
  // drives its size/position through CSS vars in its own style object — so this
  // part carries no style attribute of ours. Zag-forced wrapper (rule 6) — but
  // the triage brief maps it 1:1 onto upstream's unslotted `cn-navigation-menu-
  // viewport-wrapper` div (NavigationMenuViewport's own wrapping <div>, no
  // data-slot upstream either), so it carries that anchor verbatim rather than
  // going bare.
// Zag's aria-labelledby on the content div points back at the trigger, but
// the div carries no role, so the reference is inert (html-validate
// aria-label-misuse). Strip it; Marko does not dedupe spread attrs.
<const/contentProps=(value: string) => {
  const props = api().getContentProps({ value }) as Record<string, unknown>;
  delete props["aria-labelledby"];
  return props;
}/>

  <div
    ...api().getViewportPositionerProps({ align: input.align })
    data-slot="navigation-menu-viewport-positioner"
    class=styles.viewportPositioner
  >
    <div
      ...api().getViewportProps({ align: input.align })
      data-slot="navigation-menu-viewport"
      class=styles.viewport
    >
      <for|item| of=menuItems()>
        <const/menuContent=menuContentByValue.get(item.value)/>
        <div
          ...contentProps(item.value)
          inert=(api().value !== item.value ? true : undefined)
          data-slot="navigation-menu-content"
          class=styles.content
        >
          <if=item.links?.length>
            <const/isFeatured=hasFeatured(item)/>
            <div class=cn(styles.linksGrid, isFeatured ? styles.linksGridFeatured : styles.linksGridPlain)>
              <for|link| of=featuredLinks(item)>
                <a
                  ...api().getLinkProps({ value: item.value, current: link.current })
                  href=link.href
                  data-slot="navigation-menu-link"
                  class=styles.link
                >
                  <div class=styles.featuredTitle>${link.title}</div>
                  <if=link.description>
                    <p class=styles.featuredDescription>${link.description}</p>
                  </if>
                </a>
              </for>
              <ul class=cn(styles.plainList, !isFeatured && styles.plainListCols)>
                <for|link| of=plainLinks(item)>
                  <li>
                    <a
                      ...api().getLinkProps({ value: item.value, current: link.current })
                      href=link.href
                      data-slot="navigation-menu-link"
                      class=styles.link
                    >
                      <div class=styles.plainTitle>${link.title}</div>
                      <if=link.description>
                        <p class=styles.plainDescription>${link.description}</p>
                      </if>
                    </a>
                  </li>
                </for>
              </ul>
            </div>
          </if>
          <if=menuContent>
            <${menuContent}/>
          </if>
          <else if=input.content>
            <${input.content}(contentContext(item.value))/>
          </else>
        </div>
      </for>
    </div>
  </div>
</nav>

<return=api>
