/**
 * Shared runtime of the nav-menu and mega-menu widgets (their frontend.ts call `initMenu`).
 *
 * Markup contract (render.php of both widgets):
 *  - root `data-zx-collapse`: "always", a max width in px ("1024") or "" (never); `data-zx-open`: "hover" | "click"
 *  - `[data-zx-menu-toggle]`  hamburger button (`aria-controls` = the panel id, `aria-expanded`)
 *  - `[data-zx-menu-panel]`   the list / off-canvas panel; `[data-zx-menu-close]` close button / overlay
 *  - `[data-zx-menu-item]`    an item with a dropdown (`is-open`), `[data-zx-sub-toggle]` its toggle button
 *    (`aria-controls` = the dropdown id); only toggles whose closest item is the item are used.
 *
 * Classes it sets: root `is-collapsed` (below the collapse width; server-set for "always") and `is-open` (panel
 * open), item `is-open` (dropdown open) / `is-dismissed` (Escape closed a hover dropdown until the pointer or
 * focus leaves). Keyboard: Enter/Space on toggles (native buttons), ArrowDown opens a dropdown and focuses its
 * first link, Escape closes the innermost open dropdown, then the panel, returning focus to its toggle.
 */
/** Roots of the menu widgets: a nav-menu inside a mega-menu dropdown keeps its own runtime. */
const ROOTS = '[data-zx-type="nav-menu"], [data-zx-type="mega-menu"]';

const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])';

const disposers = new WeakMap<HTMLElement, () => void>();

function own<T extends Element>(root: HTMLElement, selector: string): T[] {
  return Array.from(root.querySelectorAll<T>(selector)).filter((el) => el.closest(ROOTS) === root);
}

export function initMenu(root: HTMLElement): () => void {
  disposers.get(root)?.();
  const win = root.ownerDocument.defaultView ?? window;
  const doc = root.ownerDocument;
  const toggle = own<HTMLElement>(root, '[data-zx-menu-toggle]')[0] ?? null;
  const panel = own<HTMLElement>(root, '[data-zx-menu-panel]')[0] ?? null;
  const closers = own<HTMLElement>(root, '[data-zx-menu-close]');
  const items = own<HTMLElement>(root, '[data-zx-menu-item]');
  const hoverMode = (root.dataset.zxOpen ?? 'hover') !== 'click';
  const collapse = root.dataset.zxCollapse ?? '';
  const cleanups: Array<() => void> = [];
  const on = <K extends keyof HTMLElementEventMap>(el: HTMLElement | Document, type: K, fn: (e: HTMLElementEventMap[K]) => void): void => {
    el.addEventListener(type, fn as EventListener);
    cleanups.push(() => el.removeEventListener(type, fn as EventListener));
  };

  const subToggle = (item: HTMLElement): HTMLElement | null =>
    Array.from(item.querySelectorAll<HTMLElement>('[data-zx-sub-toggle]')).find((b) => b.closest('[data-zx-menu-item]') === item) ?? null;
  const dropdown = (item: HTMLElement): HTMLElement | null => {
    const id = subToggle(item)?.getAttribute('aria-controls');
    return id ? doc.getElementById(id) : null;
  };
  const collapsed = (): boolean => root.classList.contains('is-collapsed');
  const isOpen = (): boolean => root.classList.contains('is-open');

  const setItem = (item: HTMLElement, open: boolean): void => {
    item.classList.toggle('is-open', open);
    subToggle(item)?.setAttribute('aria-expanded', open ? 'true' : 'false');
    if (!open) {
      // Closing an item closes its open descendants too.
      for (const inner of items) if (inner !== item && item.contains(inner) && inner.classList.contains('is-open')) setItem(inner, false);
    }
  };
  const closeItems = (except?: HTMLElement): void => {
    for (const item of items) if (item !== except && !item.contains(except ?? null) && item.classList.contains('is-open')) setItem(item, false);
  };

  const setPanel = (open: boolean, focus = false): void => {
    root.classList.toggle('is-open', open);
    if (toggle) {
      toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
      const label = open ? toggle.dataset.labelClose : toggle.dataset.labelOpen;
      if (label) toggle.setAttribute('aria-label', label);
    }
    if (!open) closeItems();
    if (open && focus && panel) {
      const first = Array.from(panel.querySelectorAll<HTMLElement>(FOCUSABLE)).find((el) => !el.hasAttribute('data-zx-menu-close'));
      first?.focus();
    }
    if (!open && focus) toggle?.focus();
    root.dispatchEvent(new CustomEvent('zx:menu', { detail: { open } }));
  };

  /* ---- collapse (hamburger) ---- */
  const applyCollapse = (value: boolean): void => {
    const was = collapsed();
    root.classList.toggle('is-collapsed', value);
    if (was !== value) setPanel(false);
  };
  if (collapse === 'always') applyCollapse(true);
  else if (/^\d+$/.test(collapse) && typeof win.matchMedia === 'function') {
    const mq = win.matchMedia(`(max-width: ${collapse}px)`);
    applyCollapse(mq.matches);
    const onChange = (e: MediaQueryListEvent): void => applyCollapse(e.matches);
    if (typeof mq.addEventListener === 'function') {
      mq.addEventListener('change', onChange);
      cleanups.push(() => mq.removeEventListener('change', onChange));
    }
  } else applyCollapse(false);

  if (toggle) on(toggle, 'click', (e) => {
    e.preventDefault();
    setPanel(!isOpen(), !isOpen());
  });
  for (const closer of closers) on(closer, 'click', (e) => {
    e.preventDefault();
    setPanel(false, true);
  });

  /* ---- dropdowns ---- */
  for (const item of items) {
    const btn = subToggle(item);
    if (btn) {
      on(btn, 'click', (e) => {
        e.preventDefault();
        const open = !item.classList.contains('is-open');
        if (open && !collapsed()) closeItems(item);
        item.classList.remove('is-dismissed');
        setItem(item, open);
      });
      on(btn, 'keydown', (e) => {
        if (e.key !== 'ArrowDown') return;
        e.preventDefault();
        setItem(item, true);
        const first = dropdown(item)?.querySelector<HTMLElement>(FOCUSABLE);
        first?.focus();
      });
    }
    if (hoverMode) {
      on(item, 'mouseenter', () => {
        if (collapsed()) return;
        subToggle(item)?.setAttribute('aria-expanded', 'true');
      });
      on(item, 'mouseleave', () => {
        item.classList.remove('is-dismissed');
        if (collapsed()) return;
        if (!item.classList.contains('is-open')) subToggle(item)?.setAttribute('aria-expanded', 'false');
      });
    }
    on(item, 'focusout', (e) => {
      const next = e.relatedTarget as Node | null;
      if (next && item.contains(next)) return;
      item.classList.remove('is-dismissed');
      if (!collapsed() && item.classList.contains('is-open')) setItem(item, false);
    });
  }

  /* ---- Escape / outside click ---- */
  on(root, 'keydown', (e) => {
    if (e.key !== 'Escape') return;
    const target = e.target as HTMLElement | null;
    // Innermost item around the focus whose dropdown shows: open by click/toggle, or (hover mode, desktop)
    // shown through :hover / :focus-within until dismissed.
    const openItem = items
      .filter((i) => target !== null && i.contains(target) && subToggle(i) !== null && !i.classList.contains('is-dismissed'))
      .filter((i) => i.classList.contains('is-open') || (hoverMode && !collapsed()))
      .pop();
    if (openItem) {
      e.preventDefault();
      e.stopPropagation();
      setItem(openItem, false);
      if (!collapsed()) openItem.classList.add('is-dismissed');
      subToggle(openItem)?.focus();
      return;
    }
    if (isOpen()) {
      e.preventDefault();
      setPanel(false, true);
    }
  });
  on(doc, 'click', (e) => {
    const target = e.target as Node | null;
    if (target && root.contains(target)) return;
    if (!collapsed()) closeItems();
    else if (isOpen() && !root.classList.contains('zx-nav-menu--panel-offcanvas')) setPanel(false);
  });
  // Following an in-page anchor inside the open mobile panel closes it.
  if (panel) on(panel, 'click', (e) => {
    const a = (e.target as HTMLElement | null)?.closest?.('a[href]');
    if (a && collapsed() && isOpen() && (a.getAttribute('href') ?? '').startsWith('#')) setPanel(false);
  });

  const dispose = (): void => {
    for (const c of cleanups.splice(0)) c();
    disposers.delete(root);
  };
  disposers.set(root, dispose);
  return dispose;
}
