import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Link, getRouteApi } from '@tanstack/react-router'
import { useQueryClient } from '@tanstack/react-query'
import { ScanPacketDialog } from '@/components/plants/ScanPacketDialog'
import { Nav } from '@/components/layout/Nav'
import { ThemeButton } from '@/components/layout/ThemeButton'
import { Alert } from '@/components/ui/Alert'
import { Button, IconButton } from '@/components/ui/Button'
import { Icon, type IconName } from '@/components/ui/Icon'
import { Seg } from '@/components/ui/Seg'
import { ColorDot } from '@/components/plants/Monogram'
import { AssistantTab } from '@/editor/AssistantTab'
import { Canvas, type CanvasHandle } from '@/editor/Canvas'
import { ClearBedDialog } from '@/editor/ClearBedDialog'
import { HistoryTab } from '@/editor/HistoryTab'
import { GardenSummary, ObjectInspector, PlopInspector, plantCountIn, rosterText } from '@/editor/Inspector'
import { JournalTab, type JournalAttach } from '@/editor/JournalTab'
import { KindSwatch } from '@/editor/KindSwatch'
import { OBJECT_KINDS, objectDisplayName } from '@/editor/kinds'
import { useSeasons } from '@/editor/seasons'
import { PHONE_BREAKPOINT, isTyping } from '@/editor/shared'
import { useEditorStore, type PhoneMode, type RailTab } from '@/editor/store'
import { FillRow, LotChooser, Toolkit } from '@/editor/Toolkit'
import { lotChoices, orderedPlants, toggleArmedKind, toggleArmedPlant } from '@/editor/arming'
import type { EditorGarden, EditorObject } from '@/editor/types'
import { useUndoLast } from '@/editor/useUndoLast'
import { useCapabilities } from '@/lib/agent'
import { ApiError } from '@/lib/api'
import { useMe } from '@/lib/auth'
import { cn } from '@/lib/cn'
import { useGardens, type Garden } from '@/lib/gardens'
import { historyKey } from '@/lib/history'
import { useJournalCounts } from '@/lib/journal'
import { forgetLastGarden, rememberLastGarden } from '@/lib/lastGarden'
import { monogramMap } from '@/lib/monogram'
import {
  toEditorObject,
  useDeleteObject,
  useEnsurePlantInFull,
  useFillObject,
  useGardenFull,
  useGardenSeason,
  useRemovePlanting,
  useUpdateObject,
  useUpdatePlanting,
  type FullGarden,
} from '@/lib/objects'
import { toEditorPlanting } from '@/lib/plantings'
import { usePlants, type Plant } from '@/lib/plants'
import { lotsByPlant, useSeedLots } from '@/lib/seedLots'
import { formatSize } from '@/lib/units'
import { usePageTitle } from '@/lib/usePageTitle'

const routeApi = getRouteApi('/gardens/$gardenId')

/** Loads the garden (live, or a past season) and guards the edge cases; the
 *  Editor below is everything else. */
export function GardenEditorPage() {
  const { gardenId } = routeApi.useParams()
  const gid = Number(gardenId)
  const { focus } = routeApi.useSearch()
  const navigate = routeApi.useNavigate()
  const seasonYear = useEditorStore((s) => s.seasonYear)
  // Two queries, one shown: the live one stays mounted so returning to "now" is
  // instant and the optimistic mutation cache it owns is never displaced.
  const live = useGardenFull(gid)
  const season = useGardenSeason(gid, seasonYear)
  const full = seasonYear === null ? live : season
  const me = useMe()
  usePageTitle(full.data?.garden.name ?? 'Garden')

  // Keyed on the LIVE query: whether the garden EXISTS doesn't depend on the
  // season being viewed.
  const gardenGone = live.error instanceof ApiError && live.error.isNotFound

  // Fresh transient state on entering/switching gardens, adopting the URL's
  // focus. `focus` is read once here on purpose.
  useEffect(() => {
    const st = useEditorStore.getState()
    st.reset()
    st.setFocus(focus ?? null)
    return () => useEditorStore.getState().reset()
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [gid])

  useEffect(() => {
    if (live.isSuccess) rememberLastGarden(gid)
  }, [live.isSuccess, gid])

  useEffect(() => {
    if (gardenGone) {
      forgetLastGarden(gid)
      navigate({ to: '/gardens' })
    }
  }, [gardenGone, gid, navigate])

  if (full.isPending) return <p className="p-7 text-[13px] font-semibold text-ink-mute">Loading the garden…</p>
  if (full.isError) {
    return (
      <div className="p-7">
        <Alert>{gardenGone ? 'That garden is no longer available — taking you to your gardens…' : 'Could not load this garden.'}</Alert>
      </div>
    )
  }
  const g = full.data.garden
  const canEdit = seasonYear === null && (g.ownerId === me.data?.id || g.myRole === 'editor')
  const isOwner = me.data != null && g.ownerId === me.data.id
  return <Editor gid={gid} data={full.data} canEdit={canEdit} isOwner={isOwner} currentUserId={me.data?.id} />
}

const MODES: { id: PhoneMode; label: string; icon: IconName }[] = [
  { id: 'build', label: 'Build', icon: 'shovel' },
  { id: 'plants', label: 'Plants', icon: 'sprout' },
  { id: 'journal', label: 'Journal', icon: 'notebook' },
  { id: 'chat', label: 'Assistant', icon: 'message-circle' },
]

const TABS: { id: RailTab; label: string }[] = [
  { id: 'toolkit', label: 'Toolkit' },
  { id: 'plot', label: 'Plot' },
  { id: 'journal', label: 'Journal' },
  { id: 'history', label: 'History' },
  { id: 'chat', label: 'Assistant' },
]

/**
 * The editor: one canvas, two chromes. Above 760px of container width it is the
 * two-card workspace — the plan, and a rail whose first tab is the toolkit
 * (the handoff drew it as a third card on the left; folding it into the rail
 * gave the plan and the rail its width); below, a phone layout with the canvas
 * as the whole screen, a peek panel that docks between canvas and mode bar,
 * and a tool strip for the current mode. Same state, same components.
 */
function Editor({
  gid,
  data,
  canEdit,
  isOwner,
  currentUserId,
}: {
  gid: number
  data: FullGarden
  canEdit: boolean
  isOwner: boolean
  currentUserId?: number
}) {
  const navigate = routeApi.useNavigate()
  const g: Garden = data.garden
  const garden: EditorGarden = useMemo(
    () => ({ id: g.id, name: g.name, widthCm: g.widthCm, heightCm: g.heightCm, unitPref: g.unitPref, gridSizeCm: g.gridSizeCm, snapToGrid: g.snapToGrid }),
    [g.id, g.name, g.widthCm, g.heightCm, g.unitPref, g.gridSizeCm, g.snapToGrid],
  )
  const unit = g.unitPref

  const gardens = useGardens()
  const capabilities = useCapabilities()
  const catalog = usePlants()
  const seedLots = useSeedLots()
  const journalCounts = useJournalCounts(gid)
  const lotsByPlantId = useMemo(() => lotsByPlant(seedLots.data), [seedLots.data])

  const sel = useEditorStore((s) => s.sel)
  const setSel = useEditorStore((s) => s.setSel)
  const focusId = useEditorStore((s) => s.focusId)
  const setFocus = useEditorStore((s) => s.setFocus)
  const armedPlant = useEditorStore((s) => s.armedPlant)
  const armedLotId = useEditorStore((s) => s.armedLotId)
  const setArmedLotId = useEditorStore((s) => s.setArmedLotId)
  const armedKind = useEditorStore((s) => s.armedKind)
  const tab = useEditorStore((s) => s.tab)
  const setTab = useEditorStore((s) => s.setTab)
  const mode = useEditorStore((s) => s.mode)
  const setMode = useEditorStore((s) => s.setMode)
  const setJournalScope = useEditorStore((s) => s.setJournalScope)

  const updateObject = useUpdateObject(gid)
  const updatePlanting = useUpdatePlanting(gid)
  const deleteObject = useDeleteObject(gid)
  const removePlanting = useRemovePlanting(gid)
  const fillObject = useFillObject(gid)
  const ensurePlant = useEnsurePlantInFull(gid)
  const undoLast = useUndoLast(gid, true)
  const seasons = useSeasons(g, gardens.data)

  // Every successful mutation is a history entry, and the history list is
  // always mounted here (the Undo button reads it), so it never gets the
  // on-open refetch it used to rely on. Refresh it whenever anything lands.
  const qc = useQueryClient()
  useEffect(
    () =>
      qc.getMutationCache().subscribe((event) => {
        if (event.type === 'updated' && event.action.type === 'success') void qc.invalidateQueries({ queryKey: historyKey(gid) })
      }),
    [qc, gid],
  )

  const objects = useMemo(() => data.objects.map(toEditorObject), [data.objects])
  const plantings = useMemo(() => data.plantings.map(toEditorPlanting), [data.plantings])
  // The full payload carries the garden's referenced plants; the catalog, the
  // rest. Markers need both (a just-armed plant isn't referenced yet), and the
  // monogram collision set is the whole catalog so a letter means the same
  // thing on every screen.
  const plantsById = useMemo(() => {
    const m = new Map<number, Plant>()
    for (const p of data.plants) m.set(p.id, p)
    for (const p of catalog.data ?? []) m.set(p.id, p)
    return m
  }, [data.plants, catalog.data])
  const plants = useMemo(() => [...plantsById.values()], [plantsById])
  const letters = useMemo(() => monogramMap(plants), [plants])

  // The container's width, not the viewport's, decides the chrome.
  const rootRef = useRef<HTMLDivElement>(null)
  const [vw, setVw] = useState(() => (typeof window !== 'undefined' ? window.innerWidth : 1024))
  useEffect(() => {
    const el = rootRef.current
    if (!el) return
    const ro = new ResizeObserver(([entry]) => setVw(entry.contentRect.width))
    ro.observe(el)
    return () => ro.disconnect()
  }, [])
  const isMobile = vw < PHONE_BREAKPOINT

  const canvasRef = useRef<CanvasHandle>(null)
  const [clearing, setClearing] = useState(false)
  const [scanning, setScanning] = useState(false)

  const focused = focusId != null ? (objects.find((o) => o.id === focusId) ?? null) : null
  const focusedPlops = useMemo(() => (focusId != null ? plantings.filter((p) => p.objectId === focusId) : []), [plantings, focusId])
  const selectedObject = sel?.type === 'object' ? (objects.find((o) => o.id === sel.id) ?? null) : null
  const selectedPlop = sel?.type === 'plop' ? (plantings.find((p) => p.id === sel.id) ?? null) : null
  const hasAssistant = !!capabilities.data?.agent
  const canScan = !!capabilities.data?.vision

  // Mirror focus → ?focus so a bed is deep-linkable and survives reload.
  useEffect(() => {
    navigate({ search: (prev) => ({ ...prev, focus: focusId ?? undefined }), replace: true })
  }, [focusId, navigate])

  // A focused bed that vanished (deleted, or a stale ?focus) must not leave the
  // canvas dimmed with no way out.
  useEffect(() => {
    if (focusId != null && !objects.some((o) => o.id === focusId)) {
      setFocus(null)
      canvasRef.current?.zoomFit()
    }
  }, [focusId, objects, setFocus])

  // The phone's mode follows focus: inside a bed you're planting.
  useEffect(() => {
    const cur = useEditorStore.getState().mode
    if (focusId != null) {
      if (cur === 'build') setMode('plants')
    } else if (cur === 'plants') setMode('build')
  }, [focusId, setMode])

  // A plant chosen from the catalog may not be in this garden's payload yet;
  // make sure its plops render with a color the moment they're placed.
  useEffect(() => {
    if (armedPlant) ensurePlant(armedPlant)
  }, [armedPlant, ensurePlant])

  // If the assistant is turned off under us, don't strand the UI on its tab.
  useEffect(() => {
    if (capabilities.data && !capabilities.data.agent) {
      if (useEditorStore.getState().tab === 'chat') setTab('plot')
      if (useEditorStore.getState().mode === 'chat') setMode('build')
    }
  }, [capabilities.data, setTab, setMode])

  const exitFocus = useCallback(() => {
    const st = useEditorStore.getState()
    st.setFocus(null)
    st.setArmedPlant(null)
    st.setSel(null)
    if (isMobile) st.setMode('build')
    canvasRef.current?.zoomFit()
  }, [isMobile])

  const plantThis = (o: EditorObject) => canvasRef.current?.focusObject(o)

  const openNotes = (scope: { type: 'object' | 'plop'; id: number }) => {
    setJournalScope(scope)
    setTab('journal')
    if (isMobile) {
      setSel(null)
      setMode('journal')
    }
  }

  // Latest values for the window keydown handler, which mounts once.
  const latest = useRef({ canEdit, objects, plantings, selectedObject, selectedPlop, focused, exitFocus, undoLast, updateObject, updatePlanting, deleteObject, removePlanting })
  latest.current = { canEdit, objects, plantings, selectedObject, selectedPlop, focused, exitFocus, undoLast, updateObject, updatePlanting, deleteObject, removePlanting }
  const nudgeTimer = useRef<number | null>(null)
  const nudgeCommit = useRef<(() => void) | null>(null)

  useEffect(() => {
    const DIRS: Record<string, [number, number]> = { ArrowUp: [0, -1], ArrowDown: [0, 1], ArrowLeft: [-1, 0], ArrowRight: [1, 0] }
    function onKey(e: KeyboardEvent) {
      if (isTyping()) return
      const st = useEditorStore.getState()
      const L = latest.current
      if (e.key === 'Escape') {
        if (st.armedKind || st.armedPlant) {
          st.setArmedKind(null)
          st.setArmedPlant(null)
          st.setGhost(null)
        } else if (st.sel) st.setSel(null)
        else if (st.focusId != null) L.exitFocus()
        return
      }
      if ((e.key === 'Delete' || e.key === 'Backspace') && st.sel && L.canEdit) {
        e.preventDefault()
        if (st.sel.type === 'object') {
          if (st.focusId === st.sel.id) L.exitFocus()
          st.setSel(null)
          L.deleteObject.mutate(st.sel.id)
        } else if (L.selectedPlop) {
          st.setSel(null)
          L.removePlanting.mutate({ id: L.selectedPlop.id, version: L.selectedPlop.version })
        }
        return
      }
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'z' && !e.shiftKey) {
        if (L.canEdit && L.undoLast.canUndo) {
          e.preventDefault()
          L.undoLast.undoLast()
        }
        return
      }
      const dir = DIRS[e.key]
      if (!dir || !st.sel || !L.canEdit) return
      e.preventDefault()
      const step = e.shiftKey ? 10 : 1
      const dx = dir[0] * step
      const dy = dir[1] * step
      // Live geometry now, one PATCH after the burst settles.
      if (st.sel.type === 'object') {
        const base = st.liveObject?.id === st.sel.id ? st.liveObject : L.objects.find((o) => o.id === st.sel!.id)
        if (!base) return
        st.setLiveObject({ ...base, xCm: base.xCm + dx, yCm: base.yCm + dy })
        nudgeCommit.current = () => {
          const live = useEditorStore.getState().liveObject
          if (!live) return
          L.updateObject.mutate({ id: live.id, version: live.version, xCm: live.xCm, yCm: live.yCm })
          useEditorStore.getState().setLiveObject(null)
        }
      } else {
        const base = st.livePlanting?.id === st.sel.id ? st.livePlanting : L.plantings.find((p) => p.id === st.sel!.id)
        if (!base) return
        st.setLivePlanting({ ...base, xCm: base.xCm + dx, yCm: base.yCm + dy })
        nudgeCommit.current = () => {
          const live = useEditorStore.getState().livePlanting
          if (!live) return
          L.updatePlanting.mutate({ id: live.id, version: live.version, xCm: live.xCm, yCm: live.yCm })
          useEditorStore.getState().setLivePlanting(null)
        }
      }
      if (nudgeTimer.current != null) window.clearTimeout(nudgeTimer.current)
      nudgeTimer.current = window.setTimeout(() => {
        nudgeTimer.current = null
        const fn = nudgeCommit.current
        nudgeCommit.current = null
        fn?.()
      }, 400)
    }
    window.addEventListener('keydown', onKey)
    return () => {
      window.removeEventListener('keydown', onKey)
      if (nudgeTimer.current != null) window.clearTimeout(nudgeTimer.current)
      nudgeCommit.current?.() // flush a pending nudge so it isn't lost
      nudgeCommit.current = null
    }
  }, [])

  const banner = seasons.banner ?? (!canEdit ? 'You can look but not change this garden — ask the owner for editor access.' : null)

  // What a new journal note attaches to: selection, else the focused bed, else the garden.
  const attach: JournalAttach = selectedPlop
    ? { plantingId: selectedPlop.id, label: plantsById.get(selectedPlop.plantId)?.name ?? 'this planting' }
    : selectedObject
      ? { objectId: selectedObject.id, label: objectDisplayName(selectedObject) }
      : focused
        ? { objectId: focused.id, label: objectDisplayName(focused) }
        : { label: null }

  const inspector = selectedObject ? (
    <ObjectInspector
      key={`o${selectedObject.id}`}
      object={selectedObject}
      gardenId={gid}
      unit={unit}
      canEdit={canEdit}
      focused={focusId === selectedObject.id}
      roster={rosterText(selectedObject, plantings, plantsById)}
      plantCount={plantCountIn(selectedObject, plantings, plantsById)}
      noteCount={journalCounts.data?.get(selectedObject.id) ?? 0}
      large={isMobile}
      onPlantThis={() => plantThis(selectedObject)}
      onNotes={() => openNotes({ type: 'object', id: selectedObject.id })}
      onDeleted={() => {
        if (focusId === selectedObject.id) exitFocus()
        setSel(null)
      }}
    />
  ) : selectedPlop ? (
    <PlopInspector
      key={`p${selectedPlop.id}`}
      plop={selectedPlop}
      plant={plantsById.get(selectedPlop.plantId)}
      plants={plants}
      gardenId={gid}
      unit={unit}
      canEdit={canEdit}
      noteCount={0}
      large={isMobile}
      onNotes={() => openNotes({ type: 'plop', id: selectedPlop.id })}
      onRemoved={() => setSel(null)}
    />
  ) : null

  const journal = (
    <JournalTab
      gardenId={gid}
      canEdit={canEdit}
      currentUserId={currentUserId}
      isOwner={isOwner}
      objects={objects}
      plantings={plantings}
      attach={attach}
      composerFirst={isMobile}
      large={isMobile}
    />
  )
  const assistant = hasAssistant ? <AssistantTab gardenId={gid} canEdit={canEdit} undo={undoLast.undo} large={isMobile} /> : null

  const canvas = (
    <Canvas ref={canvasRef} garden={garden} objects={objects} plantings={plantings} plantsById={plantsById} letters={letters} canEdit={canEdit} isMobile={isMobile} />
  )

  const dialogs = (
    <>
      {scanning && <ScanPacketDialog unit={unit} onClose={() => setScanning(false)} />}
      {clearing && focused && (
        <ClearBedDialog objectId={focused.id} objectName={objectDisplayName(focused)} plopCount={focusedPlops.length} gardenId={gid} onClose={() => setClearing(false)} />
      )}
    </>
  )

  // ── phone ───────────────────────────────────────────────────────────────
  if (isMobile) {
    const peekInsp = !!inspector
    const peekJournal = !inspector && mode === 'journal'
    const peekChat = !inspector && mode === 'chat' && hasAssistant
    const peek = peekInsp || peekJournal || peekChat
    const stripKinds = !peek && mode === 'build'
    const stripPlants = !peek && mode === 'plants'
    const modes = MODES.filter((m) => m.id !== 'chat' || hasAssistant)
    const closePeek = () => {
      setSel(null)
      if (mode === 'journal' || mode === 'chat') setMode('build')
    }
    const selectMode = (id: PhoneMode) => {
      const st = useEditorStore.getState()
      st.setMode(st.mode === id && (id === 'journal' || id === 'chat') ? 'build' : id)
      st.setSel(null)
      st.setArmedKind(null)
      st.setGhost(null)
      if (id !== 'plants') st.setArmedPlant(null)
    }
    const choices = lotChoices(armedPlant, lotsByPlantId)

    return (
      <div ref={rootRef} className="flex h-dvh flex-col bg-bg">
        <div className="flex flex-none items-center gap-2 px-3 py-2.5">
          {focused ? (
            <IconButton label="Back to the plan" icon="chevron-left" iconSize={17} size={38} onClick={exitFocus} />
          ) : (
            <Link to="/gardens" className="btn btn-icon btn-secondary no-underline" style={{ width: 38, height: 38 }} title="Your gardens" aria-label="Your gardens">
              <Icon name="chevron-left" size={17} />
            </Link>
          )}
          <span className="min-w-0 truncate font-heading text-[17px]" title={focused?.name ?? g.name}>
            {focused?.name ?? g.name}
          </span>
          <span className="ml-auto flex flex-none items-center gap-2">
            <ThemeButton size={38} iconSize={16} />
            {seasons.options.length > 1 && (
              <button type="button" className="tag tag-accent-2 cursor-pointer border-0" onClick={seasons.cycle} title="Switch season">
                {seasons.short}
              </button>
            )}
            <IconButton label="Undo" icon="undo-2" iconSize={16} size={38} disabled={!canEdit || !undoLast.canUndo} onClick={undoLast.undoLast} />
          </span>
        </div>
        {banner && <div className="flex-none bg-accent-2-200 px-3.5 py-1.5 text-xs font-semibold text-accent-2-800">{banner}</div>}

        <div className="relative min-h-0 flex-1">
          {canvas}
          <IconButton label="Fit the garden" icon="maximize" iconSize={16} variant="plain" size={40} className="elev-sm absolute bottom-3 right-3 border border-divider bg-surface" onClick={() => canvasRef.current?.zoomFit()} />
        </div>

        {peek && (
          <div className="elev-lg flex max-h-[45%] flex-none flex-col rounded-t-[22px] border-t border-divider bg-neutral-100">
            <div className="flex items-center gap-2 px-3.5 pb-0.5 pt-2.5">
              <span className="font-heading text-[15px]">{peekInsp ? (selectedObject ? 'Selected' : 'Planting') : peekJournal ? 'Journal' : 'Assistant'}</span>
              <IconButton label="Close" icon="x" variant="plain" size={32} className="ml-auto" onClick={closePeek} />
            </div>
            <div className="flex min-h-0 flex-1 flex-col gap-2.5 overflow-y-auto px-4 pb-4 pt-2">
              {peekInsp && inspector}
              {peekJournal && journal}
              {peekChat && assistant}
            </div>
          </div>
        )}

        {(stripKinds || stripPlants) && (
          <div className="flex flex-none flex-col gap-2 border-t border-divider bg-bg px-3 py-2.5">
            {stripPlants && choices.length > 0 && <LotChooser lots={choices} value={armedLotId} onChange={setArmedLotId} compact />}
            {stripPlants && armedPlant && focused && canEdit && <FillRow plant={armedPlant} busy={fillObject.isPending} onFill={(layout) => fillObject.mutate({ objectId: focused.id, plantId: armedPlant.id, layout })} />}
            <div className="flex gap-2 overflow-x-auto">
              {!canEdit && <span className="self-center whitespace-nowrap px-1 text-xs text-ink-mute">View only — you can look but not change this garden.</span>}
              {stripPlants && focused && (
                <Button variant="primary" tall className="flex-none" onClick={exitFocus}>
                  Done
                </Button>
              )}
              {stripKinds &&
                canEdit &&
                OBJECT_KINDS.map((k) => (
                  <button key={k.kind} type="button" className={chipClass(armedKind === k.kind)} aria-pressed={armedKind === k.kind} onClick={() => toggleArmedKind(k.kind)}>
                    <KindSwatch def={k} compact />
                    <span className="whitespace-nowrap text-[13px] font-bold">{k.label}</span>
                  </button>
                ))}
              {stripPlants &&
                canEdit &&
                orderedPlants(plants, plantings, '').map((p) => (
                  <button key={p.id} type="button" className={chipClass(armedPlant?.id === p.id)} aria-pressed={armedPlant?.id === p.id} onClick={() => toggleArmedPlant(p, lotsByPlantId)}>
                    <ColorDot color={p.color} size={15} />
                    <span className="whitespace-nowrap text-[13px] font-bold">{p.name}</span>
                  </button>
                ))}
              {stripPlants && canEdit && focused && focusedPlops.length > 0 && (
                <button type="button" className={cn(chipClass(false), 'text-accent-700')} onClick={() => setClearing(true)}>
                  <Icon name="eraser" size={14} />
                  <span className="whitespace-nowrap text-[13px] font-bold">Clear ({focusedPlops.length})</span>
                </button>
              )}
              {stripPlants && canEdit && canScan && (
                <button type="button" className={chipClass(false)} onClick={() => setScanning(true)}>
                  <Icon name="camera" size={14} />
                  <span className="whitespace-nowrap text-[13px] font-bold">Scan a packet</span>
                </button>
              )}
            </div>
          </div>
        )}

        <div className="flex flex-none gap-1 border-t border-divider bg-surface px-2 pb-[calc(6px+env(safe-area-inset-bottom))] pt-1.5" role="tablist" aria-label="Editor mode">
          {modes.map((m) => {
            const active = mode === m.id && !inspector
            return (
              <button
                key={m.id}
                type="button"
                role="tab"
                aria-selected={active}
                onClick={() => selectMode(m.id)}
                className={cn('flex min-h-[52px] flex-1 flex-col items-center gap-[3px] rounded-[16px] border-0 px-1 py-2', active ? 'bg-accent-200 text-accent-800' : 'text-ink-soft')}
              >
                <Icon name={m.icon} size={19} />
                <span className="text-[11px] font-bold">{m.label}</span>
              </button>
            )
          })}
        </div>
        {dialogs}
      </div>
    )
  }

  // ── desktop ─────────────────────────────────────────────────────────────
  const tabs = TABS.filter((t) => t.id !== 'chat' || hasAssistant)
  const plot = inspector ?? <GardenSummary objects={objects} plantings={plantings} plantsById={plantsById} canEdit={canEdit} />
  const toolkit = (
    <Toolkit
      embedded
      unit={unit}
      plants={plants}
      plantings={plantings}
      lotsByPlant={lotsByPlantId}
      canEdit={canEdit}
      focused={focused}
      focusedPlopCount={focusedPlops.length}
      canScan={canScan}
      filling={fillObject.isPending}
      onBack={exitFocus}
      onFill={(layout) => focused && armedPlant && fillObject.mutate({ objectId: focused.id, plantId: armedPlant.id, layout })}
      onClear={() => setClearing(true)}
      onScan={() => setScanning(true)}
    />
  )

  return (
    <div ref={rootRef} className="flex h-dvh flex-col bg-bg">
      <Nav active="gardens" />
      <div className="grid min-h-0 flex-1 gap-3.5 p-3.5 pt-0 [grid-template-columns:minmax(0,1fr)_400px]">
        <div className="panel flex min-h-0 flex-col overflow-hidden">
          <div className="flex flex-wrap items-center gap-3 border-b border-divider px-[18px] py-3">
            <h4 className="text-[19px]">{g.name}</h4>
            <span className="text-[13px] font-semibold text-ink-mute">{formatSize(g.widthCm, g.heightCm, unit)}</span>
            {focused && (
              <>
                <span className="opacity-40">/</span>
                <span className="text-sm font-bold text-accent-700">{focused.name}</span>
              </>
            )}
            <span className="ml-auto flex min-w-0 flex-wrap items-center justify-end gap-2">
              {seasons.options.length > 1 && <Seg options={seasons.options} value={seasons.value} onChange={seasons.select} label="Season" optionClassName="px-[13px]" />}
              <Button icon="undo-2" className="gap-[7px]" disabled={!canEdit || !undoLast.canUndo} onClick={undoLast.undoLast} title={undoLast.target ? `Undo: ${undoLast.target.summary}` : 'Nothing to undo'}>
                Undo
              </Button>
            </span>
          </div>
          {banner && <div className="bg-accent-2-200 px-[18px] py-2 text-[13px] font-semibold text-accent-2-800">{banner}</div>}
          <div className="relative min-h-0 flex-1">
            {canvas}
            <div className="elev-sm absolute bottom-3 right-3 flex gap-1 rounded-full border border-divider bg-surface p-1">
              <IconButton label="Zoom out" icon="minus" iconSize={14} variant="plain" size={30} onClick={() => canvasRef.current?.zoomOut()} />
              <IconButton label="Fit" icon="maximize" iconSize={14} variant="plain" size={30} onClick={() => canvasRef.current?.zoomFit()} />
              <IconButton label="Zoom in" icon="plus" iconSize={14} variant="plain" size={30} onClick={() => canvasRef.current?.zoomIn()} />
            </div>
          </div>
        </div>

        <div className="panel flex min-h-0 flex-col overflow-hidden">
          <div className="flex gap-1 px-2.5 pt-2.5" role="tablist">
            {tabs.map((t) => (
              <button
                key={t.id}
                type="button"
                role="tab"
                aria-selected={tab === t.id}
                onClick={() => setTab(t.id)}
                className={cn('flex-1 rounded-full border-0 px-1 py-2 text-[12.5px] font-bold', tab === t.id ? 'bg-accent-200 text-accent-800' : 'text-ink-soft hover:text-text')}
              >
                {t.label}
              </button>
            ))}
          </div>
          <div className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-4">
            {tab === 'toolkit' && toolkit}
            {tab === 'plot' && plot}
            {tab === 'journal' && journal}
            {tab === 'history' && <HistoryTab canEdit={canEdit} undoLast={undoLast} />}
            {tab === 'chat' && assistant}
          </div>
        </div>
      </div>
      {dialogs}
    </div>
  )
}

function chipClass(armed: boolean): string {
  return cn(
    'flex min-h-11 flex-none items-center gap-2 rounded-full border px-3.5 py-2.5',
    armed ? 'border-accent-400 bg-accent-200' : 'border-divider bg-neutral-100',
  )
}
