/**
 * usePopover — the shared machinery behind every disclosure popover
 * (SettingsPopover, SortControl, SelectControl, ChartExport).
 *
 * The widget contract is the WAI-ARIA APG **disclosure** + non-modal popover
 * (docs/architecture/10): the trigger carries `aria-expanded`/`aria-controls`;
 * Esc closes and refocuses the trigger; an outside pointer press closes; at most
 * one popover is open app-wide (the PopoverStore registry). The panel is portaled
 * to `<body>` by the caller and positioned `fixed` from the trigger's rect here —
 * the panes clip their overflow, so an in-flow absolute panel would be cut off.
 *
 * Positioning is imperative (style writes, no React state), so scroll/resize
 * tracking never re-renders the component. The caller renders the panel only
 * while `open` and attaches `setPopNode` as its ref: on mount the panel is placed
 * before paint and focus moves to the first match of `initialFocus`, so keyboard
 * users land inside.
 */

import { useCallback, useEffect, useRef } from 'react';
import { usePopoverStore } from '../stores/PopoverStore';

/** Gap (px) between the trigger and the disclosed panel. */
const GAP = 6;

export interface PopoverOptions {
  /** Unique id — the single-open registry key and the panel's DOM id. */
  id: string;
  /** Which trigger edge the panel aligns to. `left` also clamps on-screen. */
  align?: 'left' | 'right';
  /** Open above the trigger when the viewport below is too short (and above fits). */
  flip?: boolean;
  /**
   * Selectors tried in order for the element to focus on open — separate queries,
   * not one list, because `querySelector('a, b')` returns the first match in
   * document order regardless of selector order.
   */
  initialFocus: readonly string[];
}

export interface PopoverHandle {
  open: boolean;
  /** Toggle from the trigger's onClick. */
  toggle: () => void;
  close: () => void;
  /** Close and return focus to the trigger (choose-an-option, Tab-out paths). */
  closeAndRefocus: () => void;
  triggerRef: React.RefObject<HTMLButtonElement | null>;
  /** The live panel node, for caller-side queries (e.g. roving key nav). */
  popRef: React.RefObject<HTMLDivElement | null>;
  /** Attach as the panel's ref: places before paint, then focuses `initialFocus`. */
  setPopNode: (node: HTMLDivElement | null) => void;
}

export function usePopover({
  id,
  align = 'left',
  flip = false,
  initialFocus,
}: PopoverOptions): PopoverHandle {
  const open = usePopoverStore((s) => s.openId === id);
  const toggleId = usePopoverStore((s) => s.toggle);
  const close = usePopoverStore((s) => s.close);
  const triggerRef = useRef<HTMLButtonElement>(null);
  const popRef = useRef<HTMLDivElement | null>(null);

  // Latest options behind a ref so place/setPopNode stay referentially stable —
  // an unstable ref callback would re-fire (null, node) on every render,
  // re-placing and stealing focus while the popover is open.
  const optsRef = useRef({ align, flip, initialFocus });
  useEffect(() => {
    optsRef.current = { align, flip, initialFocus };
  });

  const place = useCallback(() => {
    const trigger = triggerRef.current;
    const pop = popRef.current;
    if (!trigger || !pop) return;
    const { align, flip } = optsRef.current;
    const r = trigger.getBoundingClientRect();
    if (flip) {
      const below = window.innerHeight - r.bottom - GAP;
      const height = pop.offsetHeight;
      pop.style.top =
        below < height && r.top > height + GAP
          ? `${r.top - GAP - height}px`
          : `${r.bottom + GAP}px`;
    } else {
      pop.style.top = `${r.bottom + GAP}px`;
    }
    if (align === 'left') {
      // Keep the panel on-screen when the trigger sits near the right edge.
      const left = Math.min(r.left, window.innerWidth - pop.offsetWidth - GAP);
      pop.style.left = `${Math.max(GAP, left)}px`;
      pop.style.right = 'auto';
    } else {
      pop.style.right = `${window.innerWidth - r.right}px`;
      pop.style.left = 'auto';
    }
  }, []);

  // Re-place while open so the panel tracks the trigger on workspace scroll/resize.
  useEffect(() => {
    if (!open) return;
    window.addEventListener('resize', place);
    window.addEventListener('scroll', place, true); // capture: catch pane scrolls too
    return () => {
      window.removeEventListener('resize', place);
      window.removeEventListener('scroll', place, true);
    };
  }, [open, place]);

  // Esc closes + restores focus to the trigger; an outside pointer press closes
  // (APG disclosure; non-modal). Esc is captured so it settles here, not on a
  // parent that also listens (e.g. a host modal).
  useEffect(() => {
    if (!open) return;
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') {
        e.stopPropagation();
        close();
        triggerRef.current?.focus();
      }
    };
    const onPointer = (e: PointerEvent) => {
      const t = e.target as Node;
      if (!popRef.current?.contains(t) && !triggerRef.current?.contains(t)) close();
    };
    document.addEventListener('keydown', onKey, true);
    document.addEventListener('pointerdown', onPointer, true);
    return () => {
      document.removeEventListener('keydown', onKey, true);
      document.removeEventListener('pointerdown', onPointer, true);
    };
  }, [open, close]);

  // Panel ref callback: position before paint, then land focus inside. Fires once
  // per open (the panel mounts with `open`).
  const setPopNode = useCallback(
    (node: HTMLDivElement | null) => {
      popRef.current = node;
      if (node) {
        place();
        for (const selector of optsRef.current.initialFocus) {
          const target = node.querySelector<HTMLElement>(selector);
          if (target) {
            target.focus();
            break;
          }
        }
      }
    },
    [place],
  );

  const toggle = useCallback(() => toggleId(id), [toggleId, id]);
  const closeAndRefocus = useCallback(() => {
    close();
    triggerRef.current?.focus();
  }, [close]);

  return { open, toggle, close, closeAndRefocus, triggerRef, popRef, setPopNode };
}
