// Shared Three.js helpers for spine + body visualizations.
// Exported on window so all variation files can pull from one source.

let _threePromise = null;
function loadThree() {
  if (_threePromise) return _threePromise;
  if (window.THREE) {
    _threePromise = Promise.resolve(window.THREE);
    return _threePromise;
  }
  // Wait for module-loaded THREE to land on window, or fall back to UMD
  _threePromise = new Promise((resolve, reject) => {
    let elapsed = 0;
    const tick = () => {
      if (window.THREE) return resolve(window.THREE);
      elapsed += 50;
      if (elapsed > 5000) {
        // Last-ditch UMD load
        const s = document.createElement("script");
        s.src = "https://unpkg.com/three@0.160.0/build/three.min.js";
        s.onload = () => resolve(window.THREE);
        s.onerror = reject;
        document.head.appendChild(s);
        return;
      }
      setTimeout(tick, 50);
    };
    tick();
  });
  return _threePromise;
}

// ─────────────────────────────────────────────────────────────────────
// SPINE — stylized vertebrae stack. Procedurally built, no external mesh.
// Returns: { dispose, setHighlight, setRotation, setPalette, getVertebraNames }
// ─────────────────────────────────────────────────────────────────────
async function mountSpine({ container, palette, interactive = true, onPick }) {
  const THREE = await loadThree();

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(32, 1, 0.1, 100);
  camera.position.set(0, 0, 14);

  const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.setClearColor(0x000000, 0);
  container.appendChild(renderer.domElement);
  renderer.domElement.style.cssText = "width:100%;height:100%;display:block;cursor:grab;touch-action:none;";

  // Lighting
  const key = new THREE.DirectionalLight(0xffffff, 1.6);
  key.position.set(3, 4, 5);
  scene.add(key);
  const rim = new THREE.DirectionalLight(palette.accentLight || 0xff8a72, 1.2);
  rim.position.set(-3, 1, -2);
  scene.add(rim);
  const ambient = new THREE.AmbientLight(0xffffff, 0.3);
  scene.add(ambient);

  // Anatomically-shaped vertebrae.
  // Each region: cervical (small, taller spinous), thoracic (medium, sharply angled spinous + ribs facets),
  // lumbar (large kidney-bean body, blunt rectangular spinous), sacrum (fused triangle).
  const VERT_DEF = [
    ...Array.from({ length: 7 }, (_, i) => ({
      name: `C${i + 1}`, region: "cervical",
      bodyR: 0.28 + i * 0.012, bodyH: 0.13, spinousLen: 0.32, spinousAngle: -0.5,
      transverse: 0.42, gap: 0.05,
    })),
    ...Array.from({ length: 12 }, (_, i) => ({
      name: `T${i + 1}`, region: "thoracic",
      bodyR: 0.40 + i * 0.018, bodyH: 0.20, spinousLen: 0.55 + i * 0.01, spinousAngle: -0.85,
      transverse: 0.65 + i * 0.01, gap: 0.06, ribs: true,
    })),
    ...Array.from({ length: 5 }, (_, i) => ({
      name: `L${i + 1}`, region: "lumbar",
      bodyR: 0.66 + i * 0.015, bodyH: 0.30, spinousLen: 0.42, spinousAngle: -0.15,
      transverse: 0.85, gap: 0.10,
    })),
    { name: "S1", region: "sacrum", bodyR: 0.78, bodyH: 0.55, spinousLen: 0.0, spinousAngle: 0, transverse: 0.95, gap: 0 },
  ];

  const spineGroup = new THREE.Group();
  scene.add(spineGroup);

  const baseColor = new THREE.Color(palette.bone || 0xf2e7d8);
  const accentColor = new THREE.Color(palette.accent || 0xc97864);
  const discColor = new THREE.Color(0xd9b89a);

  function makeBoneMat() {
    return new THREE.MeshPhysicalMaterial({
      color: baseColor.clone(),
      roughness: 0.55, metalness: 0.02,
      clearcoat: 0.25, clearcoatRoughness: 0.5,
    });
  }
  function makeDiscMat() {
    return new THREE.MeshStandardMaterial({
      color: discColor.clone(), roughness: 0.85, metalness: 0,
    });
  }

  // Kidney-bean body via lathe-like custom shape: cylinder scaled with concave waist
  function buildVertebraBody(r, h) {
    // Make a slightly waisted cylinder — concave on sides like real vertebrae
    const segments = 28, layers = 8;
    const points = [];
    for (let i = 0; i <= layers; i++) {
      const t = i / layers; // 0..1 bottom to top
      const y = (t - 0.5) * h;
      // concave waist: pinch in middle slightly
      const pinch = 1 - Math.sin(t * Math.PI) * 0.08;
      points.push(new THREE.Vector2(r * pinch, y));
    }
    const geo = new THREE.LatheGeometry(points, segments);
    return geo;
  }

  // S-curve definition
  // Sacrum at bottom forward, lumbar arches forward (lordosis), thoracic back (kyphosis), cervical forward (lordosis)
  function curvatureZ(t) {
    // t: 0 = sacrum, 1 = top of cervical
    // Lumbar lordosis: forward bulge in 0..0.25
    // Thoracic kyphosis: backward bulge 0.25..0.7
    // Cervical lordosis: forward 0.7..1
    return (
      0.55 * Math.sin(t * Math.PI) - 0.30 * Math.sin(t * Math.PI * 2)
    );
  }

  const vertebrae = [];
  // Build bottom-up to use proper curvature, but display indices match top-down later
  // We want array order C1..S1 (top to bottom), but build bottom-up for stacking math
  const totalCount = VERT_DEF.length;
  let yCursor = 0;
  // Build in reverse: S1 first at bottom
  const buildOrder = [...VERT_DEF].reverse();
  const built = [];

  buildOrder.forEach((def, idxFromBottom) => {
    const t = idxFromBottom / (totalCount - 1); // 0..1 bottom to top
    const segGroup = new THREE.Group();
    const bodyMat = makeBoneMat();

    // Body
    const bodyGeo = buildVertebraBody(def.bodyR, def.bodyH);
    const body = new THREE.Mesh(bodyGeo, bodyMat);
    segGroup.add(body);

    // Spinous process — angled cone going posteriorly (-Z) and slightly down for thoracic
    if (def.spinousLen > 0) {
      const spGeo = new THREE.CylinderGeometry(def.bodyR * 0.13, def.bodyR * 0.22, def.spinousLen, 8);
      const sp = new THREE.Mesh(spGeo, bodyMat);
      // Position at posterior of body
      sp.position.set(0, 0, -def.bodyR * 0.6 - def.spinousLen * 0.5 * Math.cos(def.spinousAngle));
      sp.position.y += def.spinousLen * 0.5 * Math.sin(def.spinousAngle);
      sp.rotation.x = Math.PI / 2 + def.spinousAngle;
      segGroup.add(sp);

      // Spinous tip — small bulb at end
      const tipGeo = new THREE.SphereGeometry(def.bodyR * 0.18, 12, 10);
      const tip = new THREE.Mesh(tipGeo, bodyMat);
      tip.position.set(
        0,
        def.spinousLen * Math.sin(def.spinousAngle),
        -def.bodyR * 0.6 - def.spinousLen * Math.cos(def.spinousAngle)
      );
      segGroup.add(tip);
    }

    // Transverse processes — small wings on the sides
    if (def.transverse > 0) {
      [-1, 1].forEach((side) => {
        const tGeo = new THREE.CylinderGeometry(def.bodyR * 0.1, def.bodyR * 0.16, def.transverse * 0.5, 8);
        const tp = new THREE.Mesh(tGeo, bodyMat);
        tp.rotation.z = Math.PI / 2;
        tp.position.set(side * (def.bodyR + def.transverse * 0.2), 0, -def.bodyR * 0.3);
        segGroup.add(tp);

        // Tip
        const tTipGeo = new THREE.SphereGeometry(def.bodyR * 0.13, 10, 8);
        const tTip = new THREE.Mesh(tTipGeo, bodyMat);
        tTip.position.set(side * (def.bodyR + def.transverse * 0.45), 0, -def.bodyR * 0.3);
        segGroup.add(tTip);
      });
    }

    // Disc above (between this and one above) — except top vertebra
    if (idxFromBottom < totalCount - 1) {
      const discR = def.bodyR * 0.95;
      const discH = 0.06;
      const discGeo = buildVertebraBody(discR, discH);
      const disc = new THREE.Mesh(discGeo, makeDiscMat());
      disc.position.set(0, def.bodyH / 2 + discH / 2 + def.gap * 0.5, 0);
      segGroup.add(disc);
    }

    // Position with curvature
    const curveZ = curvatureZ(t) * 0.55;
    // Lean each vertebra slightly to follow curve tangent
    const dt = 0.02;
    const tangent = (curvatureZ(Math.min(1, t + dt)) - curvatureZ(Math.max(0, t - dt))) * 0.55 / (2 * dt);
    const lean = Math.atan(tangent / 1.0) * 0.6;

    segGroup.position.set(0, yCursor, curveZ);
    segGroup.rotation.x = lean;

    spineGroup.add(segGroup);

    built.push({ group: segGroup, body, def, baseMat: bodyMat, intensity: 0 });
    yCursor += def.bodyH + def.gap + 0.06; // body + disc + gap
  });

  // Reverse so vertebrae[0] = C1 (top)
  built.reverse();
  vertebrae.push(...built);

  // Auto-fit: recenter the whole stack at the origin and scale it so the full
  // C1→S1 column sits comfortably in frame (no top/bottom cropping).
  spineGroup.updateMatrixWorld(true);
  {
    const sbox = new THREE.Box3().setFromObject(spineGroup);
    const ssize = new THREE.Vector3(); sbox.getSize(ssize);
    const scenter = new THREE.Vector3(); sbox.getCenter(scenter);
    spineGroup.children.forEach((c) => {
      c.position.x -= scenter.x;
      c.position.y -= scenter.y;
      c.position.z -= scenter.z;
    });
    const targetH = 7.4;
    const fit = targetH / (ssize.y || 1);
    spineGroup.scale.setScalar(fit);
    spineGroup.position.set(0, 0, 0);
  }

  // Interaction
  let rotation = { x: 0, y: 0 };
  let targetRotation = { x: 0, y: 0 };
  let dragging = false;
  let lastPos = { x: 0, y: 0 };
  let autoRotate = true;

  const raycaster = new THREE.Raycaster();
  const mouse = new THREE.Vector2();

  function onPointerDown(e) {
    dragging = true;
    autoRotate = false;
    lastPos.x = e.clientX;
    lastPos.y = e.clientY;
    renderer.domElement.style.cursor = "grabbing";
  }
  function onPointerMove(e) {
    if (!dragging) return;
    const dx = e.clientX - lastPos.x;
    const dy = e.clientY - lastPos.y;
    targetRotation.y += dx * 0.01;
    targetRotation.x += dy * 0.005;
    targetRotation.x = Math.max(-0.6, Math.min(0.6, targetRotation.x));
    lastPos.x = e.clientX;
    lastPos.y = e.clientY;
  }
  function onPointerUp() {
    dragging = false;
    renderer.domElement.style.cursor = "grab";
  }
  function onClick(e) {
    if (!interactive || !onPick) return;
    const rect = renderer.domElement.getBoundingClientRect();
    mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
    mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
    raycaster.setFromCamera(mouse, camera);
    const meshes = vertebrae.map((v) => v.body);
    const hits = raycaster.intersectObjects(meshes, false);
    if (hits.length) {
      const hit = hits[0].object;
      const v = vertebrae.find((x) => x.body === hit);
      if (v) onPick(v.def.name);
    }
  }

  if (interactive) {
    renderer.domElement.addEventListener("pointerdown", onPointerDown);
    window.addEventListener("pointermove", onPointerMove);
    window.addEventListener("pointerup", onPointerUp);
    renderer.domElement.addEventListener("click", onClick);
  }

  function setHighlight(map) {
    // map: { 'C3': 0..1, 'L4': 0..1 } intensity 0-1
    vertebrae.forEach((v) => {
      const intensity = (map && map[v.def.name]) || 0;
      v.intensity = intensity;
      const mat = v.baseMat;
      if (intensity > 0) {
        mat.emissive.set(accentColor).multiplyScalar(intensity * 0.9);
        mat.color.copy(baseColor).lerp(accentColor, intensity * 0.6);
      } else {
        mat.emissive.setHex(0x000000);
        mat.color.copy(baseColor);
      }
    });
  }

  function setRotation(yRad) {
    targetRotation.y = yRad;
    autoRotate = false;
  }

  function resize() {
    const w = container.clientWidth;
    const h = container.clientHeight;
    if (w === 0 || h === 0) return;
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  }
  const ro = new ResizeObserver(resize);
  ro.observe(container);
  resize();

  let raf;
  function animate() {
    raf = requestAnimationFrame(animate);
    if (autoRotate && !dragging) {
      targetRotation.y += 0.003;
    }
    rotation.x += (targetRotation.x - rotation.x) * 0.08;
    rotation.y += (targetRotation.y - rotation.y) * 0.08;
    spineGroup.rotation.x = rotation.x;
    spineGroup.rotation.y = rotation.y;
    renderer.render(scene, camera);
  }
  animate();

  return {
    dispose() {
      cancelAnimationFrame(raf);
      ro.disconnect();
      renderer.domElement.removeEventListener("pointerdown", onPointerDown);
      window.removeEventListener("pointermove", onPointerMove);
      window.removeEventListener("pointerup", onPointerUp);
      renderer.domElement.removeEventListener("click", onClick);
      renderer.dispose();
      container.removeChild(renderer.domElement);
    },
    setHighlight,
    setRotation,
    resumeAuto() { autoRotate = true; },
    getVertebrae: () => VERT_DEF.map((d) => d.name),
  };
}

// ─────────────────────────────────────────────────────────────────────
// BODY — stylized humanoid for pain mapping. Front/back toggle.
// Uses simple primitives (capsules, spheres) for Vision-OS feel.
// ─────────────────────────────────────────────────────────────────────
async function mountBody({ container, palette, side = "front", onPaint }) {
  const THREE = await loadThree();

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(28, 1, 0.1, 100);
  camera.position.set(0, 0, 11);

  const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.setClearColor(0x000000, 0);
  container.appendChild(renderer.domElement);
  renderer.domElement.style.cssText = "width:100%;height:100%;display:block;cursor:crosshair;touch-action:none;";

  const key = new THREE.DirectionalLight(0xffffff, 1.4);
  key.position.set(3, 5, 6);
  scene.add(key);
  const rim = new THREE.DirectionalLight(palette.accentLight || 0xff8a72, 0.9);
  rim.position.set(-4, 2, -3);
  scene.add(rim);
  scene.add(new THREE.AmbientLight(0xffffff, 0.35));

  const bodyGroup = new THREE.Group();
  scene.add(bodyGroup);

  const skinColor = new THREE.Color(palette.skin || 0xe8d8c6);
  const accentColor = new THREE.Color(palette.accent || 0xc97864);

  function makeMat() {
    return new THREE.MeshPhysicalMaterial({
      color: skinColor.clone(),
      roughness: 0.6,
      metalness: 0.02,
      clearcoat: 0.2,
    });
  }

  // Head
  const head = new THREE.Mesh(new THREE.SphereGeometry(0.55, 32, 24), makeMat());
  head.position.y = 3.1;
  head.scale.set(0.85, 1, 0.9);
  bodyGroup.add(head);

  // Neck
  const neck = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.25, 0.35, 16), makeMat());
  neck.position.y = 2.55;
  bodyGroup.add(neck);

  // Torso (upper)
  const upperTorso = new THREE.Mesh(new THREE.CylinderGeometry(0.85, 1.0, 1.4, 24), makeMat());
  upperTorso.position.y = 1.5;
  bodyGroup.add(upperTorso);

  // Lower torso (waist)
  const waist = new THREE.Mesh(new THREE.CylinderGeometry(0.7, 0.75, 0.7, 24), makeMat());
  waist.position.y = 0.4;
  bodyGroup.add(waist);

  // Hips
  const hips = new THREE.Mesh(new THREE.CylinderGeometry(0.85, 0.7, 0.6, 24), makeMat());
  hips.position.y = -0.25;
  bodyGroup.add(hips);

  // Shoulders/arms
  function makeArm(sign) {
    const g = new THREE.Group();
    const upper = new THREE.Mesh(new THREE.CylinderGeometry(0.22, 0.25, 1.3, 16), makeMat());
    upper.position.y = -0.65;
    g.add(upper);
    const elbow = new THREE.Mesh(new THREE.SphereGeometry(0.22, 16, 12), makeMat());
    elbow.position.y = -1.3;
    g.add(elbow);
    const lower = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.22, 1.2, 16), makeMat());
    lower.position.y = -1.95;
    g.add(lower);
    const hand = new THREE.Mesh(new THREE.SphereGeometry(0.22, 16, 12), makeMat());
    hand.position.y = -2.65;
    hand.scale.set(0.8, 1.2, 0.6);
    g.add(hand);
    g.position.set(sign * 1.05, 2.1, 0);
    g.rotation.z = sign * 0.08;
    bodyGroup.add(g);
    return g;
  }
  makeArm(1);
  makeArm(-1);

  // Legs
  function makeLeg(sign) {
    const g = new THREE.Group();
    const thigh = new THREE.Mesh(new THREE.CylinderGeometry(0.32, 0.28, 1.6, 18), makeMat());
    thigh.position.y = -1.3;
    g.add(thigh);
    const knee = new THREE.Mesh(new THREE.SphereGeometry(0.28, 16, 12), makeMat());
    knee.position.y = -2.15;
    g.add(knee);
    const calf = new THREE.Mesh(new THREE.CylinderGeometry(0.24, 0.18, 1.5, 18), makeMat());
    calf.position.y = -2.95;
    g.add(calf);
    const foot = new THREE.Mesh(new THREE.SphereGeometry(0.24, 16, 12), makeMat());
    foot.position.set(0, -3.75, 0.2);
    foot.scale.set(0.9, 0.6, 1.5);
    g.add(foot);
    g.position.set(sign * 0.4, -0.4, 0);
    bodyGroup.add(g);
    return g;
  }
  makeLeg(1);
  makeLeg(-1);

  bodyGroup.position.y = 0.2;

  // Pain markers
  const markers = []; // { id, pos: Vector3, intensity, mesh, ringMesh }
  const markerGroup = new THREE.Group();
  scene.add(markerGroup);

  function addMarker(worldPos, intensity = 0.7) {
    const id = `m_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
    const sphere = new THREE.Mesh(
      new THREE.SphereGeometry(0.18, 16, 12),
      new THREE.MeshBasicMaterial({
        color: accentColor,
        transparent: true,
        opacity: 0.9,
      })
    );
    sphere.position.copy(worldPos);
    markerGroup.add(sphere);

    // Pulsing ring
    const ring = new THREE.Mesh(
      new THREE.RingGeometry(0.22, 0.32, 24),
      new THREE.MeshBasicMaterial({
        color: accentColor,
        transparent: true,
        opacity: 0.6,
        side: THREE.DoubleSide,
      })
    );
    ring.position.copy(worldPos);
    ring.lookAt(camera.position);
    markerGroup.add(ring);

    const m = { id, pos: worldPos.clone(), intensity, mesh: sphere, ring };
    markers.push(m);
    if (onPaint) onPaint(markers);
    return m;
  }

  const raycaster = new THREE.Raycaster();
  const mouse = new THREE.Vector2();

  let rotation = { y: side === "back" ? Math.PI : 0 };
  let targetRotation = { y: rotation.y };
  let dragging = false;
  let lastX = 0;

  function onPointerDown(e) {
    dragging = true;
    lastX = e.clientX;
    renderer.domElement.style.cursor = "grabbing";
    e._downAt = Date.now();
  }
  function onPointerMove(e) {
    if (!dragging) return;
    const dx = e.clientX - lastX;
    targetRotation.y += dx * 0.01;
    lastX = e.clientX;
  }
  function onPointerUp(e) {
    dragging = false;
    renderer.domElement.style.cursor = "crosshair";
  }
  function onClick(e) {
    const rect = renderer.domElement.getBoundingClientRect();
    mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
    mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
    raycaster.setFromCamera(mouse, camera);
    const hits = raycaster.intersectObjects(bodyGroup.children, true);
    if (hits.length) {
      const p = hits[0].point.clone();
      // Push slightly outward along normal so marker sits on surface
      const n = hits[0].face.normal.clone();
      n.transformDirection(hits[0].object.matrixWorld);
      p.add(n.multiplyScalar(0.08));
      // Convert to bodyGroup local space so marker rotates with body
      bodyGroup.worldToLocal(p);
      addMarkerLocal(p);
    }
  }

  function addMarkerLocal(localPos) {
    const id = `m_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
    const sphere = new THREE.Mesh(
      new THREE.SphereGeometry(0.16, 18, 14),
      new THREE.MeshBasicMaterial({ color: accentColor, transparent: true, opacity: 0.95 })
    );
    sphere.position.copy(localPos);
    bodyGroup.add(sphere);

    const ringGeo = new THREE.RingGeometry(0.2, 0.32, 32);
    const ringMat = new THREE.MeshBasicMaterial({
      color: accentColor,
      transparent: true,
      opacity: 0.5,
      side: THREE.DoubleSide,
    });
    const ring = new THREE.Mesh(ringGeo, ringMat);
    ring.position.copy(localPos);
    bodyGroup.add(ring);

    const m = { id, pos: localPos.clone(), intensity: 0.7, mesh: sphere, ring, t: 0 };
    markers.push(m);
    if (onPaint) onPaint(markers);
    return m;
  }

  function clearMarkers() {
    markers.forEach((m) => {
      bodyGroup.remove(m.mesh);
      bodyGroup.remove(m.ring);
    });
    markers.length = 0;
    if (onPaint) onPaint(markers);
  }

  function setSide(s) {
    targetRotation.y = s === "back" ? Math.PI : 0;
  }

  renderer.domElement.addEventListener("pointerdown", onPointerDown);
  window.addEventListener("pointermove", onPointerMove);
  window.addEventListener("pointerup", onPointerUp);
  // Click only fires if pointer didn't drag
  let downX = 0, downY = 0;
  renderer.domElement.addEventListener("pointerdown", (e) => { downX = e.clientX; downY = e.clientY; });
  renderer.domElement.addEventListener("click", (e) => {
    const dx = Math.abs(e.clientX - downX);
    const dy = Math.abs(e.clientY - downY);
    if (dx < 4 && dy < 4) onClick(e);
  });

  function resize() {
    const w = container.clientWidth;
    const h = container.clientHeight;
    if (w === 0 || h === 0) return;
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  }
  const ro = new ResizeObserver(resize);
  ro.observe(container);
  resize();

  let raf;
  let t = 0;
  function animate() {
    raf = requestAnimationFrame(animate);
    t += 0.02;
    rotation.y += (targetRotation.y - rotation.y) * 0.1;
    bodyGroup.rotation.y = rotation.y;

    // pulse markers
    markers.forEach((m, i) => {
      const phase = t + i * 0.4;
      const s = 1 + Math.sin(phase * 2) * 0.15;
      m.ring.scale.setScalar(s);
      m.ring.material.opacity = 0.45 + Math.sin(phase * 2) * 0.2;
      m.ring.lookAt(camera.position);
    });

    renderer.render(scene, camera);
  }
  animate();

  return {
    dispose() {
      cancelAnimationFrame(raf);
      ro.disconnect();
      renderer.domElement.removeEventListener("pointerdown", onPointerDown);
      window.removeEventListener("pointermove", onPointerMove);
      window.removeEventListener("pointerup", onPointerUp);
      renderer.dispose();
      try { container.removeChild(renderer.domElement); } catch(e) {}
    },
    setSide,
    clearMarkers,
    getMarkers: () => markers,
    addMarkerLocal,
  };
}

window.SpineKit = { mountSpine, mountBody, mountBodyGLB };

// ─────────────────────────────────────────────────────────────────────
// BODY (GLB) — real écorché anatomy model for the pain-mapping body scan.
// Drag to rotate, front/back toggle, click to drop pain markers on the
// actual mesh surface (markers parent to the model so they rotate with it).
// ─────────────────────────────────────────────────────────────────────
async function mountBodyGLB({ container, palette, side = "front", onPaint, onReady, onError }) {
  const THREE = await loadThree();
  const { GLTFLoader } = window.GLTF_NS || {};
  if (!GLTFLoader) { if (onError) onError(new Error("no_loader")); throw new Error("no_loader"); }

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(28, 1, 0.1, 100);
  camera.position.set(0, 0, 12);

  const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, preserveDrawingBuffer: true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.setClearColor(0x000000, 0);
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.outputColorSpace = THREE.SRGBColorSpace;
  container.appendChild(renderer.domElement);
  renderer.domElement.style.cssText = "width:100%;height:100%;display:block;cursor:crosshair;touch-action:none;";

  const key = new THREE.DirectionalLight(0xffffff, 1.5);
  key.position.set(3, 5, 6); scene.add(key);
  const rim = new THREE.DirectionalLight(palette.accentLight || 0xff8a72, 0.9);
  rim.position.set(-4, 2, -3); scene.add(rim);
  const back = new THREE.DirectionalLight(0xffffff, 0.4);
  back.position.set(0, -3, -2); scene.add(back);
  scene.add(new THREE.AmbientLight(0xffffff, 0.5));

  const bodyGroup = new THREE.Group();
  scene.add(bodyGroup);

  const accentColor = new THREE.Color(palette.accent || 0xff6b4a);
  const markers = [];
  const meshes = [];
  let modelReady = false;

  const loader = new GLTFLoader();
  if (window.GLTF_NS && window.GLTF_NS.dracoLoader) loader.setDRACOLoader(window.GLTF_NS.dracoLoader);
  loader.load(
    palette.modelUrl || "ecorche.glb",
    (gltf) => {
      const model = gltf.scene;
      let mc = 0;
      model.traverse((o) => { if (o.isMesh) { mc++; meshes.push(o); } });
      if (mc === 0) { if (onError) onError(new Error("empty")); return; }

      const box = new THREE.Box3().setFromObject(model);
      const size = new THREE.Vector3(); box.getSize(size);
      const center = new THREE.Vector3(); box.getCenter(center);
      model.position.sub(center);

      const wrapper = new THREE.Group();
      wrapper.add(model);
      const longestAxis = size.x > size.y && size.x > size.z ? "x"
                        : size.z > size.y ? "z" : "y";
      const heightDim = longestAxis === "x" ? size.x : longestAxis === "z" ? size.z : size.y;
      wrapper.scale.setScalar(5.6 / (heightDim || 1));
      if (longestAxis === "z") wrapper.rotation.x = -Math.PI / 2;
      else if (longestAxis === "x") wrapper.rotation.z = Math.PI / 2;

      bodyGroup.add(wrapper);
      modelReady = true;
      if (onReady) onReady();
    },
    undefined,
    (err) => { console.error("[bodyGLB]", err); if (onError) onError(err); }
  );

  const raycaster = new THREE.Raycaster();
  const mouse = new THREE.Vector2();
  let rotation = { y: side === "back" ? Math.PI : 0 };
  let targetRotation = { y: rotation.y };
  let dragging = false;
  let lastX = 0;

  function onPointerDown(e) { dragging = true; lastX = e.clientX; renderer.domElement.style.cursor = "grabbing"; }
  function onPointerMove(e) { if (!dragging) return; targetRotation.y += (e.clientX - lastX) * 0.01; lastX = e.clientX; }
  function onPointerUp() { dragging = false; renderer.domElement.style.cursor = "crosshair"; }

  function addMarkerLocal(localPos) {
    const sphere = new THREE.Mesh(
      new THREE.SphereGeometry(0.17, 18, 14),
      new THREE.MeshBasicMaterial({ color: accentColor, transparent: true, opacity: 0.95 })
    );
    sphere.position.copy(localPos);
    bodyGroup.add(sphere);
    const ring = new THREE.Mesh(
      new THREE.RingGeometry(0.22, 0.36, 32),
      new THREE.MeshBasicMaterial({ color: accentColor, transparent: true, opacity: 0.5, side: THREE.DoubleSide })
    );
    ring.position.copy(localPos);
    bodyGroup.add(ring);
    const m = { id: `m_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`, pos: localPos.clone(), intensity: 0.7, mesh: sphere, ring };
    markers.push(m);
    if (onPaint) onPaint(markers);
    return m;
  }

  function doPick(e) {
    if (!modelReady) return;
    const rect = renderer.domElement.getBoundingClientRect();
    mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
    mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
    raycaster.setFromCamera(mouse, camera);
    const hits = raycaster.intersectObjects(meshes, true);
    if (hits.length) {
      const p = hits[0].point.clone();
      bodyGroup.worldToLocal(p);
      addMarkerLocal(p);
    }
  }

  renderer.domElement.addEventListener("pointerdown", onPointerDown);
  window.addEventListener("pointermove", onPointerMove);
  window.addEventListener("pointerup", onPointerUp);
  let downX = 0, downY = 0;
  renderer.domElement.addEventListener("pointerdown", (e) => { downX = e.clientX; downY = e.clientY; });
  renderer.domElement.addEventListener("click", (e) => {
    if (Math.abs(e.clientX - downX) < 4 && Math.abs(e.clientY - downY) < 4) doPick(e);
  });

  function clearMarkers() {
    markers.forEach((m) => { bodyGroup.remove(m.mesh); bodyGroup.remove(m.ring); });
    markers.length = 0;
    if (onPaint) onPaint(markers);
  }
  function setSide(s) { targetRotation.y = s === "back" ? Math.PI : 0; }

  function resize() {
    const w = container.clientWidth, h = container.clientHeight;
    if (!w || !h) return;
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  }
  const ro = new ResizeObserver(resize);
  ro.observe(container);
  resize();

  let raf, t = 0;
  function animate() {
    raf = requestAnimationFrame(animate);
    t += 0.02;
    rotation.y += (targetRotation.y - rotation.y) * 0.1;
    bodyGroup.rotation.y = rotation.y;
    markers.forEach((m, i) => {
      const phase = t + i * 0.4;
      const s = 1 + Math.sin(phase * 2) * 0.15;
      m.ring.scale.setScalar(s);
      m.ring.material.opacity = 0.45 + Math.sin(phase * 2) * 0.2;
      m.ring.lookAt(camera.position);
    });
    renderer.render(scene, camera);
  }
  animate();

  return {
    dispose() {
      cancelAnimationFrame(raf);
      ro.disconnect();
      renderer.domElement.removeEventListener("pointerdown", onPointerDown);
      window.removeEventListener("pointermove", onPointerMove);
      window.removeEventListener("pointerup", onPointerUp);
      renderer.dispose();
      try { container.removeChild(renderer.domElement); } catch (e) {}
    },
    setSide,
    clearMarkers,
    getMarkers: () => markers,
    addMarkerLocal,
  };
}
