"use client";

import React, { useEffect, useRef } from "react";

/* ============================================================
   NeuralField — the animated neural-network + particle backdrop behind the
   MileTruth™ AI band on /solutions.

   Deliberately a plain 2D canvas rather than the R3F MileTruthCanvas scene:
   this one sits *behind* body copy at low opacity, so it has to stay cheap
   enough to run under the fold of a long marketing page. It follows the same
   rules as the other canvases on the site (WorkflowScene, MileTruthNetwork):

     · DPR-aware, resized from the parent box (ResizeObserver)
     · rAF paused while off-screen and while the tab is hidden
     · prefers-reduced-motion paints ONE static frame and stops

   Three layers, back to front:
     1. particle field  — tiny drifting motes, no links
     2. neural mesh     — drifting nodes + proximity links, accent-tinted as
                          the two ends get closer
     3. signals         — a few bright packets travelling node-to-node, which
                          is what reads as "a decision engine thinking"
   ============================================================ */

type Node = {
  x: number;
  y: number;
  vx: number;
  vy: number;
  r: number;
  hub: boolean;
  /** phase offset so hub glow does not pulse in lockstep */
  ph: number;
};

type Mote = { x: number; y: number; vy: number; sway: number; ph: number; a: number };

type Signal = { a: number; b: number; t: number; sp: number };

/** Distance at which two nodes stop being linked (CSS px). */
const LINK = 132;
/** Pointer radius that brightens the mesh. */
const CURSOR = 170;

function hexToRgb(hex: string): [number, number, number] {
  const h = hex.replace("#", "");
  const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
  const n = parseInt(full, 16);
  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}

export default function NeuralField({
  accent = "#E2354A",
  tone = "dark",
  className,
}: {
  accent?: string;
  /** Surface the field is painted on. "light" inks the mesh near-black instead
   *  of white and drops the additive glow, which is invisible on paper. */
  tone?: "dark" | "light";
  className?: string;
}) {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    const parent = canvas?.parentElement;
    if (!canvas || !parent) return;
    const ctx = canvas.getContext("2d", { alpha: true });
    if (!ctx) return;

    const [ar, ag, ab] = hexToRgb(accent);
    const light = tone === "light";
    // The ink the mesh fades towards when a link is long / far from the pointer.
    const [ir, ig, ib] = light ? [17, 17, 17] : [255, 255, 255];
    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    let w = 0;
    let h = 0;
    let nodes: Node[] = [];
    let motes: Mote[] = [];
    let signals: Signal[] = [];
    let raf = 0;
    let visible = true;
    let running = false;
    let last = 0;
    let nextSignal = 600;
    const pointer = { x: -9999, y: -9999 };

    const rand = (a: number, b: number) => a + Math.random() * (b - a);

    function build() {
      const area = w * h;
      // One node per ~24k css px², clamped — enough to read as a mesh at
      // 1440px wide without turning the O(n²) link pass into real work.
      const count = Math.max(16, Math.min(72, Math.round(area / 24000)));
      nodes = Array.from({ length: count }, (_, i) => ({
        x: Math.random() * w,
        y: Math.random() * h,
        vx: rand(-0.14, 0.14),
        vy: rand(-0.11, 0.11),
        r: rand(1.1, 2.3),
        hub: i % 9 === 0,
        ph: Math.random() * Math.PI * 2,
      }));
      motes = Array.from({ length: Math.min(140, Math.round(area / 9000)) }, () => ({
        x: Math.random() * w,
        y: Math.random() * h,
        vy: rand(-0.22, -0.06),
        sway: rand(6, 20),
        ph: Math.random() * Math.PI * 2,
        a: rand(0.06, 0.28),
      }));
      signals = [];
    }

    function resize() {
      const rect = parent!.getBoundingClientRect();
      if (!rect.width || !rect.height) return;
      const dpr = Math.min(2, window.devicePixelRatio || 1);
      w = rect.width;
      h = rect.height;
      canvas!.width = Math.round(w * dpr);
      canvas!.height = Math.round(h * dpr);
      canvas!.style.width = w + "px";
      canvas!.style.height = h + "px";
      ctx!.setTransform(dpr, 0, 0, dpr, 0, 0);
      build();
      if (reduce) draw(0, 0);
    }

    function spawnSignal() {
      if (nodes.length < 2 || signals.length >= 3) return;
      const a = Math.floor(Math.random() * nodes.length);
      // Prefer a neighbour that is actually linked, so the packet rides a
      // visible edge instead of crossing empty space.
      let b = -1;
      let best = Infinity;
      for (let i = 0; i < nodes.length; i++) {
        if (i === a) continue;
        const d = Math.hypot(nodes[i].x - nodes[a].x, nodes[i].y - nodes[a].y);
        if (d < LINK && d < best && Math.random() > 0.45) {
          best = d;
          b = i;
        }
      }
      if (b < 0) return;
      signals.push({ a, b, t: 0, sp: rand(0.5, 0.95) });
    }

    function draw(dt: number, time: number) {
      ctx!.clearRect(0, 0, w, h);

      /* ---- 1. particle field ---- */
      for (const m of motes) {
        ctx!.beginPath();
        ctx!.arc(m.x + Math.sin(time * 0.0004 + m.ph) * m.sway, m.y, 0.9, 0, Math.PI * 2);
        ctx!.fillStyle = "rgba(" + ir + "," + ig + "," + ib + "," + m.a * (light ? 0.4 : 0.55) + ")";
        ctx!.fill();
      }

      /* ---- 2. neural mesh ---- */
      ctx!.lineWidth = 1;
      for (let i = 0; i < nodes.length; i++) {
        const a = nodes[i];
        for (let j = i + 1; j < nodes.length; j++) {
          const b = nodes[j];
          const dx = a.x - b.x;
          const dy = a.y - b.y;
          const d2 = dx * dx + dy * dy;
          if (d2 > LINK * LINK) continue;
          const d = Math.sqrt(d2);
          const prox = 1 - d / LINK;
          // Near the pointer the mesh warms towards the accent and brightens.
          const mx = (a.x + b.x) * 0.5;
          const my = (a.y + b.y) * 0.5;
          const cur = Math.max(0, 1 - Math.hypot(mx - pointer.x, my - pointer.y) / CURSOR);
          const tint = Math.min(1, prox * 0.85 + cur * 0.6);
          const alpha = (prox * 0.3 + cur * 0.35) * (light ? 0.6 : 1);
          const r = Math.round(ir + (ar - ir) * tint);
          const g = Math.round(ig + (ag - ig) * tint);
          const bl = Math.round(ib + (ab - ib) * tint);
          ctx!.strokeStyle = "rgba(" + r + "," + g + "," + bl + "," + alpha.toFixed(3) + ")";
          ctx!.beginPath();
          ctx!.moveTo(a.x, a.y);
          ctx!.lineTo(b.x, b.y);
          ctx!.stroke();
        }
      }

      for (const n of nodes) {
        const cur = Math.max(0, 1 - Math.hypot(n.x - pointer.x, n.y - pointer.y) / CURSOR);
        if (n.hub) {
          const pulse = 0.5 + 0.5 * Math.sin(time * 0.0016 + n.ph);
          ctx!.save();
          // An additive bloom only reads on a dark surface; on paper it muddies.
          if (!light) {
            ctx!.shadowColor = "rgba(" + ar + "," + ag + "," + ab + ",0.9)";
            ctx!.shadowBlur = 10 + pulse * 12;
          }
          ctx!.beginPath();
          ctx!.arc(n.x, n.y, n.r + 0.7 + pulse * 0.6, 0, Math.PI * 2);
          ctx!.fillStyle = "rgba(" + ar + "," + ag + "," + ab + "," + (0.5 + pulse * 0.35) * (light ? 0.7 : 1) + ")";
          ctx!.fill();
          ctx!.restore();
        } else {
          ctx!.beginPath();
          ctx!.arc(n.x, n.y, n.r, 0, Math.PI * 2);
          ctx!.fillStyle = "rgba(" + ir + "," + ig + "," + ib + "," + (0.16 + cur * 0.4) * (light ? 0.55 : 1) + ")";
          ctx!.fill();
        }
      }

      /* ---- 3. travelling signals ---- */
      for (const s of signals) {
        const a = nodes[s.a];
        const b = nodes[s.b];
        if (!a || !b) continue;
        const x = a.x + (b.x - a.x) * s.t;
        const y = a.y + (b.y - a.y) * s.t;
        // Fade in and out so packets never pop at the endpoints.
        const fade = Math.sin(s.t * Math.PI);
        ctx!.save();
        if (!light) {
          ctx!.shadowColor = "rgba(" + ar + "," + ag + "," + ab + ",1)";
          ctx!.shadowBlur = 14;
        }
        ctx!.beginPath();
        ctx!.arc(x, y, 1.9, 0, Math.PI * 2);
        // On paper the packet is the accent itself; on dark it burns towards white.
        ctx!.fillStyle = light
          ? "rgba(" + ar + "," + ag + "," + ab + "," + fade + ")"
          : "rgba(255," + Math.round(ag * 0.7 + 90) + "," + Math.round(ab * 0.7 + 90) + "," + fade + ")";
        ctx!.fill();
        ctx!.restore();
      }

      if (reduce) return;

      /* ---- integrate ---- */
      const step = Math.min(3, dt / 16.67);
      for (const n of nodes) {
        n.x += n.vx * step;
        n.y += n.vy * step;
        if (n.x < -20) n.x = w + 20;
        if (n.x > w + 20) n.x = -20;
        if (n.y < -20) n.y = h + 20;
        if (n.y > h + 20) n.y = -20;
      }
      for (const m of motes) {
        m.y += m.vy * step;
        if (m.y < -10) {
          m.y = h + 10;
          m.x = Math.random() * w;
        }
      }
      for (let i = signals.length - 1; i >= 0; i--) {
        signals[i].t += (signals[i].sp * step) / 60;
        if (signals[i].t >= 1) signals.splice(i, 1);
      }
      nextSignal -= dt;
      if (nextSignal <= 0) {
        spawnSignal();
        nextSignal = rand(700, 1800);
      }
    }

    function frame(time: number) {
      const dt = last ? time - last : 16.67;
      last = time;
      draw(dt, time);
      raf = requestAnimationFrame(frame);
    }

    function start() {
      if (running || reduce) return;
      running = true;
      last = 0;
      raf = requestAnimationFrame(frame);
    }
    function stop() {
      running = false;
      cancelAnimationFrame(raf);
    }

    const ro = new ResizeObserver(resize);
    ro.observe(parent);
    resize();

    const io = new IntersectionObserver(
      (entries) => {
        visible = entries.some((e) => e.isIntersecting);
        if (visible && !document.hidden) start();
        else stop();
      },
      { rootMargin: "120px 0px" },
    );
    io.observe(canvas);

    const onVisibility = () => {
      if (document.hidden) stop();
      else if (visible) start();
    };
    const onMove = (e: PointerEvent) => {
      const rect = canvas.getBoundingClientRect();
      pointer.x = e.clientX - rect.left;
      pointer.y = e.clientY - rect.top;
    };
    const onLeave = () => {
      pointer.x = -9999;
      pointer.y = -9999;
    };

    document.addEventListener("visibilitychange", onVisibility);
    // Listener on the section (not the canvas — it is pointer-events: none).
    const host = parent.closest("section") ?? parent;
    host.addEventListener("pointermove", onMove as EventListener);
    host.addEventListener("pointerleave", onLeave);

    return () => {
      stop();
      ro.disconnect();
      io.disconnect();
      document.removeEventListener("visibilitychange", onVisibility);
      host.removeEventListener("pointermove", onMove as EventListener);
      host.removeEventListener("pointerleave", onLeave);
    };
  }, [accent, tone]);

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