import { useEffect, useRef, useState } from 'react'
import gsap from 'gsap'
import { Button } from '@kolkrabbi/kol-component'
import { GRAB } from '@kolkrabbi/kol-component/utilities/motion'
/* the grab pill's proximity wake, travel and dwell — ONE implementation, shared
 * with kol-framework's useDragResize (OneGrabGestureBothRails, kol-fxr
 * 2026-08-30: two rails on one screen felt different because each package had
 * built the gesture itself). It lived here until then. */
import { useGrabEdge } from '@kolkrabbi/kol-component'
import { Icon } from '@kolkrabbi/kol-icons'
import Logomark from './Logomark.jsx'

/**
 * NavRail — the flat rail. ONE fixed column, every child a direct child of it
 * (RailFlatGrabOpen, kol-mirror 2026-08-28 — user, on the monitor rail he
 * measured it against: "the div structure is super simple ok … it's 1 div
 * parent and everything is just in that container", and on the SideNav-backed
 * rail that had replaced it: it was "impossible" to get this shape out of it).
 *
 * This REVERSES RailSideNavPixelParity (0.13.0), which made the rail a collapsed
 * kol-framework `SideNav`. That ticket's pixel argument is settled the other way:
 * the rail does not share pixels with the brand sidebar because it never becomes
 * one. `RailLogomarkAtTop` stands — the mark is at the top in both states.
 *
 * THE GRAB OPENS IT (user: "when you grab it should not move the icons one
 * pixel, only reveal the title, and a chevron to see sub categories" · "when it
 * opens it should push the main content inside"). Closed, every row is the rail
 * minus its gutter — the 32px rung alone shows and the label and chevron exist
 * but are clipped by the row's own width. Open, the clip reveals them. Nothing
 * in the icon column moves between the two states, and the content column
 * follows because AppShell offsets by the same live variable.
 *
 * ONE WIDTH VARIABLE: `--kol-shell-rail-width` on `:root` — this rail is that
 * wide and AppShell's content is offset by it, written per pointermove during a
 * drag and tweened on release, so the page is pushed live through both. A drag
 * writes the DOM, not React state: a pointermove per frame is not a render.
 *
 * The pill is kol-r2b2's, verbatim (user: "make it like it is in kol-r2b2, it
 * has animation and gsap") — `.kol-rail-grab` in kol-theme's kol-animation.css,
 * its pointer numbers in kol-component's `utilities/motion` (`GRAB`).
 *
 * TWO LEVELS (RailTwoLevelSections, kol-fxr 2026-08-28 — user: "the NAV has 20px
 * icons in 32px containers, maybe level below has 12px icons in 20px container
 * aligned to right? … you expand you see that they have main 56 items each, which
 * you won't be aware of until you actually open labs, which should not happen
 * automatically"). An item with `sub` is a SECTION: the same L1 row, plus a caret.
 * Its rows are L2 — a 12px glyph in a 20px box, indented so its glyph column sits
 * to the RIGHT of L1's (glyph x 14 → 30), resting a rung quieter at `oq-64` and
 * going `oq-96` when it is the route. fxr's labs chrome is ~44 rows across four
 * method sections; on one level they flatten into an icon column built for five
 * destinations and the section names have nowhere to render at all.
 *
 * L2 is NOT a `Button`: the icon-button ladder is 22/26/32/40 (2026-09-03) and this
 * rung is 20 — there is nothing below sm, and adding one is a change to that
 * ladder's law, which is the user's call and not this ticket's. The row is
 * written directly and keys `aria-current`, so kol-theme's
 * `.kol-shell-rail .kol-btn-nav` active rules still reach it. A sub row with no
 * `icon` renders label-only, as it did before this level existed.
 *
 * NOTHING AUTO-EXPANDS. An L2 row is behind BOTH the rail's clipped width and its
 * section's own disclosure, and `open` starts `false` with no prop to get it
 * wrong — arriving on a route reveals nothing, by construction rather than by a
 * flag a consumer has to remember.
 *
 * @param {Array}  items        `{ icon, path, label, sub?: [{ icon?, path, label }] }`
 * @param {Array}  bottomItems  pinned below the rule (Settings)
 * @param {Object} logomark     `{ svgUrl, title }` — the mark, and the app name beside it when open
 * @param {string} currentPath  active match: '/' exact, else prefix → aria-current="page"
 * @param {Function} onNavigate `(path) => void` — every click, the mark included
 * @param {boolean} drawer  off-canvas mode: no grab, no drag, no token writes, rows always
 *   labelled, and the rail sizes itself from `--kol-shell-drawer-width` (default 240px).
 *   AppShell sets this from `touch="drawer"` — see ShellRailNoDrawerOnMobile, 2026-08-31.
 */
const RAIL_W = '--kol-shell-rail-width'
const CLOSED = 48

/* the OPEN width is the sidenav's ladder (kol-framework: 264, 320 from 1536),
 * read at drag time so the rail lands on whichever rung the window is on */
const openWidth = () => {
  const v = getComputedStyle(document.documentElement).getPropertyValue('--kol-sidenav-w').trim()
  const px = parseFloat(v)
  return Number.isFinite(px) ? (v.endsWith('rem') ? px * 16 : px) : 264
}

/* THE GRAB EDGE — proximity, not contact: the pill wakes within `GRAB.near` of
 * the line and sleeps only past `GRAB.sleep` (hovering the line itself flapped
 * the class every frame and restarted the fade). Along the line it DWELLS (user
 * ruling 2026-08-28: "make it come to the cursor but have some sticky time where
 * it lands, so it's not constantly jerking"): it targets the pointer's own
 * position and re-targets only once the pointer is `GRAB.stick` px away, so it
 * lands on you, holds while you move inside the radius, then travels to where you
 * are now — inside `GRAB.range`, the middle band of the edge, so it never rides
 * up beside the logomark. One window listener, rAF-throttled. */

/* THE DRAG — hold the pill and the width follows the pointer between closed and
 * open; release snaps to the nearer state, a click (no travel past GRAB.slop)
 * toggles. `onSnap` reports the resting state once — the sub rows render only
 * while open, because closed they would still take their height and push the
 * rungs below them. */
function useRailDrag(railRef, grabRef, onSnap, snapRef, enabled = true) {
  useEffect(() => {
    const strip = grabRef.current, rail = railRef.current, root = document.documentElement
    /* A DRAWER IS NOT A DRAGGABLE RAIL (ShellRailNoDrawerOnMobile, kol-chess
     * 2026-08-31). In drawer mode the rail is off-canvas at a fixed width and
     * the content owns the whole viewport, so the drag must not run at all —
     * it writes `--kol-shell-rail-width` on `:root` per pointermove, which is
     * exactly the inline token a consumer could not reach without `!important`. */
    if (!enabled) return undefined
    if (!strip || !rail) return undefined
    gsap.set(root, { [RAIL_W]: `${CLOSED}px` })
    let drag = null
    const snapTo = (w) => {
      gsap.to(root, { [RAIL_W]: `${w}px`, ...GRAB.snap, overwrite: 'auto' })
      onSnap(w !== CLOSED)
    }
    /* the ONLY way out of this effect (RailSectionPressOpensRail, kol-fxr
     * 2026-08-30). `snapTo` closes over the animation and the CLOSED/open
     * ladder, so a section press has to reach it rather than reimplement it —
     * a consumer setting `--kol-shell-rail-width` from outside widens the rail
     * but leaves `railOpen` false, and the L2 rows render behind it: a wide,
     * EMPTY rail, worse than the dead press this fixes. */
    /* `(false)` closes, anything else opens — the same seam the L2 rows use
     * to open, now also the tap opener's toggle */
    if (snapRef) snapRef.current = (open = true) => snapTo(open === false ? CLOSED : openWidth())
    const onDown = (e) => {
      strip.setPointerCapture(e.pointerId)
      gsap.killTweensOf(root)
      drag = { x: e.clientX, w: rail.offsetWidth, open: openWidth(), moved: false }
      strip.classList.add('is-dragging')
    }
    const onMove = (e) => {
      if (!drag) return
      const dx = e.clientX - drag.x
      if (Math.abs(dx) > GRAB.slop) drag.moved = true
      root.style.setProperty(RAIL_W, `${Math.min(Math.max(drag.w + dx, CLOSED), drag.open)}px`)
    }
    const onUp = () => {
      if (!drag) return
      const w = rail.offsetWidth
      const open = drag.open
      snapTo(drag.moved ? (w > (CLOSED + open) / 2 ? open : CLOSED) : (drag.w > CLOSED ? CLOSED : open))
      drag = null
      strip.classList.remove('is-dragging')
    }
    strip.addEventListener('pointerdown', onDown)
    strip.addEventListener('pointermove', onMove)
    strip.addEventListener('pointerup', onUp)
    strip.addEventListener('pointercancel', onUp)
    return () => {
      strip.removeEventListener('pointerdown', onDown)
      strip.removeEventListener('pointermove', onMove)
      strip.removeEventListener('pointerup', onUp)
      strip.removeEventListener('pointercancel', onUp)
    }
  }, [railRef, grabRef, onSnap, snapRef])
}

/* one row: the rung exactly where the closed rail had it, then the label and
 * (with `sub`) the chevron — the row clips to the rail's width */
/* the L2 glyph goes through the same injection seam Button gives L1 */
function IconAt({ name, size, component }) {
  const Cmp = component || Icon
  return <Cmp name={name} size={size} />
}

function RailItem({ icon, path, label, sub, currentPath, onNavigate, iconComponent, railOpen, onOpenRail }) {
  const [open, setOpen] = useState(false)
  /* A SECTION ROW IS NOT A DESTINATION (RailSectionPressOpensRail, kol-fxr
   * 2026-08-30 — user: *"if I press effects from collapsed nav it should maybe
   * open? currently pressing it does nothing"*).
   *
   * Closed, the row clips to 48px and the disclosure caret sits OUTSIDE that
   * clip, so the only reachable control was the icon — which called
   * `onNavigate` with a path no consumer dispatches, because a section has no
   * action. A guaranteed dead press, in the rail's default state.
   *
   * Pressing it now opens the rail and expands that section. This is the
   * behaviour `SideNav` had before the flat-rail reversal. It does not fight
   * "nothing auto-expands" — that rule is about ARRIVING on a route; this is an
   * explicit press. */
  const isSection = sub?.length > 0
  const press = () => {
    if (isSection && !railOpen) { onOpenRail?.(); setOpen(true); return }
    onNavigate?.(path)
  }
  const active = path === '/' ? currentPath === '/' : currentPath.startsWith(path)
  return (
    <>
      <div className="flex items-center gap-3 w-full overflow-hidden shrink-0">
        <Button
          iconOnly={icon}
          iconSize={20}
          iconComponent={iconComponent}
          variant="nav"
          size="md"
          className="shrink-0"
          /* nav ACTIVE keys off aria-current="page" (kol-components-atoms.css) —
           * a brighter glyph, never the pressed fill */
          aria-current={active ? 'page' : undefined}
          /* oq-96 on every rung: the nav variant's oq-64 rest is site-nav quiet
           * and too dim on an icon rail (user: "make the color .96 on every icon
           * in the rail") */
          style={{ color: 'var(--kol-oq-96)' }}
          onClick={press}
          title={label}
          aria-label={label}
        />
        <span
          className="kol-helper-12 uppercase text-oq-96 flex-1 min-w-0 truncate cursor-pointer"
          onClick={press}
        >
          {label}
        </span>
        {sub?.length > 0 && (
          <Button
            iconOnly={open ? 'chevron-down' : 'chevron-right'}
            iconSize={16}
            iconComponent={iconComponent}
            variant="nav"
            size="sm"
            className="shrink-0"
            style={{ color: 'var(--kol-oq-96)' }}
            aria-expanded={open}
            onClick={() => setOpen((v) => !v)}
            title={label}
            aria-label={`${label} sub categories`}
          />
        )}
      </div>
      {railOpen && open && sub.map((s) => {
        const on = s.path === '/' ? currentPath === '/' : currentPath.startsWith(s.path)
        return (
          <div
            key={s.path}
            /* 18px of gutter puts the 20px box at x 26, so its 12px glyph starts
             * at 30 — one step right of L1's 20-in-32 at 14 (measured in fxr) */
            className="flex items-center gap-3 w-full overflow-hidden shrink-0 cursor-pointer"
            style={{ paddingLeft: 18 }}
            onClick={() => onNavigate?.(s.path)}
            aria-current={on ? 'page' : undefined}
            title={s.label}
          >
            {s.icon && (
              <span
                className="w-5 h-5 shrink-0 inline-flex items-center justify-center"
                style={{ color: on ? 'var(--kol-oq-96)' : 'var(--kol-oq-64)' }}
                aria-hidden="true"
              >
                <IconAt name={s.icon} size={12} component={iconComponent} />
              </span>
            )}
            <span
              className="kol-helper-12 uppercase flex-1 min-w-0 truncate"
              style={{ color: on ? 'var(--kol-oq-96)' : 'var(--kol-oq-64)' }}
            >
              {s.label}
            </span>
          </div>
        )
      })}
    </>
  )
}

export default function NavRail({
  items = [],
  bottomItems = [],
  logomark,
  currentPath = '',
  onNavigate,
  iconComponent,
  hidden = false,
  drawer = false,
}) {
  const railRef = useRef(null)
  const grabRef = useRef(null)
  const [railOpen, setRailOpen] = useState(false)
  const snapOpenRef = useRef(null)
  /* the grab strip is not rendered in drawer mode, so the hook's ref stays null
   * and it no-ops on its own — no second argument needed */
  useGrabEdge(grabRef)
  useRailDrag(railRef, grabRef, setRailOpen, snapOpenRef, !drawer)
  if (hidden) return null
  const row = (item) => (
    <RailItem key={item.path} {...item} currentPath={currentPath} onNavigate={onNavigate} iconComponent={iconComponent} railOpen={drawer || railOpen} onOpenRail={() => snapOpenRef.current?.()} />
  )
  return (
    <div
      ref={railRef}
      /* ONE SIDE, ALWAYS LEFT (ShellDrawerSideCorrection, kol-chess 2026-09-01).
        * 0.34.0 mirrored the panel to fix a button: the complaint was the
        * trigger travelling to mid-screen, never the navigation's side. The
        * trigger is now pinned top-right in both states, so a left panel has
        * nothing to collide with and the drawer opens where the rail lives. */
      className="kol-shell-rail bg-surface-primary fixed inset-y-0 left-0 flex flex-col items-start pt-4 pb-4 px-2 gap-2 border-r border-fg-08"
      /* A drawer takes its own width, NOT the live rail token — that token is
        * zeroed in drawer mode so the content gets the whole viewport back. */
      style={{ width: drawer ? 'var(--kol-shell-drawer-width, 240px)' : `var(${RAIL_W})` }}
    >
      {!drawer && (
        /* A BUTTON, and on touch A THICKER LINE (ShellRailCollapsedWithTapOpen,
         * kol-mirror 2026-09-01; corrected by RailGrabTapIsALine, kol-monitor
         * 2026-09-02 — user: "it was a thicker line" · "who decided it should be
         * a chevron?"): `touch="shell"` keeps the 48px rail visible and its
         * only opener was this 8px strip, whose pill wakes on pointer
         * PROXIMITY — a thumb never hovers. The 0.38.0 answer was a disc with a
         * chevron; that was the DS agent's shape, not the user's ruling. The
         * opener is the strip ITSELF: under `(pointer: coarse)` the theme
         * widens it to a thumb-sized hit and shows the pill thick at rest — the
         * same affordance a fine pointer drags, no glyph. A press with no travel
         * already toggled; it just had no hit area on touch. The drag hook binds
         * pointer events, so a button works exactly as the div did, and it says
         * what it is. Enter/Space toggle for the keyboard. */
        <button
          type="button"
          ref={grabRef}
          className="kol-rail-grab"
          aria-label={railOpen ? 'Collapse navigation' : 'Expand navigation'}
          aria-expanded={railOpen}
          onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); snapOpenRef.current?.(!railOpen) } }}
        />
      )}
      {logomark && (
        /* the mark, and the app name beside it when open — uppercase like the
         * rows (user 2026-08-28: "uppercase CONSISTENCY"). The `w-8` centring box
         * puts the 20px mark on the same x as the 20px glyphs below it. */
        <div
          onClick={() => onNavigate?.('/')}
          className="text-oq-96 cursor-pointer flex items-center gap-3 w-full overflow-hidden shrink-0 pt-1 mb-4"
          title={logomark.title}
        >
          <span className="w-8 flex justify-center shrink-0"><Logomark svgUrl={logomark.svgUrl} size={20} /></span>
          <span className="kol-helper-12 uppercase flex-1 min-w-0 truncate">{logomark.title}</span>
        </div>
      )}
      {items.map(row)}
      {/* the spacer IS the layout */}
      <div className="flex-1" />
      {/* the rule runs the full rail width, out past the px-2 */}
      {bottomItems.length > 0 && <div className="self-stretch -mx-2 border-t border-fg-08" />}
      {bottomItems.map(row)}
    </div>
  )
}
