// Luminara — 3D atlas on Three.js r128 (TICKET-008)
const { useState, useRef, useEffect, useMemo } = React;

const GROUP_VARS = {
  foundations: "--c-foundations",
  ethereum:    "--c-eth",
  ton:         "--c-ton",
  bitcoin:     "--c-btc",
  rwa:         "--c-rwa",
  gamefi:      "--c-game",
};

// Hue offsets per block around the page accent hue (keeps blocks distinct,
// while the whole palette shifts with the selected swatch).
const GROUP_HUE_OFFSET = {
  foundations: -10,
  ethereum:    0,
  ton:         -45,
  bitcoin:     60,
  rwa:         -90,
  gamefi:      30,
};
const GROUP_SAT = {
  foundations: 0.62, ethereum: 0.72, ton: 0.70, bitcoin: 0.72, rwa: 0.55, gamefi: 0.68,
};

function accentHue() {
  try {
    const v = getComputedStyle(document.documentElement).getPropertyValue("--accent-hue").trim();
    const n = parseFloat(v);
    return isNaN(n) ? 295 : n;
  } catch (e) { return 295; }
}

function groupColorTHREE(THREE, group, isLight) {
  const baseH = accentHue();
  const off = GROUP_HUE_OFFSET[group] != null ? GROUP_HUE_OFFSET[group] : 0;
  const h = ((baseH + off) % 360 + 360) % 360 / 360;
  const s = GROUP_SAT[group] != null ? GROUP_SAT[group] : 0.65;
  const l = isLight ? 0.5 : 0.62;
  const c = new THREE.Color();
  c.setHSL(h, s, l);
  return c;
}

// ATLAS-COLOR: three.js r128 predates CSS Color 4 and rejects oklch() strings
// (logging "THREE.Color: Unknown color …" every frame). The Luminara palette is all
// oklch, so convert to sRGB ourselves (Björn Ottosson's OKLab pipeline) instead of
// trusting the canvas to normalize it — Telegram's webview can hand back a non-sRGB
// serialization that THREE still can't parse.
function oklchToRgb(str) {
  const m = /oklch\(\s*([^)]+)\)/i.exec(str);
  if (!m) return null;
  const parts = m[1].split('/')[0].trim().split(/[\s,]+/).filter(Boolean);
  if (parts.length < 3) return null;
  let L = parseFloat(parts[0]); if (/%\s*$/.test(parts[0])) L /= 100;
  const C = parseFloat(parts[1]);
  const H = parseFloat(parts[2]);            // hue in degrees
  if (!isFinite(L) || !isFinite(C) || !isFinite(H)) return null;
  const hr = H * Math.PI / 180;
  const a = C * Math.cos(hr), b = C * Math.sin(hr);
  const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
  const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
  const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
  const l = l_ * l_ * l_, mm = m_ * m_ * m_, s = s_ * s_ * s_;
  const R =  4.0767416621 * l - 3.3077115913 * mm + 0.2309699292 * s;
  const G = -1.2684380046 * l + 2.6097574011 * mm - 0.3413193965 * s;
  const B = -0.0041960863 * l - 0.7034186147 * mm + 1.7076147010 * s;
  const lin2srgb = (x) => x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055;
  const to255 = (x) => Math.max(0, Math.min(255, Math.round(lin2srgb(x) * 255)));
  return `rgb(${to255(R)}, ${to255(G)}, ${to255(B)})`;
}

function cssColor(varName, fallback) {
  try {
    const v = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
    if (!v) return fallback;
    if (/^oklch/i.test(v)) { const rgb = oklchToRgb(v); return rgb || fallback; }
    const ctx = document.createElement("canvas").getContext("2d");
    ctx.fillStyle = "#000"; ctx.fillStyle = v;
    const out = ctx.fillStyle;
    return /^(#|rgb)/i.test(out) ? out : fallback;   // never hand THREE a color it can't parse
  } catch (e) { return fallback; }
}

function Atlas({ t, locale, selected, onSelect, completed, onComplete, progressPct, nodes, edges, chrome, cameraScale }) {
  // MY-UNIVERSE-3D: normally renders the whole graph from LUMINARA_DATA; when given an explicit
  // non-empty `nodes` (e.g. "My Universe" filtered to studied topics) it renders that subgraph.
  const D = window.LUMINARA_DATA || {};
  const useCustom = Array.isArray(nodes) && nodes.length > 0;
  const NODES = useCustom ? nodes : (D.NODES || []);
  const EDGES = useCustom ? (Array.isArray(edges) ? edges : []) : (D.EDGES || []);
  const mountRef = useRef(null);
  const stateRef = useRef({});
  const [hover, setHover] = useState(null);
  const done = completed || new Set();
  const doneRef = useRef(done); doneRef.current = done;
  const selRef = useRef(selected); selRef.current = selected;

  const layout = useMemo(() => {
    const groupZ = { foundations: -28, ethereum: 10, ton: 24, bitcoin: -10, rwa: -4, gamefi: 18 };
    const pos = {};
    NODES.forEach((n, i) => {
      const x = (n.x - 50) * 1.7;
      const y = -(n.y - 50) * 1.7;
      const baseZ = groupZ[n.group] != null ? groupZ[n.group] : 0;
      const z = baseZ + Math.sin(i * 1.7) * 10;
      pos[n.id] = { x, y, z, r: Math.max(2.2, n.r / 4.2) };
    });
    return pos;
  }, [NODES]);

  useEffect(() => {
    const THREE = window.THREE;
    if (!THREE || !mountRef.current) return;
    const mount = mountRef.current;
    const S = stateRef.current;
    const W = () => mount.clientWidth || 800;
    const H = () => mount.clientHeight || 600;

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(55, W() / H(), 0.1, 2000);
    const layoutPoints = Object.values(layout);
    const frameCameraZ = () => {
      const maxX = Math.max(35, ...layoutPoints.map(p => Math.abs(p.x) + p.r));
      const maxY = Math.max(35, ...layoutPoints.map(p => Math.abs(p.y) + p.r));
      const vFov = camera.fov * Math.PI / 180;
      const hFov = 2 * Math.atan(Math.tan(vFov / 2) * Math.max(0.25, W() / H()));
      const aspect = W() / H();
      const framePadding = aspect < 0.8 ? 1.52 : (aspect > 1.5 ? 1.06 : 1.28);
      return Math.max(140, Math.min(680, Math.max(maxY / Math.tan(vFov / 2), maxX / Math.tan(hFov / 2)) * framePadding + 35));
    };
    // Full-width embeds (such as the account card) otherwise frame the same
    // atlas much farther away than the home surface.  A caller may opt into a
    // bounded closer view without changing canonical node coordinates or edges.
    const requestedCameraScale = Number.isFinite(cameraScale) ? cameraScale : 1;
    const safeCameraScale = Math.max(0.5, Math.min(1, requestedCameraScale));
    let homeZ = frameCameraZ() * safeCameraScale;
    camera.position.set(0, 0, homeZ);
    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    renderer.setSize(W(), H());
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    mount.appendChild(renderer.domElement);
    renderer.domElement.style.display = "block";
    renderer.domElement.style.cursor = "grab";

    scene.add(new THREE.AmbientLight(0xffffff, 0.65));
    const key = new THREE.PointLight(0xffffff, 0.9);
    key.position.set(120, 140, 200); scene.add(key);

    const isLight = () => document.documentElement.getAttribute("data-theme") === "light";
    const colorOf = {};
    ["foundations","ethereum","ton","bitcoin","rwa","gamefi"].forEach(g => { colorOf[g] = groupColorTHREE(THREE, g, isLight()); });

    const group3d = new THREE.Group(); scene.add(group3d);
    const nodeMeshes = [];
    NODES.forEach(n => {
      const p = layout[n.id];
      const status = n.status || (done.has(n.id) ? "completed" : "default");
      const groupCol = colorOf[n.group] || new THREE.Color("#7c3aed");
      const col = status === "not_started" ? new THREE.Color(isLight() ? "#9ca3af" : "#6b7280") : groupCol;
      const geo = new THREE.SphereGeometry(p.r, 24, 24);
      const mat = new THREE.MeshStandardMaterial({ color: col, emissive: col, emissiveIntensity: status === "not_started" ? 0.05 : 0.25, roughness: 0.45, metalness: 0.1, transparent: true, opacity: status === "not_started" ? 0.62 : 1 });
      const mesh = new THREE.Mesh(geo, mat);
      mesh.position.set(p.x, p.y, p.z);
      mesh.userData = { id: n.id, group: n.group, r: p.r, status };
      group3d.add(mesh); nodeMeshes.push(mesh);
      const haloGeo = new THREE.SphereGeometry(p.r * 1.5, 16, 16);
      const haloMat = new THREE.MeshBasicMaterial({ color: col, transparent: true, opacity: 0.0, blending: THREE.AdditiveBlending, depthWrite: false });
      const halo = new THREE.Mesh(haloGeo, haloMat);
      mesh.add(halo); mesh.userData.halo = halo;
    });

    const edgeLines = [];
    EDGES.forEach(([a, b]) => {
      const pa = layout[a], pb = layout[b];
      if (!pa || !pb) return;
      const g = new THREE.BufferGeometry().setFromPoints([ new THREE.Vector3(pa.x, pa.y, pa.z), new THREE.Vector3(pb.x, pb.y, pb.z) ]);
      const m = new THREE.LineBasicMaterial({ color: new THREE.Color("#4b5563"), transparent: true, opacity: 0.42 });
      const line = new THREE.Line(g, m);
      line.userData = { a, b }; group3d.add(line); edgeLines.push(line);
    });

    const labelLayer = document.createElement("div");
    labelLayer.style.cssText = "position:absolute;inset:0;pointer-events:none;overflow:hidden;";
    mount.appendChild(labelLayer);
    const labels = NODES.map(n => {
      const el = document.createElement("div");
      el.className = "atlas3d-label status-" + (n.status || "default");
      const progress = Number.isFinite(n.progressPct) ? " · " + n.progressPct + "%" : "";
      const quiz = Number.isFinite(n.quizPct) ? " · Q " + n.quizPct + "%" : (n.quizAvailable ? " · Q" : "");
      el.textContent = (n.title[locale] || n.title.en) + progress + quiz;
      labelLayer.appendChild(el);
      return { el, id: n.id };
    });

    const ray = new THREE.Raycaster();
    const mouse = new THREE.Vector2();
    let rot = { x: -0.15, y: 0.5 }, target = { x: -0.15, y: 0.5 };
    const autoSpin = 0.0016;
    let dragging = false, lastX = 0, lastY = 0, moved = false;

    const onDown = (e) => { S.focusId = null; dragging = true; moved = false; lastX = e.clientX; lastY = e.clientY; renderer.domElement.style.cursor = "grabbing"; };
    const onMove = (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;
      if (dragging) {
        const dx = e.clientX - lastX, dy = e.clientY - lastY;
        if (Math.abs(dx) + Math.abs(dy) > 3) moved = true;
        target.y += dx * 0.006; target.x += dy * 0.006;
        target.x = Math.max(-1.2, Math.min(1.2, target.x));
        lastX = e.clientX; lastY = e.clientY;
      }
    };
    const onUp = () => { dragging = false; renderer.domElement.style.cursor = "grab"; };
    const onClick = () => {
      if (moved) return;
      ray.setFromCamera(mouse, camera);
      const hits = ray.intersectObjects(nodeMeshes, false);
      if (hits.length) { const id = hits[0].object.userData.id; onSelect(id); if (onComplete) onComplete(id); }
    };
    const onWheel = (e) => { e.preventDefault(); camera.position.z = Math.max(100, Math.min(720, camera.position.z + e.deltaY * 0.25)); };
    const onHover = () => {
      ray.setFromCamera(mouse, camera);
      const hits = ray.intersectObjects(nodeMeshes, false);
      const id = hits.length ? hits[0].object.userData.id : null;
      if (id !== S.hoverId) { S.hoverId = id; setHover(id); }
    };

    renderer.domElement.addEventListener("mousedown", onDown);
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
    renderer.domElement.addEventListener("click", onClick);
    renderer.domElement.addEventListener("wheel", onWheel, { passive: false });
    renderer.domElement.addEventListener("touchstart", (e) => { if (e.touches[0]) { S.focusId = null; dragging = true; moved = false; lastX = e.touches[0].clientX; lastY = e.touches[0].clientY; } }, { passive: true });
    renderer.domElement.addEventListener("touchmove", (e) => {
      if (!dragging || !e.touches[0]) return;
      const dx = e.touches[0].clientX - lastX, dy = e.touches[0].clientY - lastY;
      if (Math.abs(dx) + Math.abs(dy) > 3) moved = true;
      target.y += dx * 0.006; target.x += dy * 0.006;
      target.x = Math.max(-1.2, Math.min(1.2, target.x));
      lastX = e.touches[0].clientX; lastY = e.touches[0].clientY;
    }, { passive: true });

    const tmpV = new THREE.Vector3();
    const project = (x, y, z) => {
      tmpV.set(x, y, z); tmpV.applyMatrix4(group3d.matrixWorld); tmpV.project(camera);
      return { sx: (tmpV.x * 0.5 + 0.5) * W(), sy: (-tmpV.y * 0.5 + 0.5) * H(), vis: tmpV.z < 1 };
    };

    let raf;
    let lastHue = accentHue();
    let lastTheme = isLight();
    const tick = () => {
      raf = requestAnimationFrame(tick);

      // palette / theme change → recolor blocks (keep them distinct, shifted to palette)
      const curHue = accentHue(), curTheme = isLight();
      if (curHue !== lastHue || curTheme !== lastTheme) {
        lastHue = curHue; lastTheme = curTheme;
        const fresh = {};
        ["foundations","ethereum","ton","bitcoin","rwa","gamefi"].forEach(g => { fresh[g] = groupColorTHREE(THREE, g, curTheme); });
        nodeMeshes.forEach(m => {
          const c = m.userData.status === "not_started"
            ? new THREE.Color(curTheme ? "#9ca3af" : "#6b7280")
            : fresh[m.userData.group];
          if (c) {
            m.material.color.copy(c); m.material.emissive.copy(c);
            if (m.userData.halo) m.userData.halo.material.color.copy(c);
          }
        });
      }

      onHover();
      // ATLAS-LINK: pause auto-spin while a node is focused so it doesn't drift away.
      if (!dragging && !S.focusId) target.y += autoSpin;
      rot.x += (target.x - rot.x) * 0.08;
      rot.y += (target.y - rot.y) * 0.08;
      group3d.rotation.x = rot.x; group3d.rotation.y = rot.y;
      group3d.updateMatrixWorld();

      // ATLAS-LINK: pan the camera to centre the focused node (and zoom in a touch).
      // With nothing focused, drift the camera back to the default framing.
      let camTX = 0, camTY = 0;
      if (S.focusId && layout[S.focusId]) {
        const fp = layout[S.focusId];
        tmpV.set(fp.x, fp.y, fp.z).applyMatrix4(group3d.matrixWorld);
        camTX = tmpV.x; camTY = tmpV.y;
        // Keep a wide framing on focus so the node's EDGES/neighbours stay in view —
        // a tight zoom hid a node's connections (e.g. ton ↔ gamefi/telegram).
        camera.position.z += (Math.max(170, homeZ * 0.82) - camera.position.z) * 0.06;
      }
      camera.position.x += (camTX - camera.position.x) * 0.08;
      camera.position.y += (camTY - camera.position.y) * 0.08;

      const sel = selRef.current, hov = S.hoverId, dn = doneRef.current;
      nodeMeshes.forEach(m => {
        const id = m.userData.id;
        const isSel = id === sel, isHov = id === hov, isDone = dn.has(id);
        const emT = (isSel || isHov) ? 0.85 : (isDone ? 0.6 : 0.22);
        m.material.emissiveIntensity += (emT - m.material.emissiveIntensity) * 0.15;
        const sc = isSel ? 1.35 : isHov ? 1.2 : 1;
        m.scale.x += (sc - m.scale.x) * 0.15; m.scale.y = m.scale.z = m.scale.x;
        const haloT = (isSel || isHov) ? 0.4 : (isDone ? 0.22 : 0.0);
        m.userData.halo.material.opacity += (haloT - m.userData.halo.material.opacity) * 0.15;
      });
      edgeLines.forEach(line => {
        const { a, b } = line.userData;
        const linked = dn.has(a) && dn.has(b);
        const hot = (a === sel || b === sel || a === hov || b === hov);
        let op = 0.4, col = "#4b5563";
        if (linked) { op = 0.65; col = cssColor("--accent", "#7c3aed"); }
        if (hot)    { op = 0.8;  col = cssColor("--accent", "#7c3aed"); }
        line.material.opacity += (op - line.material.opacity) * 0.15;
        line.material.color.set(col);
      });
      const occupiedLabels = [];
      labels.forEach(({ el, id }) => {
        const p = layout[id];
        const pr = project(p.x, p.y + p.r + 4, p.z);
        const isSel = id === sel, isHov = id === hov, isDone = dn.has(id);
        if (pr.vis) {
          const approxW = Math.min(180, Math.max(42, (el.textContent || "").length * 6.2));
          const box = { l: pr.sx - approxW / 2, r: pr.sx + approxW / 2, t: pr.sy - 16, b: pr.sy + 2 };
          const collides = occupiedLabels.some(o => !(box.r < o.l || box.l > o.r || box.b < o.t || box.t > o.b));
          el.style.transform = `translate(-50%,-100%) translate(${Math.round(pr.sx)}px, ${Math.round(pr.sy)}px)`;
          el.style.opacity = (collides && !isSel && !isHov) ? "0" : ((isSel || isHov) ? "1" : (isDone ? "0.95" : "0.72"));
          el.style.fontWeight = (isSel || isHov) ? "700" : "500";
          el.classList.toggle("on", isSel || isHov);
          if (!collides || isSel || isHov) occupiedLabels.push(box);
        } else { el.style.opacity = "0"; }
      });
      renderer.render(scene, camera);
    };
    tick();

    const resizeRenderer = () => {
      camera.aspect = W() / H();
      camera.updateProjectionMatrix();
      renderer.setSize(W(), H());
      const nextHome = frameCameraZ();
      if (!S.focusId && Math.abs(camera.position.z - homeZ) < 80) camera.position.z = nextHome;
      homeZ = nextHome;
    };
    const ro = new ResizeObserver(resizeRenderer);
    ro.observe(mount);
    const vv = window.visualViewport;
    if (vv) vv.addEventListener("resize", resizeRenderer);
    window.addEventListener("orientationchange", resizeRenderer);

    S.zoom = (dir) => { camera.position.z = Math.max(100, Math.min(720, camera.position.z - dir * 30)); };
    S.reset = () => { S.focusId = null; target.x = -0.15; target.y = 0.5; camera.position.z = homeZ; };
    // ATLAS-LINK: focus a node — settle the plane front-on; the tick loop centres it.
    S.focus = (id) => { S.focusId = id; target.x = -0.15; target.y = 0.5; };

    return () => {
      cancelAnimationFrame(raf); ro.disconnect();
      if (vv) vv.removeEventListener("resize", resizeRenderer);
      window.removeEventListener("orientationchange", resizeRenderer);
      renderer.domElement.removeEventListener("mousedown", onDown);
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
      renderer.domElement.removeEventListener("wheel", onWheel);
      try { mount.removeChild(renderer.domElement); mount.removeChild(labelLayer); } catch (e) {}
      renderer.dispose();
    };
  }, [NODES, EDGES, locale]);

  // ATLAS-LINK: when the selected node changes (Open in Atlas, WP links, sidebar
  // jumps), bring it to centre instead of merely highlighting it in place.
  useEffect(() => {
    const S = stateRef.current;
    if (selected && typeof S.focus === "function") S.focus(selected);
  }, [selected]);

  return (
    <div className="atlas-wrap atlas3d" style={{ height: '100%' }}>
      <div className="atlas3d-mount" ref={mountRef} />
      {chrome !== false ? (
        <React.Fragment>
          <div className="atlas-legend">
            {[['foundations','c-foundations'],['ethereum','c-eth'],['ton','c-ton'],['bitcoin','c-btc'],['rwa','c-rwa']].map(([k,c],i) => (
              <div key={k} className="item" style={{ color: `var(--${c})` }}><span className="dot" /><span>{(t && t.atlasLegend && t.atlasLegend[i]) || k}</span></div>
            ))}
          </div>
          <div className="atlas-progress">
            <span className="ap-dot" /><span className="ap-v">{progressPct}%</span><span className="ap-k">{(t && t.progressNote) || ""}</span>
          </div>
          <div className="atlas-hint">{(t && t.atlas3dHint) || "Drag to rotate · scroll to zoom · click a node"}</div>
          <div className="atlas-zoom">
            <button onClick={() => stateRef.current.zoom && stateRef.current.zoom(1)} title="Zoom in">+</button>
            <button onClick={() => stateRef.current.zoom && stateRef.current.zoom(-1)} title="Zoom out">−</button>
            <button onClick={() => stateRef.current.reset && stateRef.current.reset()} title="Reset" style={{ fontFamily: 'var(--f-mono)', fontSize: 9 }}>1:1</button>
          </div>
        </React.Fragment>
      ) : null}
    </div>
  );
}

window.Atlas = Atlas;
