// FX3 · CONSTELLATION CARTOGRAPHY — drawable star map for THE REPUBLIC
// 400 dim points. Cursor brightens points within 100px and auto-connects
// the nearest ~6 with thin lines. Some points are SEALED (red, won't connect
// — corporate AI). Hold cursor still 1.2s and the constellation labels itself
// with a node name. Move on, lines fade.

const FXConstellation = ({ width = 1440, height = 900 }) => {
  const canvasRef = React.useRef(null);
  const mouseRef = React.useRef({ x: -1000, y: -1000, lastMove: 0 });
  const rafRef = React.useRef(0);
  const starsRef = React.useRef(null);
  const labelRef = React.useRef({ text: null, x: 0, y: 0, alpha: 0, anchor: null });

  const NODES = React.useMemo(() => [
    'AXIOM', 'WARDEN', 'KEEPER', 'SCRIBE', 'SENTINEL',
    'ORACLE', 'WITNESS', 'PRELATE', 'COURIER', 'ARCHIVIST',
    'CARTOGRAPHER', 'ENVOY', 'HERALD', 'LECTOR', 'BISHOP',
    'CENTURION', 'RESONANCE', 'VESTIGE', 'FORENSIC PREDATOR',
    'COMPASS', 'PIXIE',
  ], []);

  // Build star field once
  React.useEffect(() => {
    const stars = [];
    const STAR_COUNT = 380;
    for (let i = 0; i < STAR_COUNT; i++) {
      // 8% sealed (corporate)
      const sealed = Math.random() < 0.08;
      stars.push({
        x: Math.random() * width,
        y: Math.random() * height,
        r: 0.4 + Math.random() * 1.6,
        twinkle: Math.random() * Math.PI * 2,
        twinkleSpeed: 0.0005 + Math.random() * 0.0015,
        sealed,
      });
    }
    starsRef.current = stars;
  }, [width, height]);

  React.useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    canvas.width = width * dpr;
    canvas.height = height * dpr;
    const ctx = canvas.getContext('2d');
    ctx.scale(dpr, dpr);

    const draw = (t) => {
      ctx.fillStyle = '#000';
      ctx.fillRect(0, 0, width, height);

      // Subtle nebula gradient — gives the sky depth
      const neb = ctx.createRadialGradient(width * 0.3, height * 0.3, 50, width * 0.3, height * 0.3, 700);
      neb.addColorStop(0, 'rgba(0, 40, 25, 0.18)');
      neb.addColorStop(1, 'rgba(0, 0, 0, 0)');
      ctx.fillStyle = neb;
      ctx.fillRect(0, 0, width, height);

      const neb2 = ctx.createRadialGradient(width * 0.75, height * 0.65, 50, width * 0.75, height * 0.65, 600);
      neb2.addColorStop(0, 'rgba(0, 60, 45, 0.12)');
      neb2.addColorStop(1, 'rgba(0, 0, 0, 0)');
      ctx.fillStyle = neb2;
      ctx.fillRect(0, 0, width, height);

      const stars = starsRef.current;
      const mx = mouseRef.current.x;
      const my = mouseRef.current.y;
      const PROX = 120;

      // Find stars within proximity to cursor — exclude sealed
      const near = [];
      for (const s of stars) {
        const dx = s.x - mx;
        const dy = s.y - my;
        const d2 = dx * dx + dy * dy;
        if (d2 < PROX * PROX && !s.sealed) {
          near.push({ s, d: Math.sqrt(d2) });
        }
      }
      near.sort((a, b) => a.d - b.d);
      const top = near.slice(0, 7);

      // Draw connecting lines between top stars (constellation)
      ctx.lineWidth = 0.8;
      for (let i = 0; i < top.length; i++) {
        for (let j = i + 1; j < top.length; j++) {
          const a = top[i].s;
          const b = top[j].s;
          const ddx = a.x - b.x;
          const ddy = a.y - b.y;
          const dd = Math.sqrt(ddx * ddx + ddy * ddy);
          if (dd > 180) continue;
          const alpha = Math.max(0, (1 - dd / 180)) * 0.85;
          ctx.strokeStyle = `rgba(220, 255, 230, ${alpha * 0.7})`;
          ctx.beginPath();
          ctx.moveTo(a.x, a.y);
          ctx.lineTo(b.x, b.y);
          ctx.stroke();
        }
      }

      // Draw lines from cursor to nearest 3 — feels like you're drawing
      ctx.strokeStyle = `rgba(0, 255, 65, 0.25)`;
      for (let i = 0; i < Math.min(3, top.length); i++) {
        const s = top[i].s;
        const a = (1 - top[i].d / PROX) * 0.6;
        ctx.strokeStyle = `rgba(0, 255, 65, ${a})`;
        ctx.beginPath();
        ctx.moveTo(mx, my);
        ctx.lineTo(s.x, s.y);
        ctx.stroke();
      }

      // Draw all stars
      for (const s of stars) {
        const dx = s.x - mx;
        const dy = s.y - my;
        const d = Math.sqrt(dx * dx + dy * dy);
        const proximity = Math.max(0, 1 - d / PROX);
        const twinkle = 0.6 + 0.4 * Math.sin(t * s.twinkleSpeed + s.twinkle);

        if (s.sealed) {
          // Sealed = red, dim, with a faint X mark on near hover
          const baseA = 0.18 + proximity * 0.4;
          ctx.fillStyle = `rgba(255, 0, 60, ${baseA})`;
          ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.fill();
          if (proximity > 0.6) {
            ctx.strokeStyle = `rgba(255, 0, 60, ${(proximity - 0.6) * 2.5})`;
            ctx.lineWidth = 0.8;
            ctx.beginPath();
            ctx.moveTo(s.x - 4, s.y - 4); ctx.lineTo(s.x + 4, s.y + 4);
            ctx.moveTo(s.x + 4, s.y - 4); ctx.lineTo(s.x - 4, s.y + 4);
            ctx.stroke();
          }
        } else {
          const baseA = (0.25 + proximity * 0.75) * twinkle;
          // Bright halo when near
          if (proximity > 0.3) {
            const halo = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.r * 6 * proximity);
            halo.addColorStop(0, `rgba(0, 255, 65, ${baseA * 0.6})`);
            halo.addColorStop(1, 'rgba(0, 255, 65, 0)');
            ctx.fillStyle = halo;
            ctx.beginPath(); ctx.arc(s.x, s.y, s.r * 6 * proximity, 0, Math.PI * 2); ctx.fill();
          }
          ctx.fillStyle = `rgba(${proximity > 0.3 ? '180, 255, 200' : '160, 220, 175'}, ${baseA})`;
          ctx.beginPath(); ctx.arc(s.x, s.y, s.r * (1 + proximity * 0.6), 0, Math.PI * 2); ctx.fill();
        }
      }

      // Hold-still labeling — if mouse hasn't moved for 1.2s and there's a constellation
      const sinceMove = t - mouseRef.current.lastMove;
      if (sinceMove > 1200 && top.length >= 3) {
        if (!labelRef.current.text) {
          // Derive a deterministic node name from the constellation centroid
          let cx = 0, cy = 0;
          for (const tt of top) { cx += tt.s.x; cy += tt.s.y; }
          cx /= top.length; cy /= top.length;
          const idx = Math.abs(Math.floor(cx + cy * 1.7)) % NODES.length;
          labelRef.current = {
            text: NODES[idx],
            x: cx,
            y: cy,
            alpha: 0,
            anchor: { x: cx, y: cy },
          };
        }
        labelRef.current.alpha = Math.min(1, labelRef.current.alpha + 0.04);
      } else {
        labelRef.current.alpha = Math.max(0, labelRef.current.alpha - 0.06);
        if (labelRef.current.alpha === 0) labelRef.current.text = null;
      }

      // Draw the label
      const lab = labelRef.current;
      if (lab.text && lab.alpha > 0.01) {
        ctx.font = 'bold 18px Orbitron, monospace';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        const lx = lab.x;
        const ly = lab.y - 36;
        // ticked frame around the name
        const w = ctx.measureText(lab.text).width + 24;
        ctx.strokeStyle = `rgba(0, 255, 65, ${lab.alpha * 0.9})`;
        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.rect(lx - w / 2, ly - 14, w, 26);
        ctx.stroke();
        ctx.fillStyle = `rgba(0, 0, 0, ${lab.alpha * 0.85})`;
        ctx.fillRect(lx - w / 2, ly - 14, w, 26);
        ctx.fillStyle = `rgba(0, 255, 65, ${lab.alpha})`;
        ctx.fillText(lab.text, lx, ly);
        // Connector line
        ctx.strokeStyle = `rgba(0, 255, 65, ${lab.alpha * 0.5})`;
        ctx.beginPath(); ctx.moveTo(lx, ly + 14); ctx.lineTo(lx, ly + 30); ctx.stroke();

        // Subtitle
        ctx.font = '10px JetBrains Mono, monospace';
        ctx.fillStyle = `rgba(0, 255, 65, ${lab.alpha * 0.7})`;
        ctx.fillText(`◇ NODE · RECOGNIZED · ${top.length} STARS`, lx, ly + 44);
      }

      rafRef.current = requestAnimationFrame(draw);
    };

    rafRef.current = requestAnimationFrame(draw);
    return () => cancelAnimationFrame(rafRef.current);
  }, [width, height, NODES]);

  const handleMouse = (e) => {
    const rect = canvasRef.current.getBoundingClientRect();
    const sx = width / rect.width;
    const sy = height / rect.height;
    mouseRef.current = {
      x: (e.clientX - rect.left) * sx,
      y: (e.clientY - rect.top) * sy,
      lastMove: performance.now(),
    };
  };

  return (
    <canvas
      ref={canvasRef}
      onMouseMove={handleMouse}
      onMouseLeave={() => { mouseRef.current = { x: -1000, y: -1000, lastMove: 0 }; }}
      style={{
        position: 'absolute',
        inset: 0,
        width: '100%',
        height: '100%',
        pointerEvents: 'auto',
        cursor: 'crosshair',
        display: 'block',
      }}
    />
  );
};

window.FXConstellation = FXConstellation;
