import { useEffect, useRef, type ReactNode } from 'react'

// Tabbable controls inside the dialog, in DOM order. type="hidden" inputs are
// excluded — they'd match `input:not([disabled])` and, sitting at a boundary,
// break the wrap math. Hoisted out of the handler so it isn't rebuilt per Tab.
const FOCUSABLE_SELECTOR =
  'a[href], button:not([disabled]), textarea:not([disabled]), ' +
  'input:not([disabled]):not([type="hidden"]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'

/**
 * A centered modal dialog over a dimmed backdrop. Closes on Escape or a backdrop
 * click, unless `busy` (a mutation is in flight) — then it stays put so the
 * action can finish and report. The caller owns open/closed state (render only
 * when open).
 */
export function Modal({
  title,
  onClose,
  busy = false,
  children,
}: {
  title: string
  onClose: () => void
  busy?: boolean
  children: ReactNode
}) {
  const cardRef = useRef<HTMLDivElement>(null)
  // Keep the latest onClose/busy in refs so the mount-only effect below never
  // re-runs (which would re-attach the listener and steal focus on every parent
  // re-render, e.g. during a background refetch).
  const onCloseRef = useRef(onClose)
  onCloseRef.current = onClose
  const busyRef = useRef(busy)
  busyRef.current = busy

  useEffect(() => {
    const card = cardRef.current
    // Remember who opened the dialog so focus can return there on close —
    // otherwise it lands on <body> and a keyboard user loses their place.
    const opener = document.activeElement as HTMLElement | null
    card?.focus()

    const focusable = () =>
      Array.from(card?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR) ?? [])

    function onKey(e: KeyboardEvent) {
      if (e.key === 'Escape' && !busyRef.current) {
        onCloseRef.current()
        return
      }
      if (e.key !== 'Tab') return
      const items = focusable()
      if (items.length === 0) {
        e.preventDefault()
        card?.focus()
        return
      }
      const first = items[0]
      const last = items[items.length - 1]
      const active = document.activeElement
      // If focus is NOT inside the dialog, pull it back in rather than let Tab
      // escape. This is the robust case that covers focus having fallen to
      // <body> — a control that was removed (ShareGardenModal's remove-share
      // button) or disabled while busy — as well as any externally-stolen focus.
      if (!card || !card.contains(active)) {
        e.preventDefault()
        ;(e.shiftKey ? last : first).focus()
        return
      }
      if (e.shiftKey && (active === first || active === card)) {
        e.preventDefault()
        last.focus()
      } else if (!e.shiftKey && active === last) {
        e.preventDefault()
        first.focus()
      }
    }
    document.addEventListener('keydown', onKey)
    return () => {
      document.removeEventListener('keydown', onKey)
      // Restore focus to the opener only if it's still in the document — the
      // delete/clear flows this trap targets often remove the element that
      // opened the dialog (a garden card, a plop row). A disconnected node's
      // focus() silently no-ops and leaves focus on <body>, so fall through to
      // that case explicitly rather than pretend it worked.
      if (opener && opener.isConnected) opener.focus()
    }
  }, [])

  return (
    <div
      className="fixed inset-0 z-50 flex items-end justify-center bg-black/40 p-4 sm:items-center"
      onMouseDown={(e) => {
        if (e.target === e.currentTarget && !busy) onClose()
      }}
    >
      <div
        ref={cardRef}
        role="dialog"
        aria-modal="true"
        aria-label={title}
        tabIndex={-1}
        className="w-full max-w-md rounded-xl border border-border bg-surface p-6 shadow-lg outline-none"
      >
        <h2 className="text-lg font-semibold tracking-tight text-fg">{title}</h2>
        <div className="mt-4">{children}</div>
      </div>
    </div>
  )
}
