import { useEffect, useId, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from 'react'
import {
  screenToWorld,
  snapLocalToBedGrid,
  snapPoint,
  visibleGridStepCm,
  worldToLocal,
  type Rect,
  type Size,
} from '@/lib/geometry'
import { formatCm, type UnitPref } from '@/lib/units'
import { useCreateObject, useCreatePlanting } from '@/lib/objects'
import type { Plant } from '@/lib/plants'
import type { EditorPlanting } from '@/lib/plantings'
import { ObjectShape } from './ObjectShape'
import { PlopLayer } from './PlopLayer'
import { PlopOverlay } from './PlopOverlay'
import { SelectionOverlay } from './SelectionOverlay'
import { kindDef } from './kinds'
import { DIMMED_OPACITY, objectTransform } from './shared'
import { useEditorStore } from './store'
import { useViewport } from './useViewport'
import type { EditorGarden, EditorObject } from './types'

const GRID_MIN_CELL_PX = 6
const GRID_OPACITY = 0.18
const OVERLAY_BADGE_CLASS = 'rounded-md bg-surface/80 px-2 py-1 text-xs text-muted backdrop-blur'
const BED_GRID_MAX_LINES = 200 // safety cap on lines drawn inside one bed

/**
 * What to tell the user about the grid they're looking at, or null when the
 * drawn lines are simply the garden's own grid and need no explanation. Two
 * states are worth naming rather than leaving them to infer: lines drawn every
 * Nth grid cell (the grid was too fine to draw at this zoom), and — degenerate —
 * a grid so fine no multiple of it is drawable, which matters most when snapping
 * is on and would otherwise be invisibly active (#47).
 */
function gridNoteFor(
  drawnCm: number | null,
  gridCm: number,
  snapToGrid: boolean,
  unit: UnitPref,
): string | null {
  if (drawnCm == null) return snapToGrid ? 'grid too fine to draw — zoom in' : null
  if (drawnCm === gridCm) return null
  return `grid every ${formatCm(drawnCm, unit)}`
}

/** The world-space bounds rect of an object (center + size → top-left rect). */
function objectRect(o: EditorObject): Rect {
  return { x: o.xCm - o.widthCm / 2, y: o.yCm - o.heightCm / 2, w: o.widthCm, h: o.heightCm }
}

/** Grid-line offsets (from a bed's top-left corner, in the object-local frame)
 *  for a bed of the given half-extents and grid step. Starts at the corner so the
 *  lines match snapLocalToBedGrid, and is capped so a tiny grid on a big bed can't
 *  emit thousands of lines. Returns cm offsets along one axis. */
function bedGridLines(half: number, step: number): number[] {
  if (!(step > 0) || (2 * half) / step > BED_GRID_MAX_LINES) return []
  const lines: number[] = []
  for (let v = -half; v <= half + 1e-6; v += step) lines.push(v)
  return lines
}

/**
 * The editor canvas. Pan/zoom/pinch, tap-to-place objects, select+move/resize/
 * rotate an object, and — the #15 core — focus into a plantable object to place,
 * move, resize and remove plops with semantic-zoom rendering. The object/plop
 * being dragged renders from liveObject/livePlanting for instant feedback; the
 * PATCH fires on release.
 */
export function GardenCanvas({
  garden,
  objects,
  plantings,
  plantsById,
  canEdit,
}: {
  garden: EditorGarden
  objects: EditorObject[]
  plantings: EditorPlanting[]
  plantsById: Map<number, Plant>
  canEdit: boolean
}) {
  const svgRef = useRef<SVGSVGElement>(null)
  const containerRef = useRef<HTMLDivElement>(null)
  const [size, setSize] = useState<Size>({ w: 0, h: 0 })
  const fitKeyRef = useRef<string | null>(null)
  const gridId = 'garden-grid-' + useId().replace(/:/g, '')

  const viewport = useEditorStore((s) => s.viewport)
  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 liveObject = useEditorStore((s) => s.liveObject)
  const livePlanting = useEditorStore((s) => s.livePlanting)
  const { fitToRect } = useViewport(svgRef)
  const createObject = useCreateObject(garden.id)
  const createPlanting = useCreatePlanting(garden.id)

  const gardenRect: Rect = useMemo(
    () => ({ x: 0, y: 0, w: garden.widthCm, h: garden.heightCm }),
    [garden.widthCm, garden.heightCm],
  )

  useEffect(() => {
    const el = containerRef.current
    if (!el) return
    const ro = new ResizeObserver(([entry]) => setSize({ w: entry.contentRect.width, h: entry.contentRect.height }))
    ro.observe(el)
    return () => ro.disconnect()
  }, [])

  // Single fit mechanism: frame the focused object, else the whole garden, and
  // animate whenever that target (or the garden) changes. The key guard stops a
  // refit on unrelated re-renders (a plop add, a bed drag).
  useEffect(() => {
    if (size.w === 0 || size.h === 0 || garden.widthCm === 0 || garden.heightCm === 0) return
    const key = `${garden.id}:${focusedObjectId}`
    if (fitKeyRef.current === key) return
    const target = focusedObjectId != null ? objects.find((o) => o.id === focusedObjectId) : undefined
    if (focusedObjectId != null && !target) return // object not loaded yet; try again when it is
    fitKeyRef.current = key
    fitToRect(target ? objectRect(target) : gardenRect, size)
  }, [size, garden.id, garden.widthCm, garden.heightCm, focusedObjectId, objects, gardenRect, fitToRect])

  // Draw the true grid when its cells are legible, else the coarsest-but-smallest
  // multiple of it that is (see visibleGridStepCm). Snapping still uses gridCm —
  // the drawn lines are a subset of the snap positions, never a different grid.
  const gridCm = garden.gridSizeCm
  const drawnGridCm = visibleGridStepCm(gridCm, viewport.scale, GRID_MIN_CELL_PX)
  const gridNote = gridNoteFor(drawnGridCm, gridCm, garden.snapToGrid, garden.unitPref)

  const sorted = useMemo(() => [...objects].sort((a, b) => a.zIndex - b.zIndex), [objects])
  const rendered = useMemo(
    () => (liveObject ? sorted.map((o) => (o.id === liveObject.id ? liveObject : o)) : sorted),
    [sorted, liveObject],
  )
  // Merge the in-flight live plop over the active list, same as liveObject.
  const renderedPlops = useMemo(
    () => (livePlanting ? plantings.map((p) => (p.id === livePlanting.id ? livePlanting : p)) : plantings),
    [plantings, livePlanting],
  )
  const selectedObject = rendered.find((o) => o.id === selectedId) ?? null
  const selectedPlop = renderedPlops.find((p) => p.id === selectedPlantingId) ?? null
  const selectedPlopObject = selectedPlop ? rendered.find((o) => o.id === selectedPlop.objectId) ?? null : null
  const focusedObject = focusedObjectId != null ? rendered.find((o) => o.id === focusedObjectId) ?? null : null

  // A pointerdown reaching the svg is empty space: place the armed object kind,
  // or exit focus mode, or just deselect.
  function onCanvasPointerDown(e: ReactPointerEvent) {
    const armed = useEditorStore.getState().armedKind
    // Defense in depth: a viewer can't place objects even if a stale armed kind
    // slipped through (the palette isn't rendered for them).
    if (armed && canEdit) {
      useEditorStore.getState().setArmedKind(null)
      const def = kindDef(armed)
      const rect = svgRef.current?.getBoundingClientRect()
      if (!def || !rect) return
      let world = screenToWorld({ x: e.clientX - rect.left, y: e.clientY - rect.top }, viewport)
      // Snap the new object's center to the garden grid when the garden opts in.
      if (garden.snapToGrid) world = snapPoint(world, gridCm)
      createObject.mutate(
        { kind: def.kind, shape: def.shape, xCm: world.x, yCm: world.y, widthCm: def.widthCm, heightCm: def.heightCm, zIndex: def.defaultZ },
        { onSuccess: (o) => select(o.id) },
      )
      return
    }
    // Empty-space tap while focused exits focus (and any placement); otherwise
    // just clears the selection.
    if (focusedObjectId != null) {
      setFocusedObject(null)
      useEditorStore.getState().setArmedPlant(null)
    }
    select(null)
    selectPlanting(null)
  }

  // Drop a plop where the user taps inside the focused object (placement stays
  // armed for repeat-placement until Escape / Done).
  function onPlace(e: ReactPointerEvent) {
    e.stopPropagation()
    if (!canEdit || !focusedObject || !armedPlant || !focusedObject.plantable) return
    const rect = svgRef.current?.getBoundingClientRect()
    if (!rect) return
    const world = screenToWorld({ x: e.clientX - rect.left, y: e.clientY - rect.top }, viewport)
    const local = worldToLocal(world, { x: focusedObject.xCm, y: focusedObject.yCm }, focusedObject.rotationDeg)
    // snapLocalToBedGrid clamps to the bed either way; step 0 (snapping off) makes
    // it a pure clamp, so both paths go through one helper.
    const { x, y } = snapLocalToBedGrid(
      local,
      focusedObject.snapToGrid ? focusedObject.gridSizeCm : 0,
      focusedObject.widthCm / 2,
      focusedObject.heightCm / 2,
    )
    const radiusCm = Math.max(1.5 * armedPlant.spacingCm, 15)
    // Stay armed for repeat-placement; don't select (the placement sheet covers
    // the object, so a selection would be hidden until placement ends anyway).
    createPlanting.mutate({ objectId: focusedObject.id, plantId: armedPlant.id, xCm: x, yCm: y, radiusCm })
  }

  const halfFW = focusedObject ? focusedObject.widthCm / 2 : 0
  const halfFH = focusedObject ? focusedObject.heightCm / 2 : 0

  // Draw the bed's own grid inside a focused plantable bed that has snapping on,
  // so the user sees exactly where plants will land. Lines are in the bed's local
  // frame (origin at center), anchored to the corner to match snapLocalToBedGrid.
  // Memoized so a high-frequency plop drag (which re-renders the canvas on every
  // pointermove) doesn't rebuild the line arrays each frame. null when the focused
  // object isn't a snapping plantable bed.
  const bedGrid = useMemo(() => {
    if (!focusedObject || !focusedObject.plantable || !focusedObject.snapToGrid) return null
    return {
      v: bedGridLines(focusedObject.widthCm / 2, focusedObject.gridSizeCm),
      h: bedGridLines(focusedObject.heightCm / 2, focusedObject.gridSizeCm),
    }
  }, [focusedObject])

  return (
    <div ref={containerRef} className="relative h-full w-full overflow-hidden rounded-xl border border-border bg-bg">
      <svg
        ref={svgRef}
        className="h-full w-full select-none"
        style={{ touchAction: 'none' }}
        onPointerDown={onCanvasPointerDown}
      >
        <g transform={`translate(${viewport.tx} ${viewport.ty}) scale(${viewport.scale})`}>
          {drawnGridCm != null && (
            <>
              <defs>
                <pattern id={gridId} width={drawnGridCm} height={drawnGridCm} patternUnits="userSpaceOnUse">
                  <path d={`M ${drawnGridCm} 0 L 0 0 0 ${drawnGridCm}`} fill="none" stroke="#808080" strokeOpacity={GRID_OPACITY} strokeWidth={1} vectorEffect="non-scaling-stroke" />
                </pattern>
              </defs>
              <rect x={0} y={0} width={garden.widthCm} height={garden.heightCm} fill={`url(#${gridId})`} />
            </>
          )}

          <rect x={0} y={0} width={garden.widthCm} height={garden.heightCm} fill="none" stroke="#3f8f4f" strokeOpacity={0.7} strokeWidth={1.5} vectorEffect="non-scaling-stroke" />

          {rendered.map((o) => (
            <g key={o.id} opacity={focusedObjectId != null && focusedObjectId !== o.id ? DIMMED_OPACITY : 1}>
              <ObjectShape object={o} selected={o.id === selectedId} onSelect={select} />
            </g>
          ))}

          {focusedObject && bedGrid && (
            <g transform={objectTransform(focusedObject)} pointerEvents="none">
              {bedGrid.v.map((x) => (
                <line key={`v${x}`} x1={x} y1={-halfFH} x2={x} y2={halfFH} stroke="#3f8f4f" strokeOpacity={0.3} strokeWidth={1} vectorEffect="non-scaling-stroke" />
              ))}
              {bedGrid.h.map((y) => (
                <line key={`h${y}`} x1={-halfFW} y1={y} x2={halfFW} y2={y} stroke="#3f8f4f" strokeOpacity={0.3} strokeWidth={1} vectorEffect="non-scaling-stroke" />
              ))}
            </g>
          )}

          <PlopLayer
            objects={rendered}
            plantings={renderedPlops}
            plantsById={plantsById}
            scale={viewport.scale}
            focusedObjectId={focusedObjectId}
            selectedPlantingId={selectedPlantingId}
            onSelectPlop={selectPlanting}
          />

          {/* Edit handles are mounted only for editors/owners; viewers can still
              select to inspect (read-only), but never move/resize. */}
          {canEdit && selectedObject && (
            <SelectionOverlay
              object={selectedObject}
              gardenId={garden.id}
              svgRef={svgRef}
              snap={garden.snapToGrid}
              gridCm={gridCm}
            />
          )}
          {canEdit && selectedPlop && selectedPlopObject && (
            <PlopOverlay
              plop={selectedPlop}
              object={selectedPlopObject}
              gardenId={garden.id}
              svgRef={svgRef}
              snap={selectedPlopObject.snapToGrid}
              gridCm={selectedPlopObject.gridSizeCm}
            />
          )}

          {/* Placement capture: a transparent sheet over the focused object while a
              plant is armed, so taps drop plops instead of selecting the object. */}
          {canEdit && focusedObject && armedPlant && focusedObject.plantable && (
            <g transform={objectTransform(focusedObject)}>
              <rect
                x={-halfFW}
                y={-halfFH}
                width={halfFW * 2}
                height={halfFH * 2}
                fill="transparent"
                pointerEvents="all"
                style={{ cursor: 'crosshair' }}
                onPointerDown={onPlace}
              />
            </g>
          )}
        </g>
      </svg>

      <div className="pointer-events-none absolute left-3 top-3 flex flex-col items-start gap-1">
        <span className={OVERLAY_BADGE_CLASS}>{viewport.scale.toFixed(2)} px/cm</span>
        {gridNote && <span className={OVERLAY_BADGE_CLASS}>{gridNote}</span>}
      </div>
      <button
        type="button"
        onClick={() => size.w > 0 && fitToRect(gardenRect, size)}
        className="absolute bottom-3 right-3 rounded-md border border-border bg-surface/90 px-3 py-1.5 text-sm font-medium text-fg shadow-sm outline-none backdrop-blur transition-colors hover:bg-border/50 focus-visible:ring-2 focus-visible:ring-accent/40"
      >
        Fit
      </button>
    </div>
  )
}
