/**
 * Copyright Aquera Inc 2025
 *
 * 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 { CSSResultArray, PropertyValues } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { styles } from './nile-floating-panel.css';
import NileElement from '../internal/nile-element';
import tippy, {
  Instance,
  Props,
  roundArrow,
  followCursor as followCursorPlugin,
} from 'tippy.js';
import {
  parseFollowCursor,
  parseDuration,
} from '../nile-lite-tooltip/utils';
import { VisibilityManager } from '../utilities/visibility-manager.js';

/**
 * Nile floating-panel component.
 *
 * A popover that supports rich content (title, body, actions).
 *
 * **Wrapper mode** (default): first child element is the trigger.
 * **For mode**: set `for="elementId"` to attach to an external element.
 *
 * @tag nile-floating-panel
 *
 * @fires nile-init   - Component initialized.
 * @fires nile-destroy - Component destroyed.
 * @fires nile-show   - Panel opened.
 * @fires nile-hide   - Panel closed.
 * @fires nile-after-show - Panel fully visible after animation.
 * @fires nile-after-hide - Panel fully hidden after animation.
 * @fires nile-toggle - Open/close transition (detail.open).
 * @fires nile-visibility-change - Hidden by scroll/tab change.
 */
@customElement('nile-floating-panel')
export class NileFloatingPanel extends NileElement {
  private static _groups = new Map<string, Set<NileFloatingPanel>>();

  private static _reducedMotionQuery: MediaQueryList | null = null;

  private static get prefersReducedMotion(): boolean {
    if (!NileFloatingPanel._reducedMotionQuery) {
      NileFloatingPanel._reducedMotionQuery =
        window.matchMedia('(prefers-reduced-motion: reduce)');
    }
    return NileFloatingPanel._reducedMotionQuery.matches;
  }

  public static get styles(): CSSResultArray {
    return [styles];
  }

  protected createRenderRoot() {
    return this;
  }

  // ─── Tippy.js props ───

  @property({ type: String })
  placement:
    | 'top'
    | 'top-start'
    | 'top-end'
    | 'right'
    | 'right-start'
    | 'right-end'
    | 'bottom'
    | 'bottom-start'
    | 'bottom-end'
    | 'left'
    | 'left-start'
    | 'left-end'
    | 'auto'
    | 'auto-start'
    | 'auto-end' = 'bottom';

  @property({ type: String }) trigger: string = 'click';

  @property({ type: Number }) distance = 12;

  @property({ type: Number }) skidding = 0;

  @property({ type: String, reflect: true })
  arrow: 'round' | 'default' | 'none' = 'round';

  @property({ type: String, reflect: true }) animation: string = 'fade';

  @property({ type: String, reflect: true }) duration:
    | string
    | number
    | [number, number] = 200;

  @property({ type: String, reflect: true }) delay:
    | number
    | [number, number] = 0;

  @property({ type: Boolean, reflect: true }) interactive = true;

  @property({ type: Number, reflect: true }) interactiveBorder = 2;

  @property({ type: String, reflect: true }) maxWidth: string | number = 'none';

  @property({ type: Number, reflect: true }) zIndex = 9999;

  @property({ type: String, reflect: true })
  followCursor:
    | boolean
    | 'initial'
    | 'horizontal'
    | 'vertical'
    | 'true'
    | 'false' = false;

  @property({ type: Boolean, reflect: true }) hideOnClick:
    | boolean
    | 'toggle' = true;

  @property({ type: Boolean, reflect: true }) inertia = false;

  @property({ type: Boolean, reflect: true }) allowHTML = false;

  @property({ type: Boolean, reflect: true }) flip = true;

  // ─── Popover-like props ───

  @property({ type: String, attribute: 'for' }) for: string | null = null;

  @property({ type: Boolean, reflect: true }) open = false;

  @property({ type: Boolean, reflect: true }) preventOverlayClose = false;

  @property({ type: String, reflect: true }) title = '';

  @property({ type: Boolean, reflect: true }) disabled = false;

  @property({ type: String, reflect: true }) width?: string;

  @property({ type: String, reflect: true }) height?: string;

  /** When set, only one panel in the same group can be open at a time. */
  @property({ type: String, reflect: true }) group: string | null = null;

  /** Close the panel when Escape is pressed. */
  @property({ type: Boolean, reflect: true }) closeOnEscape = true;

  /** Custom CSS class(es) added to the Tippy popper element, allowing per-instance styling from the host. */
  @property({ type: String, reflect: true, attribute: true}) panelClass: string = '';

  // ─── Visibility manager props ───

  @property({ type: Boolean, reflect: true }) enableVisibilityEffect = false;

  @property({ type: Boolean, reflect: true }) enableTabClose = false;

  // ─── Internal state ───

  private tippyInstance: Instance | null = null;
  private visibilityManager?: VisibilityManager;
  private panelContainer: HTMLElement | null = null;
  private anchorEl: HTMLElement | null = null;
  private _suppressOpenWatch = false;
  private _panelId = `nile-fp-${Math.random().toString(36).slice(2, 9)}`;
  private _boundEscHandler = this._handleEscapeKey.bind(this);
  private _pendingShowListener: (() => void) | null = null;
  private _pendingHideListener: (() => void) | null = null;

  // ─── Lifecycle ───

  protected firstUpdated(): void {
    this._buildDOM();
    this._attachTippy();
    this._joinGroup();

    this.visibilityManager = new VisibilityManager({
      host: this,
      target: this.anchorEl || null,
      enableVisibilityEffect: this.enableVisibilityEffect,
      enableTabClose: this.enableTabClose,
      isOpen: () => this.open,
      onAnchorOutOfView: () => {
        this._setOpen(false);
        this.tippyInstance?.hide();
        this.emit('nile-visibility-change', {
          visible: false,
          reason: 'anchor-out-of-view',
        });
      },
      onDocumentHidden: () => {
        this._setOpen(false);
        this.tippyInstance?.hide();
        this.emit('nile-visibility-change', {
          visible: false,
          reason: 'document-hidden',
        });
      },
      emit: (event, detail) => this.emit(`nile-${event}`, detail),
    });

    this.emit('nile-init');
  }

  disconnectedCallback(): void {
    super.disconnectedCallback();
    this._cleanupPendingShowListener();
    this._cleanupPendingHideListener();
    this.visibilityManager?.cleanup();
    this._leaveGroup();
    this._removeEscListener();
    this._destroyTippy();
    this.emit('nile-destroy');
  }

  updated(changed: PropertyValues): void {
    super.updated(changed);

    if (!this.panelContainer) return;

    if (changed.has('open') && !this._suppressOpenWatch) {
      if (this.open) {
        this.visibilityManager?.setup();
        queueMicrotask(() => this.tippyInstance?.show());
      } else {
        this.visibilityManager?.cleanup();
        this.tippyInstance?.hide();
      }
    }

    if (changed.has('group')) {
      this._leaveGroup(changed.get('group') as string | null);
      this._joinGroup();
    }

    const rebuildProps: string[] = [
      'placement', 'trigger', 'distance', 'skidding', 'arrow',
      'animation', 'duration', 'delay', 'interactive', 'interactiveBorder',
      'maxWidth', 'zIndex', 'followCursor', 'hideOnClick', 'inertia',
      'allowHTML', 'flip', 'preventOverlayClose', 'disabled', 'width', 'height',
      'panelClass'
    ];

    if (rebuildProps.some(p => changed.has(p))) {
      this._attachTippy();
    }
  }

  // ─── Public API ───

  /** Programmatically shows the panel. Returns a promise that resolves after the show animation. */
  public show(): Promise<void> {
    this.open = true;
    return new Promise<void>(resolve => {
      this._cleanupPendingShowListener();
      const handler = () => {
        this._pendingShowListener = null;
        resolve();
      };
      this._pendingShowListener = handler;
      this.addEventListener('nile-after-show', handler, { once: true });
    });
  }

  /** Programmatically hides the panel. Returns a promise that resolves after the hide animation. */
  public hide(): Promise<void> {
    this.open = false;
    return new Promise<void>(resolve => {
      this._cleanupPendingHideListener();
      const handler = () => {
        this._pendingHideListener = null;
        resolve();
      };
      this._pendingHideListener = handler;
      this.addEventListener('nile-after-hide', handler, { once: true });
    });
  }

  private _cleanupPendingShowListener(): void {
    if (this._pendingShowListener) {
      this.removeEventListener('nile-after-show', this._pendingShowListener);
      this._pendingShowListener = null;
    }
  }

  private _cleanupPendingHideListener(): void {
    if (this._pendingHideListener) {
      this.removeEventListener('nile-after-hide', this._pendingHideListener);
      this._pendingHideListener = null;
    }
  }

  public toggle(): void {
    this.open = !this.open;
  }

  public refresh(): void {
    this._attachTippy();
  }

  /** Returns the current resolved placement from Tippy/Popper. */
  public getCurrentPlacement(): string {
    const popper = this.tippyInstance?.popper;
    const box = popper?.querySelector('.tippy-box') as HTMLElement | null;
    return box?.dataset.placement ?? this.placement;
  }

  /** Returns true if the resolved placement matches the requested placement. */
  public isPositioningOptimal(): boolean {
    return this.getCurrentPlacement() === this.placement;
  }

  // ─── Group management ───

  private _joinGroup(): void {
    if (!this.group) return;
    let set = NileFloatingPanel._groups.get(this.group);
    if (!set) {
      set = new Set();
      NileFloatingPanel._groups.set(this.group, set);
    }
    set.add(this);
  }

  private _leaveGroup(oldGroup?: string | null): void {
    const key = oldGroup ?? this.group;
    if (!key) return;
    const set = NileFloatingPanel._groups.get(key);
    if (set) {
      set.delete(this);
      if (set.size === 0) NileFloatingPanel._groups.delete(key);
    }
  }

  private _hideGroupSiblings(): void {
    if (!this.group) return;
    const set = NileFloatingPanel._groups.get(this.group);
    if (!set) return;
    set.forEach(panel => {
      if (panel !== this && panel.open) {
        panel._setOpen(false);
        panel.tippyInstance?.hide();
      }
    });
  }

  // ─── Escape key ───

  private _addEscListener(): void {
    if (this.closeOnEscape) {
      document.addEventListener('keydown', this._boundEscHandler);
    }
  }

  private _removeEscListener(): void {
    document.removeEventListener('keydown', this._boundEscHandler);
  }

  private _handleEscapeKey(e: KeyboardEvent): void {
    if (e.key === 'Escape' && this.open) {
      this._setOpen(false);
      this.tippyInstance?.hide();
    }
  }

  // ─── ARIA ───

  private _applyAria(): void {
    if (!this.anchorEl || !this.panelContainer) return;
    this.panelContainer.setAttribute('role', 'dialog');
    this.panelContainer.id = this._panelId;
    this.anchorEl.setAttribute('aria-haspopup', 'dialog');
    this._syncAriaExpanded();
  }

  private _syncAriaExpanded(): void {
    this.anchorEl?.setAttribute('aria-expanded', String(this.open));
    if (this.open) {
      this.anchorEl?.setAttribute('aria-describedby', this._panelId);
    } else {
      this.anchorEl?.removeAttribute('aria-describedby');
    }
  }

  // ─── DOM construction ───

  private _buildDOM(): void {
    const children = Array.from(this.childNodes);

    this.anchorEl = null;
    const titleNodes: Node[] = [];
    const actionNodes: Node[] = [];
    const bodyNodes: Node[] = [];
    let firstElementSeen = false;

    for (const child of children) {
      if (child instanceof HTMLElement) {
        const slot = child.getAttribute('slot');
        if (slot === 'title') {
          child.removeAttribute('slot');
          titleNodes.push(child);
          continue;
        }
        if (slot === 'action') {
          child.removeAttribute('slot');
          actionNodes.push(child);
          continue;
        }
        if (!firstElementSeen && !this.for) {
          this.anchorEl = child;
          firstElementSeen = true;
          continue;
        }
      }
      bodyNodes.push(child);
    }

    if (this.for) {
      const anchor = document.getElementById(this.for);
      if (anchor) {
        this.anchorEl = anchor;
      }
    }

    while (this.firstChild) {
      this.removeChild(this.firstChild);
    }

    if (this.anchorEl && !this.for) {
      this.appendChild(this.anchorEl);
    }

    this.panelContainer = document.createElement('div');
    this.panelContainer.className = 'nile-floating-panel__content';
    this.panelContainer.style.display = 'none';

    const body = document.createElement('div');
    body.className = 'nile-floating-panel__body';

    if (titleNodes.length > 0 || this.title) {
      const titleDiv = document.createElement('div');
      titleDiv.className = 'nile-floating-panel__title';
      if (this.title) {
        titleDiv.textContent = this.title;
      } else {
        titleNodes.forEach(n => titleDiv.appendChild(n));
      }
      body.appendChild(titleDiv);
    }

    if (bodyNodes.length > 0) {
      const mainDiv = document.createElement('div');
      mainDiv.className = 'nile-floating-panel__main';
      bodyNodes.forEach(n => mainDiv.appendChild(n));
      body.appendChild(mainDiv);
    }

    if (actionNodes.length > 0) {
      const actionDiv = document.createElement('div');
      actionDiv.className = 'nile-floating-panel__action';
      actionNodes.forEach(n => actionDiv.appendChild(n));
      body.appendChild(actionDiv);
    }

    this.panelContainer.appendChild(body);
    this.appendChild(this.panelContainer);

    this._applyAria();
  }

  // ─── Tippy management ───

  private _resolveArrow() {
    switch (this.arrow) {
      case 'round': return roundArrow;
      case 'none': return false as const;
      default: return true as const;
    }
  }

  private _setOpen(value: boolean): void {
    this._suppressOpenWatch = true;
    this.open = value;
    this._syncAriaExpanded();
    this._suppressOpenWatch = false;
  }

  private _getEffectiveDuration(): number | [number, number] {
    if (NileFloatingPanel.prefersReducedMotion) return 0;
    return parseDuration(this.duration);
  }

  private _getEffectiveAnimation(): string | false {
    if (NileFloatingPanel.prefersReducedMotion) return false;
    return this.animation;
  }

  private _attachTippy(): void {
    this._destroyTippy();

    if (this.disabled || !this.anchorEl || !this.panelContainer) return;

    const resolvedFollowCursor = parseFollowCursor(this.followCursor);
    const effectiveHideOnClick = this.preventOverlayClose ? false : this.hideOnClick;

    const options: Partial<Props> = {
      content: this.panelContainer,
      placement: this.placement,
      trigger: this.trigger,
      offset: [this.skidding, this.distance],
      theme: 'floating-panel',
      animation: this._getEffectiveAnimation(),
      interactive: this.interactive,
      arrow: this._resolveArrow(),
      duration: this._getEffectiveDuration(),
      allowHTML: this.allowHTML,
      delay: this.delay as any,
      maxWidth: this.maxWidth,
      zIndex: this.zIndex,
      hideOnClick: effectiveHideOnClick,
      inertia: NileFloatingPanel.prefersReducedMotion ? false : this.inertia,
      interactiveBorder: this.interactiveBorder,
      appendTo: document.body,
      followCursor: resolvedFollowCursor,
      plugins: resolvedFollowCursor ? [followCursorPlugin] : [],
      popperOptions: {
        modifiers: [{ name: 'flip', enabled: this.flip }],
      },
      onMount: (instance) => {
        if (this.panelContainer) this.panelContainer.style.display = '';
        if (this.panelClass) {
          this.panelClass.split(/\s+/).filter(Boolean).forEach(cls => {
            instance.popper.classList.add(cls);
          });
        }
      },
      onShow: (instance) => {
        if (this.panelContainer) this.panelContainer.style.display = '';
        const tc = instance.popper.querySelector('.tippy-content') as HTMLElement | null;
        if (tc) {
          if (this.width) tc.style.width = this.width;
          if (this.height) { tc.style.height = this.height; tc.style.overflow = 'auto'; }
        }
        this._hideGroupSiblings();
        this._setOpen(true);
        this._addEscListener();
        this.dispatchEvent(new CustomEvent('nile-show', { detail: { instance, target: instance.reference } }));
        this.dispatchEvent(new CustomEvent('nile-toggle', { detail: { open: true, instance, target: instance.reference } }));
        return undefined;
      },
      onShown: (instance) => {
        this.dispatchEvent(new CustomEvent('nile-after-show', { detail: { instance, target: instance.reference } }));
      },
      onHide: (instance) => {
        this._setOpen(false);
        this._removeEscListener();
        this.dispatchEvent(new CustomEvent('nile-hide', { detail: { instance, target: instance.reference } }));
        this.dispatchEvent(new CustomEvent('nile-toggle', { detail: { open: false, instance, target: instance.reference } }));
        return undefined;
      },
      onHidden: (instance) => {
        if (this.panelContainer) this.panelContainer.style.display = 'none';
        this.dispatchEvent(new CustomEvent('nile-after-hide', { detail: { instance, target: instance.reference } }));
      },
    };

    this.tippyInstance = tippy(this.anchorEl, options);

    if (this.open) {
      queueMicrotask(() => this.tippyInstance?.show());
    }
  }

  private _destroyTippy(): void {
    if (this.tippyInstance) {
      this.tippyInstance.destroy();
      this.tippyInstance = null;
    }
    if (this.panelContainer) {
      this.panelContainer.style.display = 'none';
      if (this.panelContainer.parentElement !== this) {
        this.appendChild(this.panelContainer);
      }
    }
  }
}

export default NileFloatingPanel;

declare global {
  interface HTMLElementTagNameMap {
    'nile-floating-panel': NileFloatingPanel;
  }
}
