// solar-3d.jsx — 3D version of the system view (Three.js).
// Same gravity hierarchy as 2D: perspectives orbit patterns, patterns orbit
// essences, essences orbit the nucleus; constants pinned on the inner ring;
// solo bodies orbit nucleus dashed.
//
// Camera tumbles slowly. Click-drag rotates. Wheel zooms. Hover via raycaster.

const { useEffect: _s3_ue, useRef: _s3_ur, useState: _s3_us, useMemo: _s3_um } = React;

function hash01b(str) {
  let h = 2166136261;
  for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
  return ((h >>> 0) % 100000) / 100000;
}

// CSS-var → hex for Three.
function cssColor(varName, fallback) {
  const v = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
  return v || fallback;
}

function makeNucGradTexture(THREE, concept) {
  const size = 256;
  const cv = document.createElement('canvas');
  cv.width = cv.height = size;
  const ctx = cv.getContext('2d');
  const css = v => getComputedStyle(document.documentElement).getPropertyValue(v).trim();
  // Edge color = outermost filled orbit
  let edge = css('--c-nucleus');
  if ((concept.perspectives||[]).length > 0) edge = css('--c-perspective');
  else if ((concept.patterns||[]).length > 0) edge = css('--c-pattern');
  else if ((concept.essences||[]).length > 0) edge = css('--c-essence');
  else if ((concept.functions && concept.functions.some(f => f.body)) || concept.function) edge = css('--c-function');
  else if (concept.consist)  edge = css('--c-consist');
  const nucleus = css('--c-nucleus');
  const c = size / 2;
  const g = ctx.createRadialGradient(c, c, 0, c, c, c);
  g.addColorStop(0,    'rgba(255,255,255,0.9)');
  g.addColorStop(0.38, nucleus);
  g.addColorStop(1,    edge);
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, size, size);
  return new THREE.CanvasTexture(cv);
}

function buildScene(THREE, concept, themeDark, scalesRef, initOrbitWidth) {
  const scene = new THREE.Scene();
  scene.background = null; // we composite over the page bg

  // Colors (pulled at build time)
  const COL = {
    nucleus:     new THREE.Color(cssColor("--c-nucleus",     "#FF8800")),
    consist:     new THREE.Color(cssColor("--c-consist",     "#E8C547")),
    function:    new THREE.Color(cssColor("--c-function",    "#7BADC8")),
    perspective: new THREE.Color(cssColor("--c-perspective", "#5DCAA5")),
    pattern:     new THREE.Color(cssColor("--c-pattern",     "#AFA9EC")),
    essence:     new THREE.Color(cssColor("--c-essence",     "#FF8800")),
  };
  // Lights — soft ambient + golden key from nucleus
  scene.add(new THREE.AmbientLight(0xffffff, themeDark ? 0.65 : 0.85));
  const key = new THREE.PointLight(COL.nucleus, themeDark ? 2.4 : 1.6, 200);
  key.position.set(0, 0, 0);
  scene.add(key);

  // Nucleus — gradient texture from filled levels
  const nucTex = makeNucGradTexture(THREE, concept);
  const nucleus = new THREE.Mesh(
    new THREE.SphereGeometry(2.4, 48, 32),
    new THREE.MeshStandardMaterial({
      map: nucTex,
      emissiveMap: nucTex,
      emissive: new THREE.Color(0xffffff),
      emissiveIntensity: 0.85,
      roughness: 0.35, metalness: 0.05
    })
  );
  nucleus.userData = { kind: "nucleus", label: concept.title, sub: concept.category, id: "nucleus" };
  scene.add(nucleus);

  const ESS_R = 18;
  const PAT_R = 5.2;
  const PER_R = 2.7;
  const SOLO_PAT_R = 14.9;
  const SOLO_PER_R = 12.6;
  const orbits = [];
  const tubeR = () => Math.max(0.05, (initOrbitWidth || 1.2) * 0.25);
  function makeOrbitRing(r, color, opacity) {
    const geo = new THREE.TorusGeometry(r, tubeR(), 8, 128);
    const mat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity, depthWrite: false, side: THREE.DoubleSide });
    const ring = new THREE.Mesh(geo, mat);
    ring.rotation.x = Math.PI / 2; // lie in XZ plane
    scene.add(ring);
    return ring;
  }

  // Body factory — kind & radius scale
  function makeBody(kind, color, r, label, sub, id, solo) {
    const geo = new THREE.SphereGeometry(r, 28, 20);
    const mat = new THREE.MeshStandardMaterial({
      color, emissive: color, emissiveIntensity: kind === "essence" ? 0.6 : 0.4,
      roughness: 0.4, metalness: 0.1,
      transparent: solo, opacity: solo ? 0.6 : 1
    });
    const m = new THREE.Mesh(geo, mat);
    m.userData = { kind, label, sub, id, solo, color };
    return m;
  }

  const bodies = [];

  const CONST_W_3D = 0.12;
  const W_ESS_3D      = 0.05;
  const W_PAT_3D      = 0.20;
  const W_PER_3D      = 0.45;
  const W_SOLO_PAT_3D = 0.08;
  const W_SOLO_PER_3D = 0.12;
  const innerR_C = 4.1;
  const innerR_F = 6.0;

  if (concept.consist) {
    const m = makeBody("constant", COL.consist, 0.55, "Consist", concept.consist.slice(0, 90), "consist", false);
    scene.add(m);
    orbits.push({ ring: makeOrbitRing(innerR_C, COL.consist, 0.22), getCenter: null });
    bodies.push({ mesh: m, update(t) {
      const ds = scalesRef.current.distScale;
      m.position.set(Math.cos(t * CONST_W_3D) * innerR_C * ds, 0, Math.sin(t * CONST_W_3D) * innerR_C * ds);
    }});
  }
  {
    const _fnList3d = (concept.functions && concept.functions.length > 0)
      ? concept.functions.filter(f => f.body)
      : (concept.function ? [{ id: "fn0", body: concept.function }] : []);
    if (_fnList3d.length > 0) {
      orbits.push({ ring: makeOrbitRing(innerR_F, COL.function, 0.22), getCenter: null });
      _fnList3d.forEach((fn, i) => {
        const spread = Math.PI / Math.max(_fnList3d.length, 1);
        const baseAngle = Math.PI - ((_fnList3d.length - 1) / 2) * spread;
        const phaseOffset = baseAngle + i * spread;
        const label = i === 0 ? "Function" : `Function ${i + 1}`;
        const m = makeBody("constant", COL.function, 0.55, label, fn.body.slice(0, 90), "function_" + fn.id, false);
        scene.add(m);
        bodies.push({ mesh: m, update(t) {
          const ds = scalesRef.current.distScale;
          m.position.set(Math.cos(t * CONST_W_3D + phaseOffset) * innerR_F * ds, 0, Math.sin(t * CONST_W_3D + phaseOffset) * innerR_F * ds);
        }});
      });
    }
  }

  if ((concept.essences || []).length > 0) {
    orbits.push({ ring: makeOrbitRing(ESS_R, COL.essence, 0.18), getCenter: null });
  }
  (concept.essences || []).forEach((e, i) => {
    const baseAngle = (i / Math.max(concept.essences.length, 1)) * Math.PI * 2 + hash01b(e.id) * 0.3;
    const tilt = (hash01b(e.id + "tilt") - 0.5) * 0.4;
    const ess = makeBody("essence", COL.essence, 0.9, "Essence", (e.body || "").slice(0, 90), e.id, false);
    scene.add(ess);
    const eUpdate = (t) => {
      const ds = scalesRef.current.distScale;
      const a = baseAngle + t * W_ESS_3D;
      ess.position.set(Math.cos(a) * ESS_R * ds, 0, Math.sin(a) * ESS_R * ds);
      ess.position.applyAxisAngle(new THREE.Vector3(1, 0, 0), tilt);
    };
    bodies.push({ mesh: ess, update: eUpdate });

    const linkedPats = (concept.patterns || []).filter(p => p.essenceId === e.id);
    if (linkedPats.length > 0) {
      const essRef = ess;
      orbits.push({ ring: makeOrbitRing(PAT_R, COL.pattern, 0.20), getCenter: () => essRef.position });
    }
    linkedPats.forEach((p, j) => {
      const ba = (j / Math.max(linkedPats.length, 1)) * Math.PI * 2 + hash01b(p.id) * 0.7;
      const pTilt = (hash01b(p.id + "tilt") - 0.5) * 0.7;
      const pat = makeBody("pattern", COL.pattern, 0.55, p.title || "Pattern", (p.body || "").slice(0, 90), p.id, false);
      scene.add(pat);
      const pUpdate = (t) => {
        const ds = scalesRef.current.distScale;
        const a = ba + t * W_PAT_3D;
        const lp = new THREE.Vector3(Math.cos(a) * PAT_R * ds, 0, Math.sin(a) * PAT_R * ds);
        lp.applyAxisAngle(new THREE.Vector3(1, 0, 0), pTilt);
        pat.position.copy(ess.position).add(lp);
      };
      bodies.push({ mesh: pat, update: pUpdate });

      const linkedPers = (concept.perspectives || []).filter(x => x.patternId === p.id);
      if (linkedPers.length > 0) {
        const patRef = pat;
        orbits.push({ ring: makeOrbitRing(PER_R, COL.perspective, 0.23), getCenter: () => patRef.position });
      }
      linkedPers.forEach((per, k) => {
        const pba = (k / Math.max(linkedPers.length, 1)) * Math.PI * 2 + hash01b(per.id);
        const peTilt = (hash01b(per.id + "tilt") - 0.5) * 1.2;
        const perMesh = makeBody("perspective", COL.perspective, 0.35, per.title || "Perspective", (per.body || "").slice(0, 90), per.id, false);
        scene.add(perMesh);
        const perUpdate = (t) => {
          const ds = scalesRef.current.distScale;
          const a = pba + t * W_PER_3D;
          const lp = new THREE.Vector3(Math.cos(a) * PER_R * ds, 0, Math.sin(a) * PER_R * ds);
          lp.applyAxisAngle(new THREE.Vector3(1, 0, 0), peTilt);
          perMesh.position.copy(pat.position).add(lp);
        };
        bodies.push({ mesh: perMesh, update: perUpdate });
      });
    });
  });

  const soloPatterns = (concept.patterns || []).filter(p => !p.essenceId);
  if (soloPatterns.length > 0) {
    orbits.push({ ring: makeOrbitRing(SOLO_PAT_R, COL.pattern, 0.16), getCenter: null });
  }
  soloPatterns.forEach((p, i) => {
    const ba = (i / Math.max(soloPatterns.length, 1)) * Math.PI * 2 + hash01b(p.id);
    const tilt = (hash01b(p.id + "tilt") - 0.5) * 0.5;
    const pat = makeBody("pattern", COL.pattern, 0.55, p.title || "Pattern", (p.body || "").slice(0, 90), p.id, true);
    scene.add(pat);
    const upd = (t) => {
      const ds = scalesRef.current.distScale;
      const a = ba + t * W_SOLO_PAT_3D;
      const lp = new THREE.Vector3(Math.cos(a) * SOLO_PAT_R * ds, 0, Math.sin(a) * SOLO_PAT_R * ds);
      lp.applyAxisAngle(new THREE.Vector3(1, 0, 0), tilt);
      pat.position.copy(lp);
    };
    bodies.push({ mesh: pat, update: upd });

    const linkedPers = (concept.perspectives || []).filter(x => x.patternId === p.id);
    if (linkedPers.length > 0) {
      const patRef = pat;
      orbits.push({ ring: makeOrbitRing(PER_R, COL.perspective, 0.20), getCenter: () => patRef.position });
    }
    linkedPers.forEach((per, k) => {
      const pba = (k / Math.max(linkedPers.length, 1)) * Math.PI * 2 + hash01b(per.id);
      const peTilt = (hash01b(per.id + "tilt") - 0.5) * 1.0;
      const perMesh = makeBody("perspective", COL.perspective, 0.35, per.title || "Perspective", (per.body || "").slice(0, 90), per.id, false);
      scene.add(perMesh);
      const perUpd = (t) => {
        const ds = scalesRef.current.distScale;
        const a = pba + t * W_PER_3D;
        const lp = new THREE.Vector3(Math.cos(a) * PER_R * ds, 0, Math.sin(a) * PER_R * ds);
        lp.applyAxisAngle(new THREE.Vector3(1, 0, 0), peTilt);
        perMesh.position.copy(pat.position).add(lp);
      };
      bodies.push({ mesh: perMesh, update: perUpd });
    });
  });

  const soloPerspectives = (concept.perspectives || []).filter(p => !p.patternId);
  if (soloPerspectives.length > 0) {
    orbits.push({ ring: makeOrbitRing(SOLO_PER_R, COL.perspective, 0.16), getCenter: null });
  }
  soloPerspectives.forEach((per, i) => {
    const ba = (i / Math.max(soloPerspectives.length, 1)) * Math.PI * 2 + hash01b(per.id);
    const tilt = (hash01b(per.id + "tilt") - 0.5) * 0.9;
    const perMesh = makeBody("perspective", COL.perspective, 0.35, per.title || "Perspective", (per.body || "").slice(0, 90), per.id, true);
    scene.add(perMesh);
    const upd = (t) => {
      const ds = scalesRef.current.distScale;
      const a = ba + t * W_SOLO_PER_3D;
      const lp = new THREE.Vector3(Math.cos(a) * SOLO_PER_R * ds, 0, Math.sin(a) * SOLO_PER_R * ds);
      lp.applyAxisAngle(new THREE.Vector3(1, 0, 0), tilt);
      perMesh.position.copy(lp);
    };
    bodies.push({ mesh: perMesh, update: upd });
  });

  return { scene, bodies, nucleus, orbits };
}

function Solar3D({ concept, paused, themeDark, hovered, setHover, onSelect, nodeScale = 1, distScale = 1, orbitWidth = 1.2 }) {
  const containerRef = _s3_ur(null);
  const stateRef = _s3_ur(null);
  const orbitsRef = _s3_ur([]);
  const scalesRef = _s3_ur({ nodeScale, distScale });
  scalesRef.current.nodeScale = nodeScale;
  scalesRef.current.distScale = distScale;

  _s3_ue(() => {
    const THREE = window.THREE;
    if (!THREE) return;
    const container = containerRef.current;
    if (!container) return;

    const w = container.clientWidth;
    const h = container.clientHeight;

    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(w, h);
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    container.appendChild(renderer.domElement);

    const camera = new THREE.PerspectiveCamera(40, w / h, 0.1, 500);
    // Spherical-ish camera state
    const camState = {
      r: 52,
      azimuth: Math.PI * 0.25,
      polar: Math.PI * 0.34, // 0 = top, π/2 = equator
      target: new THREE.Vector3(0, 0, 0)
    };
    function applyCam() {
      const x = camState.r * Math.sin(camState.polar) * Math.cos(camState.azimuth);
      const y = camState.r * Math.cos(camState.polar);
      const z = camState.r * Math.sin(camState.polar) * Math.sin(camState.azimuth);
      camera.position.set(x, y, z);
      camera.lookAt(camState.target);
    }
    applyCam();

    const { scene, bodies, nucleus, orbits } = buildScene(THREE, concept, themeDark, scalesRef, orbitWidth);
    orbitsRef.current = orbits;

    // Raycaster for hover — use mesh objects from bodies
    const raycaster = new THREE.Raycaster();
    const mouseNDC = new THREE.Vector2(-2, -2); // off-screen by default
    const meshes = [nucleus, ...bodies.map(b => b.mesh)];

    // Drag rotation
    let dragging = false;
    let lastX = 0, lastY = 0;
    let autoRotate = true;
    let autoAzimuth = 0;
    let clickStartX = 0, clickStartY = 0;

    const onPointerDown = (e) => {
      dragging = true;
      autoRotate = false;
      lastX = e.clientX; lastY = e.clientY;
      clickStartX = e.clientX; clickStartY = e.clientY;
      renderer.domElement.setPointerCapture?.(e.pointerId);
    };
    const onPointerMove = (e) => {
      const rect = renderer.domElement.getBoundingClientRect();
      mouseNDC.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
      mouseNDC.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
      if (dragging) {
        const dx = e.clientX - lastX;
        const dy = e.clientY - lastY;
        camState.azimuth -= dx * 0.005;
        camState.polar  = Math.max(0.15, Math.min(Math.PI - 0.15, camState.polar - dy * 0.005));
        lastX = e.clientX; lastY = e.clientY;
        applyCam();
      }
    };
    const onPointerUp = (e) => {
      const dx = Math.abs(e.clientX - clickStartX);
      const dy = Math.abs(e.clientY - clickStartY);
      if (dx < 5 && dy < 5 && lastHoveredId) {
        stateRef.current.onSelect?.(lastHoveredId);
      }
      dragging = false;
      renderer.domElement.releasePointerCapture?.(e.pointerId);
    };
    const onPointerLeave = () => {
      mouseNDC.x = -2; mouseNDC.y = -2;
    };
    const onWheel = (e) => {
      e.preventDefault();
      const delta = e.deltaY * 0.04;
      camState.r = Math.max(16, Math.min(140, camState.r + delta));
      applyCam();
    };

    renderer.domElement.addEventListener("pointerdown", onPointerDown);
    renderer.domElement.addEventListener("pointermove", onPointerMove);
    window.addEventListener("pointerup", onPointerUp);
    renderer.domElement.addEventListener("pointerleave", onPointerLeave);
    renderer.domElement.addEventListener("wheel", onWheel, { passive: false });

    const onResize = () => {
      const w = container.clientWidth;
      const h = container.clientHeight;
      renderer.setSize(w, h);
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
    };
    const ro = new ResizeObserver(onResize);
    ro.observe(container);

    // Animate
    let raf;
    let t = 0;
    let last = performance.now();
    let lastHoveredId = null;
    const tick = (now) => {
      const dt = (now - last) / 1000;
      last = now;
      if (!stateRef.current.paused) t += dt;

      // Auto-orbit camera when no drag
      if (autoRotate) {
        autoAzimuth += dt * 0.06;
        camState.azimuth = Math.PI * 0.25 + autoAzimuth;
        applyCam();
      }

      // Update body positions (negative t → counter-clockwise)
      bodies.forEach(b => b.update(-t));

      // Apply live scale settings
      const ns = scalesRef.current.nodeScale;
      const ds = scalesRef.current.distScale;
      bodies.forEach(b => b.mesh.scale.setScalar(ns));

      // Update orbit ring positions and scale
      orbits.forEach(({ ring, getCenter }) => {
        if (getCenter) ring.position.copy(getCenter());
        ring.scale.setScalar(ds);
      });

      // Nucleus subtle pulse
      const pulse = 1 + Math.sin(t * 1.4) * 0.04;
      nucleus.scale.setScalar(pulse * ns);

      // Raycast
      raycaster.setFromCamera(mouseNDC, camera);
      const intersects = raycaster.intersectObjects(meshes, false);
      const hitId = intersects[0]?.object?.userData?.id || null;
      if (hitId !== lastHoveredId) {
        lastHoveredId = hitId;
        if (hitId) {
          const m = intersects[0].object;
          stateRef.current.onHover({
            id: hitId,
            kind: m.userData.kind,
            label: m.userData.label,
            sub: m.userData.sub,
            color: "#" + m.userData.color?.getHexString?.() || "#ffffff",
            solo: m.userData.solo,
          });
        } else {
          stateRef.current.onHover(null);
        }
      }

      renderer.render(scene, camera);
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);

    stateRef.current = { paused: false, onHover: () => {} };
    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
      renderer.domElement.removeEventListener("pointerdown", onPointerDown);
      renderer.domElement.removeEventListener("pointermove", onPointerMove);
      window.removeEventListener("pointerup", onPointerUp);
      renderer.domElement.removeEventListener("pointerleave", onPointerLeave);
      renderer.domElement.removeEventListener("wheel", onWheel);
      renderer.dispose();
      scene.traverse(o => {
        if (o.geometry) o.geometry.dispose();
        if (o.material) {
          if (Array.isArray(o.material)) o.material.forEach(m => m.dispose());
          else o.material.dispose();
        }
      });
      if (renderer.domElement.parentNode) renderer.domElement.parentNode.removeChild(renderer.domElement);
    };
  // Rebuild when concept identity or theme changes
  }, [concept.id, themeDark]);

  // Sync mutable state to the engine
  _s3_ue(() => {
    if (stateRef.current) {
      stateRef.current.paused = paused;
      stateRef.current.onHover = setHover;
      stateRef.current.onSelect = onSelect;
    }
  }, [paused, setHover, onSelect]);

  // Rebuild torus tube geometry when orbitWidth changes
  _s3_ue(() => {
    const THREE = window.THREE;
    if (!THREE || !orbitsRef.current.length) return;
    const tube = Math.max(0.05, orbitWidth * 0.25);
    orbitsRef.current.forEach(({ ring }) => {
      const r = ring.geometry.parameters.radius;
      ring.geometry.dispose();
      ring.geometry = new THREE.TorusGeometry(r, tube, 8, 128);
    });
  }, [orbitWidth]);

  // Live-update bodies when concept content changes (without rebuilding scene)
  // Simple approach: when concept reference changes (and same id), rebuild.
  // We rebuild in the main effect via key prop instead — see SolarScreen.

  return (
    <div ref={containerRef} style={{ position: "absolute", inset: 0 }} />
  );
}

Object.assign(window, { Solar3D });
