/**
 * Non-side-effect class export for `<menu-ui>` (+ co-located primitives).
 *
 * Importing this file gives you the class(es) without auto-registering the tag.
 * Useful for test isolation, subclassing with tag-name override, or selective
 * composition.
 *
 * The auto-register path stays at `@adia-ai/web-components/components/menu`
 * (which imports this file + calls `defineIfFree()`).
 *
 * @see ../../USAGE.md#registration--auto-vs-explicit
 */

/**
 * <menu-ui>
 *   <button-ui slot="trigger" icon="dots-three-vertical" variant="ghost"></button-ui>
 *   <menu-item-ui icon="pencil" text="Edit" value="edit"></menu-item-ui>
 *   <menu-item-ui icon="copy" text="Duplicate" value="duplicate"></menu-item-ui>
 *   <menu-divider-ui></menu-divider-ui>
 *   <menu-item-ui icon="trash" text="Delete" value="delete" variant="danger"></menu-item-ui>
 * </menu-ui>
 *
 * Dropdown action menu. Fires `action` events, does not hold a value.
 *
 * Platform:
 *   • Popover API (popover="manual") → top-layer, escapes overflow/z-index.
 *   • CSS Anchor Positioning (via @core/anchor.js) with JS fallback.
 *   • Roving focus + arrow/Home/End/Enter/Escape keyboard nav.
 */

import { UIElement } from '../../core/element.js';
import { anchorPopover } from '../../core/anchor.js';

export class UIMenu extends UIElement {
  static properties = {
    open:      { type: Boolean, default: false, reflect: true },
    placement: { type: String,  default: 'bottom-start', reflect: true },
    gap:       { type: Number,  default: 4, reflect: true },
  };

  static template = () => null;

  #anchorCleanup = null;
  #popover = null;
  #bound = false;
  #rafId = null;
  #focusRaf = null;

  connected() {
    if (!this.#bound) {
      this.#bound = true;
      this.addEventListener('click', this.#onTriggerClick);
      this.addEventListener('keydown', this.#onTriggerKey);
    }
  }

  render() {
    if (this.open) this.#show();
    else this.#hide();
  }

  // ── Public API ──
  show()   { this.open = true; }
  hide()   { this.open = false; }
  toggle() { this.open = !this.open; }

  // ── Internal ──

  #ensurePopover() {
    if (this.#popover) return this.#popover;
    const el = document.createElement('div');
    el.setAttribute('data-menu-popover', '');
    el.setAttribute('popover', 'manual');
    el.setAttribute('role', 'menu');
    el.addEventListener('toggle', this.#onToggle);
    el.addEventListener('click', this.#onItemClick);
    el.addEventListener('keydown', this.#onMenuKey);
    this.appendChild(el);
    this.#popover = el;
    return el;
  }

  #show() {
    const trigger = this.querySelector('[slot="trigger"]');
    if (!trigger) return;
    const pop = this.#ensurePopover();

    // Move menu items into the popover (so they render in the top layer).
    // Use a DESCENDANT query — not `:scope >` — so items rendered via the
    // template engine's `.map()` / `repeat()` are collected too: those wrap
    // every interpolated child in a `display:contents` <span> (core/template.js
    // wrap()), making the items grandchildren that a direct-child query skips
    // → an empty popover (FEEDBACK-92). Skip anything already relocated into
    // the popover so a re-entrant #show() (reactive re-render while open)
    // doesn't reorder items. Mirrors the descendant query #hide() already uses.
    const items = this.querySelectorAll('menu-item-ui, menu-divider-ui');
    for (const item of items) {
      if (!pop.contains(item)) pop.appendChild(item);
    }

    if (!pop.matches(':popover-open')) pop.showPopover?.();

    this.#anchorCleanup?.();
    this.#anchorCleanup = anchorPopover(trigger, pop, {
      placement: this.placement,
      gap: this.gap,
    });

    // Focus first enabled item (only when opened via keyboard).
    if (this.#openedByKeyboard) {
      this.#openedByKeyboard = false;
      this.#focusRaf = requestAnimationFrame(() => {
        this.#focusRaf = null;
        this.#focusItem(0);
      });
    }

    this.#rafId = requestAnimationFrame(() => {
      this.#rafId = null;
      document.addEventListener('pointerdown', this.#onOutside);
      document.addEventListener('keydown', this.#onDocKey, true);
    });
  }

  #hide() {
    this.#anchorCleanup?.();
    this.#anchorCleanup = null;

    const pop = this.#popover;
    if (pop) {
      if (pop.matches(':popover-open')) pop.hidePopover?.();
      // Move items back to host (before the popover div) so next open can re-adopt them.
      const items = pop.querySelectorAll('menu-item-ui, menu-divider-ui');
      for (const item of items) this.insertBefore(item, pop);
    }

    if (this.#rafId != null) {
      cancelAnimationFrame(this.#rafId);
      this.#rafId = null;
    }
    if (this.#focusRaf != null) {
      cancelAnimationFrame(this.#focusRaf);
      this.#focusRaf = null;
    }
    document.removeEventListener('pointerdown', this.#onOutside);
    document.removeEventListener('keydown', this.#onDocKey, true);
  }

  #enabledItems() {
    return Array.from(this.#popover?.querySelectorAll('menu-item-ui') ?? [])
      .filter((el) => !el.disabled);
  }

  #focusItem(index) {
    const items = this.#enabledItems();
    if (!items.length) return;
    const i = (index + items.length) % items.length;
    const item = items[i];
    item.setAttribute('tabindex', '-1');
    item.focus({ preventScroll: true });
  }

  #focusedIndex() {
    const items = this.#enabledItems();
    return items.indexOf(document.activeElement);
  }

  // ── Events ──

  #openedByKeyboard = false;

  #onTriggerClick = (e) => {
    const trigger = this.querySelector('[slot="trigger"]');
    if (!trigger) return;
    if (e.target === trigger || trigger.contains(e.target)) {
      this.#openedByKeyboard = false;
      this.open = !this.open;
    }
  };

  #onTriggerKey = (e) => {
    const trigger = this.querySelector('[slot="trigger"]');
    if (!trigger || !(e.target === trigger || trigger.contains(e.target))) return;
    if (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      this.#openedByKeyboard = true;
      this.open = true;
    }
  };

  #onItemClick = (e) => {
    const item = e.target.closest('menu-item-ui');
    if (!item || item.disabled) return;
    this.dispatchEvent(new CustomEvent('action', {
      bubbles: true,
      detail: { value: item.value, text: item.text },
    }));
    this.open = false;
    // Return focus to trigger for continued keyboard flow.
    this.querySelector('[slot="trigger"]')?.focus?.({ preventScroll: true });
  };

  #onMenuKey = (e) => {
    const items = this.#enabledItems();
    if (!items.length) return;
    const i = this.#focusedIndex();
    switch (e.key) {
      case 'ArrowDown': e.preventDefault(); this.#focusItem(i + 1); break;
      case 'ArrowUp':   e.preventDefault(); this.#focusItem(i - 1); break;
      case 'Home':      e.preventDefault(); this.#focusItem(0); break;
      case 'End':       e.preventDefault(); this.#focusItem(items.length - 1); break;
      case 'Enter':
      case ' ': {
        e.preventDefault();
        const item = items[i] ?? items[0];
        item?.click();
        break;
      }
    }
  };

  #onOutside = (e) => {
    if (!this.open) return;
    if (this.contains(e.target)) return;
    this.open = false;
  };

  #onDocKey = (e) => {
    if (e.key === 'Escape' && this.open) {
      e.stopPropagation();
      e.preventDefault();
      this.open = false;
      this.querySelector('[slot="trigger"]')?.focus?.({ preventScroll: true });
    } else if (e.key === 'Tab' && this.open) {
      // Tab dismisses the menu; let focus move naturally.
      this.open = false;
    }
  };

  #onToggle = (e) => {
    const nowOpen = e.newState === 'open';
    if (nowOpen !== this.open) this.open = nowOpen;
  };

  disconnected() {
    this.#hide();
    if (this.#popover) {
      this.#popover.removeEventListener('toggle', this.#onToggle);
      this.#popover.removeEventListener('click', this.#onItemClick);
      this.#popover.removeEventListener('keydown', this.#onMenuKey);
    }
    this.#popover = null;
    this.#bound = false;
  }
}

export class UIMenuItem extends UIElement {
  static properties = {
    icon:     { type: String,  default: '', reflect: true },
    text:     { type: String,  default: '', reflect: true },
    value:    { type: String,  default: '', reflect: true },
    variant:  { type: String,  default: 'default', reflect: true },
    disabled: { type: Boolean, default: false, reflect: true },
  };

  static template = () => null;

  connected() {
    // role="menuitem" ONLY when actually inside a menu container. A menu-ui
    // closes by moving its items back into its own light DOM (see #hide()),
    // where there is no role="menu"/menubar ancestor — a bare role="menuitem"
    // there trips axe `aria-required-parent` on every closed menu (FB-95). The
    // open popover (role="menu") adopts items via appendChild, which re-fires
    // connected(), so the role re-applies when the menu opens and is dropped
    // again on close.
    if (this.closest('[role="menu"], [role="menubar"]')) {
      this.setAttribute('role', 'menuitem');
      if (!this.hasAttribute('tabindex')) this.setAttribute('tabindex', '-1');
    } else {
      this.removeAttribute('role');
      this.removeAttribute('tabindex');
    }
    this.#stamp();
    this.#syncAria();
  }

  // Mark slot elements we create so render() never deletes consumer-provided ones.
  // See ADR-0010 (slot content is source of truth).
  #stampMark(el) { el.dataset.menuItemStamped = '1'; return el; }
  #wasStamped(el) { return el?.dataset?.menuItemStamped === '1'; }

  #ownChild(selector) {
    // wrapper-trap-ok: finds component-stamped children (data-menuItemStamped); consumer menu-item-ui children are adopted by #show() using logical children
    for (const ch of this.children) {
      if (ch.matches(selector)) return ch;
    }
    return null;
  }

  #stamp() {
    if (this.#ownChild('[slot="text"]')) return;

    if (this.icon && !this.#ownChild('[slot="icon"]')) {
      const iconEl = this.#stampMark(document.createElement('icon-ui'));
      iconEl.setAttribute('slot', 'icon');
      iconEl.setAttribute('name', this.icon);
      this.appendChild(iconEl);
    }

    if (this.text) {
      const span = this.#stampMark(document.createElement('span'));
      span.setAttribute('slot', 'text');
      span.textContent = this.text;
      this.appendChild(span);
    }
  }

  #syncAria() {
    if (this.disabled) this.setAttribute('aria-disabled', 'true');
    else this.removeAttribute('aria-disabled');
  }

  render() {
    // Sync icon — only touch elements we stamped.
    const iconEl = this.#ownChild('[slot="icon"]');
    if (this.icon) {
      if (iconEl) {
        if (this.#wasStamped(iconEl)) iconEl.setAttribute('name', this.icon);
      } else {
        const el = this.#stampMark(document.createElement('icon-ui'));
        el.setAttribute('slot', 'icon');
        el.setAttribute('name', this.icon);
        this.prepend(el);
      }
    } else if (this.#wasStamped(iconEl)) {
      iconEl.remove();
    }

    // Sync text — only touch elements we stamped.
    const textEl = this.#ownChild('[slot="text"]');
    if (this.#wasStamped(textEl)) textEl.textContent = this.text;

    this.#syncAria();
  }
}

export class UIMenuDivider extends UIElement {
  static properties = {};
  static template = () => null;

  connected() {
    this.setAttribute('role', 'separator');
  }
}
