import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Canvas, useThree } from '@react-three/fiber'
import { OrbitControls, useGLTF, Environment } from '@react-three/drei'
import * as THREE from 'three'
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
import { Loader2, Box, RefreshCw, Grid3X3, Layers, Sun, AlertCircle, EyeOff, Zap } from 'lucide-react'
import { toast } from 'sonner'
import { listMediaAssets as getMediaAssets } from '../../api/media'
import { generateGltfGeometry, getPartMaterials, type PartMaterialMap } from '../../api/cad'
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'

type ViewMode = 'solid' | 'wireframe'
type LightPreset = 'studio' | 'warehouse' | 'sunset' | 'park' | 'city'

const LIGHT_PRESETS: { id: LightPreset; label: string }[] = [
  { id: 'studio', label: 'Studio' },
  { id: 'warehouse', label: 'Warehouse' },
  { id: 'sunset', label: 'Sunset' },
  { id: 'park', label: 'Park' },
  { id: 'city', label: 'City' },
]

// ---------------------------------------------------------------------------
// CameraAutoFit — auto-fits camera to model bounding box on first load
// ---------------------------------------------------------------------------

function CameraAutoFit({
  sceneRef,
  controlsRef,
  fitTrigger,
}: {
  sceneRef: React.MutableRefObject<THREE.Object3D | null>
  controlsRef: React.RefObject<any>
  fitTrigger: number
}) {
  const { camera, size } = useThree()

  useEffect(() => {
    if (fitTrigger === 0 || !sceneRef.current) return
    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 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.05, center.y + maxDim * 0.2, 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)
      controlsRef.current.minDistance = maxDim * 0.05
      controlsRef.current.maxDistance = maxDim * 20
      controlsRef.current.update()
    }
  }, [fitTrigger]) // eslint-disable-line react-hooks/exhaustive-deps

  return null
}

// ---------------------------------------------------------------------------
// GlbModelWithFit — loads GLB, stores scene ref, signals ready, pointer events
// ---------------------------------------------------------------------------

function GlbModelWithFit({
  url,
  wireframe,
  sceneRef,
  onReady,
  onPointerOver,
  onPointerOut,
  onClick,
}: {
  url: string
  wireframe: boolean
  sceneRef: React.MutableRefObject<THREE.Object3D | null>
  onReady: () => void
  onPointerOver?: (e: any) => void
  onPointerOut?: () => void
  onClick?: (e: any) => void
}) {
  const { scene } = useGLTF(url)
  const cloned = useRef<THREE.Group | null>(null)

  if (!cloned.current) {
    cloned.current = scene.clone(true)
    cloned.current.traverse((obj) => {
      if (obj instanceof THREE.Mesh) {
        if (obj.geometry) {
          let geo = obj.geometry.clone()
          if (!geo.index) geo = mergeVertices(geo)
          // Only compute normals if the geometry doesn't already have them.
          // GLBs from our pipeline include smooth normals — overwriting them
          // with computeVertexNormals() produces flat/faceted shading.
          if (!geo.attributes.normal) geo.computeVertexNormals()
          obj.geometry = geo
        }
        // Clone materials so emissive / color changes don't affect the shared GLTF cache
        if (obj.material) {
          obj.material = Array.isArray(obj.material)
            ? obj.material.map((m: THREE.Material) => m.clone())
            : obj.material.clone()
        }
      }
    })
  }

  useEffect(() => {
    cloned.current?.traverse((obj) => {
      if (obj instanceof THREE.Mesh && obj.material) {
        const mats = Array.isArray(obj.material) ? obj.material : [obj.material]
        mats.forEach((m) => {
          ;(m as THREE.MeshStandardMaterial).wireframe = wireframe
          m.needsUpdate = true
        })
      }
    })
  }, [wireframe])

  useEffect(() => {
    sceneRef.current = cloned.current
    onReady()
  }, []) // eslint-disable-line react-hooks/exhaustive-deps

  return (
    <primitive
      object={cloned.current}
      onPointerOver={onPointerOver}
      onPointerOut={onPointerOut}
      onClick={onClick}
    />
  )
}

const HEIGHT = 560

function ToolbarBtn({
  active, onClick, children, title,
}: { active: boolean; onClick: () => void; children: React.ReactNode; title?: string }) {
  return (
    <button
      title={title}
      onClick={onClick}
      className={`px-2 py-1 text-[11px] flex items-center gap-1 transition-colors rounded ${
        active ? 'bg-white/20 text-white' : 'text-white/50 hover:text-white/80 hover:bg-white/10'
      }`}
    >
      {children}
    </button>
  )
}

export default function InlineCadViewer({
  cadFileId,
  thumbnailUrl,
  initialPartMaterials,
}: {
  cadFileId: string
  thumbnailUrl?: string | null
  initialPartMaterials?: PartMaterialMap
}) {
  const token = useAuthStore((s) => s.token)
  const qc = useQueryClient()

  // GLB source / display state
  const [glbBlobUrl, setGlbBlobUrl] = useState<string | null>(null)
  const [loadingGlb, setLoadingGlb] = useState(false)
  const [generating, setGenerating] = useState(false)
  const [viewMode, setViewMode] = useState<ViewMode>('solid')
  const [lightPreset, setLightPreset] = useState<LightPreset>('studio')
  const [modelReady, setModelReady] = useState(false)
  const [fitTrigger, setFitTrigger] = useState(0)

  // Material assignment state
  const [pinnedPart, setPinnedPart] = useState<string | null>(null)
  const [showUnassigned, setShowUnassigned] = useState(false)
  const [hideAssigned, setHideAssigned] = useState(false)
  const [isolateMode, setIsolateMode] = useState<IsolateMode>('none')
  const [perfMode, setPerfMode] = useState(false)
  const [totalMeshCount, setTotalMeshCount] = useState(0)
  const [glbMeshNames, setGlbMeshNames] = useState<Set<string>>(new Set())
  const [partKeyMap, setPartKeyMap] = useState<Record<string, string>>({})

  const sceneRef    = useRef<THREE.Object3D | null>(null)
  const controlsRef = useRef<any>(null)
  const hoveredMeshRef = useRef<THREE.Mesh | null>(null)
  const meshRegistryRef = useRef<MeshRegistryEntry[]>([])

  // Media asset queries
  const { data: gltfAssets } = useQuery({
    queryKey: ['media-assets', cadFileId, 'gltf_geometry'],
    queryFn: () => getMediaAssets({ cad_file_id: cadFileId, asset_types: ['gltf_geometry'] }),
    staleTime: 0,
    refetchInterval: generating ? 4_000 : false,
  })

  // Part-material assignments — from CadFile (manual assignments in viewer)
  const { data: savedPartMaterials = {} } = useQuery({
    queryKey: ['part-materials', cadFileId],
    queryFn: () => getPartMaterials(cadFileId),
    staleTime: 30_000,
    retry: false,
  })

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

  // Merge: initialPartMaterials (from Product Excel data) as base; savedPartMaterials overrides
  // Remap keys through partKeyMap so Excel-imported names match partKey slugs
  const partMaterials = useMemo(
    () => remapToPartKeys({ ...initialPartMaterials, ...savedPartMaterials } as PartMaterialMap, partKeyMap),
    [initialPartMaterials, savedPartMaterials, 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, partMaterials)).length,
    [glbMeshNames, partMaterials],
  )

  useEffect(() => {
    if (generating && gltfAssets && gltfAssets.length > 0) setGenerating(false)
  }, [generating, gltfAssets])

  const hasGeometry = (gltfAssets?.length ?? 0) > 0

  const activeDownloadUrl = gltfAssets?.[0]?.download_url

  // Fetch active GLB as blob URL (needs auth header)
  useEffect(() => {
    if (!activeDownloadUrl || !token) return
    setGlbBlobUrl(null)
    setModelReady(false)
    setLoadingGlb(true)
    let blobUrl = ''
    fetch(activeDownloadUrl, { headers: { Authorization: `Bearer ${token}` } })
      .then((r) => r.blob())
      .then((blob) => {
        blobUrl = URL.createObjectURL(blob)
        setGlbBlobUrl(blobUrl)
      })
      .catch(() => toast.error('Failed to load 3D model'))
      .finally(() => setLoadingGlb(false))
    return () => { if (blobUrl) URL.revokeObjectURL(blobUrl) }
  }, [activeDownloadUrl, token])

  // Apply saved material colors + PBR properties after model loads (uses MeshRegistry)
  useEffect(() => {
    if (!modelReady || meshRegistryRef.current.length === 0) return
    if (Object.keys(pbrMap).length === 0) return
    for (const { mesh, partKey } of meshRegistryRef.current) {
      const entry = resolvePartMaterial(partKey, partMaterials as PartMaterialMap)
      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, partMaterials, resolvePartKey, pbrMap])

  // Unassigned glow — uses MeshRegistry instead of traverse
  useEffect(() => {
    if (!modelReady || meshRegistryRef.current.length === 0) return
    const hasAnyAssignment = Object.keys(partMaterials).length > 0
    for (const { mesh, partKey } of meshRegistryRef.current) {
      forEachMeshMaterial(mesh, (mat) => {
        if (!('emissive' in mat)) return
        if (showUnassigned && hasAnyAssignment) {
          const assigned = !!resolvePartMaterial(partKey, partMaterials as PartMaterialMap)
          mat.emissive.set(assigned ? 0x000000 : 0xff4400)
          mat.emissiveIntensity = assigned ? 0 : 0.8
        } else {
          mat.emissive.set(0x000000)
          mat.emissiveIntensity = 0
        }
      })
    }
  }, [modelReady, showUnassigned, partMaterials, 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(partMaterials).length === 0) setHideAssigned(false)
  }, [partMaterials])

  // 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, partMaterials)

      // 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, partMaterials, resolvePartKey])

  // Dev-only: log normalized GLB mesh names vs stored keys to diagnose mismatches
  useEffect(() => {
    if (!import.meta.env.DEV || !modelReady || meshRegistryRef.current.length === 0) return
    const names = new Set<string>(meshRegistryRef.current.map(e => e.partKey))
    const keys = Object.keys(partMaterials)
    const matched = keys.filter(k => names.has(k))
    const unmatched = keys.filter(k => !names.has(k))
    console.debug('[CAD] Match status:', {
      totalGlbMeshes: names.size,
      totalStoredKeys: keys.length,
      matched: matched.length,
      unmatched: unmatched.length,
      unmatchedKeys: unmatched,
      glbNames: [...names].sort(),
    })
  }, [modelReady, partMaterials])

  const generateMut = useMutation({
    mutationFn: () => generateGltfGeometry(cadFileId),
    onSuccess: () => {
      toast.info('Generating 3D model…')
      setGenerating(true)
      qc.invalidateQueries({ queryKey: ['media-assets', cadFileId, 'gltf_geometry'] })
    },
    onError: () => toast.error('Failed to queue GLB generation'),
  })

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

  // Hover highlight
  const handlePointerOver = useCallback((e: any) => {
    e.stopPropagation()
    const mesh = e.object as THREE.Mesh
    // Restore previous hovered mesh (correctly preserve unassigned glow)
    if (hoveredMeshRef.current && hoveredMeshRef.current !== mesh) {
      const prev = hoveredMeshRef.current
      const prevMats = Array.isArray(prev.material) ? prev.material : [prev.material]
      const hasAny = Object.keys(partMaterials).length > 0
      prevMats.forEach((m) => {
        const mat = m as THREE.MeshStandardMaterial
        if (!mat || !('emissive' in mat)) return
        const prevPk = (prev.userData?.partKey as string) || resolvePartKey(normalizeMeshName((prev.userData?.name as string) || prev.name))
        if (showUnassigned && hasAny && !resolvePartMaterial(prevPk, partMaterials as PartMaterialMap)) {
          mat.emissive.set(0xff4400); mat.emissiveIntensity = 0.8
        } else {
          mat.emissive.set(0x000000); mat.emissiveIntensity = 0
        }
      })
    }
    hoveredMeshRef.current = mesh
    const mats = 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, partMaterials, resolvePartKey])

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

  const handleClick = useCallback((e: any) => {
    e.stopPropagation()
    const meshObj = e.object as THREE.Mesh
    const pk = (meshObj?.userData?.partKey as string) || resolvePartKey(normalizeMeshName((meshObj?.userData?.name as string) || meshObj?.name || ''))
    if (pk) setPinnedPart(pk)
  }, [resolvePartKey])

  // ── Render: model loaded ──────────────────────────────────────────────────

  if (glbBlobUrl) {
    const pm = partMaterials as PartMaterialMap

    return (
      <div
        className="w-full rounded-lg border border-border-default bg-gray-950 flex flex-col overflow-hidden"
        style={{ height: HEIGHT }}
        onClick={() => setPinnedPart(null)}
      >
        {/* ── Toolbar row — real block element above the canvas ── */}
        <div
          className="shrink-0 flex items-center gap-0.5 px-2 py-1 bg-black/70 border-b border-white/10 flex-wrap"
          onClick={(e) => e.stopPropagation()}
        >
          {/* View mode */}
          <ToolbarBtn active={viewMode === 'solid'} onClick={() => setViewMode('solid')} title="Solid">
            <Layers size={11} /> Solid
          </ToolbarBtn>
          <ToolbarBtn active={viewMode === 'wireframe'} onClick={() => setViewMode('wireframe')} title="Wireframe">
            <Grid3X3 size={11} /> Wire
          </ToolbarBtn>

          <div className="w-px h-4 bg-white/10 mx-0.5" />

          {/* Lighting */}
          <Sun size={11} className="text-white/30 mx-1" />
          {LIGHT_PRESETS.map((p) => (
            <ToolbarBtn key={p.id} active={lightPreset === p.id} onClick={() => setLightPreset(p.id)} title={p.label}>
              {p.label}
            </ToolbarBtn>
          ))}

          <div className="w-px h-4 bg-white/10 mx-0.5" />

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

          {/* Show unassigned + hide assigned toggles */}
          {modelReady && (
            <>
              <div className="w-px h-4 bg-white/10 mx-0.5" />
              <ToolbarBtn
                active={showUnassigned}
                onClick={() => setShowUnassigned(v => !v)}
                title={`Highlight unassigned parts (${assignedCount}/${totalMeshCount} assigned)`}
              >
                <AlertCircle size={11} />
                <span className="tabular-nums text-[10px]">{assignedCount}/{totalMeshCount}</span>
              </ToolbarBtn>
              {assignedCount > 0 && (
                <ToolbarBtn
                  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>
                </ToolbarBtn>
              )}
            </>
          )}
        </div>

        {/* ── Canvas area ── */}
        <div className="flex-1 relative" onClick={(e) => e.stopPropagation()}>
          <WebGLErrorBoundary>
          <Canvas
            gl={{ powerPreference: 'high-performance', antialias: true }}
            dpr={[1, 1.5]}
            camera={{ position: [0, 0, 2], fov: 45 }}
          >
            <Suspense fallback={null}>
              <Environment preset={lightPreset} background={false} />
              <GlbModelWithFit
                key={glbBlobUrl}
                url={glbBlobUrl}
                wireframe={viewMode === 'wireframe'}
                sceneRef={sceneRef}
                onReady={() => {
                  // Extract partKeyMap from 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 partKey from parent Group or partKeyMap
                    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
                      }
                    }
                    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))
                  setModelReady(true)
                  setFitTrigger(t => t + 1)
                }}
                onPointerOver={perfMode ? undefined : handlePointerOver}
                onPointerOut={perfMode ? undefined : handlePointerOut}
                onClick={perfMode ? undefined : handleClick}
              />
            </Suspense>
            <OrbitControls ref={controlsRef} makeDefault />
            <CameraAutoFit sceneRef={sceneRef} controlsRef={controlsRef} fitTrigger={fitTrigger} />
          </Canvas>
          </WebGLErrorBoundary>

          {/* Material assignment panel */}
          {pinnedPart && (
            <MaterialPanel
              partName={pinnedPart}
              cadFileId={cadFileId}
              currentEntry={resolvePartMaterial(pinnedPart, pm)}
              partMaterials={pm}
              onClose={() => setPinnedPart(null)}
              isolateMode={isolateMode}
              onIsolateModeChange={setIsolateMode}
              pbrMap={pbrMap}
            />
          )}

          {/* Hint */}
          <div className="absolute bottom-1.5 right-2 text-gray-600 text-[10px] pointer-events-none select-none">
            click part to assign material
          </div>
        </div>
      </div>
    )
  }

  // ── Render: loading ───────────────────────────────────────────────────────

  if (loadingGlb) {
    return (
      <div
        className="w-full rounded-lg border border-border-default bg-surface-muted flex items-center justify-center"
        style={{ height: HEIGHT }}
      >
        <div className="flex flex-col items-center gap-2 text-content-muted">
          <Loader2 size={28} className="animate-spin" />
          <span className="text-xs">Loading 3D model…</span>
        </div>
      </div>
    )
  }

  // ── Render: no GLB yet ────────────────────────────────────────────────────

  return (
    <div
      className="w-full rounded-lg border border-border-default bg-surface-muted flex flex-col items-center justify-center gap-3"
      style={{ height: HEIGHT }}
    >
      {thumbnailUrl ? (
        <img src={thumbnailUrl} alt="CAD thumbnail" className="max-h-52 object-contain" />
      ) : (
        <Box size={48} className="text-content-muted" />
      )}
      <button
        className="btn-secondary text-xs"
        onClick={() => generateMut.mutate()}
        disabled={generateMut.isPending || generating}
        title="Export geometry GLB from STEP via OCC and load 3D viewer"
      >
        <RefreshCw size={12} className={generating ? 'animate-spin' : ''} />
        {generating ? 'Generating…' : generateMut.isPending ? 'Queuing…' : 'Load 3D Model'}
      </button>
    </div>
  )
}
