/* global THREE */
// 3D viewer for a single mech.
// - If a GLB full-body model is selected, loads it via THREE.GLTFLoader.
// - If an STL head model is selected, loads it via THREE.STLLoader.
// - If user drops an .stl file onto the canvas, loads it.
// - Otherwise builds a placeholder mech-head silhouette out of boxes colored by the mech's palette.
// Controls: orbit (drag), zoom (wheel), keyboard reset (R).

function getViewerAssetExtension(url, fallback) {
  const cleanUrl = (url || "").split("?")[0].split("#")[0];
  const match = cleanUrl.match(/\.([a-z0-9]+)$/i);
  return match ? match[1].toLowerCase() : fallback;
}

const VIEWER_ASSET_REFRESH_VERSION = "20260622-mk23-release";
const VIEWER_ARCHIVE_ASSET_BASE_URL = "https://sin1.contabostorage.com/48ea6e0cdf344a96ad45b1c7eb1c766a:anomaliexperiment/assets/";

function versionViewerAssetUrl(url) {
  if (!url || /^(?:data|blob):/i.test(url) || url.includes("assetv=")) return url;
  const resolved = url.startsWith("assets/")
    ? VIEWER_ARCHIVE_ASSET_BASE_URL + url.slice("assets/".length)
    : url;
  const hashIndex = resolved.indexOf("#");
  const path = hashIndex >= 0 ? resolved.slice(0, hashIndex) : resolved;
  const hash = hashIndex >= 0 ? resolved.slice(hashIndex) : "";
  return path + (path.includes("?") ? "&" : "?") + "assetv=" + VIEWER_ASSET_REFRESH_VERSION + hash;
}

function cleanViewerAssetName(url) {
  return (url || "").split("?")[0].split("#")[0].split("/").pop();
}

function MekaViewer({ mech, model }) {
  const wrapRef = React.useRef(null);
  const stateRef = React.useRef({});
  const loadTokenRef = React.useRef(0);
  const [status, setStatus] = React.useState("INIT");
  const [hasModel, setHasModel] = React.useState(false);
  const [wireframe, setWireframe] = React.useState(false);
  const [autoRotate, setAutoRotate] = React.useState(true);
  const [modelName, setModelName] = React.useState(null);
  const [loadState, setLoadState] = React.useState({ active: false, kind: null, loaded: 0, total: 0, percent: null });
  const [bgIdx, setBgIdx] = React.useState(0);

  const PEDESTAL_TOP_Y = -40;
  const backgrounds = [
    "radial-gradient(circle at 50% 36%, #f2f2f0 0%, #d7d8d6 48%, #bfc0bd 100%)",
    "radial-gradient(circle at 50% 38%, rgba(255,238,170,0.08) 0%, rgba(65,58,41,0.24) 34%, rgba(21,20,17,0.96) 73%, #080706 100%)",
    "radial-gradient(circle at 50% 38%, rgba(120,220,255,0.08) 0%, rgba(36,63,66,0.24) 34%, rgba(13,24,27,0.96) 73%, #070a0c 100%)",
    "radial-gradient(circle at 50% 38%, rgba(255,132,82,0.08) 0%, rgba(71,45,35,0.24) 34%, rgba(25,18,15,0.96) 73%, #080604 100%)"
  ];
  const activeModel = model || (mech.fullbodyModel
    ? {
        id: "fullbody",
        label: "FULLBODY",
        kind: "glb",
        url: versionViewerAssetUrl(mech.fullbodyModel),
        downloadUrl: versionViewerAssetUrl(mech.fullbodyDownloadModel || mech.fullbodyModel),
        downloadName: mech.id + "_" + mech.codename.replace(/\s+/g, "_") + "_FULLBODY." + getViewerAssetExtension(mech.fullbodyDownloadModel || mech.fullbodyModel, "glb")
      }
    : (mech.stl
      ? {
          id: "head",
          label: "HEAD",
          kind: "stl",
          url: versionViewerAssetUrl(mech.stl),
          downloadUrl: versionViewerAssetUrl(mech.stl),
          downloadName: mech.id + "_" + mech.codename.replace(/\s+/g, "_") + "_HEAD.stl"
        }
      : { id: "placeholder", label: "PROCEDURAL", kind: "placeholder", url: null, downloadUrl: null, downloadName: null }));

  // initialize scene
  React.useEffect(() => {
    const wrap = wrapRef.current;
    if (!wrap) return;
    const isMobileViewer = typeof window !== "undefined"
      && window.matchMedia
      && window.matchMedia("(max-width: 720px), (pointer: coarse)").matches;
    const dprCap = isMobileViewer ? 1.15 : 1.75;
    const roundSegments = isMobileViewer ? 48 : 96;

    const scene = new THREE.Scene();
    scene.background = null;
    scene.fog = new THREE.Fog(0xd4d5d3, 380, 860);
    wrap.style.background = backgrounds[0];

    const w = wrap.clientWidth, h = wrap.clientHeight;
    const camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 2000);
    camera.position.set(0, 24, 155);

    const renderer = new THREE.WebGLRenderer({
      antialias: !isMobileViewer,
      alpha: true,
      powerPreference: isMobileViewer ? "default" : "high-performance"
    });
    renderer.setPixelRatio(Math.min(dprCap, window.devicePixelRatio || 1));
    renderer.setSize(w, h);
    renderer.setClearColor(0x000000, 0);
    renderer.shadowMap.enabled = !isMobileViewer;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.outputEncoding = THREE.sRGBEncoding;
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 0.96;
    wrap.appendChild(renderer.domElement);

    // Thumbnail-style studio lighting: soft, bright, and forgiving on imperfect uploaded meshes.
    const hemi = new THREE.HemisphereLight(0xffffff, 0x24272f, 0.9);
    scene.add(hemi);

    const key = new THREE.DirectionalLight(0xffffff, 2.75);
    key.position.set(72, 130, 96);
    key.castShadow = !isMobileViewer;
    key.shadow.mapSize.set(isMobileViewer ? 512 : 1536, isMobileViewer ? 512 : 1536);
    key.shadow.bias = -0.00015;
    key.shadow.camera.left = -120;
    key.shadow.camera.right = 120;
    key.shadow.camera.top = 120;
    key.shadow.camera.bottom = -120;
    key.shadow.camera.near = 10;
    key.shadow.camera.far = 320;
    scene.add(key);

    const rim = new THREE.DirectionalLight(0x9ed0ff, 1.55);
    rim.position.set(-95, 82, -110);
    scene.add(rim);

    const fill = new THREE.DirectionalLight(0xfff2d0, 0.95);
    fill.position.set(-68, 38, 88);
    scene.add(fill);

    const top = new THREE.DirectionalLight(0xffffff, 0.55);
    top.position.set(0, 150, 20);
    scene.add(top);

    scene.add(new THREE.AmbientLight(0xffffff, 0.36));

    // ground turntable
    const turntable = new THREE.Mesh(
      new THREE.CylinderGeometry(50, 52, 3.4, roundSegments),
      new THREE.MeshStandardMaterial({ color: 0x28282d, metalness: 0.05, roughness: 0.86 })
    );
    turntable.position.y = PEDESTAL_TOP_Y - 1.7;
    turntable.receiveShadow = !isMobileViewer;
    scene.add(turntable);

    const pedestalRing = new THREE.Mesh(
      new THREE.TorusGeometry(49.5, 0.7, 8, roundSegments),
      new THREE.MeshStandardMaterial({
        color: 0xf5cf3a,
        emissive: 0x3a2b08,
        emissiveIntensity: 0.25,
        metalness: 0.3,
        roughness: 0.42
      })
    );
    pedestalRing.position.y = PEDESTAL_TOP_Y + 0.25;
    pedestalRing.rotation.x = Math.PI / 2;
    scene.add(pedestalRing);

    const contactCanvas = document.createElement("canvas");
    contactCanvas.width = 256;
    contactCanvas.height = 256;
    const shadowCtx = contactCanvas.getContext("2d");
    const shadowGradient = shadowCtx.createRadialGradient(128, 128, 12, 128, 128, 118);
    shadowGradient.addColorStop(0, "rgba(0,0,0,0.36)");
    shadowGradient.addColorStop(0.42, "rgba(0,0,0,0.20)");
    shadowGradient.addColorStop(1, "rgba(0,0,0,0)");
    shadowCtx.fillStyle = shadowGradient;
    shadowCtx.fillRect(0, 0, 256, 256);
    const contactTexture = new THREE.CanvasTexture(contactCanvas);
    const contactShadow = new THREE.Mesh(
      new THREE.CircleGeometry(42, roundSegments),
      new THREE.MeshBasicMaterial({ map: contactTexture, transparent: true, depthWrite: false })
    );
    contactShadow.position.y = PEDESTAL_TOP_Y + 0.06;
    contactShadow.rotation.x = -Math.PI / 2;
    contactShadow.renderOrder = 3;
    scene.add(contactShadow);

    // grid floor
    const grid = new THREE.GridHelper(400, 40, 0x3a352c, 0x2a2620);
    grid.position.y = PEDESTAL_TOP_Y - 4;
    grid.material.opacity = 0.4;
    grid.material.transparent = true;
    scene.add(grid);

    // model group
    const modelGroup = new THREE.Group();
    scene.add(modelGroup);

    // controls
    const controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.08;
    controls.minDistance = 50;
    controls.maxDistance = 400;
    controls.target.set(0, 0, 0);
    controls.autoRotate = true;
    controls.autoRotateSpeed = 0.22;

    stateRef.current = { scene, camera, renderer, controls, modelGroup, key, rim, fill, turntable, pedestalRing, contactShadow, isMobileViewer };

    // resize handler
    const onResize = () => {
      const w = wrap.clientWidth, h = wrap.clientHeight;
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
      renderer.setSize(w, h);
      ensureTick();
    };
    const ro = new ResizeObserver(onResize);
    ro.observe(wrap);

    // animation loop: run while visible, then back off so scroll stays smooth.
    let raf = 0;
    let disposed = false;
    let isInView = true;
    let isDocumentVisible = !document.hidden;
    const canRender = () => isInView && isDocumentVisible;
    const tick = () => {
      if (disposed) return;
      if (!canRender()) {
        raf = 0;
        return;
      }
      controls.update();
      renderer.render(scene, camera);
      raf = requestAnimationFrame(tick);
    };
    const ensureTick = () => {
      if (!raf && !disposed) raf = requestAnimationFrame(tick);
    };
    const visibilityObserver = typeof IntersectionObserver !== "undefined"
      ? new IntersectionObserver(([entry]) => {
          isInView = Boolean(entry?.isIntersecting);
          if (isInView) ensureTick();
        }, { root: null, rootMargin: "120px 0px", threshold: 0.01 })
      : null;
    if (visibilityObserver) visibilityObserver.observe(wrap);
    const onVisibilityChange = () => {
      isDocumentVisible = !document.hidden;
      if (isDocumentVisible) ensureTick();
    };
    document.addEventListener("visibilitychange", onVisibilityChange);
    ensureTick();

    // keyboard reset
    const onKey = (e) => {
      if (e.key === "r" || e.key === "R") resetView();
    };
    window.addEventListener("keydown", onKey);

    // dragdrop
    const onDragOver = (e) => { e.preventDefault(); wrap.classList.add("drag-over"); };
    const onDragLeave = () => wrap.classList.remove("drag-over");
    const onDrop = (e) => {
      e.preventDefault();
      wrap.classList.remove("drag-over");
      const f = e.dataTransfer?.files?.[0];
      if (!f) return;
      if (!/\.stl$/i.test(f.name)) { setStatus("FAIL - NOT STL"); return; }
      const reader = new FileReader();
      reader.onload = (ev) => {
        loadSTLFromBuffer(ev.target.result, f.name);
      };
      reader.readAsArrayBuffer(f);
    };
    wrap.addEventListener("dragover", onDragOver);
    wrap.addEventListener("dragleave", onDragLeave);
    wrap.addEventListener("drop", onDrop);

    return () => {
      disposed = true;
      cancelAnimationFrame(raf);
      ro.disconnect();
      visibilityObserver?.disconnect();
      document.removeEventListener("visibilitychange", onVisibilityChange);
      window.removeEventListener("keydown", onKey);
      wrap.removeEventListener("dragover", onDragOver);
      wrap.removeEventListener("dragleave", onDragLeave);
      wrap.removeEventListener("drop", onDrop);
      renderer.dispose();
      if (renderer.domElement.parentNode) renderer.domElement.parentNode.removeChild(renderer.domElement);
    };
  }, []);

  // load model whenever mech or selected model mode changes
  React.useEffect(() => {
    if (!stateRef.current.scene) return;
    const token = ++loadTokenRef.current;
    const isCurrentLoad = () => token === loadTokenRef.current;
    setModelName(null);
    setHasModel(false);
    if (activeModel.kind === "glb" && activeModel.url) {
      beginLoad("GLB");
      const loader = new THREE.GLTFLoader();
      loader.load(
        activeModel.url,
        (gltf) => {
          if (!isCurrentLoad()) return;
          attachScene(gltf.scene || gltf.scenes?.[0], cleanViewerAssetName(activeModel.url), "GLB");
        },
        (event) => {
          if (!isCurrentLoad()) return;
          updateLoadProgress(event, "GLB");
        },
        () => {
          if (!isCurrentLoad()) return;
          finishLoad();
          setStatus("GLB FAIL - PLACEHOLDER ACTIVE");
          buildPlaceholder();
        }
      );
    } else if (activeModel.kind === "stl" && activeModel.url) {
      beginLoad("STL");
      const loader = new THREE.STLLoader();
      loader.load(
        activeModel.url,
        (geom) => {
          if (!isCurrentLoad()) return;
          attachGeometry(geom, cleanViewerAssetName(activeModel.url));
        },
        (event) => {
          if (!isCurrentLoad()) return;
          updateLoadProgress(event, "STL");
        },
        () => {
          if (!isCurrentLoad()) return;
          finishLoad();
          setStatus("STL FAIL - PLACEHOLDER ACTIVE");
          buildPlaceholder();
        }
      );
    } else {
      finishLoad();
      buildPlaceholder();
    }
    return () => {
      if (loadTokenRef.current === token) loadTokenRef.current += 1;
    };
  }, [mech.id, activeModel.kind, activeModel.url]);

  // autorotate / wireframe toggles
  React.useEffect(() => {
    if (stateRef.current.controls) stateRef.current.controls.autoRotate = autoRotate;
  }, [autoRotate]);

  React.useEffect(() => {
    const g = stateRef.current.modelGroup;
    if (!g) return;
    g.traverse((c) => {
      if (!c.isMesh || !c.material) return;
      const materials = Array.isArray(c.material) ? c.material : [c.material];
      materials.forEach((mat) => {
        mat.wireframe = wireframe;
        mat.needsUpdate = true;
      });
    });
  }, [wireframe]);

  React.useEffect(() => {
    if (stateRef.current.scene) stateRef.current.scene.background = null;
    if (wrapRef.current) wrapRef.current.style.background = backgrounds[bgIdx];
  }, [bgIdx]);

  function beginLoad(kind) {
    setLoadState({ active: true, kind, loaded: 0, total: 0, percent: null });
    setStatus("DOWNLOADING " + kind + "...");
  }

  function updateLoadProgress(event, kind) {
    const loaded = event?.loaded || 0;
    const total = event?.total || 0;
    const percent = total > 0 ? Math.min(99, Math.max(1, Math.round((loaded / total) * 100))) : null;
    setLoadState({ active: true, kind, loaded, total, percent });
    setStatus(total > 0 ? ("DOWNLOADING " + kind + " - " + percent + "%") : ("DOWNLOADING " + kind + " - " + formatBytes(loaded)));
  }

  function finishLoad() {
    setLoadState({ active: false, kind: null, loaded: 0, total: 0, percent: null });
  }

  function formatBytes(bytes) {
    if (!bytes) return "0 B";
    const units = ["B", "KB", "MB", "GB"];
    const i = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
    const value = bytes / Math.pow(1024, i);
    return (i === 0 ? value.toFixed(0) : value.toFixed(value >= 10 ? 1 : 2)) + " " + units[i];
  }

  function clearGroup() {
    const g = stateRef.current.modelGroup;
    while (g.children.length) {
      const c = g.children.pop();
      c.traverse?.((node) => {
        if (node.geometry) node.geometry.dispose();
        if (Array.isArray(node.material)) {
          node.material.forEach((mat) => mat?.dispose?.());
        } else if (node.material) {
          node.material.dispose?.();
        }
      });
    }
  }

  function copyTextureTransform(source, target) {
    target.wrapS = source.wrapS;
    target.wrapT = source.wrapT;
    target.flipY = source.flipY;
    target.repeat.copy(source.repeat);
    target.offset.copy(source.offset);
    target.rotation = source.rotation;
    target.center.copy(source.center);
  }

  function prepareStudioTexture(texture, kind = "STL", role = "color") {
    if (!texture) return null;
    const image = texture.image;
    let studioTexture = texture;
    const isColorTexture = role === "color" || role === "emissive";

    if (kind === "GLB" && isColorTexture && image && image.width && image.height) {
      const maxSide = stateRef.current.isMobileViewer ? 1024 : 2048;
      const scale = Math.min(1, maxSide / Math.max(image.width, image.height));
      const canvas = document.createElement("canvas");
      canvas.width = Math.max(1, Math.round(image.width * scale));
      canvas.height = Math.max(1, Math.round(image.height * scale));
      const ctx = canvas.getContext("2d");
      ctx.imageSmoothingEnabled = true;
      ctx.imageSmoothingQuality = "high";
      ctx.filter = "contrast(1.18) saturate(1.14) brightness(1.02)";
      ctx.drawImage(image, 0, 0, canvas.width, canvas.height);

      studioTexture = new THREE.CanvasTexture(canvas);
      copyTextureTransform(texture, studioTexture);
    } else if (kind !== "GLB" && image && image.width && image.height) {
      const thumb = document.createElement("canvas");
      thumb.width = 28;
      thumb.height = 28;
      const thumbCtx = thumb.getContext("2d");
      thumbCtx.imageSmoothingEnabled = true;
      thumbCtx.imageSmoothingQuality = "high";
      thumbCtx.drawImage(image, 0, 0, thumb.width, thumb.height);

      const canvas = document.createElement("canvas");
      canvas.width = 512;
      canvas.height = 512;
      const ctx = canvas.getContext("2d");
      ctx.imageSmoothingEnabled = true;
      ctx.imageSmoothingQuality = "high";
      ctx.drawImage(thumb, 0, 0, canvas.width, canvas.height);

      studioTexture = new THREE.CanvasTexture(canvas);
      copyTextureTransform(texture, studioTexture);
    }
    if (isColorTexture) studioTexture.encoding = THREE.sRGBEncoding;
    studioTexture.minFilter = THREE.LinearMipmapLinearFilter;
    studioTexture.magFilter = THREE.LinearFilter;
    const anisotropyCap = stateRef.current.isMobileViewer ? 4 : (kind === "GLB" ? 16 : 8);
    studioTexture.anisotropy = Math.min(anisotropyCap, stateRef.current.renderer?.capabilities?.getMaxAnisotropy?.() || 1);
    studioTexture.needsUpdate = true;
    return studioTexture;
  }

  function makeStudioMaterial(sourceMaterial, fallbackColor, kind) {
    const sourceColor = sourceMaterial?.color;
    const sourceMap = prepareStudioTexture(sourceMaterial?.map || null, kind, "color");
    const sourceLooksDefault = !sourceColor || (sourceColor.r > 0.9 && sourceColor.g > 0.9 && sourceColor.b > 0.9);
    const color = sourceMap
      ? (sourceLooksDefault ? new THREE.Color(0xffffff) : sourceColor.clone())
      : (sourceLooksDefault ? new THREE.Color(fallbackColor) : sourceColor.clone());
    color.offsetHSL(0, kind === "GLB" ? 0.035 : 0, kind === "GLB" ? -0.012 : 0.1);
    const emissive = sourceMaterial?.emissive ? sourceMaterial.emissive.clone() : new THREE.Color(0x000000);
    return new THREE.MeshStandardMaterial({
      color,
      map: sourceMap,
      normalMap: prepareStudioTexture(sourceMaterial?.normalMap || null, kind, "data"),
      roughnessMap: prepareStudioTexture(sourceMaterial?.roughnessMap || null, kind, "data"),
      metalnessMap: prepareStudioTexture(sourceMaterial?.metalnessMap || null, kind, "data"),
      aoMap: prepareStudioTexture(sourceMaterial?.aoMap || null, kind, "data"),
      emissiveMap: prepareStudioTexture(sourceMaterial?.emissiveMap || null, kind, "emissive"),
      alphaMap: prepareStudioTexture(sourceMaterial?.alphaMap || null, kind, "data"),
      emissive,
      emissiveIntensity: sourceMaterial?.emissiveIntensity ? Math.min(sourceMaterial.emissiveIntensity, kind === "GLB" ? 0.28 : 0.18) : 0,
      metalness: kind === "GLB" ? Math.min(Math.max(sourceMaterial?.metalness ?? 0.18, 0.06), 0.52) : 0.24,
      roughness: kind === "GLB" ? Math.min(Math.max(sourceMaterial?.roughness ?? 0.48, 0.34), 0.7) : 0.58,
      flatShading: false,
      side: THREE.DoubleSide,
      transparent: Boolean(sourceMaterial?.transparent),
      opacity: sourceMaterial?.opacity ?? 1,
      alphaTest: sourceMaterial?.alphaTest ?? 0,
      vertexColors: sourceMaterial?.vertexColors || false,
      skinning: sourceMaterial?.skinning || false,
      morphTargets: sourceMaterial?.morphTargets || false,
      morphNormals: sourceMaterial?.morphNormals || false
    });
  }

  function applyStudioPreview(node, kind, meshIndex = 0) {
    if (!node.geometry) return;
    node.geometry.computeVertexNormals();
    const palette = mech.palette.length ? mech.palette : ["#cccccc"];
    const original = Array.isArray(node.material) ? node.material : [node.material];
    const studioMaterials = original.map((mat, matIndex) => {
      const fallback = palette[(meshIndex + matIndex) % palette.length] || "#cccccc";
      return makeStudioMaterial(mat, fallback, kind);
    });
    node.material = Array.isArray(node.material) ? studioMaterials : studioMaterials[0];
    const materials = Array.isArray(node.material) ? node.material : [node.material];
    materials.forEach((mat) => {
      mat.wireframe = wireframe;
      mat.needsUpdate = true;
    });
  }

  function settleOnPedestal(object) {
    object.updateMatrixWorld(true);
    const box = new THREE.Box3().setFromObject(object);
    if (!Number.isFinite(box.min.y) || !Number.isFinite(box.max.y)) return;
    const center = box.getCenter(new THREE.Vector3());
    object.position.x -= center.x;
    object.position.z -= center.z;
    object.updateMatrixWorld(true);
    box.setFromObject(object);
    object.position.y += PEDESTAL_TOP_Y - box.min.y;
    object.updateMatrixWorld(true);
  }

  function attachGeometry(geom, name) {
    clearGroup();
    geom.computeBoundingBox();
    geom.center();
    geom.computeVertexNormals();
    const size = new THREE.Vector3();
    geom.boundingBox.getSize(size);
    const maxDim = Math.max(size.x, size.y, size.z);
    const scale = 56 / maxDim;
    const mat = makeStudioMaterial(null, mech.palette[0] || "#cccccc", "STL");
    const mesh = new THREE.Mesh(geom, mat);
    mesh.scale.setScalar(scale);
    mesh.castShadow = true;
    mesh.receiveShadow = true;
    stateRef.current.modelGroup.add(mesh);
    settleOnPedestal(mesh);
    frameView("stl");
    finishLoad();
    setHasModel(true);
    setModelName(name);
    setStatus("LIVE - STL");
  }

  function attachScene(root, name, kind) {
    if (!root) {
      finishLoad();
      setStatus(kind + " FAIL - EMPTY SCENE");
      buildPlaceholder();
      return;
    }
    clearGroup();
    let meshIndex = 0;
    root.traverse((node) => {
      if (!node.isMesh) return;
      node.castShadow = true;
      node.receiveShadow = true;
      applyStudioPreview(node, kind, meshIndex++);
    });

    const box = new THREE.Box3().setFromObject(root);
    const center = box.getCenter(new THREE.Vector3());
    const size = box.getSize(new THREE.Vector3());
    const maxDim = Math.max(size.x, size.y, size.z) || 1;
    root.position.sub(center);

    const previewGroup = new THREE.Group();
    previewGroup.add(root);
    previewGroup.rotation.y = Math.PI;
    previewGroup.scale.setScalar(92 / maxDim);
    stateRef.current.modelGroup.add(previewGroup);
    settleOnPedestal(previewGroup);
    frameView("glb");
    finishLoad();
    setHasModel(true);
    setModelName(name);
    setStatus("LIVE - " + kind);
  }

  function loadSTLFromBuffer(buffer, name) {
    setStatus("PARSING " + name);
    try {
      const loader = new THREE.STLLoader();
      const geom = loader.parse(buffer);
      attachGeometry(geom, name);
    } catch (err) {
      setStatus("PARSE FAIL");
    }
  }

  // Build a procedural mech-head placeholder out of stacked boxes,
  // colored using the mech's palette. Different mechs → different silhouettes.
  function buildPlaceholder() {
    clearGroup();
    const g = stateRef.current.modelGroup;
    const pal = mech.palette;
    const main = new THREE.Color(pal[0] || "#888");
    const accent1 = new THREE.Color(pal[1] || "#444");
    const accent2 = new THREE.Color(pal[2] || "#aaa");
    const dark = new THREE.Color(pal[4] || pal[3] || "#111");

    const matMain = new THREE.MeshStandardMaterial({ color: main, metalness: 0.35, roughness: 0.55, flatShading: true });
    const matA1 = new THREE.MeshStandardMaterial({ color: accent1, metalness: 0.55, roughness: 0.4, flatShading: true });
    const matA2 = new THREE.MeshStandardMaterial({ color: accent2, metalness: 0.6, roughness: 0.35, flatShading: true, emissive: accent2, emissiveIntensity: 0.18 });
    const matDark = new THREE.MeshStandardMaterial({ color: dark, metalness: 0.5, roughness: 0.5, flatShading: true });

    const addBox = (mat, w, h, d, x, y, z, rx = 0, ry = 0, rz = 0) => {
      const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat);
      m.position.set(x, y, z);
      m.rotation.set(rx, ry, rz);
      m.castShadow = true; m.receiveShadow = true;
      g.add(m);
      return m;
    };

    // Seed silhouette off the mech id so each placeholder looks different
    const seed = parseInt(mech.id.replace(/\D/g, ""), 10) || 0;
    const tall = (seed % 3) === 0 ? 1.2 : 1.0;
    const wide = ((seed >> 1) % 2) === 0 ? 1.05 : 0.95;

    // Crown / shoulders
    addBox(matMain, 44 * wide, 18 * tall, 32, 0, 22 * tall, 0);
    addBox(matA1, 30 * wide, 4, 28, 0, 32 * tall, 4);
    // forehead emblem
    addBox(matA1, 6, 10, 6, 0, 28 * tall, 14);
    // antenna / horn pair
    if (seed % 2 === 0) {
      addBox(matMain, 2, 30, 2, -10, 50 * tall, -2, 0, 0, 0.1);
      addBox(matMain, 2, 30, 2,  10, 50 * tall, -2, 0, 0, -0.1);
    } else {
      // spike crown
      for (let i = -3; i <= 3; i++) {
        const h = 14 + Math.abs(i) * 2;
        addBox(matMain, 3, 24 - Math.abs(i) * 2, 4, i * 5, 38 * tall + h * 0.3, -2);
      }
    }
    // face plate
    addBox(matDark, 30, 18, 10, 0, 6, 14);
    // eyes
    addBox(matA2, 8, 3, 2, -7, 10, 19);
    addBox(matA2, 8, 3, 2,  7, 10, 19);
    // jaw
    addBox(matA1, 18, 8, 14, 0, -4, 12);
    addBox(matDark, 14, 4, 12, 0, -10, 13);
    // ears / cheek pods
    addBox(matMain, 6, 16, 14, -22 * wide, 8, 4);
    addBox(matMain, 6, 16, 14,  22 * wide, 8, 4);
    // neck
    addBox(matDark, 22, 10, 18, 0, -22, 2);
    // chest plate
    addBox(matMain, 50 * wide, 14, 24, 0, -34, 4);
    addBox(matA1, 36 * wide, 4, 22, 0, -28, 5);

    setHasModel(true);
    setModelName(null);
    setStatus("PLACEHOLDER - NO MODEL");
  }

  function resetView() {
    frameView();
  }

  function frameView(kind = activeModel.kind) {
    const { camera, controls, modelGroup } = stateRef.current;
    if (!camera) return;
    const box = modelGroup ? new THREE.Box3().setFromObject(modelGroup) : null;
    if (box && Number.isFinite(box.min.y) && Number.isFinite(box.max.y)) {
      const center = box.getCenter(new THREE.Vector3());
      const size = box.getSize(new THREE.Vector3());
      const maxDim = Math.max(size.x, size.y, size.z, 1);
      const distance = Math.max(kind === "glb" ? 185 : 126, maxDim * (kind === "glb" ? 2.48 : 2.35));
      camera.position.set(center.x + distance * 0.2, center.y + size.y * 0.24, center.z + distance);
      controls.target.set(center.x, center.y + size.y * 0.08, center.z);
    } else {
      if (kind === "glb") camera.position.set(0, 34, 190);
      else camera.position.set(0, 24, 155);
      controls.target.set(0, 0, 0);
    }
    controls.update();
  }

  function downloadModel() {
    const targetUrl = activeModel.downloadUrl || activeModel.url;
    if (!targetUrl) return;
    const a = document.createElement("a");
    a.href = targetUrl;
    a.download = activeModel.downloadName || targetUrl.split("/").pop();
    a.click();
  }

  const downloadUrl = activeModel.downloadUrl || activeModel.url;
  const isSplitFullbodyDownload = activeModel.kind === "glb" && activeModel.downloadUrl && activeModel.downloadUrl !== activeModel.url;
  const downloadExtension = getViewerAssetExtension(downloadUrl, activeModel.kind === "glb" ? "glb" : "stl").toUpperCase();
  const downloadLabel = isSplitFullbodyDownload ? ("PRINT " + downloadExtension) : downloadExtension;
  const loadPercent = loadState.percent || 0;
  const loadText = loadState.active
    ? (loadState.total
      ? formatBytes(loadState.loaded) + " / " + formatBytes(loadState.total)
      : formatBytes(loadState.loaded) + " downloaded")
    : "";

  return (
    <div className="viewer-stage">
      <div className="viewer-canvas" ref={wrapRef}>
        <div className="viewer-overlay-tl">
          <div>VIEWPORT · {mech.id}</div>
          <div><b>WEBGL · {activeModel.kind === "glb" ? "GLB" : (activeModel.kind === "stl" ? "STL" : "PROC")}</b></div>
        </div>
        <div className="viewer-overlay-tr">
          <div><span className="dot-rec">●</span>{status}</div>
          <div style={{ marginTop: 4 }}>{modelName || (activeModel.url ? cleanViewerAssetName(activeModel.url) : "no-model-bound.bin")}</div>
        </div>
        {loadState.active && (
          <div className="viewer-loading">
            <div className="viewer-loading-card">
              <div className="viewer-loader-mark">3D</div>
              <div><b>DOWNLOADING {activeModel.label} MODEL</b></div>
              <div className={"viewer-progress " + (loadState.total ? "" : "is-indeterminate")}>
                <i style={{ width: loadState.total ? loadPercent + "%" : "42%" }}></i>
              </div>
              <div>{loadState.percent ? loadState.percent + "% - " : ""}{loadText}</div>
              <div className="viewer-loading-note">Large 3D files can take a moment.</div>
            </div>
          </div>
        )}
        <div className="viewer-readout">
          <div>AXES · <span>XYZ</span></div>
          <div>SCALE · <span>1:1</span></div>
          <div>SHADING · <span>{wireframe ? "WIRE" : "HERO"}</span></div>
        </div>
        {!activeModel.url && (
          <div className="viewer-empty">
            <div className="viewer-empty-card">
              <div><b>3D MODEL NOT YET ATTACHED</b></div>
              <div>drag .stl onto viewport to preview</div>
              <div>· · ·</div>
              <div>showing procedural silhouette</div>
            </div>
          </div>
        )}
      </div>
      <div className="viewer-controls">
        <button className={autoRotate ? "primary" : ""} onClick={() => setAutoRotate(!autoRotate)}>
          {autoRotate ? "■ ROTATING" : "▶ ROTATE"}
        </button>
        <button onClick={() => setWireframe(!wireframe)}>
          {wireframe ? "◧ SOLID" : "◫ WIRE"}
        </button>
        <button onClick={() => setBgIdx((bgIdx + 1) % backgrounds.length)}>BG · {bgIdx + 1}/{backgrounds.length}</button>
        <button onClick={resetView}>↺ RESET</button>
        <div className="spacer"></div>
        <span className="status">DRAG · ORBIT &nbsp;·&nbsp; WHEEL · ZOOM &nbsp;·&nbsp; R · RESET</span>
        <button className={downloadUrl ? "primary" : ""} disabled={!downloadUrl} onClick={downloadModel} style={!downloadUrl ? { opacity: 0.4, cursor: "not-allowed" } : {}}>
          ↓ {downloadLabel}
        </button>
      </div>
    </div>
  );
}

window.MekaViewer = MekaViewer;
