import { useCallback, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import type { KeyboardEvent, ReactNode } from 'react';
import { cx } from '../../utils/cx';
import { useFallbackId } from '../../utils/useId';
import { useTheme } from '../ThemeProvider/ThemeProvider';
import './Modal.css';

export type ModalSize = 'sm' | 'md' | 'lg';

export interface ModalProps {
  /** Whether the dialog is rendered. Nothing mounts while false. */
  open: boolean;
  /** Called on close button, Escape, and overlay click. */
  onClose: () => void;
  title?: ReactNode;
  /** Secondary line under the title, wired to `aria-describedby`. */
  description?: ReactNode;
  size?: ModalSize;
  /** Action row pinned to the bottom. Usually a pair of Buttons. */
  footer?: ReactNode;
  closeOnOverlayClick?: boolean;
  closeOnEscape?: boolean;
  hideCloseButton?: boolean;
  closeLabel?: string;
  children?: ReactNode;
  className?: string;
}

const FOCUSABLE =
  'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';

/**
 * Centred dialog rendered in a portal on `document.body`. Traps focus while
 * open, restores it on close, and locks background scroll.
 */
export function Modal({
  open,
  onClose,
  title,
  description,
  size = 'md',
  footer,
  closeOnOverlayClick = true,
  closeOnEscape = true,
  hideCloseButton = false,
  closeLabel = 'Close',
  children,
  className,
}: ModalProps) {
  const theme = useTheme();
  const baseId = useFallbackId();
  const titleId = `${baseId}-title`;
  const descriptionId = `${baseId}-description`;
  const dialogRef = useRef<HTMLDivElement | null>(null);
  const restoreFocusRef = useRef<HTMLElement | null>(null);

  // Move focus in on open, put it back where it came from on close.
  useEffect(() => {
    if (!open) return;
    restoreFocusRef.current = document.activeElement as HTMLElement | null;
    const first = dialogRef.current?.querySelector<HTMLElement>(FOCUSABLE);
    (first ?? dialogRef.current)?.focus();
    return () => restoreFocusRef.current?.focus?.();
  }, [open]);

  // Background scroll lock.
  useEffect(() => {
    if (!open) return;
    const previous = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => {
      document.body.style.overflow = previous;
    };
  }, [open]);

  const onKeyDown = useCallback(
    (event: KeyboardEvent<HTMLDivElement>) => {
      if (event.key === 'Escape' && closeOnEscape) {
        event.stopPropagation();
        onClose();
        return;
      }
      if (event.key !== 'Tab') return;

      const focusable = dialogRef.current?.querySelectorAll<HTMLElement>(FOCUSABLE);
      if (!focusable || focusable.length === 0) {
        event.preventDefault();
        return;
      }
      const first = focusable[0]!;
      const last = focusable[focusable.length - 1]!;
      if (!event.shiftKey && document.activeElement === last) {
        event.preventDefault();
        first.focus();
      } else if (event.shiftKey && document.activeElement === first) {
        event.preventDefault();
        last.focus();
      }
    },
    [closeOnEscape, onClose],
  );

  if (!open || typeof document === 'undefined') return null;

  return createPortal(
    <div
      className="ds-modal__overlay"
      data-ds-theme={theme}
      onMouseDown={(event) => {
        if (closeOnOverlayClick && event.target === event.currentTarget) onClose();
      }}
    >
      <div
        ref={dialogRef}
        role="dialog"
        aria-modal="true"
        aria-labelledby={title != null ? titleId : undefined}
        aria-describedby={description != null ? descriptionId : undefined}
        tabIndex={-1}
        className={cx('ds-modal', `ds-modal--${size}`, className)}
        onKeyDown={onKeyDown}
      >
        {(title != null || !hideCloseButton) && (
          <div className="ds-modal__header">
            <div className="ds-modal__header-text">
              {title != null && (
                <h2 id={titleId} className="ds-modal__title">
                  {title}
                </h2>
              )}
              {description != null && (
                <p id={descriptionId} className="ds-modal__description">
                  {description}
                </p>
              )}
            </div>
            {!hideCloseButton && (
              <button type="button" className="ds-modal__close" onClick={onClose}>
                <span className="ds-visually-hidden">{closeLabel}</span>
                <svg
                  viewBox="0 0 16 16"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="1.5"
                  aria-hidden="true"
                >
                  <path d="M4 4l8 8M12 4l-8 8" strokeLinecap="round" />
                </svg>
              </button>
            )}
          </div>
        )}
        {children != null && <div className="ds-modal__body">{children}</div>}
        {footer != null && <div className="ds-modal__footer">{footer}</div>}
      </div>
    </div>,
    document.body,
  );
}
