import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import Button from '../atoms/Button.jsx'
import Input from '../atoms/Input.jsx'
import ToggleCheckbox from '../atoms/ToggleCheckbox.jsx'

/**
 * Modal — promise-based prompt + confirm + alert dialogs.
 *
 *   const { prompt, confirm, alert } = useModal()
 *   const name    = await prompt('Name this frame:', 'Untitled')
 *   const proceed = await confirm('Discard unsaved changes?')
 *   const restore = await confirm('Restore your last canvas?',
 *                                 { okLabel: 'Restore', cancelLabel: 'New file' })
 *   await alert('Upload failed: quota exceeded')
 *
 * Both take an options object — `{ okLabel, cancelLabel }` (prompt: third
 * arg, after defaultValue) — so the buttons can SAY the outcome; defaults
 * stay OK / Cancel, existing callers untouched (ModalConfirmLabels,
 * 2026-08-12). Enter/Escape keep their meanings regardless of labels.
 *
 * PASSWORD (ModalPromptPassword, kol-atlas 2026-10-19): `prompt(title, '', { type: 'password' })`
 * masks the field (`autoComplete="current-password"`) for the family's sign-in prompts. The
 * native fallback cannot mask — mount a ModalProvider. `placeholder` sits in the same options.
 *
 * FORM (HubAccountRow, kol-fxr 2026-10-28 — sign-in asks Email AND Password): `form(title, fields, { okLabel, cancelLabel })`
 * draws one field per `{ id, placeholder, type?, autoComplete?, defaultValue? }` and resolves `{ [id]: value }` on submit,
 * `null` on cancel. The first field takes the focus; Enter in any field submits.
 *
 * THE PAGE HOLDS STILL UNDER THE KEYBOARD (PromptHoldsWithKeyboard, kol-fxr 2026-10-28 — user: *"does the UI have to move
 * upp to show keyboard?"*): every dialog sits in the VISUAL viewport (`visualViewport` resize/scroll), so on an iPhone it
 * rides above the keyboard; a field takes its focus without scrolling (`preventScroll`), and the document's scroll is put
 * back when the keyboard closes and when the dialog does.
 *
 * KEYBOARD: the dialog opens focused (prompt → its field, confirm/alert → OK), Tab cycles inside
 * it, and Enter on a focused button presses that button rather than submitting.
 *
 * Returned promise resolves to:
 *   - prompt  → string (value) on submit, `null` on cancel
 *   - confirm → boolean: `true` on confirm, `false` on cancel
 *   - confirm with `options` → `{ ok, values: { [id]: boolean } }` (below)
 *   - alert   → undefined, once dismissed. One button; `okLabel` names it.
 *     Added 2026-09-22 (media-pages-route-dialogs-through-usemodal) so an
 *     error report needs no native `alert()` beside DS prompts.
 *
 * TOGGLES (upload-dialog-optimise-and-keep-originals, kol-website 2026-10-09): `confirm` takes
 * `options: [{ id, label, hint?, defaultValue? }]`, drawn as `ToggleCheckbox` rows between the
 * title and the buttons, so one dialog asks a question and its settings:
 *
 *   const { ok, values } = await confirm('Web-optimise raster images?', {
 *     okLabel: 'Upload',
 *     options: [
 *       { id: 'optimise', label: 'Optimise', hint: '≤2560 px, ≤500 KB', defaultValue: true },
 *       { id: 'keepOriginals', label: 'Keep originals', hint: 'original/<name>', defaultValue: true },
 *     ],
 *   })
 *
 * `values` comes back on cancel too (the toggles as left), so a consumer can remember them either
 * way. Without `options`, confirm still resolves a plain boolean — existing callers untouched.
 *
 * Mounted once at the app root (BrandLayout). Renders into `document.body`
 * via portal, so it floats above any rail / scroll-context.
 */

const ModalCtx = (globalThis[Symbol.for('@kolkrabbi/kol-component/ModalCtx')] ??= createContext(null))

export function ModalProvider({ children }) {
  const [state, setState] = useState(null)

  const closeWith = useCallback((value) => {
    setState((s) => {
      if (s) s.resolve(value)
      return null
    })
  }, [])

  const prompt = useCallback((title, defaultValue = '', { okLabel, cancelLabel, type = 'text', placeholder } = {}) =>
    new Promise((resolve) => setState({ kind: 'prompt', title, defaultValue, okLabel, cancelLabel, type, placeholder, resolve })),
  [])

  const confirm = useCallback((title, { okLabel, cancelLabel, options } = {}) =>
    new Promise((resolve) => setState({ kind: 'confirm', title, okLabel, cancelLabel, options: options?.length ? options : null, resolve })),
  [])

  const alert = useCallback((title, { okLabel } = {}) =>
    new Promise((resolve) => setState({ kind: 'alert', title, okLabel, resolve })),
  [])

  const form = useCallback((title, fields = [], { okLabel, cancelLabel } = {}) =>
    new Promise((resolve) => setState({ kind: 'form', title, fields, okLabel, cancelLabel, resolve })),
  [])

  return (
    <ModalCtx.Provider value={{ prompt, confirm, alert, form }}>
      {children}
      {state && typeof document !== 'undefined' && createPortal(
        <ModalView state={state} closeWith={closeWith} />,
        document.body,
      )}
    </ModalCtx.Provider>
  )
}

function ModalView({ state, closeWith }) {
  const [val, setVal] = useState(state.defaultValue ?? '')
  const [values, setValues] = useState(() => Object.fromEntries((state.options ?? []).map((o) => [o.id, !!o.defaultValue])))
  const [fieldVals, setFieldVals] = useState(() => Object.fromEntries((state.fields ?? []).map((f) => [f.id, f.defaultValue ?? ''])))
  const inputRef = useRef(null)
  const dialogRef = useRef(null)
  const overlayRef = useRef(null)
  const typing = state.kind === 'prompt' || state.kind === 'form'

  /* the visual viewport (PromptHoldsWithKeyboard): the scrim is sized to it, so the dialog centres above the keyboard
   * instead of the browser scrolling the page to clear it; the scroll the page had is restored when the keyboard goes
   * (the visual viewport back to full height) and when the dialog closes */
  useEffect(() => {
    if (typeof window === 'undefined') return undefined
    const y0 = window.scrollY
    const vv = window.visualViewport
    const fit = () => {
      const el = overlayRef.current
      if (!vv || !el) return
      el.style.top = `${vv.offsetTop}px`
      el.style.left = `${vv.offsetLeft}px`
      el.style.width = `${vv.width}px`
      el.style.height = `${vv.height}px`
      if (vv.height >= window.innerHeight - 1 && window.scrollY !== y0) window.scrollTo(0, y0)
    }
    fit()
    vv?.addEventListener('resize', fit)
    vv?.addEventListener('scroll', fit)
    return () => {
      vv?.removeEventListener('resize', fit)
      vv?.removeEventListener('scroll', fit)
      if (window.scrollY !== y0) window.scrollTo(0, y0)
    }
  }, [])

  /* alert resolves `undefined` whichever way it is dismissed; a confirm with toggles resolves
   * `{ ok, values }` */
  const answer = (ok) => (state.options ? { ok, values } : ok)
  const submit = () => closeWith(state.kind === 'prompt' ? val : state.kind === 'form' ? fieldVals : state.kind === 'alert' ? undefined : answer(true))
  const cancel = () => closeWith(typing ? null : state.kind === 'alert' ? undefined : answer(false))

  /* focus once on open — the key effect below re-runs per keystroke and would steal it back */
  useEffect(() => {
    if (typing) inputRef.current?.focus({ preventScroll: true })
    else dialogRef.current?.querySelector('[data-modal-ok]')?.focus()
  }, [state.kind])

  useEffect(() => {
    const onKey = (e) => {
      if (e.key === 'Escape') cancel()
      if (e.key === 'Enter' && e.target.tagName !== 'BUTTON') submit()
      if (e.key === 'Tab') {
        const els = [...(dialogRef.current?.querySelectorAll('button, input') ?? [])].filter((el) => !el.disabled)
        if (!els.length) return
        const i = els.indexOf(document.activeElement)
        const next = e.shiftKey ? (i <= 0 ? els.length - 1 : i - 1) : (i === els.length - 1 ? 0 : i + 1)
        e.preventDefault()
        els[next].focus()
      }
    }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [state.kind, val, values, fieldVals, closeWith]) // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <div
      ref={overlayRef}
      onMouseDown={cancel}
      /* THE scrim (overlay-scrim-outliers sweep, 2026-09-03): the docs said Modal
         wore `.kol-overlay-scrim`; the source drew a raw `rgba(0,0,0,0.5)`. The
         z stays above the drawers it can open from. */
      className="kol-overlay-scrim"
      style={{
        position: 'fixed', inset: 0, zIndex: 1000,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }}
    >
      <div
        ref={dialogRef}
        role={typing ? 'dialog' : 'alertdialog'}
        aria-modal="true"
        aria-labelledby="kol-modal-title"
        onMouseDown={(e) => e.stopPropagation()}
        className="kol-modal flex flex-col gap-4 p-5 rounded border border-fg-08"
        style={{
          background: 'var(--kol-surface-primary)',
          color: 'var(--kol-surface-on-primary)',
          minWidth: 320,
          maxWidth: '90vw',
        }}
      >
        {/* kol-mono-12, not helper: dialog copy WRAPS, and helper's
          * line-height 1 is single-line chrome only (type protocol). */}
        {/* pre-line: an alert can carry a failure list, one per line */}
        <p id="kol-modal-title" className="kol-mono-12 text-emphasis whitespace-pre-line">{state.title}</p>
        {state.kind === 'prompt' && (
          <Input
            ref={inputRef}
            type={state.type}
            autoComplete={state.type === 'password' ? 'current-password' : undefined}
            placeholder={state.placeholder}
            variant="outline"
            size="sm"
            value={val}
            onChange={(e) => setVal(e.target.value)}
            className="w-full"
          />
        )}
        {state.kind === 'form' && state.fields.map((f, i) => (
          <Input
            key={f.id}
            ref={i === 0 ? inputRef : undefined}
            type={f.type ?? 'text'}
            autoComplete={f.autoComplete}
            placeholder={f.placeholder}
            aria-label={f.placeholder ?? f.id}
            variant="outline"
            size="sm"
            value={fieldVals[f.id]}
            onChange={(e) => setFieldVals((v) => ({ ...v, [f.id]: e.target.value }))}
            className="w-full"
          />
        ))}
        {state.options && (
          <div className="flex flex-col gap-3">
            {state.options.map((o) => (
              <ToggleCheckbox
                key={o.id}
                label={o.label}
                hint={o.hint}
                checked={values[o.id]}
                onChange={(next) => setValues((v) => ({ ...v, [o.id]: next }))}
              />
            ))}
          </div>
        )}
        <div className="flex gap-2 justify-end">
          {state.kind !== 'alert' && <Button size="sm" onClick={cancel}>{state.cancelLabel ?? 'Cancel'}</Button>}
          {/* inverted, not primary: since tone-is-the-ground-axis (2026-09-03) primary is the
            * dark ground, so OK drew the same as Cancel and nothing showed what Enter does */}
          <Button tone="inverted"  size="sm" onClick={submit} data-modal-ok>{state.okLabel ?? 'OK'}</Button>
        </div>
      </div>
    </div>
  )
}

/* Warn once, not per call — the fallback swallowing a missing ModalProvider
 * silently is how kol-fxr ran months on native window.confirm without
 * noticing (ModalConfirmLabels, 2026-08-12). */
let warnedNoProvider = false

export function useModal() {
  const ctx = useContext(ModalCtx)
  if (ctx) return ctx
  /* No-context fallback — falls back to native prompt/confirm so callers
   * don't need to null-check. */
  if (!warnedNoProvider && typeof console !== 'undefined') {
    warnedNoProvider = true
    console.warn('[kol] useModal(): no <ModalProvider> mounted — falling back to native window.prompt/confirm/alert. Custom labels are ignored on the fallback.')
  }
  return {
    prompt:  async (title, def = '') => {
      if (typeof window === 'undefined') return null
      const v = window.prompt(title, def)
      return v
    },
    /* the fallback cannot draw toggles: options come back at their defaults */
    confirm: async (title, { options } = {}) => {
      const ok = typeof window !== 'undefined' && window.confirm(title)
      return options?.length ? { ok, values: Object.fromEntries(options.map((o) => [o.id, !!o.defaultValue])) } : ok
    },
    alert: async (title) => {
      if (typeof window !== 'undefined') window.alert(title)
    },
  }
}
