import { useEffect, useMemo, useRef, useState } from 'react'
import { getRouteApi } from '@tanstack/react-router'
import { Alert } from '@/components/ui/Alert'
import { Button } from '@/components/ui/Button'
import { GardenCanvas } from '@/editor/GardenCanvas'
import { Inspector } from '@/editor/Inspector'
import { PlopInspector } from '@/editor/PlopInspector'
import { PlantPicker } from '@/editor/PlantPicker'
import { Palette } from '@/editor/Palette'
import { SeedTray } from '@/editor/SeedTray'
import { ClearBedModal } from '@/editor/ClearBedModal'
import { EditorHint } from '@/editor/EditorHint'
import { objectDisplayName } from '@/editor/kinds'
import { useEditorStore } from '@/editor/store'
import type { EditorGarden } from '@/editor/types'
import { ShareGardenModal } from '@/components/gardens/ShareGardenModal'
import { useMe } from '@/lib/auth'
import { toEditorObject, useEnsurePlantInFull, useGardenFull, useUpdateObject, useUpdatePlanting } from '@/lib/objects'
import { toEditorPlanting } from '@/lib/plantings'
import type { Plant } from '@/lib/plants'
import { useSeedTray } from '@/lib/seedTray'
import { usePageTitle } from '@/lib/usePageTitle'

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

export function GardenEditorPage() {
  const { gardenId } = routeApi.useParams()
  const gid = Number(gardenId)
  const { focus } = routeApi.useSearch()
  const navigate = routeApi.useNavigate()
  const full = useGardenFull(gid)
  const me = useMe()
  usePageTitle(full.data?.garden.name ?? 'Garden')

  const selectedId = useEditorStore((s) => s.selectedId)
  const select = useEditorStore((s) => s.select)
  const selectedPlantingId = useEditorStore((s) => s.selectedPlantingId)
  const selectPlanting = useEditorStore((s) => s.selectPlanting)
  const focusedObjectId = useEditorStore((s) => s.focusedObjectId)
  const setFocusedObject = useEditorStore((s) => s.setFocusedObject)
  const armedPlant = useEditorStore((s) => s.armedPlant)
  const setArmedPlant = useEditorStore((s) => s.setArmedPlant)
  const setArmedKind = useEditorStore((s) => s.setArmedKind)
  const setLiveObject = useEditorStore((s) => s.setLiveObject)
  const setLivePlanting = useEditorStore((s) => s.setLivePlanting)

  const updatePlanting = useUpdatePlanting(gid)
  const updateObject = useUpdateObject(gid)
  const ensurePlant = useEnsurePlantInFull(gid)
  const { trayPlants, add: addToTray, remove: removeFromTray } = useSeedTray(gid)

  // Which plant-picker flow is open: 'place' arms a plant for repeat placement;
  // 'change' swaps the selected plop's plant.
  const [picker, setPicker] = useState<'place' | 'change' | null>(null)
  const [sharing, setSharing] = useState(false)
  const [clearing, setClearing] = useState(false)
  const nudgeTimer = useRef<number | null>(null)
  const nudgeFire = useRef<(() => void) | null>(null)

  const serverObjects = full.data?.objects
  const objects = useMemo(() => serverObjects?.map(toEditorObject) ?? [], [serverObjects])
  const serverPlantings = full.data?.plantings
  const plantings = useMemo(() => serverPlantings?.map(toEditorPlanting) ?? [], [serverPlantings])
  const plants = useMemo(() => full.data?.plants ?? [], [full.data?.plants])
  const plantsById = useMemo(() => new Map(plants.map((p) => [p.id, p])), [plants])

  // Role gating, computed before the effects/returns so the nudge handler can use
  // it. Ownership is the authoritative ownerId==me check.
  const gd = full.data?.garden
  const canEdit = gd != null && (gd.ownerId === me.data?.id || gd.myRole === 'editor')
  const isOwner = gd != null && me.data != null && gd.ownerId === me.data.id

  // Latest values for the mount-once nudge keydown handler to read, so its effect
  // never re-subscribes (which would cancel a pending debounced commit).
  const nudgeCtx = useRef({ canEdit, objects, plantings, updateObject, updatePlanting })
  nudgeCtx.current = { canEdit, objects, plantings, updateObject, updatePlanting }

  // Adopt the URL's focus and clear transient editor state on entering/switching
  // gardens (and on leaving). `focus` is intentionally read once here, not a dep,
  // so URL syncs below don't retrigger a full reset.
  useEffect(() => {
    const clear = () => {
      select(null)
      selectPlanting(null)
      setArmedKind(null)
      setArmedPlant(null)
      setLiveObject(null)
      setLivePlanting(null)
    }
    clear()
    setFocusedObject(focus ?? null)
    return () => {
      clear()
      setFocusedObject(null)
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [gid])

  // Mirror focusedObjectId → ?focus so focus mode is deep-linkable and survives
  // reload. Declared after the reset effect so the initial adopt wins.
  useEffect(() => {
    navigate({ search: (prev) => ({ ...prev, focus: focusedObjectId ?? undefined }), replace: true })
  }, [focusedObjectId, navigate])

  // If the focused object vanished — deleted, or a stale ?focus id on load — leave
  // focus mode so the canvas isn't stuck dimmed with no way out.
  useEffect(() => {
    if (focusedObjectId != null && full.data && !objects.some((o) => o.id === focusedObjectId)) {
      setFocusedObject(null)
    }
  }, [focusedObjectId, objects, full.data, setFocusedObject])

  const exitFocus = () => {
    setFocusedObject(null)
    setArmedPlant(null)
    select(null)
    selectPlanting(null)
  }

  // Escape peels back one layer: stop placing → deselect plop → exit focus → deselect.
  useEffect(() => {
    function onKey(e: KeyboardEvent) {
      if (e.key !== 'Escape') return
      const s = useEditorStore.getState()
      if (s.armedPlant) s.setArmedPlant(null)
      else if (s.selectedPlantingId != null) s.selectPlanting(null)
      else if (s.focusedObjectId != null) exitFocus()
      else if (s.selectedId != null) s.select(null)
    }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  // Desktop keyboard nudging: arrows move the selected object/plop 1cm (Shift =
  // 10cm); the PATCH is debounced ~400ms on key-idle so a held key doesn't spam.
  // Plops nudge in their object's local frame and clamp to its (local) bounds.
  // Mounted once, reading live values from nudgeCtx so a data refetch can't
  // re-subscribe and cancel a pending commit; the pending commit is flushed on
  // unmount, and a fire only commits if its live value is still present (a drag
  // that cleared it already committed its own PATCH).
  useEffect(() => {
    const DIRS: Record<string, [number, number]> = {
      ArrowUp: [0, -1],
      ArrowDown: [0, 1],
      ArrowLeft: [-1, 0],
      ArrowRight: [1, 0],
    }
    const commitLater = (fire: () => void) => {
      nudgeFire.current = fire
      if (nudgeTimer.current != null) window.clearTimeout(nudgeTimer.current)
      nudgeTimer.current = window.setTimeout(() => {
        nudgeTimer.current = null
        const fn = nudgeFire.current
        nudgeFire.current = null
        fn?.()
      }, 400)
    }
    function onKey(e: KeyboardEvent) {
      const { canEdit: canNudge, objects: objs, plantings: plops, updateObject: uo, updatePlanting: up } =
        nudgeCtx.current
      if (!canNudge) return
      const dir = DIRS[e.key]
      if (!dir) return
      const el = document.activeElement
      if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT')) return
      const s = useEditorStore.getState()
      if (s.objectDragging) return // don't fight an active pointer drag
      const step = e.shiftKey ? 10 : 1
      if (s.selectedId != null) {
        const base = s.liveObject?.id === s.selectedId ? s.liveObject : objs.find((o) => o.id === s.selectedId)
        if (!base) return
        e.preventDefault()
        s.setLiveObject({ ...base, xCm: base.xCm + dir[0] * step, yCm: base.yCm + dir[1] * step })
        commitLater(() => {
          const live = useEditorStore.getState().liveObject
          if (live?.id !== s.selectedId) return // a drag cleared it and committed
          uo.mutate({ id: live.id, version: live.version, xCm: live.xCm, yCm: live.yCm })
          useEditorStore.getState().setLiveObject(null)
        })
      } else if (s.selectedPlantingId != null) {
        const base =
          s.livePlanting?.id === s.selectedPlantingId ? s.livePlanting : plops.find((p) => p.id === s.selectedPlantingId)
        if (!base) return
        e.preventDefault()
        const obj = objs.find((o) => o.id === base.objectId)
        let nx = base.xCm + dir[0] * step
        let ny = base.yCm + dir[1] * step
        if (obj) {
          nx = Math.max(-obj.widthCm / 2, Math.min(obj.widthCm / 2, nx))
          ny = Math.max(-obj.heightCm / 2, Math.min(obj.heightCm / 2, ny))
        }
        s.setLivePlanting({ ...base, xCm: nx, yCm: ny })
        commitLater(() => {
          const live = useEditorStore.getState().livePlanting
          if (live?.id !== s.selectedPlantingId) return
          up.mutate({ id: live.id, version: live.version, xCm: live.xCm, yCm: live.yCm })
          useEditorStore.getState().setLivePlanting(null)
        })
      }
    }
    window.addEventListener('keydown', onKey)
    return () => {
      window.removeEventListener('keydown', onKey)
      if (nudgeTimer.current != null) {
        window.clearTimeout(nudgeTimer.current)
        nudgeTimer.current = null
      }
      const fn = nudgeFire.current // flush a pending move so it isn't lost
      nudgeFire.current = null
      fn?.()
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  if (full.isPending) return <p className="p-6 text-sm text-muted">Loading garden…</p>
  if (full.isError)
    return (
      <div className="p-6">
        <Alert>Could not load this garden.</Alert>
      </div>
    )

  const g = full.data.garden
  const garden: EditorGarden = {
    id: g.id,
    name: g.name,
    widthCm: g.widthCm,
    heightCm: g.heightCm,
    unitPref: g.unitPref,
    gridSizeCm: g.gridSizeCm,
    snapToGrid: g.snapToGrid,
  }

  const selectedObject = objects.find((o) => o.id === selectedId) ?? null
  const focusedObject = focusedObjectId != null ? objects.find((o) => o.id === focusedObjectId) ?? null : null
  // Committed selected plop; PlopInspector applies live drag geometry itself.
  const selectedPlop = plantings.find((p) => p.id === selectedPlantingId) ?? null
  const focusedPlops = focusedObjectId != null ? plantings.filter((p) => p.objectId === focusedObjectId) : []

  // Arm a plant for tap-to-place. The full catalog carries plants not yet in this
  // garden's /full payload, so make sure the chosen one is in the cache first —
  // otherwise its placed plops render without an icon/color until a refetch.
  function armPlant(plant: Plant) {
    ensurePlant(plant)
    setArmedPlant(plant)
  }

  // Removing the armed plant from the tray also disarms it, so placement doesn't
  // silently continue for a plant the user just cleared out.
  function removeFromTrayAndDisarm(id: number) {
    removeFromTray(id)
    if (armedPlant?.id === id) setArmedPlant(null)
  }

  // The picker hands back the full Plant (from the whole catalog), so use it
  // directly rather than re-resolving against the garden's referenced-plant map —
  // a not-yet-placed plant isn't in that map, which used to silently abort the
  // pick (nothing armed, picker left open). Picking (place OR change) makes sure
  // the plant renders and drops it into this garden's tray for quick reuse.
  function onPickPlant(plant: Plant) {
    if (!canEdit) return // defense in depth: viewers can't reach the picker anyway
    ensurePlant(plant)
    addToTray(plant)
    if (picker === 'change' && selectedPlop) {
      updatePlanting.mutate({ id: selectedPlop.id, version: selectedPlop.version, plantId: plant.id })
    } else {
      setArmedPlant(plant) // 'place' mode: arm for repeat placement
    }
    setPicker(null)
  }

  return (
    <div className="flex h-[calc(100vh-8rem)] flex-col gap-3 md:flex-row">
      <div className="shrink-0 md:w-40">
        <h1 className="mb-2 truncate text-lg font-semibold tracking-tight" title={garden.name}>
          {garden.name}
        </h1>
        {isOwner && (
          <Button variant="ghost" className="mb-2 w-full text-sm" onClick={() => setSharing(true)}>
            Share
          </Button>
        )}
        {!canEdit && (
          <p className="mb-2 rounded-md bg-border/40 px-2 py-1 text-xs text-muted">👁 View only</p>
        )}
        {focusedObjectId == null && canEdit && <Palette />}
      </div>

      <div className="relative min-h-0 flex-1">
        {focusedObject && (
          <div className="absolute left-2 top-2 z-20 flex flex-wrap items-center gap-2 rounded-lg border border-border bg-surface/90 px-2 py-1.5 text-sm shadow-sm backdrop-blur">
            <button type="button" onClick={exitFocus} className="rounded px-1.5 py-0.5 font-medium text-accent-strong hover:underline">
              ← {garden.name}
            </button>
            <span className="max-w-[8rem] truncate text-muted">{objectDisplayName(focusedObject)}</span>
            {canEdit &&
              (focusedObject.plantable ? (
                <>
                  <SeedTray
                    trayPlants={trayPlants}
                    armedPlantId={armedPlant?.id ?? null}
                    onArm={armPlant}
                    onRemove={removeFromTrayAndDisarm}
                    onOpenPicker={() => setPicker('place')}
                  />
                  {armedPlant && (
                    <Button variant="ghost" className="px-2 py-1 text-xs" onClick={() => setArmedPlant(null)}>
                      Done
                    </Button>
                  )}
                  {focusedPlops.length > 0 && (
                    <Button
                      variant="ghost"
                      className="px-2 py-1 text-xs text-red-600 dark:text-red-400"
                      onClick={() => setClearing(true)}
                    >
                      Clear ({focusedPlops.length})
                    </Button>
                  )}
                </>
              ) : (
                <span className="text-xs text-muted">Not plantable</span>
              ))}
          </div>
        )}
        <GardenCanvas garden={garden} objects={objects} plantings={plantings} plantsById={plantsById} canEdit={canEdit} />

        {/* Empty-state hints (non-interactive overlays). */}
        {canEdit && focusedObjectId == null && objects.length === 0 && (
          <EditorHint>Pick a shape from the palette, then tap the field to place your first bed.</EditorHint>
        )}
        {canEdit && focusedObject?.plantable && focusedPlops.length === 0 && !armedPlant && (
          <EditorHint position="top">Pick a plant from the tray, then tap inside the bed to place it.</EditorHint>
        )}
      </div>

      {(selectedObject || selectedPlop) && (
        <div className="fixed inset-x-0 bottom-0 z-30 max-h-[65vh] overflow-y-auto rounded-t-xl border-t border-border bg-surface p-4 shadow-lg md:static md:max-h-none md:w-72 md:shrink-0 md:rounded-xl md:border md:p-4 md:shadow-sm">
          {selectedObject && (
            <Inspector
              key={`obj-${selectedObject.id}`}
              object={selectedObject}
              gardenId={gid}
              unit={garden.unitPref}
              readOnly={!canEdit}
              onFocus={() => {
                setFocusedObject(selectedObject.id)
                select(null)
              }}
            />
          )}
          {selectedPlop && (
            <PlopInspector
              key={`plop-${selectedPlop.id}`}
              plop={selectedPlop}
              plant={plantsById.get(selectedPlop.plantId)}
              gardenId={gid}
              unit={garden.unitPref}
              readOnly={!canEdit}
              onChangePlant={() => setPicker('change')}
              onClose={() => selectPlanting(null)}
            />
          )}
        </div>
      )}

      {picker && (
        <PlantPicker
          unit={garden.unitPref}
          onClose={() => setPicker(null)}
          onSelect={onPickPlant}
        />
      )}

      {sharing && <ShareGardenModal garden={g} onClose={() => setSharing(false)} />}

      {clearing && focusedObject && (
        <ClearBedModal
          objectName={objectDisplayName(focusedObject)}
          plops={focusedPlops.map((p) => ({ id: p.id, version: p.version }))}
          gardenId={gid}
          onClose={() => setClearing(false)}
        />
      )}
    </div>
  )
}
