/**
 * `<context-menu-ui>` — right-click activated menu (OS context-menu pattern).
 *
 * Two binding shapes:
 *   A. Wrap a target — first non-menu-item-ui default-slot child becomes
 *      the contextmenu host.
 *   B. Point at targets via [for="<selector>"] — useful for whole-table
 *      / whole-canvas menus where wrapping isn't practical.
 *
 * Architecture: reuses the existing anchor pattern (core/anchor.js) by
 * creating a virtual 1×1 anchor element at the pointer coords on each
 * open. anchorPopover() does the rest — handles native CSS anchor-
 * positioning + JS fallback + viewport clamping + scroll/resize updates.
 * This keeps coord-anchored popovers using the same code path as every
 * other popover surface in the system (menu-ui, popover-ui, etc.).
 *
 * @see ../../core/anchor.js
 * @see ../menu/menu.class.js — peer button-triggered menu primitive.
 */

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

const LONG_PRESS_DEFAULT = 500;

export class UIContextMenu extends UIElement {
  static properties = {
    for:         { type: String,  default: '',                 reflect: true },
    open:        { type: Boolean, default: false,              reflect: true },
    longPressMs: { type: Number,  default: LONG_PRESS_DEFAULT, reflect: false, attribute: 'long-press-ms' },
  };

  static template = () => null;

  #surface = null;
  #virtualAnchor = null;
  #anchorCleanup = null;
  #targets = [];
  #touchTimer = null;
  #lastTarget = null;
  #outsideHandler = null;
  #keyHandler = null;

  connected() {
    super.connected();
    this.#bindTargets();
  }

  disconnected() {
    super.disconnected();
    this.#unbindTargets();
    this.#teardown();
    this.#surface?.remove();
    this.#surface = null;
  }

  /* ── Public API ──────────────────────────────────────────────────── */

  openAt(x, y, target = null) {
    target ??= this.#targets[0] ?? null;
    if (!target) return;
    this.#lastTarget = target;
    if (x == null || y == null) {
      const r = target.getBoundingClientRect();
      x = r.left + r.width / 2;
      y = r.top + r.height / 2;
    }
    this.#show(x, y);
  }

  close(reason = 'manual') {
    this.#hide(reason);
  }

  /* ── Target binding ──────────────────────────────────────────────── */

  #bindTargets() {
    this.#targets = this.#resolveTargets();
    for (const t of this.#targets) {
      t.addEventListener('contextmenu', this.#onContextMenu);
      t.addEventListener('touchstart',  this.#onTouchStart, { passive: true });
      t.addEventListener('touchend',    this.#onTouchEnd);
      t.addEventListener('touchcancel', this.#onTouchEnd);
      t.addEventListener('keydown',     this.#onTargetKeydown);
    }
  }

  #unbindTargets() {
    for (const t of this.#targets) {
      t.removeEventListener('contextmenu', this.#onContextMenu);
      t.removeEventListener('touchstart',  this.#onTouchStart);
      t.removeEventListener('touchend',    this.#onTouchEnd);
      t.removeEventListener('touchcancel', this.#onTouchEnd);
      t.removeEventListener('keydown',     this.#onTargetKeydown);
    }
    this.#targets = [];
  }

  #resolveTargets() {
    if (this.for) {
      try { return [...document.querySelectorAll(this.for)]; }
      catch (e) {
        // eslint-disable-next-line no-console
        console.warn(`[context-menu-ui] invalid [for] selector: ${this.for}`, e);
        return [];
      }
    }
    // wrapper-trap-ok: detects non-menu-item children as custom content; context-menu items are always static declarative children (right-click actions are not .map()'d)
    for (const child of this.children) {
      const tag = child.tagName.toLowerCase();
      if (tag !== 'menu-item-ui' && tag !== 'menu-divider-ui') return [child];
    }
    return [];
  }

  /* ── Trigger event handlers ──────────────────────────────────────── */

  #onContextMenu = (e) => {
    if (e.defaultPrevented) return;
    if (!this.#hasItems()) return;
    e.preventDefault();
    this.#lastTarget = e.currentTarget;
    this.#show(e.clientX, e.clientY);
  };

  #onTouchStart = (e) => {
    if (!this.#hasItems()) return;
    const touch = e.touches[0];
    if (!touch) return;
    this.#touchTimer = setTimeout(() => {
      this.#lastTarget = e.currentTarget;
      this.#show(touch.clientX, touch.clientY);
    }, this.longPressMs ?? LONG_PRESS_DEFAULT);
  };

  #onTouchEnd = () => {
    if (this.#touchTimer) clearTimeout(this.#touchTimer);
    this.#touchTimer = null;
  };

  #onTargetKeydown = (e) => {
    if ((e.shiftKey && e.key === 'F10') || e.key === 'ContextMenu') {
      if (!this.#hasItems()) return;
      e.preventDefault();
      this.#lastTarget = e.currentTarget;
      this.openAt(null, null, e.currentTarget);
    }
  };

  /* ── Show / hide ─────────────────────────────────────────────────── */

  #hasItems() {
    return [...this.children].some((c) => c.tagName.toLowerCase() === 'menu-item-ui');
  }

  #ensureSurface() {
    if (this.#surface) return this.#surface;
    const surface = document.createElement('div');
    surface.setAttribute('data-context-menu-surface', '');
    surface.setAttribute('popover', 'manual');
    surface.setAttribute('role', 'menu');
    surface.setAttribute('aria-label', this.getAttribute('aria-label') || 'Context menu');
    surface.tabIndex = -1;
    // Inline outline reset — the surface is appended to document.body
    // (outside the @scope(context-menu-ui) boundary), so a scoped CSS
    // rule wouldn't match. UA paints a 3-px default focus ring on the
    // tabindex=-1 surface when it receives programmatic focus on open.
    surface.style.outline = 'none';
    surface.addEventListener('click', this.#onSurfaceClick);
    document.body.appendChild(surface);
    this.#surface = surface;
    return surface;
  }

  #ensureVirtualAnchor(x, y) {
    if (!this.#virtualAnchor) {
      const anchor = document.createElement('div');
      anchor.setAttribute('data-context-menu-anchor', '');
      anchor.style.cssText = 'position:fixed;width:1px;height:1px;pointer-events:none;opacity:0;';
      document.body.appendChild(anchor);
      this.#virtualAnchor = anchor;
    }
    this.#virtualAnchor.style.left = `${x}px`;
    this.#virtualAnchor.style.top  = `${y}px`;
    return this.#virtualAnchor;
  }

  #show(x, y) {
    const surface = this.#ensureSurface();
    // Hoist items into the surface.
    surface.replaceChildren();
    for (const child of [...this.children]) {
      const tag = child.tagName.toLowerCase();
      if (tag === 'menu-item-ui' || tag === 'menu-divider-ui') {
        surface.appendChild(child.cloneNode(true));
      }
    }
    // Promote to top layer FIRST so anchorPopover can measure its size.
    try { surface.showPopover(); } catch { /* popover API unavailable */ }

    // Reuse the canonical anchor pattern — virtual 1×1 element at pointer
    // coords, then anchorPopover() handles native CSS anchor-positioning
    // (with position-try-fallbacks for viewport flips) + JS fallback.
    const anchor = this.#ensureVirtualAnchor(x, y);
    this.#anchorCleanup?.();
    this.#anchorCleanup = anchorPopover(anchor, surface, { placement: 'bottom-start', gap: 0 });

    this.open = true;
    this.#setupOpenHandlers();
    const first = surface.querySelector('menu-item-ui:not([disabled])');
    queueMicrotask(() => first?.focus?.());

    this.dispatchEvent(new CustomEvent('context-menu-open', {
      bubbles: true,
      detail: { target: this.#lastTarget, x, y },
    }));
  }

  #hide(reason = 'manual') {
    if (!this.open) return;
    this.open = false;
    this.#teardown();
    this.dispatchEvent(new CustomEvent('context-menu-close', {
      bubbles: true,
      detail: { reason },
    }));
    try { this.#lastTarget?.focus?.(); } catch { /* noop */ }
  }

  #teardown() {
    this.#teardownOpenHandlers();
    this.#anchorCleanup?.();
    this.#anchorCleanup = null;
    try { this.#surface?.hidePopover(); } catch { /* noop */ }
    if (this.#virtualAnchor) {
      this.#virtualAnchor.remove();
      this.#virtualAnchor = null;
    }
  }

  /* ── While-open document handlers ────────────────────────────────── */

  #setupOpenHandlers() {
    this.#outsideHandler = (e) => {
      if (!this.#surface?.contains(e.target)) this.#hide('outside');
    };
    this.#keyHandler = (e) => {
      if (e.key === 'Escape') { e.stopPropagation(); this.#hide('escape'); }
    };
    requestAnimationFrame(() => {
      document.addEventListener('pointerdown', this.#outsideHandler);
      document.addEventListener('keydown', this.#keyHandler);
    });
  }

  #teardownOpenHandlers() {
    if (this.#outsideHandler) document.removeEventListener('pointerdown', this.#outsideHandler);
    if (this.#keyHandler)     document.removeEventListener('keydown', this.#keyHandler);
    this.#outsideHandler = null;
    this.#keyHandler = null;
  }

  /* ── Item activation ─────────────────────────────────────────────── */

  #onSurfaceClick = (e) => {
    const item = e.target.closest('menu-item-ui');
    if (!item || item.hasAttribute('disabled')) return;
    const value = item.getAttribute('value') || '';
    const text  = item.getAttribute('text') || item.textContent.trim();
    this.dispatchEvent(new CustomEvent('context-menu-select', {
      bubbles: true,
      detail: { value, text },
    }));
    this.#hide('select');
  };
}
