/**
 * FloatingWindow — the shared modal-overlay primitive (ticket #16, gate G1).
 *
 * Consolidates the `fixed inset-0 role="dialog" aria-modal` pattern that was
 * hand-rolled in ~7 features. Mounted at the **chrome level** (ProjectsView/App),
 * never inside LayoutGrid/LeafView (G5). It provides:
 *   - a focus-trap (mandatory: xterm captures keyboard focus, so a window opened
 *     over a terminal must steal *and* trap focus, then restore it on close),
 *   - Escape-to-close and backdrop-click-to-close,
 *   - the frozen `floatingWindow` z-index token (50).
 *
 * Purely presentational: the caller owns `open`/`onClose` state and the body.
 * Rendered as `null` while closed so the body (and its hooks) only run when the
 * window is actually open.
 */

import { useEffect, useRef, type ReactNode } from "react";

import { cn } from "../lib/cn";
import { Button } from "./Button";
import { zIndex } from "./zIndex";

/** Width presets; height is capped at 80vh with an internal scroll region. */
export type FloatingWindowSize = "sm" | "md" | "lg" | "xl";

const SIZE_CLASS: Record<FloatingWindowSize, string> = {
  sm: "max-w-sm",
  md: "max-w-lg",
  lg: "max-w-2xl",
  xl: "max-w-4xl",
};

/** Tabbable-element selector used by the focus-trap. */
const FOCUSABLE =
  'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';

/**
 * Stack of currently-mounted windows (most-recent last). Only the topmost window
 * reacts to Escape and traps Tab, so a window opened over another (e.g. a ticket
 * detail over the tickets list) doesn't close/steal from the one beneath it.
 */
const windowStack: object[] = [];

export interface FloatingWindowProps {
  /** Whether the window is shown. Rendered as `null` when closed. */
  open: boolean;
  /** Accessible dialog title, shown in the header. */
  title: string;
  /** Called on Escape, backdrop click, or the Close button. */
  onClose: () => void;
  /** Width preset (default "md"). */
  size?: FloatingWindowSize;
  children: ReactNode;
}

export function FloatingWindow({
  open,
  title,
  onClose,
  size = "md",
  children,
}: FloatingWindowProps) {
  if (!open) return null;
  return (
    <FloatingWindowBody title={title} onClose={onClose} size={size}>
      {children}
    </FloatingWindowBody>
  );
}

/**
 * The mounted window. Split out so the focus-trap effect and the body only run
 * while `open` — mounting/unmounting on `open` gives clean focus save/restore.
 */
function FloatingWindowBody({
  title,
  onClose,
  size,
  children,
}: Omit<FloatingWindowProps, "open">) {
  const dialogRef = useRef<HTMLDivElement>(null);

  // Keep the latest `onClose` in a ref so the mount-only effect below never
  // re-runs when the caller passes an unstable `onClose` closure (a new
  // identity on each parent render). Re-running the effect would move focus
  // back to the first focusable element on every keystroke, so a controlled
  // input inside the window could only accept one character before losing
  // focus. The Escape handler reads the current value via this ref.
  const onCloseRef = useRef(onClose);
  onCloseRef.current = onClose;

  useEffect(() => {
    const previouslyFocused = document.activeElement as HTMLElement | null;
    const node = dialogRef.current;
    // Register on the window stack so only the topmost handles keys.
    const token = {};
    windowStack.push(token);
    const isTopmost = () => windowStack[windowStack.length - 1] === token;
    // Move focus into the window (first focusable, else the dialog itself).
    const first = node?.querySelector<HTMLElement>(FOCUSABLE);
    (first ?? node)?.focus();

    function onKeyDown(e: KeyboardEvent) {
      // Ignore keys while a later window is stacked on top of this one.
      if (!isTopmost()) return;
      if (e.key === "Escape") {
        e.preventDefault();
        onCloseRef.current();
        return;
      }
      if (e.key !== "Tab" || !node) return;
      const focusables = Array.from(
        node.querySelectorAll<HTMLElement>(FOCUSABLE),
      );
      if (focusables.length === 0) {
        // Nothing focusable inside: keep focus on the dialog container.
        e.preventDefault();
        node.focus();
        return;
      }
      const firstEl = focusables[0];
      const lastEl = focusables[focusables.length - 1];
      const active = document.activeElement;
      if (e.shiftKey && active === firstEl) {
        e.preventDefault();
        lastEl.focus();
      } else if (!e.shiftKey && active === lastEl) {
        e.preventDefault();
        firstEl.focus();
      }
    }

    document.addEventListener("keydown", onKeyDown);
    return () => {
      document.removeEventListener("keydown", onKeyDown);
      const i = windowStack.indexOf(token);
      if (i >= 0) windowStack.splice(i, 1);
      previouslyFocused?.focus?.();
    };
    // Mount-only: focus save/restore and listener wiring must not re-run when
    // `onClose` changes identity (see `onCloseRef` above).
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <div
      className="fixed inset-0 flex items-center justify-center bg-black/55 p-4"
      style={{ zIndex: zIndex.floatingWindow }}
      onMouseDown={(e) => {
        // Backdrop click closes; clicks inside the panel don't reach here.
        if (e.target === e.currentTarget) onClose();
      }}
    >
      <div
        ref={dialogRef}
        role="dialog"
        aria-modal="true"
        aria-label={title}
        tabIndex={-1}
        className={cn(
          "flex max-h-[80vh] w-full flex-col overflow-hidden rounded-lg",
          "border border-border bg-surface shadow-xl outline-none",
          SIZE_CLASS[size ?? "md"],
        )}
      >
        <header className="flex shrink-0 items-center justify-between gap-3 border-b border-border px-4 py-3">
          <span className="text-sm font-medium text-content">{title}</span>
          <Button
            size="sm"
            variant="ghost"
            aria-label={`close ${title}`}
            onClick={onClose}
          >
            Close
          </Button>
        </header>
        <div className="min-h-0 flex-1 overflow-auto p-4">{children}</div>
      </div>
    </div>
  );
}
