import { type ReactNode, useEffect, useRef } from "react";
import "./dialog.scss";

/**
 * App-wide modal primitive — a native `<dialog>` (`showModal()`), not a
 * `role="dialog"` div: the browser handles the modal semantics, the
 * focus trap, Escape-to-close and the backdrop for free instead of this
 * component reimplementing all four. First modal in the app — every other
 * "confirm/cancel" surface so far (`RecipeDetailPanel`'s delete button,
 * the settings pages' danger zones) is an inline two-step reveal, not an
 * overlay; a full recipe catalog + filters (`RecipePickerDialog`) doesn't
 * fit inline, hence this.
 *
 * Mounted only while open (see `PlanningPage`'s conditional rendering of
 * `RecipePickerDialog`, same convention as its own `CalendarPopover`) —
 * `showModal()` fires once on mount rather than toggling on an `isOpen`
 * prop, since closing this component means the caller stops rendering it
 * rather than flipping a prop on a permanently-mounted instance.
 */
export function Dialog({
  onClose,
  title,
  children,
  className,
}: {
  onClose: () => void;
  title?: string;
  children: ReactNode;
  className?: string;
}) {
  const dialogRef = useRef<HTMLDialogElement>(null);

  useEffect(() => {
    dialogRef.current?.showModal();
  }, []);

  useEffect(() => {
    const dialog = dialogRef.current;
    if (!dialog) return;
    // `close` covers every way a native dialog can close — Escape (which
    // fires `cancel` first, then `close`) as much as a future
    // `<form method="dialog">` — so this is the one listener needed to
    // keep the caller's own "is this open" state (e.g. `PlanningPage`'s
    // `openSlot`) in sync with it.
    dialog.addEventListener("close", onClose);
    return () => dialog.removeEventListener("close", onClose);
  }, [onClose]);

  useEffect(() => {
    const dialog = dialogRef.current;
    if (!dialog) return;
    // Attached imperatively (not a JSX `onClick`) since the click-to-close
    // it implements is already reachable from the keyboard via Escape
    // (native `cancel`/`close`, wired above) — a JSX `onClick` here would
    // trip the "needs a matching keyboard handler" a11y lint for a
    // non-interactive element even though one already exists, just not in
    // a form that lint rule can see.
    function handleClick(e: MouseEvent) {
      // Re-read from the ref (not the outer `dialog` const) — TS can't
      // carry that early-return narrowing into a nested function, since it
      // can't prove the function won't run at some later point where it no
      // longer holds (even though here, as an event listener on this same
      // element, it trivially still does).
      const current = dialogRef.current;
      if (!current) return;
      // A click lands on the `<dialog>` element itself both for the
      // backdrop *and* for its own unfilled padding/margin — comparing
      // against its content box (not just `e.target`) is what actually
      // distinguishes "outside the panel" from "on it".
      const rect = current.getBoundingClientRect();
      const inside =
        e.clientX >= rect.left &&
        e.clientX <= rect.right &&
        e.clientY >= rect.top &&
        e.clientY <= rect.bottom;
      if (!inside) current.close();
    }
    dialog.addEventListener("click", handleClick);
    return () => dialog.removeEventListener("click", handleClick);
  }, []);

  return (
    <dialog
      ref={dialogRef}
      className={["dialog-panel", className].filter(Boolean).join(" ")}
      aria-label={title}
    >
      {title && (
        <div className="dialog-panel__header">
          <h2>{title}</h2>
          <button
            type="button"
            className="dialog-panel__close"
            onClick={() => dialogRef.current?.close()}
            aria-label="Fermer"
          >
            ✕
          </button>
        </div>
      )}
      <div className="dialog-panel__body">{children}</div>
    </dialog>
  );
}
