/**
 * Copyright Aquera Inc 2026
 *
 * This source code is licensed under the BSD-3-Clause license found in the
 * LICENSE file in the root directory of this source tree.
 */

import { html, CSSResultArray, TemplateResult } from 'lit';
import type { PropertyValues } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { styles } from './nile-context-menu.css';
import NileElement from '../internal/nile-element';
import type { NileContextMenuItem } from '../nile-context-menu-item';
import type { NileContextMenuGroup } from '../nile-context-menu-group';
import '../nile-floating-panel';
import '../nile-context-menu-group';
import '../nile-context-menu-item';
import '../nile-context-submenu';

// Data-driven menu entry
export interface NileContextMenuItemData {
  type: 'item';
  id: string;
  label: string;
  value?: string;
  disabled?: boolean;
  icon?: string;
  iconSet?: string;
  iconSize?: string;
  iconMethod?: 'fill' | 'stroke' | string;
  iconColor?: string;
  submenu?: NileContextMenuData[];
}

export interface NileContextMenuGroupData {
  type: 'group';
  name?: string;
  data: NileContextMenuItemData[];
}

export type NileContextMenuData = NileContextMenuGroupData;

const ITEM_TAG = 'nile-context-menu-item';
const SUBMENU_TAG = 'nile-context-submenu';
const MENU_CONTAINER_CLASS = 'nile-context-menu__menu';
const OBSERVED_ITEM_ATTRS = ['value', 'disabled'];

export type NileContextMenuTrigger = 'right' | 'left' | 'both' | 'manual' | 'global';

export interface NileContextMenuOpenOptions {
  x: number;
  y: number;
  target?: Element;
  originalEvent?: Event;
}

export type NileContextMenuCloseReason =
  | 'select'
  | 'outside-click'
  | 'escape'
  | 'programmatic';

interface NileContextMenuOpenContext {
  x: number;
  y: number;
  target: Element | null;
  originalEvent: Event | null;
}

let proxyIdSeq = 0;

/**
 * Nile context-menu component.
 *
 * @tag nile-context-menu
 *
 * @slot Default menu content.
 *
 * @event nile-change Fired on menu lifecycle and selection. `detail.type` is one of
 *   `'open'`, `'click'`, or `'close'`; remaining fields depend on the type.
 */
@customElement('nile-context-menu')
export class NileContextMenu extends NileElement {
  public static get styles(): CSSResultArray {
    return [styles];
  }

  
  @property({ attribute: true, type: String, reflect: true }) for = '';
  
  @property({ attribute: true, type: String, reflect: true }) trigger: NileContextMenuTrigger = 'right';
 
  @property({ attribute: true, type: String, reflect: true }) skipOn = '';
 
  @property({ attribute: true, type: Number, reflect: true }) zIndex = 9999;
 
  @property({ attribute: false }) public items: NileContextMenuData[] = [];
  
  private get _isDataMode(): boolean {
    return Array.isArray(this.items) && this.items.length > 0;
  }
  @query('nile-floating-panel') private _floatingPanel?: HTMLElement;
 
  @state() private _items: NileContextMenuItem[] = [];
 
  @state() private _open = false;
 
  protected _openContext: NileContextMenuOpenContext | null = null;
 
  protected _targetEl: Element | null = null;
  
  private _previouslyFocused: HTMLElement | null = null;
  
  private static _openInstances = new Set<NileContextMenu>();
  
  private _detachTriggers?: () => void;
  private _lightObserver?: MutationObserver;
  private _menuObserver?: MutationObserver;
 
  private _proxyEl?: HTMLDivElement;
  private readonly _proxyId = `nile-context-menu-anchor-${++proxyIdSeq}`;
 
  private _wasDataMode = false;

  public override connectedCallback(): void {
    super.connectedCallback();
    this._ensureProxy();
    this._resolveTarget();
    this._attachTriggers();
    this._lightObserver = new MutationObserver(() => {
      if (this._isDataMode) return;
      this._relocateLightChildren();
    });
    this._lightObserver.observe(this, { childList: true });
  }

  protected override updated(changed: PropertyValues): void {
    super.updated(changed);
    if ((changed.has('for') || changed.has('trigger') || changed.has('skipOn')) && this._open) {
      this.close('programmatic');
    }
    if (changed.has('for')) {
      this._resolveTarget();
      this._attachTriggers();
    } else if (changed.has('trigger') || changed.has('skipOn')) {
      this._attachTriggers();
    }
    if (changed.has('items') && this._menuContainerRef) {
      if (this._isDataMode) {
        this._renderDataItems();
        this._wasDataMode = true;
      } else if (this._wasDataMode) {
        this._renderDataItems();
        this._wasDataMode = false;
      }
    }
  }

  private _resolveTarget(): void {
    const value = this.for?.trim();
    if (!value) {
      this._targetEl = null;
      return;
    }
    const root = (this.getRootNode() as Document | ShadowRoot) ?? document;
    if (/^[#.\[:]/.test(value)) {
      this._targetEl = (root as ParentNode).querySelector(value);
    } else if ('getElementById' in root) {
      this._targetEl = (root as Document | ShadowRoot).getElementById(value);
    } else {
      this._targetEl = document.getElementById(value);
    }
  }

  public get targetElement(): Element | null {
    return this._targetEl;
  }

  private _attachTriggers(): void {
    this._detachTriggers?.();
    this._detachTriggers = undefined;

    const trigger = this.trigger;
    if (trigger === 'manual') return;

    const cleanups: Array<() => void> = [];

    if (trigger === 'global') {
      const skipSelector = this.skipOn?.trim() ?? '';
      const isInteractive = (el: Element | null): boolean => {
        if (!el || !skipSelector) return false;
        return !!el.closest(skipSelector);
      };
      const handler = (e: Event) => {
        const me = e as MouseEvent;
        if (isInteractive(me.target as Element | null)) return;
        me.preventDefault();
        if (this._open) return;
        this.open({
          x: me.clientX,
          y: me.clientY,
          target: (me.target as Element | null) ?? undefined,
          originalEvent: me,
        });
      };
      window.addEventListener('contextmenu', handler, true);
      cleanups.push(() => window.removeEventListener('contextmenu', handler, true));
      this._detachTriggers = () => cleanups.forEach(fn => fn());
      return;
    }

    const target = this._targetEl;
    if (!target) return;

    const wantsContext = trigger === 'right' || trigger === 'both';
    const wantsClick = trigger === 'left' || trigger === 'both';

    if (wantsContext) {
      const handler = (e: Event) => {
        const me = e as MouseEvent;
        me.preventDefault();
        this.open({
          x: me.clientX,
          y: me.clientY,
          target,
          originalEvent: me,
        });
      };
      target.addEventListener('contextmenu', handler);
      cleanups.push(() => target.removeEventListener('contextmenu', handler));
    }

    if (wantsClick) {
      const handler = (e: Event) => {
        const me = e as MouseEvent;
        this.open({
          x: me.clientX,
          y: me.clientY,
          target,
          originalEvent: me,
        });
      };
      target.addEventListener('click', handler);
      cleanups.push(() => target.removeEventListener('click', handler));
    }

    if (cleanups.length === 0) return;
    this._detachTriggers = () => cleanups.forEach(fn => fn());
  }

  public override disconnectedCallback(): void {
    super.disconnectedCallback();
    NileContextMenu._openInstances.delete(this);
    this._open = false;
    this._detachTriggers?.();
    this._detachTriggers = undefined;
    this._lightObserver?.disconnect();
    this._lightObserver = undefined;
    this._menuObserver?.disconnect();
    this._menuObserver = undefined;
    this._menuContainerRef?.removeEventListener('click', this._onMenuClick);
    this._menuContainerRef?.removeEventListener('mouseover', this._onMenuMouseOver);
    this._menuContainerRef?.removeEventListener(
      'nile-change',
      this._onDescendantSelect as EventListener,
    );
    document.removeEventListener('pointerdown', this._onOutsidePointer, true);
    document.removeEventListener('keydown', this._onKeydown, true);
    window.removeEventListener('scroll', this._onScroll, true);
    this._proxyEl?.remove();
    this._proxyEl = undefined;
  }

  private _menuContainerRef: HTMLDivElement | null = null;

  protected override firstUpdated(): void {
    this._menuContainerRef = this.renderRoot.querySelector('.nile-context-menu__menu') as HTMLDivElement | null;
    if (this._isDataMode) {
      this._renderDataItems();
      this._wasDataMode = true;
    } else {
      this._relocateLightChildren();
    }
    if (this._menuContainerRef) {
      this._menuObserver = new MutationObserver(() => this._parseChildren());
      this._menuObserver.observe(this._menuContainerRef, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: OBSERVED_ITEM_ATTRS,
      });
      this._menuContainerRef.addEventListener('click', this._onMenuClick);
      this._menuContainerRef.addEventListener('mouseover', this._onMenuMouseOver);
      this._menuContainerRef.addEventListener(
        'nile-change',
        this._onDescendantSelect as EventListener,
      );
    }
    this._parseChildren();
  }

  private _ensureProxy(): void {
    if (this._proxyEl) return;
    const el = document.createElement('div');
    el.id = this._proxyId;
    el.setAttribute('aria-hidden', 'true');
    el.style.cssText =
      'position: fixed; top: 0; left: 0; width: 0; height: 0; pointer-events: none;';
    document.body.appendChild(el);
    this._proxyEl = el;
  }

  private _positionProxyAt(x: number, y: number): void {
    if (!this._proxyEl) return;
    this._proxyEl.style.left = `${x}px`;
    this._proxyEl.style.top = `${y}px`;
  }

  private _relocateLightChildren(): void {
    if (!this._menuContainerRef) return;
    const kids = Array.from(this.children);
    if (kids.length === 0) return;
    for (const kid of kids) {
      this._menuContainerRef.appendChild(kid);
    }
  }

  private _renderDataItems(): void {
    const container = this._menuContainerRef;
    if (!container) return;
    while (container.firstChild) container.removeChild(container.firstChild);
    for (const entry of this.items) {
      const node = this._createDataNode(entry);
      if (node) container.appendChild(node);
    }
  }

  private _createDataNode(entry: NileContextMenuData): HTMLElement | null {
    if (entry.type === 'group') {
      const group = document.createElement('nile-context-menu-group') as NileContextMenuGroup;
      if (entry.name) group.label = entry.name;
      for (const item of entry.data ?? []) {
        group.appendChild(this._createItemNode(item));
      }
      return group;
    }
    return null;
  }

  private _createItemNode(data: NileContextMenuItemData): NileContextMenuItem {
    const item = document.createElement('nile-context-menu-item') as NileContextMenuItem;
    if (data.id) item.id = data.id;
    item.value = data.value ?? data.id ?? '';
    if (data.disabled) item.disabled = true;
    if (data.icon) {
      const glyph = document.createElement('nile-glyph');
      glyph.setAttribute('slot', 'icon');
      glyph.setAttribute('name', data.icon);
      if (data.iconSet) glyph.setAttribute('set', data.iconSet);
      if (data.iconSize) glyph.setAttribute('size', data.iconSize);
      if (data.iconMethod) glyph.setAttribute('method', data.iconMethod);
      if (data.iconColor) glyph.setAttribute('color', data.iconColor);
      item.appendChild(glyph);
    }
    item.appendChild(document.createTextNode(data.label));
    if (data.submenu && data.submenu.length > 0) {
      const submenu = document.createElement('nile-context-submenu');
      for (const entry of data.submenu) {
        const node = this._createDataNode(entry);
        if (node) submenu.appendChild(node);
      }
      item.appendChild(submenu);
    }
    return item;
  }

  private _parseChildren(): void {
    const root: Element = this._menuContainerRef ?? this;
    const all = Array.from(root.querySelectorAll(ITEM_TAG)) as NileContextMenuItem[];
    this._items = all.filter(item => !this._isInsideDescendantSubmenu(item, root));
  }

  private _isInsideDescendantSubmenu(item: Element, container: Element): boolean {
    let cur: Element | null = item.parentElement;
    while (cur && cur !== container) {
      if (cur.tagName.toLowerCase() === SUBMENU_TAG) return true;
      cur = cur.parentElement;
    }
    return false;
  }

  public get menuItems(): readonly NileContextMenuItem[] {
    return this._items;
  }

  public getItemByValue(value: string): NileContextMenuItem | undefined {
    return this._items.find(item => item.value === value);
  }

  public get isOpen(): boolean {
    return this._open;
  }

  public open(options: NileContextMenuOpenOptions): void {
    if (this._open) return;
    for (const other of NileContextMenu._openInstances) {
      if (other !== this) other.close('programmatic');
    }
    this._previouslyFocused = document.activeElement as HTMLElement | null;
    this._openContext = {
      x: options.x,
      y: options.y,
      target: options.target ?? null,
      originalEvent: options.originalEvent ?? null,
    };
    this._ensureProxy();
    this._positionProxyAt(options.x, options.y);
    this._open = true;
    NileContextMenu._openInstances.add(this);
    document.addEventListener('pointerdown', this._onOutsidePointer, true);
    document.addEventListener('keydown', this._onKeydown, true);
    requestAnimationFrame(() => {
      if (this._open) window.addEventListener('scroll', this._onScroll, true);
    });
    this.emit('nile-change', { type: 'open', ...this._openContext });
  }

  public close(reason: NileContextMenuCloseReason = 'programmatic'): void {
    if (!this._open) return;
    this._open = false;
    this._openContext = null;
    NileContextMenu._openInstances.delete(this);
    const subs = this._menuContainerRef?.querySelectorAll(SUBMENU_TAG);
    subs?.forEach(s => (s as unknown as { closeSubmenu?: () => void }).closeSubmenu?.());
    document.removeEventListener('pointerdown', this._onOutsidePointer, true);
    document.removeEventListener('keydown', this._onKeydown, true);
    window.removeEventListener('scroll', this._onScroll, true);
    const active = document.activeElement;
    const focusInMenu =
      active === this ||
      this._items.some(item => item === active || item.shadowRoot?.contains(active as Node));
    if (focusInMenu) {
      this._previouslyFocused?.focus?.();
    }
    this._previouslyFocused = null;
    this.emit('nile-change', { type: 'close', reason });
  }

  private _enabledItems(): NileContextMenuItem[] {
    return this._items.filter(item => !item.disabled);
  }

  private _getFocusedItem(): NileContextMenuItem | null {
    const active = document.activeElement;
    for (const item of this._items) {
      if (item === active || item.shadowRoot?.contains(active as Node)) return item;
    }
    return null;
  }

  private _getFocusedItemAnyLevel(): {
    item: NileContextMenuItem | null;
    container: Element | null;
  } {
    let node: Element | null = document.activeElement as Element | null;
    while (node && node.tagName?.toLowerCase() !== ITEM_TAG) {
      const root = node.getRootNode();
      node = root instanceof ShadowRoot ? root.host : node.parentElement;
    }
    if (!node) return { item: null, container: null };
    const container = node.closest(`.${MENU_CONTAINER_CLASS}`);
    return { item: node as NileContextMenuItem, container };
  }

  private _enabledItemsIn(container: Element): NileContextMenuItem[] {
    const all = Array.from(container.querySelectorAll(ITEM_TAG)) as NileContextMenuItem[];
    return all.filter(item => {
      if (item.disabled) return false;
      return !this._isInsideDescendantSubmenu(item, container);
    });
  }

  private _focusFirstEnabled(): void {
    const enabled = this._enabledItems();
    enabled[0]?.focus();
  }

  private _moveFocus(delta: number): void {
    const { item: focused, container } = this._getFocusedItemAnyLevel();
    const root: Element = container ?? this._menuContainerRef ?? this;
    const enabled = this._enabledItemsIn(root);
    if (enabled.length === 0) return;
    const current = focused ? enabled.indexOf(focused) : -1;
    const next = current + delta;
    const wrapped = ((next % enabled.length) + enabled.length) % enabled.length;
    enabled[wrapped].focus();
  }

  private _onKeydown = (e: KeyboardEvent): void => {
    if (!this._open) return;
    switch (e.key) {
      case 'Escape':
        e.preventDefault();
        this.close('escape');
        return;
      case 'Tab':
        this.close('programmatic');
        return;
      case 'ArrowDown':
        e.preventDefault();
        this._moveFocus(1);
        return;
      case 'ArrowUp':
        e.preventDefault();
        this._moveFocus(-1);
        return;
      case 'ArrowRight': {
        const { item } = this._getFocusedItemAnyLevel();
        const sub = item?.querySelector(`:scope > ${SUBMENU_TAG}`) as
          | (HTMLElement & { openSubmenu?: () => void })
          | null;
        if (!sub) return;
        e.preventDefault();
        sub.openSubmenu?.();
        requestAnimationFrame(() => {
          (sub as unknown as { focusFirstItem?: () => void }).focusFirstItem?.();
        });
        return;
      }
      case 'ArrowLeft': {
        const { item, container } = this._getFocusedItemAnyLevel();
        if (!item || !container) return;
        const sub = this._findSubmenuOwning(container);
        if (!sub) return;
        e.preventDefault();
        sub.closeSubmenu?.();
        sub.parentItem?.focus();
        return;
      }
      case 'Enter':
      case ' ': {
        e.preventDefault();
        const { item } = this._getFocusedItemAnyLevel();
        if (!item || item.disabled) return;
        const sub = item.querySelector(`:scope > ${SUBMENU_TAG}`) as
          | (HTMLElement & { openSubmenu?: () => void })
          | null;
        if (sub) {
          sub.openSubmenu?.();
          requestAnimationFrame(() => {
            const panel = sub.shadowRoot?.querySelector(`.${MENU_CONTAINER_CLASS}`);
            const first = panel
              ? (this._enabledItemsIn(panel)[0] as NileContextMenuItem | undefined)
              : undefined;
            first?.focus();
          });
          return;
        }
        this._selectItem(item);
        return;
      }
    }
  };

  private _findSubmenuOwning(container: Element):
    | (HTMLElement & { closeSubmenu?: () => void; parentItem?: NileContextMenuItem | null })
    | null {
    type SubCtor = {
      findByContainer?: (c: Element) => HTMLElement & {
        closeSubmenu?: () => void;
        parentItem?: NileContextMenuItem | null;
      } | null;
    };
    const customElements = window.customElements;
    const ctor = customElements.get(SUBMENU_TAG) as unknown as SubCtor | undefined;
    return ctor?.findByContainer?.(container) ?? null;
  }

  private _onScroll = (e: Event): void => {
    if (!this._open) return;
    const path = e.composedPath();
    // Scroll inside the root menu container?
    if (this._menuContainerRef && path.includes(this._menuContainerRef)) return;
    // Scroll inside any descendant submenu's portaled panel?
    for (const node of path) {
      if (node instanceof HTMLElement && node.classList?.contains(MENU_CONTAINER_CLASS)) {
        return;
      }
    }
    this.close('programmatic');
  };

  private _onOutsidePointer = (e: Event): void => {
    if (!this._open) return;
    const path = e.composedPath();
    if (this._menuContainerRef && path.includes(this._menuContainerRef)) return;
    for (const node of path) {
      if (
        node instanceof HTMLElement &&
        node.classList?.contains(MENU_CONTAINER_CLASS)
      ) {
        return;
      }
    }
    this.close('outside-click');
  };

  private _onDescendantSelect = (e: CustomEvent): void => {
    if (e.detail?.type !== 'click') return;
    this.close('select');
  };
  private _stop(e: Event): void {
    e.stopPropagation();
  }

  private _onPanelShown = (e: Event): void => {
    e.stopPropagation();
  };

  private _onMenuMouseOver = (e: MouseEvent): void => {
    const item = e
      .composedPath()
      .find(
        node =>
          node instanceof HTMLElement &&
          node.tagName.toLowerCase() === ITEM_TAG
      ) as NileContextMenuItem | undefined;
    if (!item || item.disabled) return;
    item.focus();
  };

  private _onMenuClick = (e: MouseEvent): void => {
    const item = e
      .composedPath()
      .find(
        node =>
          node instanceof HTMLElement &&
          node.tagName.toLowerCase() === ITEM_TAG
      ) as NileContextMenuItem | undefined;

    if (!item || item.disabled) return;
    if (item.querySelector(`:scope > ${SUBMENU_TAG}`)) return;
    this._selectItem(item);
  };

  private _selectItem(item: NileContextMenuItem): void {
    const detail = {
      id: item.id,
      value: item.value,
      name: (item.textContent ?? '').trim(),
      target: this._openContext?.target ?? null,
      originalEvent: this._openContext?.originalEvent ?? null,
    };
    try {
      item.onSelect?.(detail);
    } catch (err) {
      console.error('[nile-context-menu] onSelect callback threw:', err);
    }
    this.emit('nile-change', { type: 'click', ...detail });
    this.close('select');
  }

  public render(): TemplateResult {
    return html`
      <nile-floating-panel
        for=${this._proxyId}
        trigger="manual"
        placement="bottom-start"
        panelClass="nile-context-menu-panel"
        .zIndex=${this.zIndex}
        ?open=${this._open}
        .interactive=${true}
        .hideOnClick=${false}
        .closeOnEscape=${false}
        .arrow=${'none'}
        .distance=${0}
        @nile-init=${this._stop}
        @nile-destroy=${this._stop}
        @nile-show=${this._stop}
        @nile-hide=${this._stop}
        @nile-after-show=${this._onPanelShown}
        @nile-after-hide=${this._stop}
        @nile-toggle=${this._stop}
        @nile-visibility-change=${this._stop}
      >
        <div class="nile-context-menu__menu" role="menu"></div>
      </nile-floating-panel>
    `;
  }
}

export default NileContextMenu;

declare global {
  interface HTMLElementTagNameMap {
    'nile-context-menu': NileContextMenu;
  }
}
