// The integrated node network. Not a backdrop: its geometry is pinned to real content
// anchors ([data-node] elements). One accent node detaches from the hero "box" and travels
// down the page as a scroll-progress trail, threading through each system and landing at
// contact — "one node always leaves the box" becomes the spine of the whole scroll.

const { useState, useLayoutEffect, useMemo, useRef } = React;

// deterministic PRNG so the faint constellation is stable across renders
function mulberry32(a) {
  return function () {
    a |= 0; a = (a + 0x6D2B79F5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

// the brand zigzag from LogoMark, in its own 0..44 / 0..30 space
const LOGO = [[4, 7], [12, 25], [22, 12], [30, 25], [38, 7]];
const LOGO_TIP = [42.5, 2];
const LOGO_C = [23, 13];

function polyLen(p) {
  let L = 0;
  for (let i = 1; i < p.length; i++) L += Math.hypot(p[i].x - p[i - 1].x, p[i].y - p[i - 1].y);
  return L;
}
// sample a point at fraction t (0..1) of a polyline, plus how many full vertices precede it
function sampleAt(p, t) {
  if (p.length < 2) return { pt: p[0] || { x: 0, y: 0 }, upto: 0 };
  const L = polyLen(p);
  let target = t * L;
  for (let i = 1; i < p.length; i++) {
    const seg = Math.hypot(p[i].x - p[i - 1].x, p[i].y - p[i - 1].y);
    if (target <= seg || i === p.length - 1) {
      const f = seg ? Math.min(1, target / seg) : 0;
      return { pt: { x: p[i - 1].x + (p[i].x - p[i - 1].x) * f, y: p[i - 1].y + (p[i].y - p[i - 1].y) * f }, upto: i - 1 };
    }
    target -= seg;
  }
  return { pt: p[p.length - 1], upto: p.length - 2 };
}

// sample a polyline at a target Y (page coords) — used to make the node track the viewport
// vertically as you scroll, riding the spine left/right as it descends through each waypoint
function sampleByY(p, y) {
  if (!p.length) return { x: 0, y: 0, idx: 0 };
  if (y <= p[0].y) return { x: p[0].x, y: p[0].y, idx: 0 };
  for (let i = 1; i < p.length; i++) {
    if (y <= p[i].y) {
      const dy = p[i].y - p[i - 1].y;
      const f = dy ? (y - p[i - 1].y) / dy : 0;
      return { x: p[i - 1].x + (p[i].x - p[i - 1].x) * f, y, idx: i - 1 };
    }
  }
  return { x: p[p.length - 1].x, y: p[p.length - 1].y, idx: p.length - 2 };
}

// measure every [data-node] anchor in document (page-pixel) coordinates
function useAnchors() {
  const [m, setM] = useState({ pts: {}, w: 0, h: 0, ver: 0 });
  useLayoutEffect(() => {
    let raf;
    const measure = () => {
      const sx = window.scrollX, sy = window.scrollY;
      const pts = {};
      document.querySelectorAll('[data-node]').forEach((el) => {
        const r = el.getBoundingClientRect();
        pts[el.getAttribute('data-node')] = { x: r.left + sx + r.width / 2, y: r.top + sy + r.height / 2 };
      });
      const h = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
      const w = document.documentElement.clientWidth;
      setM((prev) => ({ pts, w, h, ver: prev.ver + 1 }));
    };
    const schedule = () => { cancelAnimationFrame(raf); raf = requestAnimationFrame(measure); };
    schedule();
    window.addEventListener('resize', schedule);
    window.addEventListener('load', schedule);
    window.addEventListener('scroll', schedule, { passive: true });
    // reveal animations move anchored elements without changing size — re-measure when they finish
    const onTransitionEnd = (e) => { if (e.target && e.target.classList && e.target.classList.contains('wb-reveal')) schedule(); };
    document.addEventListener('transitionend', onTransitionEnd);
    const ro = new ResizeObserver(schedule);
    ro.observe(document.body);
    if (document.fonts && document.fonts.ready) document.fonts.ready.then(schedule);
    const timers = [600, 1500, 3000].map((t) => window.setTimeout(schedule, t));
    return () => {
      window.removeEventListener('resize', schedule);
      window.removeEventListener('load', schedule);
      window.removeEventListener('scroll', schedule);
      document.removeEventListener('transitionend', onTransitionEnd);
      timers.forEach((t) => window.clearTimeout(t));
      ro.disconnect();
      cancelAnimationFrame(raf);
    };
  }, []);
  return m;
}

function Network({ progress, reduced }) {
  const { pts, w, h } = useAnchors();

  // formation (the "box") laid out around the hero formation anchor, scaled up from LogoMark
  const formation = useMemo(() => {
    const fa = pts['formation'];
    if (!fa) return null;
    const scale = reduced ? 3.2 : (w < 760 ? 3.0 : 4.4);
    const map = ([x, y]) => ({ x: fa.x + (x - LOGO_C[0]) * scale, y: fa.y + (y - LOGO_C[1]) * scale });
    return { nodes: LOGO.map(map), tip: map(LOGO_TIP) };
  }, [pts, w, reduced]);

  // the spine the detached node travels: formation tip → node → systems → contact.
  // A couple of waypoints get small hand-tuned nudges so nodes don't collide with titles
  // and the final descent reads as a clean vertical:
  //  · sys-01 lifts off the "Analytics" port title (it was sitting right on it)
  //  · handoff snaps to sys-04's X so the last segment drops straight down, then lifts a
  //    touch so the node sits just over the "04 Handoff" chapter marker instead of below it
  const spine = useMemo(() => {
    if (!formation) return [];
    const seq = [formation.tip];
    WB.spine.forEach((id) => {
      const a = pts[id];
      if (!a) return;
      let p = { x: a.x, y: a.y };
      if (id === 'sys-01') p = { x: p.x, y: p.y - 34 };
      if (id === 'handoff') p = { x: (pts['sys-04'] ? pts['sys-04'].x : p.x), y: p.y - 16 };
      seq.push(p);
    });
    return seq;
  }, [formation, pts]);

  // faint deterministic constellation woven between anchors (intricacy without noise)
  const stars = useMemo(() => {
    if (reduced || !w || !h) return { dots: [], edges: [] };
    const n = Math.max(16, Math.min(64, Math.round(h / 110)));
    const rnd = mulberry32(20240620);
    const dots = [];
    for (let i = 0; i < n; i++) dots.push({ x: rnd() * w, y: 40 + rnd() * (h - 80) });
    const edges = [];
    for (let i = 0; i < dots.length; i++) {
      let best = -1, bd = Infinity;
      for (let j = 0; j < dots.length; j++) {
        if (i === j) continue;
        const d = (dots[i].x - dots[j].x) ** 2 + (dots[i].y - dots[j].y) ** 2;
        if (d < bd) { bd = d; best = j; }
      }
      if (best > i) edges.push([i, best]);
    }
    return { dots, edges };
  }, [w, h, reduced]);

  if (!formation || spine.length < 2) {
    return <svg className="wb-net-svg" width={w} height={h} style={{ height: h }} aria-hidden="true" />;
  }

  // cumulative fraction at each spine vertex → which waypoints the traveling node has reached
  const segLens = [];
  for (let i = 1; i < spine.length; i++) segLens.push(Math.hypot(spine[i].x - spine[i - 1].x, spine[i].y - spine[i - 1].y));
  const total = segLens.reduce((a, b) => a + b, 0) || 1;
  let acc = 0;
  const vertexFrac = [0];
  for (let i = 0; i < segLens.length; i++) { acc += segLens[i]; vertexFrac.push(acc / total); }

  // The node tracks the viewport vertically: it sits at the spine point whose Y equals the
  // current scroll center, so it moves WITH the site and passes cleanly through every waypoint
  // (incl. sys-04) on its way down — no snapping, no jumping the line.
  const vh = (typeof window !== 'undefined' ? window.innerHeight : 800);
  const spineTopY = spine[0].y, spineBotY = spine[spine.length - 1].y;
  const targetY = Math.max(spineTopY, Math.min(spineBotY, progress * (h - vh) + vh * 0.5));
  const headPt = sampleByY(spine, targetY);
  const headPos = { x: headPt.x, y: headPt.y };

  // trail = every vertex above the node, then down to the node
  const trailVerts = spine.filter((pt) => pt.y < targetY);
  let trail = 'M ' + (trailVerts.length ? trailVerts : [spine[0]]).map((p) => `${p.x} ${p.y}`).join(' L ');
  trail += ` L ${headPos.x} ${headPos.y}`;

  const fullSpine = 'M ' + spine.map((p) => `${p.x} ${p.y}`).join(' L ');
  const formPath = 'M ' + formation.nodes.map((p) => `${p.x} ${p.y}`).join(' L ');

  // every waypoint with a destination is a live link (always clickable, always iridescent)
  const linkFor = {};
  (WB.systems || []).forEach((s) => { linkFor[s.id] = s.href; });

  return (
    <svg className="wb-net-svg" width={w} height={h} style={{ height: h, display: 'block' }} aria-hidden="true">
      {/* iridescent sheen — on-brand: hues drift around the teal accent at matched lightness */}
      <defs>
        <linearGradient id="wb-iri" x1="0%" y1="0%" x2="100%" y2="100%" gradientUnits="objectBoundingBox">
          <stop offset="0%" stopColor="#2f9b91" />
          <stop offset="34%" stopColor="#2bb0b8" />
          <stop offset="66%" stopColor="#3aa874" />
          <stop offset="100%" stopColor="#2f8fb0" />
          {!reduced && (
            <animateTransform attributeName="gradientTransform" type="rotate"
              from="0 0.5 0.5" to="360 0.5 0.5" dur="7s" repeatCount="indefinite" />
          )}
        </linearGradient>
        <radialGradient id="wb-iri-soft">
          <stop offset="0%" stopColor="#2bb0b8" />
          <stop offset="100%" stopColor="#2f9b91" />
        </radialGradient>
      </defs>

      {/* constellation — barely-there intricacy */}
      <g stroke="var(--wb-node)" strokeWidth="1" opacity="0.12">
        {stars.edges.map(([a, b], i) => (
          <line key={i} x1={stars.dots[a].x} y1={stars.dots[a].y} x2={stars.dots[b].x} y2={stars.dots[b].y} />
        ))}
      </g>
      <g fill="var(--wb-node)" opacity="0.22">
        {stars.dots.map((d, i) => <circle key={i} cx={d.x} cy={d.y} r="1.4" />)}
      </g>

      {/* full spine, faint but readable */}
      <path d={fullSpine} fill="none" stroke="var(--wb-node)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.2" />

      {/* formation: the box the node leaves */}
      <path d={formPath} fill="none" stroke="var(--wb-node)" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" opacity="0.85" />
      <path d={`M ${formation.nodes[4].x} ${formation.nodes[4].y} L ${formation.tip.x} ${formation.tip.y}`}
            fill="none" stroke="var(--wb-accent)" strokeWidth="2.4" strokeLinecap="round" opacity={progress > 0.02 ? 0.3 : 0.9} />
      {formation.nodes.map((p, i) => <circle key={i} cx={p.x} cy={p.y} r="3" fill="var(--wb-node)" />)}

      {/* traveled accent trail */}
      <path d={trail} fill="none" stroke="var(--wb-accent)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />

      {/* waypoint nodes — every one with a destination is a link (always clickable);
          they clip/light up iridescent as the trail reaches them */}
      {spine.slice(1).map((p, i) => {
        const id = WB.spine[i];
        const reached = p.y <= targetY + 2;
        const href = linkFor[id];
        const lit = !!href || reached; // link points are always iridescent
        const dot = (
          <React.Fragment>
            {(reached || href) && <circle cx={p.x} cy={p.y} r="9" fill="url(#wb-iri-soft)" opacity={reached ? 0.18 : 0.1} />}
            <circle cx={p.x} cy={p.y} r={lit ? 4 : 3} fill={lit ? 'url(#wb-iri)' : 'var(--wb-node)'} />
          </React.Fragment>
        );
        if (!href) return <g key={i}>{dot}</g>;
        return (
          <a key={i} className="wb-net-link" href={href} target="_blank" rel="noopener noreferrer" style={{ pointerEvents: 'auto', cursor: 'pointer' }}>
            <circle cx={p.x} cy={p.y} r="11" className="wb-net-link__ring" fill="none" stroke="url(#wb-iri)" strokeWidth="1.5" opacity="0" style={{ pointerEvents: 'none' }} />
            {dot}
            <circle cx={p.x} cy={p.y} r="16" fill="#000" fillOpacity="0" style={{ pointerEvents: 'all' }} />
          </a>
        );
      })}

      {/* the traveling detached node — the camera you follow down the page */}
      <g className={reduced ? '' : 'wb-net-head'} style={{ transform: `translate(${headPos.x}px, ${headPos.y}px)` }}>
        <circle r="16" fill="url(#wb-iri-soft)" opacity="0.10" />
        <circle r="8" fill="url(#wb-iri-soft)" opacity="0.24" />
        <circle r="3.6" fill="url(#wb-iri)" />
      </g>
    </svg>
  );
}

window.Network = Network;
