import {
  Suspense,
  useRef,
  useCallback,
  useState,
  useEffect,
  useMemo,
  Component,
  type ErrorInfo,
  type ReactNode,
} from 'react'
import { useQuery } from '@tanstack/react-query'
import { Canvas, useThree, useFrame } from '@react-three/fiber'
import {
  OrbitControls,
  useGLTF,
  Environment,
  PerspectiveCamera,
  OrthographicCamera,
  ContactShadows,
  GizmoHelper,
  GizmoViewport,
} from '@react-three/drei'
import * as THREE from 'three'
import { toast } from 'sonner'
import {
  X, Camera, Loader2, AlertTriangle, Box, Download, ChevronDown,
  Maximize2, Grid3X3, Sun, AlertCircle, EyeOff, Layers,
} from 'lucide-react'
import api from '../../api/client'
import { getParsedObjects, getPartMaterials, getManualOverrides, type PartMaterialMap } from '../../api/cad'
import { fetchSceneManifest } from '../../api/sceneManifest'
import { useAuthStore } from '../../store/auth'
import MaterialPanel, { type IsolateMode } from './MaterialPanel'
import { normalizeMeshName, resolvePartMaterial, remapToPartKeys, applyPBRToMaterial, previewColorForEntry, forEachMeshMaterial, type MeshRegistryEntry } from './cadUtils'
import { fetchMaterialPBR, type MaterialPBRMap } from '../../api/assetLibraries'
import { useGeometryMerge } from './useGeometryMerge'
import WebGLErrorBoundary from './WebGLErrorBoundary'

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface ThreeDViewerProps {
  cadFileId: string
  onClose: () => void
  /** URL for the geometry-only GLB (from OCC export) */
  geometryGltfUrl?: string
  hasGeometryGlb?: boolean
  onGenerateGeometry?: () => void
  isGeneratingGeometry?: boolean
  downloadUrls?: { glb?: string; blend?: string }
  /** Pre-loaded material assignments from Product.cad_part_materials (Excel-driven) */
  initialPartMaterials?: PartMaterialMap
}

const ENV_PRESETS = [
  'city', 'sunset', 'dawn', 'night', 'warehouse',
  'forest', 'apartment', 'studio', 'park', 'lobby',
] as const
type EnvPreset = typeof ENV_PRESETS[number]

const BG_COLORS = [
  { label: 'Black', value: '#0d1117' },
  { label: 'Dark',  value: '#111827' },
  { label: 'Slate', value: '#1e293b' },
  { label: 'Light', value: '#e2e8f0' },
]

interface SceneInfo {
  center: THREE.Vector3
  maxDim: number
  groundY: number
}

// ---------------------------------------------------------------------------
// CameraFit — runs inside Canvas; repositions camera to fit the loaded model
// ---------------------------------------------------------------------------

function CameraFit({
  sceneRef,
  controlsRef,
  fitTrigger,
  isOrtho,
  onFitted,
}: {
  sceneRef: React.MutableRefObject<THREE.Object3D | null>
  controlsRef: React.RefObject<any>
  fitTrigger: number
  isOrtho: boolean
  onFitted: (info: SceneInfo) => void
}) {
  const { camera, size } = useThree()

  useEffect(() => {
    if (fitTrigger === 0 || !sceneRef.current) return

    // Compute bbox from meshes only (ignore lights / empty groups)
    const box = new THREE.Box3()
    sceneRef.current.traverse((obj) => {
      if ((obj as THREE.Mesh).isMesh) box.expandByObject(obj)
    })
    if (box.isEmpty()) return

    const center = box.getCenter(new THREE.Vector3())
    const sizeVec = box.getSize(new THREE.Vector3())
    const maxDim = Math.max(sizeVec.x, sizeVec.y, sizeVec.z)
    const groundY = box.min.y

    if (isOrtho) {
      const oc = camera as THREE.OrthographicCamera
      const aspect = size.width / size.height
      const halfH = maxDim * 0.8
      oc.left   = -halfH * aspect
      oc.right  =  halfH * aspect
      oc.top    =  halfH
      oc.bottom = -halfH
      oc.near   = -maxDim * 20
      oc.far    =  maxDim * 20
      oc.zoom   = 1
      oc.updateProjectionMatrix()
      camera.position.set(center.x, center.y, center.z + maxDim * 5)
    } else {
      const pc = camera as THREE.PerspectiveCamera
      const fovRad = (pc.fov * Math.PI) / 180
      const aspect = size.width / size.height
      const fovH = 2 * Math.atan(Math.tan(fovRad / 2) * aspect)
      const dist = (maxDim / 2) / Math.tan(Math.min(fovRad, fovH) / 2) * 1.6
      camera.position.set(center.x + maxDim * 0.1, center.y + maxDim * 0.3, center.z + dist)
    }

    camera.near = maxDim * 0.001
    camera.far  = maxDim * 100
    camera.updateProjectionMatrix()
    camera.lookAt(center)

    if (controlsRef.current) {
      controlsRef.current.target.copy(center)
      if (!isOrtho) {
        controlsRef.current.minDistance = maxDim * 0.05
        controlsRef.current.maxDistance = maxDim * 20
      }
      controlsRef.current.update()
    }

    onFitted({ center, maxDim, groundY })
  }, [fitTrigger]) // eslint-disable-line react-hooks/exhaustive-deps

  return null
}

// ---------------------------------------------------------------------------
// SceneBackground — syncs background colour into the Three.js scene
// ---------------------------------------------------------------------------

function SceneBackground({ color }: { color: string }) {
  const { scene } = useThree()
  useEffect(() => {
    scene.background = new THREE.Color(color)
    return () => { scene.background = null }
  }, [color, scene])
  return null
}

// ---------------------------------------------------------------------------
// CameraPositionTracker — keeps an up-to-date ref of camera position
// ---------------------------------------------------------------------------

function CameraPositionTracker({ posRef }: { posRef: React.MutableRefObject<[number, number, number]> }) {
  const { camera } = useThree()
  useFrame(() => {
    posRef.current = [camera.position.x, camera.position.y, camera.position.z]
  })
  return null
}

// ---------------------------------------------------------------------------
// ModelWithReady — loads GLB; exposes scene ref + pointer events
// ---------------------------------------------------------------------------

interface ModelWithReadyProps {
  url: string
  wireframe: boolean
  onReady: () => void
  sceneRef: React.MutableRefObject<THREE.Object3D | null>
  onPointerOver?: (e: any) => void
  onPointerOut?: () => void
  onClick?: (e: any) => void
}

function ModelWithReady({ url, wireframe, onReady, sceneRef, onPointerOver, onPointerOut, onClick }: ModelWithReadyProps) {
  const { scene } = useGLTF(url)

  // Clone materials once on scene load so color assignments don't share references
  useEffect(() => {
    scene.traverse((child: any) => {
      if (child.isMesh) {
        child.material = Array.isArray(child.material)
          ? child.material.map((m: any) => m.clone())
          : child.material.clone()
      }
    })
    sceneRef.current = scene
    onReady()
  }, [scene]) // eslint-disable-line react-hooks/exhaustive-deps

  // Wireframe toggle: set property only — never replace the material object
  useEffect(() => {
    scene.traverse((child: any) => {
      if (child.isMesh) {
        const mats: any[] = Array.isArray(child.material) ? child.material : [child.material]
        mats.forEach((m) => { m.wireframe = wireframe; m.needsUpdate = true })
      }
    })
  }, [scene, wireframe])

  return (
    <primitive
      object={scene}
      onPointerOver={onPointerOver}
      onPointerOut={onPointerOut}
      onClick={onClick}
    />
  )
}

// ---------------------------------------------------------------------------
// ScreenshotCapture
// ---------------------------------------------------------------------------

function ScreenshotCapture({ enabled, cadFileId, onDone }: {
  enabled: boolean; cadFileId: string; onDone: () => void
}) {
  const { gl } = useThree()
  const didCapture = useRef(false)

  useFrame(() => {
    if (!enabled || didCapture.current) return
    didCapture.current = true

    const dataUrl = gl.domElement.toDataURL('image/png')
    const [header, b64] = dataUrl.split(',')
    const mime = (header.match(/:(.*?);/) || [])[1] || 'image/png'
    const bytes = atob(b64)
    const arr = new Uint8Array(bytes.length)
    for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i)
    const blob = new Blob([arr], { type: mime })
    const fd = new FormData()
    fd.append('thumbnail', blob, 'thumbnail.png')

    api.post(`/cad/${cadFileId}/regenerate-thumbnail`, fd, {
      headers: { 'Content-Type': 'multipart/form-data' },
    })
      .then(() => toast.success('Thumbnail captured and saved'))
      .catch(() => toast.error('Failed to save thumbnail'))
      .finally(() => { didCapture.current = false; onDone() })
  })

  return null
}

// ---------------------------------------------------------------------------
// GltfErrorBoundary
// ---------------------------------------------------------------------------

class GltfErrorBoundary extends Component<
  { children: ReactNode; onError: (msg: string) => void },
  { hasError: boolean }
> {
  constructor(props: { children: ReactNode; onError: (msg: string) => void }) {
    super(props)
    this.state = { hasError: false }
  }
  static getDerivedStateFromError(): { hasError: boolean } { return { hasError: true } }
  componentDidCatch(error: Error, _info: ErrorInfo): void {
    this.props.onError(error.message || 'Failed to parse GLTF')
  }
  render(): ReactNode {
    if (this.state.hasError) return null
    return this.props.children
  }
}

// ---------------------------------------------------------------------------
// LoadingOverlay
// ---------------------------------------------------------------------------

function LoadingOverlay() {
  return (
    <div className="absolute inset-0 flex flex-col items-center justify-center bg-gray-900 text-white gap-3 pointer-events-none z-10">
      <Loader2 size={40} className="animate-spin text-accent" />
      <p className="text-sm text-gray-300">Loading 3D model…</p>
    </div>
  )
}

// ---------------------------------------------------------------------------
// EnvDropdown
// ---------------------------------------------------------------------------

function EnvDropdown({ value, onChange }: { value: EnvPreset; onChange: (v: EnvPreset) => void }) {
  const [open, setOpen] = useState(false)
  return (
    <div className="relative">
      <button
        onClick={() => setOpen(o => !o)}
        className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md bg-gray-700 hover:bg-gray-600 text-white text-xs font-medium transition-colors"
      >
        {value} <ChevronDown size={11} />
      </button>
      {open && (
        <div className="absolute right-0 top-full mt-1 z-50 bg-gray-800 border border-gray-700 rounded-md shadow-xl min-w-[120px]">
          {ENV_PRESETS.map(p => (
            <button
              key={p}
              onClick={() => { onChange(p); setOpen(false) }}
              className={`w-full text-left px-3 py-1.5 text-xs hover:bg-gray-700 transition-colors ${p === value ? 'text-accent font-semibold' : 'text-gray-300'}`}
            >
              {p}
            </button>
          ))}
        </div>
      )}
    </div>
  )
}

// ---------------------------------------------------------------------------
// TBtn — small toolbar toggle button
// ---------------------------------------------------------------------------

function TBtn({ active, onClick, title, children, disabled }: {
  active?: boolean; onClick: () => void; title?: string
  children: ReactNode; disabled?: boolean
}) {
  return (
    <button
      onClick={onClick}
      title={title}
      disabled={disabled}
      className={`flex items-center gap-1 px-2.5 py-1.5 rounded-md text-xs font-medium border transition-colors ${
        active
          ? 'bg-accent border-accent text-white'
          : 'bg-gray-800 border-gray-700 text-gray-300 hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed'
      }`}
    >
      {children}
    </button>
  )
}

// ---------------------------------------------------------------------------
// Main ThreeDViewer component
// ---------------------------------------------------------------------------

export default function ThreeDViewer({
  cadFileId,
  onClose,
  geometryGltfUrl,
  hasGeometryGlb,
  onGenerateGeometry,
  isGeneratingGeometry,
  downloadUrls,
  initialPartMaterials,
}: ThreeDViewerProps) {
  const token = useAuthStore((s) => s.token)

  // View state
  const [wireframe, setWireframe] = useState(false)
  const [envPreset, setEnvPreset] = useState<EnvPreset>('city')
  const [capturing, setCapturing] = useState(false)
  const [loadError, setLoadError] = useState<string | null>(null)
  const [modelReady, setModelReady] = useState(false)
  const [blobUrl, setBlobUrl]     = useState<string | null>(null)

  // Tier 1 — fit
  const [fitTrigger, setFitTrigger] = useState(0)
  const [sceneInfo, setSceneInfo]   = useState<SceneInfo | null>(null)

  // Tier 2 — scene options
  const [isOrtho, setIsOrtho]       = useState(false)
  const [showGrid, setShowGrid]     = useState(false)
  const [showShadows, setShowShadows] = useState(false)
  const [bgColor, setBgColor]       = useState('#111827')
  const [hoverInfo, setHoverInfo]   = useState<{ name: string; x: number; y: number } | null>(null)

  // Task 5 — hovered mesh ref for emissive highlight
  const hoveredMeshRef = useRef<THREE.Mesh | null>(null)

  // partKey map from GLB extras: normalizedMeshName → partKey slug
  const [partKeyMap, setPartKeyMap] = useState<Record<string, string>>({})

  // Task 7 — clicked (pinned) part for material panel
  const [pinnedPart, setPinnedPart] = useState<string | null>(null)

  // Task 8 — show unassigned toggle
  const [showUnassigned, setShowUnassigned] = useState(false)

  // Hide assigned toggle — hides all parts that already have a material
  const [hideAssigned, setHideAssigned] = useState(false)

  // Reconciliation panel (unmatched source rows + unassigned parts)
  const [showReconcile, setShowReconcile] = useState(false)

  // Isolation mode — ghost/hide other parts while a part is pinned
  const [isolateMode, setIsolateMode] = useState<IsolateMode>('none')

  // Performance mode — merge geometries by material to reduce draw calls
  const [perfMode, setPerfMode] = useState(false)

  // Refs
  const sceneRef   = useRef<THREE.Object3D | null>(null)
  const controlsRef = useRef<any>(null)
  const camPosRef  = useRef<[number, number, number]>([0, 0.1, 0.3])

  // MeshRegistry: flat array of {mesh, partKey} built once on model load.
  // Replaces per-effect scene.traverse() calls with direct iteration.
  const meshRegistryRef = useRef<MeshRegistryEntry[]>([])

  // Dimension data from parsed_objects
  const { data: parsedData } = useQuery({
    queryKey: ['cad-parsed-objects', cadFileId],
    queryFn: () => getParsedObjects(cadFileId),
    staleTime: Infinity,
    retry: false,
  })
  const dims = parsedData?.parsed_objects?.dimensions_mm

  // Scene manifest (non-blocking — 404 expected when USD master not yet generated)
  const { data: sceneManifest } = useQuery({
    queryKey: ['scene-manifest', cadFileId],
    queryFn: () => fetchSceneManifest(cadFileId),
    staleTime: Infinity,
    retry: false,
  })

  // Manual material overrides keyed by partKey slug (from PUT /manual-material-overrides)
  const { data: manualOverrides = {} } = useQuery({
    queryKey: ['manual-overrides', cadFileId],
    queryFn: () => getManualOverrides(cadFileId),
    staleTime: 30_000,
    retry: false,
  })

  // Total unique normalized mesh count (set once when model is ready)
  const [totalMeshCount, setTotalMeshCount] = useState(0)
  const [glbMeshNames, setGlbMeshNames] = useState<Set<string>>(new Set())

  // Task 6 — load saved part-material assignments (manual overrides from the viewer)
  const { data: savedPartMaterials = {} } = useQuery({
    queryKey: ['part-materials', cadFileId],
    queryFn: () => getPartMaterials(cadFileId),
    staleTime: 30_000,
    retry: false,
  })

  // PBR material properties from Blender asset library (metallic, roughness, base_color)
  const { data: pbrMap = {} as MaterialPBRMap } = useQuery({
    queryKey: ['material-pbr'],
    queryFn: fetchMaterialPBR,
    staleTime: 300_000,
  })

  // Merge: initialPartMaterials (Product Excel data) as base; savedPartMaterials overrides
  const partMaterials = useMemo(
    () => ({ ...initialPartMaterials, ...savedPartMaterials } as PartMaterialMap),
    [initialPartMaterials, savedPartMaterials],
  )

  // Effective materials: remap Excel-imported keys to partKey slugs (when
  // partKeyMap is available), then layer manual overrides on top.
  const effectiveMaterials = useMemo(() => {
    // Remap normalized OCC name keys → partKey slugs so they match mesh resolution
    const remapped = remapToPartKeys(partMaterials, partKeyMap)
    // Manual overrides are already keyed by partKey slug
    const fromManual: PartMaterialMap = Object.fromEntries(
      Object.entries(manualOverrides).map(([k, v]) => [
        k,
        { type: (v.startsWith('#') ? 'hex' : 'library') as 'hex' | 'library', value: v },
      ])
    )
    return { ...remapped, ...fromManual }
  }, [partMaterials, manualOverrides, partKeyMap])

  // Resolve partKey from normalized mesh name (identity fallback when no map loaded)
  const resolvePartKey = useCallback(
    (normalizedName: string): string => partKeyMap[normalizedName] ?? normalizedName,
    [partKeyMap],
  )

  // Count how many unique GLB mesh types have a resolved material assignment
  const assignedCount = useMemo(
    () => [...glbMeshNames].filter(n => !!resolvePartMaterial(n, effectiveMaterials)).length,
    [glbMeshNames, effectiveMaterials],
  )

  // Raw URL (used as stable key before blob fetch)
  const rawActiveUrl = geometryGltfUrl

  // Resolved blob URL used in useGLTF (requires auth header)
  const activeUrl = blobUrl

  const handleModelReady  = useCallback(() => setModelReady(true), [])
  const handleError       = useCallback((msg: string) => setLoadError(msg), [])
  const handleCaptureDone = useCallback(() => setCapturing(false), [])
  const handleFitted      = useCallback((info: SceneInfo) => setSceneInfo(info), [])

  // Reset + fetch blob URL when selected model changes
  useEffect(() => {
    setModelReady(false)
    setLoadError(null)
    setSceneInfo(null)
    setFitTrigger(0)
    setBlobUrl(null)

    if (!rawActiveUrl || !token) return

    let objUrl = ''
    fetch(rawActiveUrl, { headers: { Authorization: `Bearer ${token}` } })
      .then((r) => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`)
        return r.blob()
      })
      .then((blob) => {
        objUrl = URL.createObjectURL(blob)
        setBlobUrl(objUrl)
      })
      .catch(() => setLoadError('Failed to load 3D model'))

    return () => { if (objUrl) URL.revokeObjectURL(objUrl) }
  }, [rawActiveUrl, token]) // eslint-disable-line react-hooks/exhaustive-deps

  // Auto-fit when model finishes loading
  useEffect(() => {
    if (modelReady) setFitTrigger(t => t + 1)
  }, [modelReady])

  // Build MeshRegistry + extract partKeyMap from GLB extras (runs once on model load).
  // Single traverse replaces what was previously 2 separate traversals.
  useEffect(() => {
    if (!modelReady || !sceneRef.current) return

    // Extract partKeyMap injected by export_step_to_gltf.py into GLB extras
    const glbExtras = (sceneRef.current as any).userData ?? {}
    const map = glbExtras.partKeyMap as Record<string, string> | undefined
    if (map && Object.keys(map).length > 0) {
      setPartKeyMap(map)
    }

    // Single traverse: stamp partKey, build registry, count unique parts
    const registry: MeshRegistryEntry[] = []
    const names = new Set<string>()
    sceneRef.current.traverse((obj) => {
      if (!(obj instanceof THREE.Mesh)) return

      // Stamp userData.partKey (propagate from parent Group for multi-primitive GLB nodes)
      if (!obj.userData.partKey) {
        const parentPk = obj.parent?.userData?.partKey as string | undefined
        if (parentPk) {
          obj.userData.partKey = parentPk
        } else if (map) {
          const normalized = normalizeMeshName((obj.userData?.name as string) || obj.name)
          obj.userData.partKey = map[normalized] ?? normalized
        }
      }

      // Resolve partKey for this mesh
      const pk = (obj.userData?.partKey as string) ||
        normalizeMeshName((obj.userData?.name as string) || obj.name)

      registry.push({ mesh: obj, partKey: pk })
      if (pk) names.add(pk)
    })
    meshRegistryRef.current = registry
    setTotalMeshCount(names.size)
    setGlbMeshNames(new Set(names))
  }, [modelReady])

  // Re-fit when switching projection mode
  useEffect(() => {
    if (modelReady) setFitTrigger(t => t + 1)
  }, [isOrtho]) // eslint-disable-line react-hooks/exhaustive-deps

  // Apply saved material colors + PBR properties after model loads (uses MeshRegistry)
  useEffect(() => {
    if (!modelReady || meshRegistryRef.current.length === 0) return
    // Skip when pbrMap hasn't loaded yet — avoid setting grey fallback prematurely
    if (Object.keys(pbrMap).length === 0) return
    for (const { mesh, partKey } of meshRegistryRef.current) {
      const entry = resolvePartMaterial(partKey, effectiveMaterials)
      if (!entry) continue
      // Clone materials on first PBR application (GLB loader shares instances)
      if (!mesh.userData._pbrApplied) {
        mesh.material = Array.isArray(mesh.material)
          ? mesh.material.map((m: THREE.Material) => m.clone())
          : mesh.material.clone()
        mesh.userData._pbrApplied = true
      }
      forEachMeshMaterial(mesh, (mat) => {
        if (entry.type === 'library' && pbrMap[entry.value]) {
          applyPBRToMaterial(mat, pbrMap[entry.value])
        } else {
          mat.color.set(previewColorForEntry(entry, pbrMap))
        }
      })
    }
  }, [modelReady, effectiveMaterials, resolvePartKey, pbrMap])

  // Apply/remove unassigned highlight — uses MeshRegistry instead of traverse
  useEffect(() => {
    if (!modelReady || meshRegistryRef.current.length === 0) return
    const hasAnyAssignment = Object.keys(effectiveMaterials).length > 0
    for (const { mesh, partKey } of meshRegistryRef.current) {
      forEachMeshMaterial(mesh, (mat) => {
        if (!('emissive' in mat)) return
        if (showUnassigned && hasAnyAssignment) {
          const hasAssignment = !!resolvePartMaterial(partKey, effectiveMaterials)
          mat.emissive.set(hasAssignment ? 0x000000 : 0xff4400)
          mat.emissiveIntensity = hasAssignment ? 0 : 0.8
        } else {
          mat.emissive.set(0x000000)
          mat.emissiveIntensity = 0
        }
      })
    }
  }, [modelReady, showUnassigned, effectiveMaterials, resolvePartKey])

  // Reset isolateMode when no part is pinned
  useEffect(() => {
    if (!pinnedPart) setIsolateMode('none')
  }, [pinnedPart])

  // Reset hideAssigned when all assignments are cleared
  useEffect(() => {
    if (Object.keys(effectiveMaterials).length === 0) setHideAssigned(false)
  }, [effectiveMaterials])

  // Combined visibility effect — uses MeshRegistry instead of traverse
  useEffect(() => {
    if (!modelReady || meshRegistryRef.current.length === 0) return
    for (const { mesh, partKey } of meshRegistryRef.current) {
      const isSelected = partKey === pinnedPart
      const isAssigned = !!resolvePartMaterial(partKey, effectiveMaterials)

      // Default: fully visible + raycasting enabled
      mesh.visible = true
      mesh.raycast = THREE.Mesh.prototype.raycast
      forEachMeshMaterial(mesh, (mat) => {
        if ('opacity' in mat) { mat.opacity = 1; mat.transparent = false; mat.depthWrite = true; mat.needsUpdate = true }
      })

      // hideAssigned: hide all assigned meshes (except the currently selected part)
      if (hideAssigned && isAssigned && !isSelected) {
        mesh.visible = false
        mesh.raycast = () => {}
        continue
      }

      // isolateMode: ghost or hide non-selected meshes when a part is pinned
      if (!isSelected && pinnedPart && isolateMode !== 'none') {
        if (isolateMode === 'hide') {
          mesh.visible = false
          mesh.raycast = () => {}
        } else {
          forEachMeshMaterial(mesh, (mat) => {
            if ('opacity' in mat) { mat.opacity = 0.08; mat.transparent = true; mat.depthWrite = false; mat.needsUpdate = true }
          })
        }
      }
    }
  }, [modelReady, pinnedPart, isolateMode, hideAssigned, effectiveMaterials, resolvePartKey])

  // Keyboard shortcuts
  useEffect(() => {
    const down = (e: KeyboardEvent) => {
      // Ignore when typing in an input/textarea
      if ((e.target as HTMLElement).tagName === 'INPUT' || (e.target as HTMLElement).tagName === 'TEXTAREA') return
      if (e.key === 'f' || e.key === 'F') setFitTrigger(t => t + 1)
      if (e.key === 'w' || e.key === 'W') setWireframe(v => !v)
      if (e.key === 'g' || e.key === 'G') setShowGrid(v => !v)
      if (e.key === 's' || e.key === 'S') setShowShadows(v => !v)
      if (e.key === 'Escape') {
        if (pinnedPart) { setPinnedPart(null); return }
        onClose()
      }
    }
    window.addEventListener('keydown', down)
    return () => window.removeEventListener('keydown', down)
  }, [onClose, pinnedPart])

  function handleDownload(url: string, filename: string) {
    const a = document.createElement('a')
    a.href = url; a.download = filename
    document.body.appendChild(a); a.click(); document.body.removeChild(a)
  }

  // Performance mode: merge geometries by material to reduce draw calls
  useGeometryMerge({
    meshRegistryRef,
    partMaterials: effectiveMaterials,
    pbrMap,
    enabled: perfMode,
    sceneRef,
  })

  // Task 5 — hover: highlight mesh with emissive, restore on out
  const handlePointerOver = useCallback((e: any) => {
    e.stopPropagation()
    const mesh = e.object as THREE.Mesh
    const raw = (mesh?.userData?.name as string) || mesh?.name || (mesh?.parent?.userData?.name as string) || mesh?.parent?.name || ''
    const normalized = normalizeMeshName(raw) || 'Part'
    // Task 3: prefer userData.partKey (set by GLB node extras or Task 2 stamp) over
    // raw normalized name so tooltip shows canonical slug (e.g. "ring_outer") not OCC name
    const displayName = (mesh?.userData?.partKey as string | undefined) ?? resolvePartKey(normalized)
    setHoverInfo({ name: displayName, x: e.nativeEvent.clientX, y: e.nativeEvent.clientY })

    // Restore previous hovered mesh (array-safe)
    if (hoveredMeshRef.current && hoveredMeshRef.current !== mesh) {
      const prev = hoveredMeshRef.current
      if (!showUnassigned) {
        const prevMats = Array.isArray(prev.material) ? prev.material : [prev.material]
        prevMats.forEach((m) => {
          const mat = m as THREE.MeshStandardMaterial
          if (mat && 'emissive' in mat) { mat.emissive.set(0x000000); mat.emissiveIntensity = 0 }
        })
      }
    }

    // Highlight new mesh
    hoveredMeshRef.current = mesh
    const mats = mesh?.material
      ? (Array.isArray(mesh.material) ? mesh.material : [mesh.material])
      : []
    mats.forEach((m) => {
      const mat = m as THREE.MeshStandardMaterial
      if (mat && 'emissive' in mat) { mat.emissive.set(0x333333); mat.emissiveIntensity = 0.5 }
    })
  }, [showUnassigned, resolvePartKey])

  const handlePointerOut = useCallback(() => {
    setHoverInfo(null)
    if (hoveredMeshRef.current) {
      const mesh = hoveredMeshRef.current
      const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
      const hasAnyAssignment = Object.keys(effectiveMaterials).length > 0
      mats.forEach((m) => {
        const mat = m as THREE.MeshStandardMaterial
        if (!mat || !('emissive' in mat)) return
        const normalized = normalizeMeshName((mesh.userData?.name as string) || mesh.name)
        if (showUnassigned && hasAnyAssignment && !resolvePartMaterial(resolvePartKey(normalized), effectiveMaterials)) {
          mat.emissive.set(0xff4400); mat.emissiveIntensity = 0.8
        } else {
          mat.emissive.set(0x000000); mat.emissiveIntensity = 0
        }
      })
      hoveredMeshRef.current = null
    }
  }, [showUnassigned, effectiveMaterials, resolvePartKey])

  const handlePointerMove = useCallback((e: React.PointerEvent) => {
    setHoverInfo(prev => prev ? { ...prev, x: e.clientX, y: e.clientY } : null)
  }, [])

  // Task 7 — click to pin material panel (resolves to partKey slug when available)
  const handleClick = useCallback((e: any) => {
    e.stopPropagation()
    const mesh = e.object as THREE.Mesh
    const normalized = normalizeMeshName((mesh?.userData?.name as string) || mesh?.name || (mesh?.parent?.userData?.name as string) || mesh?.parent?.name || '')
    if (normalized) setPinnedPart(resolvePartKey(normalized))
  }, [resolvePartKey])

  return (
    <div className="fixed inset-0 z-50 flex flex-col bg-gray-950" onClick={() => setPinnedPart(null)}>

      {/* ── Toolbar ────────────────────────────────────────────────────────── */}
      <div
        className="flex items-center justify-between px-4 py-2 bg-gray-900 border-b border-gray-800 shrink-0 gap-2 flex-wrap"
        onClick={(e) => e.stopPropagation()}
      >
        <span className="text-white font-semibold tracking-wide text-sm shrink-0">3D Viewer</span>

        <div className="flex items-center gap-1.5 flex-wrap">

          {/* Wireframe */}
          <TBtn active={wireframe} onClick={() => setWireframe(v => !v)} title="Wireframe (W)">Wire</TBtn>

          {/* Performance mode */}
          <TBtn active={perfMode} onClick={() => setPerfMode(v => !v)} title="Performance mode — merges geometries, disables per-part hover">
            <Layers size={11} />
          </TBtn>

          {/* Projection: Perspective / Ortho */}
          <div className="flex rounded-md overflow-hidden border border-gray-700">
            {(['Persp', 'Ortho'] as const).map(label => {
              const ortho = label === 'Ortho'
              return (
                <button
                  key={label}
                  onClick={() => setIsOrtho(ortho)}
                  className={`px-2.5 py-1.5 text-xs font-medium transition-colors ${
                    isOrtho === ortho ? 'bg-accent text-white' : 'bg-gray-800 text-gray-300 hover:bg-gray-700'
                  }`}
                >
                  {label}
                </button>
              )
            })}
          </div>

          {/* Grid */}
          <TBtn active={showGrid} onClick={() => setShowGrid(v => !v)} title="Grid (G)">
            <Grid3X3 size={11} />
          </TBtn>

          {/* Contact shadows */}
          <TBtn active={showShadows} onClick={() => setShowShadows(v => !v)} title="Contact shadows (S)">
            <Sun size={11} />
          </TBtn>

          {/* Fit view */}
          <TBtn
            onClick={() => setFitTrigger(t => t + 1)}
            title="Fit to model (F)"
            disabled={!modelReady}
          >
            <Maximize2 size={11} />
          </TBtn>

          {/* Show unassigned toggle — with assignment count badge */}
          {modelReady && (
            <TBtn
              active={showUnassigned}
              onClick={() => setShowUnassigned(v => !v)}
              title={`Highlight unassigned parts (${assignedCount}/${totalMeshCount} assigned)`}
            >
              <AlertCircle size={11} />
              <span className="text-[10px] tabular-nums">{assignedCount}/{totalMeshCount}</span>
            </TBtn>
          )}

          {/* Hide assigned toggle */}
          {modelReady && Object.keys(effectiveMaterials).length > 0 && (
            <TBtn
              active={hideAssigned}
              onClick={() => setHideAssigned(v => !v)}
              title="Hide parts that already have a material assigned"
            >
              <EyeOff size={11} />
              <span className="text-[10px]">Hide assigned</span>
            </TBtn>
          )}

          {/* Reconciliation button — shown when manifest has unmatched/unassigned items */}
          {sceneManifest && (sceneManifest.unmatched_source_rows.length > 0 || sceneManifest.unassigned_parts.length > 0) && (
            <TBtn
              active={showReconcile}
              onClick={() => setShowReconcile(v => !v)}
              title={`${sceneManifest.unmatched_source_rows.length} unmatched source rows · ${sceneManifest.unassigned_parts.length} unassigned parts`}
            >
              <AlertTriangle size={11} />
              <span className="text-[10px] tabular-nums">
                {sceneManifest.unmatched_source_rows.length + sceneManifest.unassigned_parts.length}
              </span>
            </TBtn>
          )}

          {/* Environment */}
          <EnvDropdown value={envPreset} onChange={setEnvPreset} />

          {/* Background colour swatches */}
          <div className="flex items-center gap-0.5 bg-gray-800 border border-gray-700 rounded-md px-1.5 py-1">
            {BG_COLORS.map(c => (
              <button
                key={c.value}
                onClick={() => setBgColor(c.value)}
                title={c.label}
                className="w-3.5 h-3.5 rounded-sm border transition-all"
                style={{
                  backgroundColor: c.value,
                  borderColor: bgColor === c.value ? '#fff' : 'transparent',
                  transform: bgColor === c.value ? 'scale(1.25)' : 'scale(1)',
                }}
              />
            ))}
          </div>

          {/* Downloads */}
          {downloadUrls?.glb && (
            <button
              onClick={() => handleDownload(downloadUrls.glb!, `${cadFileId}_geometry.glb`)}
              className="flex items-center gap-1 px-2.5 py-1.5 rounded-md bg-gray-700 hover:bg-gray-600 text-white text-xs font-medium"
            >
              <Download size={11} /> GLB
            </button>
          )}
          {downloadUrls?.blend && (
            <button
              onClick={() => handleDownload(downloadUrls.blend!, `${cadFileId}.blend`)}
              className="flex items-center gap-1 px-2.5 py-1.5 rounded-md bg-gray-700 hover:bg-gray-600 text-white text-xs font-medium"
            >
              <Download size={11} /> .blend
            </button>
          )}

          {/* Capture thumbnail */}
          <button
            onClick={() => setCapturing(true)}
            disabled={capturing || !modelReady || !!loadError}
            className="flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-accent hover:bg-accent-hover disabled:opacity-40 disabled:cursor-not-allowed text-white text-xs font-medium transition-colors"
          >
            {capturing ? <Loader2 size={12} className="animate-spin" /> : <Camera size={12} />}
            {capturing ? 'Capturing…' : 'Capture'}
          </button>

          {/* Close */}
          <button
            onClick={onClose}
            className="p-1.5 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 transition-colors"
            aria-label="Close viewer"
          >
            <X size={18} />
          </button>
        </div>
      </div>

      {/* ── Viewport ───────────────────────────────────────────────────────── */}
      {/* onClick stops propagation so mesh-clicks don't bubble to the outer setPinnedPart(null) */}
      <div className="relative flex-1" onPointerMove={handlePointerMove} onClick={(e) => e.stopPropagation()}>

        {/* Error state */}
        {loadError && (
          <div className="absolute inset-0 flex flex-col items-center justify-center bg-gray-900 text-white gap-4 z-20">
            <AlertTriangle size={48} className="text-red-400" />
            <p className="text-lg font-semibold">Failed to load 3D model</p>
            <p className="text-sm text-gray-400 max-w-sm text-center">{loadError}</p>
            <button onClick={onClose} className="mt-2 px-4 py-2 rounded-md bg-gray-700 hover:bg-gray-600 text-sm">Close</button>
          </div>
        )}

        {/* Loading overlay */}
        {!modelReady && !loadError && <LoadingOverlay />}

        {/* Dimension badge — bottom-left */}
        {modelReady && dims && (
          <div className="absolute bottom-8 left-4 z-10 pointer-events-none select-none">
            <div className="bg-black/60 backdrop-blur-sm text-white text-[11px] font-mono px-2.5 py-1 rounded-md">
              {dims.x.toFixed(1)} × {dims.y.toFixed(1)} × {dims.z.toFixed(1)} mm
            </div>
          </div>
        )}

        {/* Part-name tooltip */}
        {hoverInfo && !pinnedPart && (
          <div
            className="fixed z-20 pointer-events-none select-none bg-gray-900/90 backdrop-blur-sm text-white text-xs px-2 py-1 rounded shadow-lg"
            style={{ left: hoverInfo.x + 14, top: hoverInfo.y - 30 }}
          >
            {hoverInfo.name}
          </div>
        )}

        {/* Task 7 — Material assignment panel (pinned) */}
        {pinnedPart && (
          <MaterialPanel
            partName={pinnedPart}
            cadFileId={cadFileId}
            currentEntry={resolvePartMaterial(pinnedPart, effectiveMaterials)}
            partMaterials={effectiveMaterials}
            onClose={() => setPinnedPart(null)}
            isolateMode={isolateMode}
            onIsolateModeChange={setIsolateMode}
            sourcePartName={sceneManifest?.parts.find(p => p.part_key === pinnedPart)?.source_name}
            assignmentProvenance={sceneManifest?.parts.find(p => p.part_key === pinnedPart)?.assignment_provenance}
            isPartKeyMode={Object.keys(partKeyMap).length > 0}
            manualOverrides={manualOverrides}
            pbrMap={pbrMap}
          />
        )}

        {/* Reconciliation panel */}
        {showReconcile && sceneManifest && (
          <div
            className="absolute top-2 right-2 z-30 w-64 bg-gray-900 border border-gray-700 rounded-lg shadow-2xl max-h-[70vh] overflow-y-auto"
            onClick={(e) => e.stopPropagation()}
          >
            <div className="flex items-center justify-between px-3 py-2 border-b border-gray-700">
              <span className="text-white text-xs font-semibold flex items-center gap-1.5">
                <AlertTriangle size={12} className="text-amber-400" /> Reconciliation
              </span>
              <button onClick={() => setShowReconcile(false)} className="text-gray-400 hover:text-white p-0.5">
                <X size={14} />
              </button>
            </div>
            <div className="p-3 space-y-3">
              {sceneManifest.unassigned_parts.length > 0 && (
                <div>
                  <p className="text-gray-400 text-[10px] font-medium mb-1.5 uppercase tracking-wider">
                    Unassigned parts ({sceneManifest.unassigned_parts.length})
                  </p>
                  {sceneManifest.unassigned_parts.map(pk => (
                    <button
                      key={pk}
                      onClick={() => { setPinnedPart(pk); setShowReconcile(false) }}
                      className="block w-full text-left px-2 py-1 text-xs text-gray-300 hover:bg-gray-800 hover:text-white rounded transition-colors truncate"
                      title={pk}
                    >
                      {pk}
                    </button>
                  ))}
                </div>
              )}
              {sceneManifest.unmatched_source_rows.length > 0 && (
                <div>
                  <p className="text-gray-400 text-[10px] font-medium mb-1.5 uppercase tracking-wider">
                    Unmatched source rows ({sceneManifest.unmatched_source_rows.length})
                  </p>
                  {sceneManifest.unmatched_source_rows.map((row, i) => (
                    <div
                      key={i}
                      className="px-2 py-1 text-xs text-gray-500 truncate"
                      title={row}
                    >
                      {row}
                    </div>
                  ))}
                </div>
              )}
            </div>
          </div>
        )}

        {/* Keyboard hint — bottom-right */}
        <div className="absolute bottom-2 right-16 z-10 pointer-events-none select-none text-gray-600 text-[10px]">
          F fit · W wire · G grid · S shadow · click part to assign · Esc close
        </div>

        <WebGLErrorBoundary>
        <Canvas
          gl={{ preserveDrawingBuffer: true, powerPreference: 'high-performance' }}
          dpr={[1, 1.5]}
          style={{ width: '100%', height: '100%' }}
          onPointerMissed={() => setPinnedPart(null)}
        >
          {/* Background colour */}
          <SceneBackground color={bgColor} />

          {/* Camera — switch between perspective and orthographic */}
          {isOrtho
            ? <OrthographicCamera makeDefault near={-2000} far={2000} position={camPosRef.current} />
            : <PerspectiveCamera makeDefault fov={45} near={0.0001} far={5000} position={camPosRef.current} />
          }

          {/* Track camera position for smooth camera type switching */}
          <CameraPositionTracker posRef={camPosRef} />

          {/* Lights */}
          <ambientLight intensity={0.5} />
          <directionalLight position={[5, 10, 7]}   intensity={1.0} castShadow />
          <directionalLight position={[-5, -5, -5]} intensity={0.25} />

          {/* Model */}
          {activeUrl && (
            <GltfErrorBoundary key={rawActiveUrl} onError={handleError}>
              <Suspense fallback={null}>
                <ModelWithReady
                  key={activeUrl}
                  url={activeUrl}
                  wireframe={wireframe}
                  onReady={handleModelReady}
                  sceneRef={sceneRef}
                  onPointerOver={perfMode ? undefined : handlePointerOver}
                  onPointerOut={perfMode ? undefined : handlePointerOut}
                  onClick={perfMode ? undefined : handleClick}
                />
              </Suspense>
            </GltfErrorBoundary>
          )}

          {/* Ground grid */}
          {showGrid && sceneInfo && (
            <gridHelper
              args={[
                sceneInfo.maxDim * 6,
                Math.round(sceneInfo.maxDim * 6 / (sceneInfo.maxDim / 10)),
                new THREE.Color('#3a3a3a'),
                new THREE.Color('#222222'),
              ]}
              position={[sceneInfo.center.x, sceneInfo.groundY - 0.0001, sceneInfo.center.z]}
            />
          )}

          {/* Contact shadows */}
          {showShadows && sceneInfo && (
            <ContactShadows
              position={[sceneInfo.center.x, sceneInfo.groundY, sceneInfo.center.z]}
              opacity={0.55}
              scale={sceneInfo.maxDim * 4}
              blur={1.5}
              far={sceneInfo.maxDim * 2}
            />
          )}

          {/* Orbit controls */}
          <OrbitControls
            ref={controlsRef}
            enablePan
            enableZoom
            enableRotate
          />

          {/* Environment lighting */}
          <Environment preset={envPreset} />

          {/* Orientation gizmo (always visible, bottom-right) */}
          <GizmoHelper alignment="bottom-right" margin={[70, 70]}>
            <GizmoViewport
              axisColors={['#e74c3c', '#2ecc71', '#3498db']}
              labelColor="white"
            />
          </GizmoHelper>

          {/* Camera auto-fit */}
          <CameraFit
            sceneRef={sceneRef}
            controlsRef={controlsRef}
            fitTrigger={fitTrigger}
            isOrtho={isOrtho}
            onFitted={handleFitted}
          />

          {/* Screenshot capture */}
          {capturing && (
            <ScreenshotCapture
              enabled={capturing}
              cadFileId={cadFileId}
              onDone={handleCaptureDone}
            />
          )}
        </Canvas>
        </WebGLErrorBoundary>
      </div>
    </div>
  )
}
