// Hero background — neural network graph (v2).
// Top-left + bottom band, crisp dots (no halos), 2-3 traveling signals on bezier arcs.

const HeroNetwork = () => {
  const canvasRef = React.useRef(null);
  const rafRef = React.useRef(0);

  React.useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    const dpr = Math.min(window.devicePixelRatio || 1, 2);

    const ACCENT = [105, 163, 163]; // teal-glow
    const TARGET_NODES = 180;

    let W = 0, H = 0;
    let nodes = [];
    let edges = [];
    let adj = [];        // adj[nodeIdx] = [edgeIdx, ...]
    let pulses = [];     // {edgeIdx, fromNode, toNode, t, life, intensity}
    let signals = [];
    let lastT = 0;
    let elapsed = 0;
    let nextEdgePulseAt = 0;
    let nextSignalAt = 0.4; // first signal soon after load

    function densityWeight(x, y) {
      // Top-left: bigger blob, center near canvas edge, larger sigma → tail extends well into upper-middle.
      const tlx = (x / W - 0.05);   // center near left edge
      const tly = (y / H - 0.10);   // center near top
      const tl = Math.exp(-(tlx*tlx + tly*tly) / 0.18) * 1.8;
      // Bottom band — unchanged
      const by = (y / H - 1.05);
      const bx = (x / W - 0.55);
      const bb = Math.exp(-(by*by) / 0.05) * Math.exp(-(bx*bx) / 0.6) * 1.3;
      return Math.max(0.04, tl + bb);
    }

    function biasedPoint() {
      for (let tries = 0; tries < 80; tries++) {
        const x = Math.random() * W;
        const y = Math.random() * H;
        const w = densityWeight(x, y);
        if (Math.random() < w) return { x, y };
      }
      return { x: Math.random() * W, y: Math.random() * H };
    }

    function resize() {
      const rect = canvas.getBoundingClientRect();
      W = rect.width;
      H = rect.height;
      canvas.width = Math.floor(W * dpr);
      canvas.height = Math.floor(H * dpr);
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      seed();
    }

    function seed() {
      nodes = [];

      // Poisson-disc-lite: throw candidates at the canvas, accept if not too close to existing nodes.
      // Min distance varies by density (denser areas → closer spacing).
      // Sample a wider region than canvas so the network spills off-edges.
      const REGION = { x: -W * 0.15, y: -H * 0.15, w: W * 1.30, h: H * 1.30 };

      const minDistFor = (x, y) => {
        const w = densityWeight(x, y);
        // High density (1.5) → 30px spacing. Low density (0.05) → 95px spacing.
        return 30 + (1 - Math.min(1, w / 1.6)) * 65;
      };

      const cellSize = 30;
      const cols = Math.ceil(REGION.w / cellSize) + 2;
      const rows = Math.ceil(REGION.h / cellSize) + 2;
      const grid = new Array(cols * rows).fill(null).map(() => []);
      const cellOf = (x, y) => {
        const cx = Math.floor((x - REGION.x) / cellSize);
        const cy = Math.floor((y - REGION.y) / cellSize);
        return { cx, cy };
      };
      const tooClose = (x, y, minD) => {
        const { cx, cy } = cellOf(x, y);
        const r = Math.ceil(minD / cellSize);
        for (let dy = -r; dy <= r; dy++) {
          for (let dx = -r; dx <= r; dx++) {
            const ix = cx + dx, iy = cy + dy;
            if (ix < 0 || iy < 0 || ix >= cols || iy >= rows) continue;
            const cell = grid[iy * cols + ix];
            for (const n of cell) {
              const ddx = n.x - x, ddy = n.y - y;
              if (ddx*ddx + ddy*ddy < minD * minD) return true;
            }
          }
        }
        return false;
      };

      const place = (x, y) => {
        const n = makeNode(x, y);
        nodes.push(n);
        const { cx, cy } = cellOf(x, y);
        if (cx >= 0 && cy >= 0 && cx < cols && cy < rows) {
          grid[cy * cols + cx].push(n);
        }
      };

      // Pass 1: dense seeding via density-weighted rejection
      const TARGET = 220;
      let placed = 0;
      let tries = 0;
      const MAX_TRIES = TARGET * 60;
      while (placed < TARGET && tries < MAX_TRIES) {
        tries++;
        const x = REGION.x + Math.random() * REGION.w;
        const y = REGION.y + Math.random() * REGION.h;
        const w = densityWeight(x, y);
        // Accept proportional to density (so harvest more samples in hot zones).
        if (Math.random() > w * 0.85) continue;
        const minD = minDistFor(x, y);
        if (tooClose(x, y, minD)) continue;
        place(x, y);
        placed++;
      }
      buildEdges();
    }

    function makeNode(x, y) {
      // z = 0 (far) → 1 (near). Power curve so most are mid-far, a few are near.
      const z = Math.pow(Math.random(), 1.4);
      return {
        x, y,
        baseX: x, baseY: y,
        driftAngle: Math.random() * Math.PI * 2,
        driftSpeed: 0.03 + Math.random() * 0.07,
        phase: Math.random() * Math.PI * 2,
        pulseSpeed: 0.5 + Math.random() * 0.6,
        size: 0.6 + z * 2.2,                 // far: 0.6px, near: 2.8px
        z,
      };
    }

    function buildEdges() {
      edges = [];
      const MAX_DIST = 180;
      for (let i = 0; i < nodes.length; i++) {
        const n = nodes[i];
        const dists = [];
        for (let j = 0; j < nodes.length; j++) {
          if (i === j) continue;
          const m = nodes[j];
          const dx = m.x - n.x, dy = m.y - n.y;
          const d2 = dx*dx + dy*dy;
          if (d2 > MAX_DIST * MAX_DIST) continue;
          dists.push({ j, d2 });
        }
        dists.sort((a, b) => a.d2 - b.d2);
        const k = 2 + (Math.random() < 0.3 ? 1 : 0);
        for (let q = 0; q < Math.min(k, dists.length); q++) {
          const j = dists[q].j;
          if (i < j) edges.push({ a: i, b: j });
          else if (j < i) edges.push({ a: j, b: i });
        }
      }
      const seen = new Set();
      edges = edges.filter(e => {
        const k = e.a + ',' + e.b;
        if (seen.has(k)) return false;
        seen.add(k);
        return true;
      });

      // Edge weights — most light, ~20% heavy. Heavy edges become the "highways" for data flow.
      for (const e of edges) {
        const r = Math.random();
        if (r < 0.20) e.weight = 0.7 + Math.random() * 0.3;       // 0.7-1.0 thick
        else if (r < 0.55) e.weight = 0.35 + Math.random() * 0.25; // medium
        else e.weight = 0.10 + Math.random() * 0.15;               // thin
      }

      // Adjacency list — which edges touch each node
      adj = nodes.map(() => []);
      edges.forEach((e, idx) => {
        adj[e.a].push(idx);
        adj[e.b].push(idx);
      });
    }

    function makePulse(edgeIdx, fromNode, toNode, intensity) {
      const e = edges[edgeIdx];
      const a = nodes[e.a], b = nodes[e.b];
      const dist = Math.hypot(a.x - b.x, a.y - b.y);
      // Speed scales with edge weight (heavy edges = highways = faster) and a base
      const speed = 40 + (e.weight || 0.4) * 50; // px/sec — slow, deliberate data flow
      return {
        edgeIdx, fromNode, toNode,
        t: 0, life: dist / speed,
        intensity,
      };
    }

    function spawnDataPulse() {
      // Bias starting node toward hot zones (top-left + bottom band) so pulses don't
      // cascade across the title area.
      let bestIdx = -1, bestScore = -1;
      for (let i = 0; i < 12; i++) {
        const idx = Math.floor(Math.random() * nodes.length);
        const n = nodes[idx];
        const w = densityWeight(n.x, n.y);
        const score = w * 2 + ((adj[idx] || []).length * 0.1) + Math.random() * 0.3;
        if (score > bestScore) { bestScore = score; bestIdx = idx; }
      }
      if (bestIdx < 0) return;
      const myEdges = adj[bestIdx] || [];
      if (myEdges.length === 0) return;
      const ei = myEdges[Math.floor(Math.random() * myEdges.length)];
      const e = edges[ei];
      const next = e.a === bestIdx ? e.b : e.a;
      pulses.push(makePulse(ei, bestIdx, next, 0.95));
    }

    function tick(now) {
      if (!lastT) lastT = now;
      const dt = Math.min(0.05, (now - lastT) / 1000);
      lastT = now;
      elapsed += dt;

      for (const n of nodes) {
        n.driftAngle += (Math.random() - 0.5) * 0.02;
        n.x += Math.cos(n.driftAngle) * n.driftSpeed * dt;
        n.y += Math.sin(n.driftAngle) * n.driftSpeed * dt;
        n.x += (n.baseX - n.x) * 0.04 * dt;
        n.y += (n.baseY - n.y) * 0.04 * dt;
      }

      // Data flow: pulses travel along edges from node to node.
      // Periodically inject a new pulse at a random node; existing pulses propagate to neighbors on arrival.
      if (elapsed > nextEdgePulseAt) {
        spawnDataPulse();
        nextEdgePulseAt = elapsed + 0.9 + Math.random() * 1.4;
      }

      // Update active pulses; on completion, propagate to 0-2 neighbor edges.
      const newPulses = [];
      for (const p of pulses) {
        p.t += dt;
        if (p.t < p.life) {
          newPulses.push(p);
          continue;
        }
        // Propagate: pick neighbors of toNode (excluding the edge we came from)
        // Only propagate if both endpoints are in a "hot zone" — keeps the cascade
        // contained to the top-left + bottom band, away from the title area.
        if (p.intensity > 0.25) {
          const toNode = nodes[p.toNode];
          const toW = densityWeight(toNode.x, toNode.y);
          // If the arriving node is already weak, don't branch — let pulse die here.
          if (toW > 0.30) {
            const candidates = adj[p.toNode]
              .filter(ei => ei !== p.edgeIdx)
              .map(ei => {
                const e = edges[ei];
                const farIdx = e.a === p.toNode ? e.b : e.a;
                const far = nodes[farIdx];
                return { ei, farIdx, w: densityWeight(far.x, far.y) };
              })
              // Skip edges that would lead OUT of a hot zone
              .filter(c => c.w > 0.25);

            const branchN = Math.random() < 0.30 ? 2 : 1;
            for (let i = candidates.length - 1; i > 0; i--) {
              const j = Math.floor(Math.random() * (i + 1));
              [candidates[i], candidates[j]] = [candidates[j], candidates[i]];
            }
            const picks = candidates.slice(0, branchN);
            for (const c of picks) {
              newPulses.push(makePulse(c.ei, p.toNode, c.farIdx, p.intensity * 0.78));
            }
          }
        }
      }
      pulses = newPulses;

      // Staggered spawn: one at a time on a timer, capped at 5 alive.
      if (signals.length < 5 && elapsed > nextSignalAt) {
        spawnSignal();
        nextSignalAt = elapsed + 1.4 + Math.random() * 1.6;
      }
      for (const s of signals) s.t += dt;
      signals = signals.filter(s => s.t < s.life);

      draw();
      rafRef.current = requestAnimationFrame(tick);
    }

    function spawnSignal() {
      // Enter from one screen edge, exit on a DIFFERENT edge.
      const PAD = 60;
      const sides = ['top', 'right', 'bottom', 'left'];
      const fromSide = sides[Math.floor(Math.random() * 4)];
      const toCandidates = sides.filter(s => s !== fromSide);
      const toSide = toCandidates[Math.floor(Math.random() * toCandidates.length)];
      const pointOn = (side) => {
        if (side === 'top')    return { x: Math.random() * W,         y: -PAD };
        if (side === 'bottom') return { x: Math.random() * W,         y: H + PAD };
        if (side === 'left')   return { x: -PAD,                      y: Math.random() * H };
        return                        { x: W + PAD,                   y: Math.random() * H };
      };
      const a = pointOn(fromSide);
      const b = pointOn(toSide);
      // Gentle curve via a midpoint offset perpendicular to the path
      const dx = b.x - a.x, dy = b.y - a.y;
      const len = Math.max(1, Math.hypot(dx, dy));
      const perpX = -dy / len, perpY = dx / len;
      const curve = (Math.random() - 0.5) * 0.18 * len;
      const mx = (a.x + b.x) / 2 + perpX * curve;
      const my = (a.y + b.y) / 2 + perpY * curve;
      // Vary speed: longer life = slower
      signals.push({ a, b, mx, my, t: 0, life: 7 + Math.random() * 5 });
    }

    function draw() {
      ctx.clearRect(0, 0, W, H);

      // Static edges — opacity & width depend on average z (depth) AND weight (data importance).
      for (const e of edges) {
        const a = nodes[e.a], b = nodes[e.b];
        const z = (a.z + b.z) / 2;
        const w = e.weight || 0.4;
        const op = (0.04 + z * 0.16) * (0.5 + w * 1.0);
        const lw = (0.35 + z * 0.55) * (0.6 + w * 1.3);
        ctx.strokeStyle = `rgba(${ACCENT[0]},${ACCENT[1]},${ACCENT[2]},${op})`;
        ctx.lineWidth = lw;
        ctx.beginPath();
        ctx.moveTo(a.x, a.y);
        ctx.lineTo(b.x, b.y);
        ctx.stroke();
      }

      // Data-flow pulses: the entire edge briefly brightens as the pulse passes through it.
      // No moving dot — keeps it visually distinct from shooting stars.
      for (const p of pulses) {
        const e = edges[p.edgeIdx];
        if (!e) continue;
        const a = nodes[e.a], b = nodes[e.b];
        const k = Math.min(1, p.t / p.life);
        // sin envelope: 0 → 1 → 0 over the lifetime of the pulse
        const env = Math.sin(k * Math.PI);
        const op = 0.85 * env * p.intensity;
        if (op < 0.01) continue;
        const w = e.weight || 0.4;
        ctx.strokeStyle = `rgba(${ACCENT[0]+40},${ACCENT[1]+40},${ACCENT[2]+40},${op})`;
        ctx.lineWidth = (0.6 + w * 1.6) * (0.8 + env * 0.6);
        ctx.beginPath();
        ctx.moveTo(a.x, a.y);
        ctx.lineTo(b.x, b.y);
        ctx.stroke();
      }

      for (const s of signals) {
        const k = s.t / s.life;
        const ax = s.a.x, ay = s.a.y;
        const bx = s.b.x, by = s.b.y;
        const mx = s.mx, my = s.my;
        const bezier = (t) => {
          const u = 1 - t;
          return {
            x: u*u*ax + 2*u*t*mx + t*t*bx,
            y: u*u*ay + 2*u*t*my + t*t*by,
          };
        };
        const tail = 0.22;
        const steps = 18;
        for (let i = 0; i < steps; i++) {
          const t1 = Math.max(0, k - tail + (i / steps) * tail);
          const t2 = Math.max(0, k - tail + ((i + 1) / steps) * tail);
          if (t2 > 1) break;
          const p1 = bezier(t1), p2 = bezier(t2);
          const segOp = (i / steps) * 0.7;
          ctx.strokeStyle = `rgba(${ACCENT[0]+30},${ACCENT[1]+30},${ACCENT[2]+30},${segOp})`;
          ctx.lineWidth = 1.2;
          ctx.beginPath();
          ctx.moveTo(p1.x, p1.y);
          ctx.lineTo(p2.x, p2.y);
          ctx.stroke();
        }
        if (k <= 1) {
          const head = bezier(Math.min(1, k));
          const fade = 1 - Math.max(0, k - 0.85) * 6;
          ctx.fillStyle = `rgba(241,236,226,${0.95 * Math.max(0, fade)})`;
          ctx.beginPath();
          ctx.arc(head.x, head.y, 1.8, 0, Math.PI * 2);
          ctx.fill();
        }
      }

      // Sort nodes by z so near ones render on top of far ones (proper depth painter's algorithm).
      const sorted = [...nodes].sort((a, b) => a.z - b.z);
      for (const n of sorted) {
        const pulse = (Math.sin(elapsed * n.pulseSpeed + n.phase) + 1) * 0.5;
        // Depth cue: far nodes muted (lower opacity, cooler), near nodes bright.
        const baseOp = 0.18 + n.z * 0.65;
        const op = baseOp * (0.75 + pulse * 0.35);
        // Mix accent toward white as z increases (near = brighter)
        const mix = n.z * 0.35;
        const r = ACCENT[0] + (241 - ACCENT[0]) * mix;
        const g = ACCENT[1] + (236 - ACCENT[1]) * mix;
        const b = ACCENT[2] + (226 - ACCENT[2]) * mix;
        ctx.fillStyle = `rgba(${r|0},${g|0},${b|0},${op})`;
        ctx.beginPath();
        ctx.arc(n.x, n.y, n.size * (0.85 + pulse * 0.25), 0, Math.PI * 2);
        ctx.fill();
      }
    }

    resize();
    rafRef.current = requestAnimationFrame(tick);
    const onResize = () => resize();
    window.addEventListener('resize', onResize);

    return () => {
      cancelAnimationFrame(rafRef.current);
      window.removeEventListener('resize', onResize);
    };
  }, []);

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

window.HeroNetwork = HeroNetwork;
