import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';

type RGB = [number, number, number];
type IconShape = 'people' | 'cap' | 'paper' | 'clipboard' | 'clock' | 'chat' | 'ballot';

interface OrbitNode {
  label: string;
  points: string[];
  icon: string;
  shape: IconShape;
  theta: number;
  speed: number;
  radiusFactor: number;
  incl: number;
  color: RGB;
  size: number;
}

const MODULES: OrbitNode[] = [
  {
    label: 'دانش‌آموزان', icon: 'bi-people-fill', shape: 'people',
    points: ['ثبت‌نام و پرونده کامل هر دانش‌آموز', 'سابقه حضور، نمرات و کارنامه', 'جستجو و فیلتر راحت'],
    theta: 0.0, speed: 0.20, radiusFactor: 1.08, incl: 0.34, color: [16, 182, 130], size: 15,
  },
  {
    label: 'معلمان', icon: 'bi-mortarboard-fill', shape: 'cap',
    points: ['مدیریت کلاس‌ها و برنامه هفتگی', 'ثبت نمره و تکلیف آنلاین', 'دسترسی مبتنی‌بر نقش'],
    theta: 0.9, speed: 0.25, radiusFactor: 1.26, incl: -0.48, color: [56, 189, 248], size: 15,
  },
  {
    label: 'امتحانات آنلاین', icon: 'bi-journal-check', shape: 'paper',
    points: ['طراحی بانک سؤال', 'برگزاری و زمان‌دار کردن امتحان', 'تصحیح خودکار و گزارش'],
    theta: 1.8, speed: 0.17, radiusFactor: 1.00, incl: 0.66, color: [45, 212, 191], size: 15,
  },
  {
    label: 'نمره و کارنامه', icon: 'bi-clipboard-data', shape: 'clipboard',
    points: ['نمره‌دهی سریع و میانگین‌گیری', 'چاپ کارنامه فارسی', 'چند پایه هم‌زمان'],
    theta: 2.6, speed: 0.28, radiusFactor: 1.20, incl: -0.20, color: [129, 140, 248], size: 15,
  },
  {
    label: 'حضور و غیاب', icon: 'bi-clock-fill', shape: 'clock',
    points: ['ثبت سریع حضور و غیاب کلاسی', 'گزارش لحظه‌ای برای مدیر', 'پیامک خودکار غیبت'],
    theta: 3.4, speed: 0.22, radiusFactor: 1.30, incl: 0.44, color: [16, 185, 129], size: 15,
  },
  {
    label: 'پیامک و اطلاع‌رسانی', icon: 'bi-chat-dots-fill', shape: 'chat',
    points: ['ارسال پیامک به اولیا', 'قالب آماده و زمان‌بندی', 'اطلاع‌رسانی نمره و غیبت'],
    theta: 4.2, speed: 0.18, radiusFactor: 1.04, incl: -0.58, color: [56, 189, 248], size: 15,
  },
  {
    label: 'رأی‌گیری', icon: 'bi-check2-square', shape: 'ballot',
    points: ['ساخت نظرسنجی و رأی‌گیری', 'رأی دانش‌آموزی، معلمی و اولیا', 'نتایج زنده و نمودار'],
    theta: 5.1, speed: 0.24, radiusFactor: 1.16, incl: 0.16, color: [45, 212, 191], size: 15,
  },
];

interface Particle { x: number; y: number; z: number; r: number; tw: number; ts: number; }
interface Point3 { x: number; y: number; z: number; }
interface Proj { n: OrbitNode; r: Point3; pScale: number; X: number; Y: number; depth: number; }

function rgba(c: RGB, a: number): string {
  return `rgba(${c[0]}, ${c[1]}, ${c[2]}, ${a})`;
}

function rot(p: Point3, yaw: number, pitch: number): Point3 {
  const cy = Math.cos(yaw), sy = Math.sin(yaw);
  const x1 = p.x * cy + p.z * sy;
  const z1 = -p.x * sy + p.z * cy;
  const y1 = p.y;
  const cp = Math.cos(pitch), sp = Math.sin(pitch);
  const y2 = y1 * cp - z1 * sp;
  const z2 = y1 * sp + z1 * cp;
  return { x: x1, y: y2, z: z2 };
}

function roundRectPath(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
  const rr = Math.min(r, w / 2, h / 2);
  ctx.beginPath();
  ctx.moveTo(x + rr, y);
  ctx.arcTo(x + w, y, x + w, y + h, rr);
  ctx.arcTo(x + w, y + h, x, y + h, rr);
  ctx.arcTo(x, y + h, x, y, rr);
  ctx.arcTo(x, y, x + w, y, rr);
  ctx.closePath();
}

/* ── Icosahedron crystal geometry ── */
const PHI = (1 + Math.sqrt(5)) / 2;
const ICO_VERTS: Point3[] = [
  { x: -1, y: PHI, z: 0 }, { x: 1, y: PHI, z: 0 },
  { x: -1, y: -PHI, z: 0 }, { x: 1, y: -PHI, z: 0 },
  { x: 0, y: -1, z: PHI }, { x: 0, y: 1, z: PHI },
  { x: 0, y: -1, z: -PHI }, { x: 0, y: 1, z: -PHI },
  { x: PHI, y: 0, z: -1 }, { x: PHI, y: 0, z: 1 },
  { x: -PHI, y: 0, z: -1 }, { x: -PHI, y: 0, z: 1 },
];
const ICO_EDGES: [number, number][] = [
  [0, 1], [0, 5], [0, 7], [0, 10], [0, 11],
  [1, 5], [1, 7], [1, 8], [1, 9],
  [2, 3], [2, 4], [2, 6], [2, 10], [2, 11],
  [3, 4], [3, 6], [3, 8], [3, 9],
  [4, 5], [4, 9], [4, 11],
  [5, 9], [5, 11],
  [6, 7], [6, 8], [6, 10],
  [7, 8], [7, 10],
  [8, 9],
  [10, 11],
];

/* Draw a single person glyph (head + shoulders) centered near (cx, cy), scale s. */
function person(ctx: CanvasRenderingContext2D, cx: number, cy: number, s: number) {
  ctx.beginPath();
  ctx.arc(cx, cy - s * 0.5, s * 0.27, 0, Math.PI * 2);
  ctx.fill();
  ctx.beginPath();
  ctx.arc(cx, cy + s * 0.12, s * 0.42, Math.PI, 0);
  ctx.closePath();
  ctx.fill();
}

/* Draw the school-themed icon for a node, centered at (0,0), half-extent s. */
function drawIcon(ctx: CanvasRenderingContext2D, shape: IconShape, s: number, color: RGB, alpha: number) {
  const col = rgba(color, 0.95 * alpha);
  const white = `rgba(255,255,255,${0.92 * alpha})`;
  const dim = rgba(color, 0.6 * alpha);
  const lw = Math.max(1.2, s * 0.1);
  ctx.lineJoin = 'round';
  ctx.lineCap = 'round';

  ctx.fillStyle = col;

  if (shape === 'people') {
    person(ctx, -s * 0.42, 0, s * 0.66);
    person(ctx, s * 0.44, 0, s * 0.6);
    return;
  }

  if (shape === 'cap') {
    ctx.beginPath();
    ctx.moveTo(0, -s * 0.55);
    ctx.lineTo(s * 0.95, -s * 0.12);
    ctx.lineTo(0, s * 0.18);
    ctx.lineTo(-s * 0.95, -s * 0.12);
    ctx.closePath();
    ctx.fill();
    ctx.beginPath();
    ctx.moveTo(-s * 0.42, s * 0.05);
    ctx.lineTo(s * 0.42, s * 0.05);
    ctx.lineTo(s * 0.5, s * 0.55);
    ctx.lineTo(-s * 0.5, s * 0.55);
    ctx.closePath();
    ctx.fill();
    ctx.strokeStyle = white;
    ctx.lineWidth = lw;
    ctx.beginPath();
    ctx.moveTo(s * 0.95, -s * 0.12);
    ctx.lineTo(s * 0.95, s * 0.34);
    ctx.stroke();
    ctx.fillStyle = white;
    ctx.beginPath();
    ctx.arc(s * 0.95, s * 0.42, s * 0.1, 0, Math.PI * 2);
    ctx.fill();
    return;
  }

  if (shape === 'paper') {
    ctx.beginPath();
    ctx.moveTo(-s * 0.5, -s * 0.7);
    ctx.lineTo(s * 0.32, -s * 0.7);
    ctx.lineTo(s * 0.5, -s * 0.52);
    ctx.lineTo(s * 0.5, s * 0.7);
    ctx.lineTo(-s * 0.5, s * 0.7);
    ctx.closePath();
    ctx.fill();
    ctx.fillStyle = dim;
    ctx.beginPath();
    ctx.moveTo(s * 0.32, -s * 0.7);
    ctx.lineTo(s * 0.32, -s * 0.52);
    ctx.lineTo(s * 0.5, -s * 0.52);
    ctx.closePath();
    ctx.fill();
    ctx.strokeStyle = white;
    ctx.lineWidth = lw;
    ctx.beginPath();
    ctx.moveTo(-s * 0.2, s * 0.04);
    ctx.lineTo(-s * 0.03, s * 0.22);
    ctx.lineTo(s * 0.26, -s * 0.16);
    ctx.stroke();
    return;
  }

  if (shape === 'clipboard') {
    roundRectPath(ctx, -s * 0.5, -s * 0.58, s, s * 1.16, s * 0.13);
    ctx.fill();
    ctx.fillStyle = dim;
    roundRectPath(ctx, -s * 0.2, -s * 0.72, s * 0.4, s * 0.22, s * 0.06);
    ctx.fill();
    ctx.strokeStyle = white;
    ctx.lineWidth = lw;
    const lines = [-s * 0.28, -s * 0.04, s * 0.2];
    for (const ly of lines) {
      ctx.beginPath();
      ctx.moveTo(-s * 0.3, ly);
      ctx.lineTo(s * 0.3, ly);
      ctx.stroke();
    }
    return;
  }

  if (shape === 'clock') {
    ctx.beginPath();
    ctx.arc(0, 0, s * 0.7, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = white;
    ctx.lineWidth = lw * 1.1;
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(0, -s * 0.4);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(s * 0.32, s * 0.06);
    ctx.stroke();
    ctx.fillStyle = white;
    ctx.beginPath();
    ctx.arc(0, 0, s * 0.08, 0, Math.PI * 2);
    ctx.fill();
    return;
  }

  if (shape === 'chat') {
    roundRectPath(ctx, -s * 0.6, -s * 0.55, s * 1.2, s * 0.92, s * 0.2);
    ctx.fill();
    ctx.beginPath();
    ctx.moveTo(-s * 0.24, s * 0.34);
    ctx.lineTo(-s * 0.1, s * 0.72);
    ctx.lineTo(s * 0.06, s * 0.34);
    ctx.closePath();
    ctx.fill();
    ctx.fillStyle = white;
    for (const dx of [-s * 0.24, 0, s * 0.24]) {
      ctx.beginPath();
      ctx.arc(dx, -s * 0.09, s * 0.085, 0, Math.PI * 2);
      ctx.fill();
    }
    return;
  }

  if (shape === 'ballot') {
    roundRectPath(ctx, -s * 0.55, -s * 0.6, s * 1.1, s * 1.2, s * 0.14);
    ctx.fill();
    ctx.strokeStyle = white;
    ctx.lineWidth = lw * 1.3;
    ctx.beginPath();
    ctx.moveTo(-s * 0.2, s * 0.04);
    ctx.lineTo(-s * 0.02, s * 0.24);
    ctx.lineTo(s * 0.28, -s * 0.18);
    ctx.stroke();
    return;
  }
}

interface Props {
  focusIndex: number | null;
  onFocus: (i: number) => void;
  onExit: () => void;
}

export function HeroConstellation({ focusIndex, onFocus, onExit }: Props) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const cardRef = useRef<HTMLDivElement>(null);
  const nodesRef = useRef<OrbitNode[]>(MODULES.map((m) => ({ ...m })));
  const focusRef = useRef<number | null>(null);
  const [cardPos, setCardPos] = useState<{ left: number; top: number } | null>(null);
  useEffect(() => { focusRef.current = focusIndex; }, [focusIndex]);

  const goPrev = () => {
    const cur = focusRef.current;
    if (cur === null) return;
    onFocus((cur - 1 + MODULES.length) % MODULES.length);
  };
  const goNext = () => {
    const cur = focusRef.current;
    if (cur === null) return;
    onFocus((cur + 1) % MODULES.length);
  };

  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') { onExit(); return; }
      if (focusRef.current === null) return;
      if (e.key === 'ArrowLeft') goNext();
      else if (e.key === 'ArrowRight') goPrev();
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onExit, onFocus]);

  // Position the focus card opposite the orb, clamped so it always fits inside the hero.
  useLayoutEffect(() => {
    if (focusIndex === null) { setCardPos(null); return; }
    const hero = canvasRef.current?.parentElement as HTMLElement | null;
    const card = cardRef.current;
    if (!hero || !card) return;

    const PAN_F = 0.4;
    const ZOOM_F = 1.45;
    const FACTOR = (1 - PAN_F) * ZOOM_F;

    const compute = () => {
      const rect = hero.getBoundingClientRect();
      const W = rect.width;
      const H = rect.height;
      if (W < 2 || H < 2) return;
      const cx = W / 2;
      const cy = H / 2;
      const F = Math.min(W, H) * 1.2;
      const R = Math.min(W * 0.42, H * 0.40);
      const n = nodesRef.current[focusIndex];
      const theta = n.theta;
      const rf = n.radiusFactor;
      const incl = n.incl;
      const lx = Math.cos(theta) * R * rf;
      const ly = -Math.sin(theta) * Math.sin(incl) * R * rf;
      const lz = Math.sin(theta) * Math.cos(incl) * R * rf;
      const pScale = F / (F - lz);
      const X = lx * pScale;
      const Y = ly * pScale;
      const cardCenterX = cx - X * FACTOR;
      const cardCenterY = cy - Y * FACTOR;

      const cw = card.offsetWidth;
      const ch = card.offsetHeight;
      const padX = 16;
      const padTop = 92;
      const padBottom = 24;

      let left = cardCenterX - cw / 2;
      let top = cardCenterY - ch / 2;
      left = Math.max(padX, Math.min(left, W - padX - cw));
      top = Math.max(padTop, Math.min(top, H - padBottom - ch));
      if (ch > H - padTop - padBottom) top = padTop;

      setCardPos((prev) => {
        if (prev && Math.abs(prev.left - left) < 0.5 && Math.abs(prev.top - top) < 0.5) return prev;
        return { left, top };
      });
    };

    compute();
    const ro = new ResizeObserver(compute);
    ro.observe(hero);
    return () => ro.disconnect();
  }, [focusIndex]);

  useEffect(() => {
    const canvasEl = canvasRef.current;
    if (!canvasEl) return;
    const ctx2d = canvasEl.getContext('2d');
    if (!ctx2d) return;
    const canvas: HTMLCanvasElement = canvasEl;
    const ctx: CanvasRenderingContext2D = ctx2d;

    const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    let W = 0;
    let H = 0;

    let targetYaw = 0;
    let targetPitch = 0;
    let yaw = 0;
    let pitch = 0;
    let panX = 0;
    let panY = 0;
    let zoom = 1;
    let focusAmt = 0;
    let crystalSpin = 0;

    const nodes = nodesRef.current;
    const particles: Particle[] = [];

    function buildParticles() {
      particles.length = 0;
      const count = Math.max(40, Math.min(140, Math.round((W * H) / 11000)));
      for (let i = 0; i < count; i++) {
        particles.push({
          x: (Math.random() - 0.5) * 2,
          y: (Math.random() - 0.5) * 2,
          z: (Math.random() - 0.5) * 2,
          r: Math.random() * 1.4 + 0.3,
          tw: Math.random() * Math.PI * 2,
          ts: 0.4 + Math.random() * 1.2,
        });
      }
    }

    function resize() {
      const rect = canvas.getBoundingClientRect();
      W = rect.width;
      H = rect.height;
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      canvas.width = Math.max(1, Math.round(W * dpr));
      canvas.height = Math.max(1, Math.round(H * dpr));
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      buildParticles();
      if (reduced) draw(performance.now());
    }

    function onPointerMove(e: PointerEvent) {
      if (focusRef.current !== null) return;
      const rect = canvas.getBoundingClientRect();
      const nx = (e.clientX - rect.left) / rect.width - 0.5;
      const ny = (e.clientY - rect.top) / rect.height - 0.5;
      targetYaw = nx * 0.9;
      targetPitch = ny * 0.6;
    }

    function sceneToNode(i: number, R: number, F: number): { X: number; Y: number; rad: number } | null {
      const n = nodes[i];
      const local: Point3 = {
        x: Math.cos(n.theta) * R * n.radiusFactor,
        y: -Math.sin(n.theta) * Math.sin(n.incl) * R * n.radiusFactor,
        z: Math.sin(n.theta) * Math.cos(n.incl) * R * n.radiusFactor,
      };
      const r = rot(local, yaw, pitch);
      const pScale = F / (F - r.z);
      return { X: r.x * pScale, Y: r.y * pScale, rad: n.size * pScale };
    }

    function onClick(e: MouseEvent) {
      if (focusRef.current !== null) return;
      const rect = canvas.getBoundingClientRect();
      const px = e.clientX - rect.left;
      const py = e.clientY - rect.top;
      const cx = W / 2;
      const cy = H / 2;
      const F = Math.min(W, H) * 1.2;
      const R = Math.min(W * 0.42, H * 0.40);
      let best = -1;
      let bestD = Infinity;
      for (let i = 0; i < nodes.length; i++) {
        const p = sceneToNode(i, R, F);
        if (!p) continue;
        const sx = cx + (p.X + panX) * zoom;
        const sy = cy + (p.Y + panY) * zoom;
        const d = Math.hypot(px - sx, py - sy);
        const hit = p.rad * zoom * 1.7 + 16;
        if (d < hit && d < bestD) { best = i; bestD = d; }
      }
      if (best >= 0) onFocus(best);
    }

    function onHoverMove(e: PointerEvent) {
      if (focusRef.current !== null) { canvas.style.cursor = 'zoom-out'; return; }
      const rect = canvas.getBoundingClientRect();
      const px = e.clientX - rect.left;
      const py = e.clientY - rect.top;
      const cx = W / 2;
      const cy = H / 2;
      const F = Math.min(W, H) * 1.2;
      const R = Math.min(W * 0.42, H * 0.40);
      let over = false;
      for (let i = 0; i < nodes.length; i++) {
        const p = sceneToNode(i, R, F);
        if (!p) continue;
        const sx = cx + (p.X + panX) * zoom;
        const sy = cy + (p.Y + panY) * zoom;
        if (Math.hypot(px - sx, py - sy) < p.rad * zoom * 1.7 + 16) { over = true; break; }
      }
      canvas.style.cursor = over ? 'pointer' : 'default';
    }

    let raf = 0;
    let last = performance.now();
    const running = !reduced;

    function draw(now: number) {
      if (W < 2 || H < 2) {
        if (running) raf = requestAnimationFrame(draw);
        return;
      }
      const dt = Math.min(0.05, (now - last) / 1000);
      last = now;

      const focused = focusRef.current;

      const targetFocus = focused === null ? 0 : 1;
      focusAmt += (targetFocus - focusAmt) * 0.12;
      if (focusAmt < 0.001) focusAmt = 0;
      if (focusAmt > 0.999) focusAmt = 1;
      const isFocusing = focusAmt > 0.01;

      if (isFocusing) {
        targetYaw += (0 - targetYaw) * 0.08;
        targetPitch += (0 - targetPitch) * 0.08;
      }
      yaw += (targetYaw - yaw) * 0.06;
      pitch += (targetPitch - pitch) * 0.06;
      if (running && !isFocusing) yaw += dt * 0.12;
      if (running) crystalSpin += dt * 0.35;

      const cx = W / 2;
      const cy = H / 2;
      const F = Math.min(W, H) * 1.2;
      const R = Math.min(W * 0.42, H * 0.40);

      if (running && !isFocusing) {
        for (const n of nodes) n.theta += n.speed * dt;
      }

      const targetZoom = 1 + 0.45 * focusAmt;
      zoom += (targetZoom - zoom) * 0.1;
      const PAN_F = 0.4;
      let tPanX = 0;
      let tPanY = 0;
      if (focused !== null) {
        const p = sceneToNode(focused, R, F);
        if (p) { tPanX = -p.X * PAN_F; tPanY = -p.Y * PAN_F; }
      }
      panX += (tPanX - panX) * 0.1;
      panY += (tPanY - panY) * 0.1;

      ctx.clearRect(0, 0, W, H);
      ctx.save();
      ctx.translate(cx, cy);
      ctx.scale(zoom, zoom);
      ctx.translate(panX, panY);

      // ── background particle field (depth fog) ──
      for (const p of particles) {
        if (running) p.tw += dt * p.ts;
        const pr = rot(p, yaw * 0.4, pitch * 0.4);
        const pScale = F / (F - pr.z * R * 0.6);
        const sx = pr.x * R * 1.8 * pScale;
        const sy = pr.y * R * 1.8 * pScale;
        const a = (0.08 + 0.4 * (0.5 + 0.5 * Math.sin(p.tw))) * (0.4 + 0.6 * (0.5 + 0.5 * pr.z)) * (1 - 0.5 * focusAmt);
        ctx.beginPath();
        ctx.arc(sx, sy, p.r * pScale, 0, Math.PI * 2);
        ctx.fillStyle = `rgba(180, 235, 220, ${a})`;
        ctx.fill();
      }

      // ── perspective floor grid ("a place") ──
      const groundY = R * 0.95;
      const gridExt = R * 1.6;
      const gridStep = R * 0.42;
      const gridFade = (1 - 0.6 * focusAmt);
      ctx.lineWidth = 1;
      for (let gx = -gridExt; gx <= gridExt + 0.01; gx += gridStep) {
        const a0 = rot({ x: gx, y: groundY, z: -gridExt }, yaw, pitch);
        const a1 = rot({ x: gx, y: groundY, z: gridExt }, yaw, pitch);
        const s0 = F / (F - a0.z); const s1 = F / (F - a1.z);
        const ax0 = a0.x * s0, ay0 = a0.y * s0, ax1 = a1.x * s1, ay1 = a1.y * s1;
        const mid = (a0.z + a1.z) * 0.5;
        const ga = Math.max(0, 0.13 - mid / (R * 6)) * gridFade;
        if (ga > 0.01) {
          ctx.strokeStyle = `rgba(45, 212, 191, ${ga})`;
          ctx.beginPath(); ctx.moveTo(ax0, ay0); ctx.lineTo(ax1, ay1); ctx.stroke();
        }
      }
      for (let gz = -gridExt; gz <= gridExt + 0.01; gz += gridStep) {
        const b0 = rot({ x: -gridExt, y: groundY, z: gz }, yaw, pitch);
        const b1 = rot({ x: gridExt, y: groundY, z: gz }, yaw, pitch);
        const s0 = F / (F - b0.z); const s1 = F / (F - b1.z);
        const bx0 = b0.x * s0, by0 = b0.y * s0, bx1 = b1.x * s1, by1 = b1.y * s1;
        const mid = (b0.z + b1.z) * 0.5;
        const ga = Math.max(0, 0.13 - mid / (R * 6)) * gridFade;
        if (ga > 0.01) {
          ctx.strokeStyle = `rgba(56, 189, 248, ${ga})`;
          ctx.beginPath(); ctx.moveTo(bx0, by0); ctx.lineTo(bx1, by1); ctx.stroke();
        }
      }

      // ── orbital guide rings (3 tilted rings) ──
      const rings = [{ incl: 0.0, rad: 1.0 }, { incl: 0.55, rad: 1.14 }, { incl: -0.4, rad: 1.28 }];
      for (const ring of rings) {
        ctx.beginPath();
        const steps = 80;
        for (let i = 0; i <= steps; i++) {
          const t = (i / steps) * Math.PI * 2;
          const local: Point3 = {
            x: Math.cos(t) * R * ring.rad,
            y: -Math.sin(t) * Math.sin(ring.incl) * R * ring.rad,
            z: Math.sin(t) * Math.cos(ring.incl) * R * ring.rad,
          };
          const r = rot(local, yaw, pitch);
          const pScale = F / (F - r.z);
          const sx = r.x * pScale;
          const sy = r.y * pScale;
          if (i === 0) ctx.moveTo(sx, sy);
          else ctx.lineTo(sx, sy);
        }
        ctx.strokeStyle = `rgba(94, 224, 178, ${0.09 * (1 - 0.6 * focusAmt)})`;
        ctx.lineWidth = 1;
        ctx.stroke();
      }

      // ── central crystal (icosahedron wireframe) ──
      const crystalSize = Math.min(W, H) * 0.075 * (1 - 0.45 * focusAmt);
      const crystalPulse = 0.5 + 0.5 * Math.sin(now / 900);
      // soft inner glow
      const hubGrad = ctx.createRadialGradient(0, 0, 0, 0, 0, crystalSize * 4);
      hubGrad.addColorStop(0, `rgba(16, 182, 130, ${(0.28 + 0.12 * crystalPulse) * (1 - 0.6 * focusAmt)})`);
      hubGrad.addColorStop(0.5, 'rgba(14, 166, 120, 0.08)');
      hubGrad.addColorStop(1, 'rgba(14, 166, 120, 0)');
      ctx.fillStyle = hubGrad;
      ctx.beginPath();
      ctx.arc(0, 0, crystalSize * 4, 0, Math.PI * 2);
      ctx.fill();

      // project crystal vertices (own spin + scene rotation)
      const cVerts = ICO_VERTS.map((v) => {
        const spun = rot(v, crystalSpin, crystalSpin * 0.55);
        const r = rot({ x: spun.x * crystalSize, y: spun.y * crystalSize, z: spun.z * crystalSize }, yaw, pitch);
        const pScale = F / (F - r.z);
        return { x: r.x * pScale, y: r.y * pScale, z: r.z, scale: pScale };
      });
      // edges, depth-sorted by average z
      const cEdges = ICO_EDGES.map(([a, b]) => {
        const va = cVerts[a]; const vb = cVerts[b];
        return { va, vb, z: (va.z + vb.z) * 0.5 };
      }).sort((p, q) => p.z - q.z);
      const cAlpha = 1 - 0.65 * focusAmt;
      for (const e of cEdges) {
        const ed = (e.z / (R || 1)) * 0.5 + 0.5;
        const ea = (0.15 + 0.5 * ed) * cAlpha;
        ctx.strokeStyle = `rgba(94, 224, 178, ${ea})`;
        ctx.lineWidth = 1 + ed * 0.8;
        ctx.beginPath();
        ctx.moveTo(e.va.x, e.va.y);
        ctx.lineTo(e.vb.x, e.vb.y);
        ctx.stroke();
      }
      // glowing crystal vertices
      for (const v of cVerts) {
        const vd = (v.z / (R || 1)) * 0.5 + 0.5;
        const vr = (2 + vd * 2.2) * (v.scale * 0.5 + 0.5);
        const vg = ctx.createRadialGradient(v.x, v.y, 0, v.x, v.y, vr * 2.2);
        vg.addColorStop(0, `rgba(180, 255, 226, ${(0.6 + 0.4 * crystalPulse) * cAlpha * vd})`);
        vg.addColorStop(1, 'rgba(94, 224, 178, 0)');
        ctx.fillStyle = vg;
        ctx.beginPath();
        ctx.arc(v.x, v.y, vr * 2.2, 0, Math.PI * 2);
        ctx.fill();
        ctx.fillStyle = `rgba(220, 255, 240, ${0.85 * cAlpha * vd})`;
        ctx.beginPath();
        ctx.arc(v.x, v.y, vr * 0.6, 0, Math.PI * 2);
        ctx.fill();
      }
      // bright core dot
      ctx.beginPath();
      ctx.arc(0, 0, crystalSize * 0.12, 0, Math.PI * 2);
      ctx.fillStyle = `rgba(180, 255, 226, ${(0.5 + 0.35 * crystalPulse) * (1 - 0.6 * focusAmt)})`;
      ctx.fill();

      // ── project orbiting module nodes ──
      const projected: Proj[] = nodes.map((n) => {
        const local: Point3 = {
          x: Math.cos(n.theta) * R * n.radiusFactor,
          y: -Math.sin(n.theta) * Math.sin(n.incl) * R * n.radiusFactor,
          z: Math.sin(n.theta) * Math.cos(n.incl) * R * n.radiusFactor,
        };
        const r = rot(local, yaw, pitch);
        const pScale = F / (F - r.z);
        return { n, r, pScale, X: r.x * pScale, Y: r.y * pScale, depth: r.z / (R || 1) };
      });
      projected.sort((a, b) => a.r.z - b.r.z);

      // ── connection lines hub -> nodes ──
      for (const p of projected) {
        const dim = focused !== null && nodes.indexOf(p.n) !== focused ? 0.25 : 1;
        const a = (0.08 + 0.20 * (0.5 + 0.5 * p.depth)) * dim * (1 - 0.4 * focusAmt);
        const grad = ctx.createLinearGradient(0, 0, p.X, p.Y);
        grad.addColorStop(0, 'rgba(94, 224, 178, 0.04)');
        grad.addColorStop(1, rgba(p.n.color, a));
        ctx.strokeStyle = grad;
        ctx.lineWidth = 1.1;
        ctx.beginPath();
        ctx.moveTo(0, 0);
        ctx.lineTo(p.X, p.Y);
        ctx.stroke();
      }

      // ── energy pulses flowing along connection lines ──
      if (focusAmt < 0.9) {
        for (const p of projected) {
          const dim = focused !== null && nodes.indexOf(p.n) !== focused ? 0.3 : 1;
          for (let k = 0; k < 3; k++) {
            const t = ((now / 2600 + k / 3 + p.n.theta / 6) % 1 + 1) % 1;
            const px = p.X * t;
            const py = p.Y * t;
            const pa = Math.sin(t * Math.PI) * 0.7 * dim * (1 - focusAmt);
            if (pa < 0.02) continue;
            const pg = ctx.createRadialGradient(px, py, 0, px, py, 6);
            pg.addColorStop(0, rgba(p.n.color, pa));
            pg.addColorStop(1, rgba(p.n.color, 0));
            ctx.fillStyle = pg;
            ctx.beginPath();
            ctx.arc(px, py, 6, 0, Math.PI * 2);
            ctx.fill();
          }
        }
      }

      // ── nodes — school-themed icons ──
      for (const p of projected) {
        const isFocusedNode = focused !== null && nodes.indexOf(p.n) === focused;
        const dim = focused !== null ? (isFocusedNode ? 1 : 0.3) : 1;
        const depthA = (0.45 + 0.55 * (0.5 + 0.5 * p.depth)) * dim;
        const rad = p.n.size * p.pScale * (isFocusedNode ? 1 + 0.35 * focusAmt : 1);

        const halo = ctx.createRadialGradient(p.X, p.Y, 0, p.X, p.Y, rad * 2.4);
        halo.addColorStop(0, rgba(p.n.color, 0.5 * depthA));
        halo.addColorStop(1, rgba(p.n.color, 0));
        ctx.fillStyle = halo;
        ctx.beginPath();
        ctx.arc(p.X, p.Y, rad * 2.4, 0, Math.PI * 2);
        ctx.fill();

        ctx.save();
        ctx.translate(p.X, p.Y);
        drawIcon(ctx, p.n.shape, rad * 1.5, p.n.color, depthA);
        ctx.restore();

        const fontSize = Math.max(11, 12 * p.pScale);
        ctx.font = `700 ${fontSize}px Estedad, Tahoma, sans-serif`;
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        const label = p.n.label;
        const tw = ctx.measureText(label).width;
        const padX = 8;
        const padY = 4;
        const bx = p.X - tw / 2 - padX;
        const by = p.Y + rad * 1.5 + 10;
        ctx.fillStyle = `rgba(6, 18, 22, ${0.55 * dim})`;
        roundRectPath(ctx, bx, by, tw + padX * 2, fontSize + padY * 2, 7);
        ctx.fill();
        ctx.fillStyle = `rgba(224, 245, 236, ${(0.6 + 0.4 * depthA) * dim})`;
        ctx.fillText(label, p.X, by + (fontSize + padY * 2) / 2);
      }

      ctx.restore();

      if (running) raf = requestAnimationFrame(draw);
    }

    const ro = new ResizeObserver(resize);
    ro.observe(canvas);
    resize();
    window.addEventListener('pointermove', onPointerMove, { passive: true });
    canvas.addEventListener('pointermove', onHoverMove, { passive: true });
    canvas.addEventListener('click', onClick);

    if (reduced) {
      draw(performance.now());
    } else {
      raf = requestAnimationFrame(draw);
    }

    const onVis = () => {
      if (document.hidden) {
        if (raf) cancelAnimationFrame(raf);
        raf = 0;
      } else if (running && !raf) {
        last = performance.now();
        raf = requestAnimationFrame(draw);
      }
    };
    document.addEventListener('visibilitychange', onVis);

    if (document.fonts && document.fonts.ready) {
      document.fonts.ready.then(() => { if (reduced) draw(performance.now()); });
    }

    return () => {
      if (raf) cancelAnimationFrame(raf);
      ro.disconnect();
      window.removeEventListener('pointermove', onPointerMove);
      canvas.removeEventListener('pointermove', onHoverMove);
      canvas.removeEventListener('click', onClick);
      document.removeEventListener('visibilitychange', onVis);
    };
  }, [onFocus]);

  const focusedModule = focusIndex !== null ? MODULES[focusIndex] : null;

  return (
    <>
      <canvas ref={canvasRef} className="lp-hero-canvas" aria-hidden="true" />

      <AnimatePresence>
        {focusedModule && (
          <motion.div
            className="lp-focus-backdrop"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            transition={{ duration: 0.3 }}
            onClick={onExit}
          />
        )}
      </AnimatePresence>

      <AnimatePresence>
        {focusedModule && (
          <motion.div
            key={focusIndex}
            ref={cardRef}
            className="lp-focus-card"
            style={cardPos ? { left: cardPos.left, top: cardPos.top } : undefined}
            initial={{ opacity: 0, scale: 0.9 }}
            animate={{ opacity: 1, scale: 1 }}
            exit={{ opacity: 0, scale: 0.95 }}
            transition={{ duration: 0.4, ease: [0.2, 0.8, 0.2, 1] }}
            onClick={(e) => e.stopPropagation()}
          >
            <button type="button" className="lp-focus-close" aria-label="بستن" onClick={onExit}>
              <i className="bi bi-x-lg" />
            </button>

            <button
              type="button"
              className="lp-focus-nav lp-focus-nav--prev"
              aria-label="قبلی"
              onClick={goPrev}
            >
              <i className="bi bi-chevron-right" />
            </button>
            <button
              type="button"
              className="lp-focus-nav lp-focus-nav--next"
              aria-label="بعدی"
              onClick={goNext}
            >
              <i className="bi bi-chevron-left" />
            </button>

            <span className="lp-focus-icon" style={{ background: rgba(focusedModule.color, 0.16), color: `rgb(${focusedModule.color.join(',')})` }}>
              <i className={`bi ${focusedModule.icon}`} />
            </span>
            <h3 className="lp-focus-title" style={{ color: `rgb(${focusedModule.color.join(',')})` }}>{focusedModule.label}</h3>
            <ul className="lp-focus-points">
              {focusedModule.points.map((pt) => (
                <li key={pt}><i className="bi bi-check2" /> {pt}</li>
              ))}
            </ul>
            <span className="lp-focus-hint">
              با کلیدهای جهت‌نما جابه‌جا شو یا بیرون را لمس کن
            </span>
          </motion.div>
        )}
      </AnimatePresence>
    </>
  );
}
