import { cloneElement, createContext, isValidElement, useContext, useLayoutEffect, useState, version as reactVersion } from 'react'
import { toneClass, TONE_VARS } from './tone.js'
import {
  useFloating,
  autoUpdate,
  offset as offsetMw,
  flip as flipMw,
  shift as shiftMw,
  size as sizeMw,
  hide as hideMw,
  FloatingPortal,
  FloatingFocusManager,
  useClick,
  useHover,
  useFocus,
  useDismiss,
  useRole,
  useInteractions,
} from '@floating-ui/react'

/**
 * Popover — anchored floating-element primitive built on @floating-ui/react.
 * Two surfaces:
 *
 *   1. `usePopover({ open, onOpenChange, ... })` — full hook. Returns refs +
 *      `getReferenceProps` / `getFloatingProps` so the consumer wires the
 *      anchor and the floater explicitly. Use when the trigger is non-trivial
 *      (custom component, span wrapper, anchor on something other than a
 *      button).
 *
 *      const popover = usePopover({ open, onOpenChange })
 *      <button ref={popover.refs.setReference} {...popover.getReferenceProps()} />
 *      <PopoverPanel popover={popover}>...</PopoverPanel>
 *
 *   2. `<PopoverPanel popover={...}>` — renders the floater into a
 *      `FloatingPortal` (escapes overflow:hidden ancestors), wraps in
 *      `FloatingFocusManager` (focus trap + return-focus), applies the
 *      `.kol-popover` panel chrome.
 *
 * Position middleware: `offset` (gap), `flip` (auto-flip on overflow),
 * `shift` (slide along the cross-axis to stay in viewport), optional
 * `size` (clamp to available width/height). `autoUpdate` keeps everything
 * synced on scroll / resize / layout shift.
 *
 * Interaction defaults: click toggles, outside-click + Esc dismiss, role
 * defaults to `dialog` (use `role: 'menu' | 'listbox' | 'tooltip'` for
 * other shapes — also drives the right ARIA attrs on the reference).
 */
export function usePopover({
  open,
  onOpenChange,
  placement = 'bottom-start',
  offset = 6,
  flip = true,
  flipPadding = 8,
  shiftPadding = 8,
  matchReferenceWidth = false,
  click = true,
  hover = false,
  hoverDelay = { open: 400, close: 100 },
  focus = false,
  dismiss = true,
  role = 'dialog',
  referenceElement = null,
} = {}) {
  const middleware = [offsetMw(offset)]
  if (flip) middleware.push(flipMw({ padding: flipPadding }))
  middleware.push(shiftMw({ padding: shiftPadding }))
  if (matchReferenceWidth) {
    middleware.push(
      sizeMw({
        padding: 8,
        apply({ rects, elements, availableHeight }) {
          /* FLOOR again, not exact (2026-08-09 evening, user: "if you have
           * to truncate 'centered' then its too narrow" — overrides the
           * morning's exact-width reading of the one-piece ruling for the
           * narrow case). The panel is never NARROWER than the trigger, and
           * grows when its rows carry chrome the trigger doesn't (the
           * selected row's check). Sole consumer is Dropdown. */
          /* EXACT width — the one-piece fusion restored (2026-08-09 night).
           * The truncation that briefly justified a floor is solved at the
           * TRIGGER instead: the ghost stack reserves the panel row's check
           * column (.kol-dd-ghost slack), so the one shared width fits every
           * row. Letting the panel size itself was the broken middle: an
           * abs-positioned float with auto width sizes against available
           * space, not content. Sole consumer is Dropdown. */
          /* VIEWPORT CLAMP (2026-08-09, kol-ds-fxr lobby ticket): flip is
           * off on Dropdown (fused edge), so a long list (the labs 30-item
           * "Add FX…" picker) otherwise renders past the viewport bottom.
           * Cap the panel to the space below the trigger; .kol-dd-list
           * scrolls inside (panel is a flex column in kol-theme). */
          Object.assign(elements.floating.style, {
            width: `${rects.reference.width}px`,
            maxHeight: `${Math.max(0, Math.floor(availableHeight))}px`,
          })
        },
      })
    )
  }

  /* `elements.reference` lets the consumer anchor the popover to an
   * external DOM node (e.g. a parent container ref) instead of wiring
   * `setReference` onto the trigger. Used by TypeBlockToolbar to anchor
   * to its TypeFrame parent. */
  middleware.push(hideMw({ strategy: 'referenceHidden' })) // last — it reads the final position
  const data = useFloating({
    open,
    onOpenChange,
    placement,
    middleware,
    whileElementsMounted: autoUpdate,
    elements: referenceElement ? { reference: referenceElement } : undefined,
  })

  /* A PANEL FOLLOWS ITS TRIGGER OUT OF SIGHT (2026-09-30 — docs/menus: an open menu painted over
   * the sticky header when its trigger scrolled under it, and floated at the page's foot when it
   * scrolled away). The panel is portalled to <body>, so no scroll container can clip it; `hide`
   * reports when the trigger is clipped, and the panel hides with it. */
  const hidden = data.middlewareData.hide?.referenceHidden
  const floatingStyles = hidden ? { ...data.floatingStyles, visibility: 'hidden' } : data.floatingStyles

  const interactions = useInteractions([
    useClick(data.context, { enabled: click }),
    /* `mouseOnly` — a hover is a mouse's (2026-10-05, found on labs' phone sheet). Touch has none:
     * a tap read as a hover that never ends, so a tooltip opened on the tap and stayed over the
     * next control. Only Tooltip opens on hover, and the keyboard still reaches it through `focus`. */
    useHover(data.context, { enabled: hover, delay: hoverDelay, move: false, mouseOnly: true }),
    useFocus(data.context, { enabled: focus }),
    /* `dismiss` may be an object of floating-ui's own options — `{ outsidePress: false }` keeps Esc
     * and drops the outside click (Dropdown `stayOpen`) */
    useDismiss(data.context, typeof dismiss === 'object' && dismiss ? { enabled: true, ...dismiss } : { enabled: dismiss }),
    useRole(data.context, { role }),
  ])

  return { ...data, floatingStyles, ...interactions, open }
}

/**
 * Tooltip — hover-triggered popover with text content. Wraps any trigger
 * element. Built on `usePopover` with `hover: true`, `click: false`,
 * `role: 'tooltip'`. Includes keyboard focus trigger so tab-focused
 * controls also reveal the tooltip.
 *
 *   <Tooltip label="Pattern" shortcut="P" placement="bottom">
 *     <Button iconOnly="ptrn-checker" ... />
 *   </Tooltip>
 */
/* A tooltip inside a tooltip's trigger (a disabled switch's reason inside its
 * row's hint) hides the outer one while it is open, and hands it back on leave
 * — the innermost wins, as the native `title` it replaced behaved. */
const TooltipNest = (globalThis[Symbol.for('@kolkrabbi/kol-component/TooltipNest')] ??= createContext(null))

/* `asChild` (the native-title sweep, 2026-09-26): the tooltip attaches to its one child element
 * instead of wrapping it in a span — for a trigger whose box IS the layout (a grid cell, a rail row,
 * an absolutely placed handle), where an inline-flex wrapper would move it. The child must be an
 * element that takes a ref: an intrinsic tag always does; a component only if it forwards one
 * (React 18 needs forwardRef), so Button / IconFrame keep the wrapper. Its own ref and handlers are
 * kept — the reference props merge over the child's. */
/* `disabled` (HardwareNativeTitle, kol-monitor 2026-10-15): the tooltip stays mounted but never
 * opens — a jack mid-patch. Dropping the label instead would swap the tree and remount the trigger
 * under the pointer. */
const childRefOf = (el) => ('ref' in (el.props ?? {}) ? el.props.ref : reactVersion.startsWith('18') ? el.ref : undefined)

export function Tooltip(props) {
  /* no label → no tooltip: several call sites pass an optional hint, and an empty panel is worse
   * than none. A separate component so the hooks below never run conditionally. */
  if (props.label == null || props.label === '') return props.children
  return <TooltipOn {...props} />
}

function TooltipOn({
  label,
  shortcut,
  placement = 'bottom',
  offset = 6,
  children,
  triggerClassName = 'inline-flex',
  asChild = false,
  disabled = false,
}) {
  const [open, setOpen] = useState(false)
  const [covered, setCovered] = useState(false)
  const parent = useContext(TooltipNest)
  const popover = usePopover({
    open: open && !covered && !disabled,
    onOpenChange: (v) => { setOpen(v); parent?.(v) },
    placement,
    offset,
    role: 'tooltip',
    click: false,
    hover: true,
    focus: true,
  })

  /* THE TONE OF WHERE IT IS (user, 2026-09-26: "tooltip should probably use same tone as dropdowns
   * and buttons"). The panel is portalled to body, outside the `kol-tone-*` wrapper that tones the
   * controls around the trigger — Dropdown's problem, solved the same way: on open, copy the
   * trigger's RESOLVED `--kol-tone-*` onto the panel. No tone in effect = nothing copied, and the
   * tooltip keeps its own surface. */
  const [ambient, setAmbient] = useState(null)
  const shown = open && !covered && !disabled
  useLayoutEffect(() => {
    const ref = popover.refs.reference.current
    /* the SURROUNDINGS' tone, not the trigger's own: an `asChild` trigger can be a variant button
     * (`kol-btn-ghost` carries its own `--kol-tone-*`), so read its parent; the wrapper span has no
     * variant and inherits the ambient values as it is */
    const el = asChild ? ref?.parentElement : ref
    if (!shown || !(el instanceof Element)) return
    const cs = getComputedStyle(el)
    const vars = {}
    for (const v of TONE_VARS) { const val = cs.getPropertyValue(v).trim(); if (val) vars[v] = val }
    setAmbient(vars)
  }, [shown]) // eslint-disable-line react-hooks/exhaustive-deps

  const child = asChild && isValidElement(children) ? children : null
  const childRef = child ? childRefOf(child) : undefined
  const setRef = (node) => {
    popover.refs.setReference(node)
    if (typeof childRef === 'function') childRef(node)
    else if (childRef) childRef.current = node
  }
  return (
    <>
      {child ? (
        <TooltipNest.Provider value={setCovered}>
          {cloneElement(child, { ...popover.getReferenceProps(child.props), ref: setRef })}
        </TooltipNest.Provider>
      ) : (
        <span
          ref={popover.refs.setReference}
          {...popover.getReferenceProps()}
          className={triggerClassName}
        >
          <TooltipNest.Provider value={setCovered}>{children}</TooltipNest.Provider>
        </span>
      )}
      <PopoverPanel
        popover={popover}
        focus={false}
        panel={false}
        className="kol-tooltip"
        style={ambient ?? undefined}
      >
        <span>{label}</span>
        {shortcut && <span className="kol-tooltip-key">{shortcut}</span>}
      </PopoverPanel>
    </>
  )
}

/**
 * PopoverPanel — A floating panel in a portal. renders the floater into a portal with default panel chrome.
 *
 * Props:
 *   popover         — the value returned from `usePopover`
 *   panel           — apply default `.kol-popover` chrome (default: true)
 *   modal           — focus modality (default: false — non-modal popover)
 *   focus           — wrap in FloatingFocusManager (default: true)
 *   className       — extra classes on the panel
 *   style           — extra inline styles merged with floatingStyles
 */
/* how many floats this one is opened from inside — context crosses portals, the DOM order does not */
const FloatDepth = (globalThis[Symbol.for('@kolkrabbi/kol-component/FloatDepth')] ??= createContext(0))

export function PopoverPanel({
  popover,
  children,
  panel = true,
  modal = false,
  focus = true,
  /* THE FLOATING SURFACE'S GROUND (editor-chrome-review, kol-fxr 2026-09-03 —
   * the user on the Editor / Labs / Randomiser list: *"can we change it border
   * oq-04 and background as tone prop?"*). The panel painted
   * `surface-secondary` unconditionally, which in dark is LIGHTER than the bar
   * it drops from, so the layering read inverted. Unset it still does exactly
   * that, so nothing existing moves; a tone paints that tone's ground instead,
   * through the same `toneClass` every other control uses. */
  tone,
  className = '',
  style: extraStyle,
}) {
  const depth = useContext(FloatDepth)
  if (!popover.open) return null

  const { refs, floatingStyles, context, getFloatingProps } = popover
  /* kol-popover-float rides every floating node (panel chrome or not): it
   * carries the stacking guarantee — floats must top overlay chrome
   * (.kol-overlay z-100), or a dropdown inside a FullscreenOverlay renders
   * under the sheet and its options can't be clicked. */
  const cls = ['kol-popover-float', panel && 'kol-popover', tone && toneClass(tone), className].filter(Boolean).join(' ')

  /* `data-editor-keep-selection` mirrors the marker on the EditorShell
   * root. Popovers render via FloatingPortal (mounted on <body>, outside
   * the shell), so the editor's document-level click-away handler would
   * otherwise treat clicks on popover content as "outside the editor"
   * and deselect. Tagging the panel here keeps that handler happy for
   * every popover — color picker, dropdowns, menus, tooltips. Harmless
   * for non-editor consumers since they ignore the attr. */
  const node = (
    <div
      ref={refs.setFloating}
      style={{ ...floatingStyles, ...(depth ? { '--kol-float-depth': depth } : null), ...extraStyle }}
      className={cls}
      data-editor-keep-selection
      {...getFloatingProps()}
    >
      <FloatDepth.Provider value={depth + 1}>{children}</FloatDepth.Provider>
    </div>
  )

  return (
    <FloatingPortal>
      {focus
        ? <FloatingFocusManager context={context} modal={modal}>{node}</FloatingFocusManager>
        : node}
    </FloatingPortal>
  )
}

export default PopoverPanel
