// LogoConstellation — drifting agency-seal nodes.
//
// Two modes (single component, parameterized — Lord Farquad's "Fork A"):
//   • "recognition" — hero use. Full-color seals at 32% opacity, ~80px.
//      12 visible at once, rotates so all 57 seals get screen time.
//   • "texture"     — site-wide ambient layer. Monochrome navy silhouettes
//      at ~12% opacity, ~50px. ~24 visible, slower drift, no rotation
//      (continuous gentle motion is enough — rotation distracts at this
//      density).
//
// Spec history:
//   2026-05-07 22:45 — v1: 19 seals, lines + plates removed
//   2026-05-07 22:53 — v2: drop lines, organic curl drift, smooth phase in/out
//   2026-05-07 23:00 — v3: 57 seals, recognition + texture modes, app-level mount

const SEALS = [
  // Cabinet departments (15)
  { name: 'state',          label: 'Department of State' },
  { name: 'treasury',       label: 'Department of the Treasury' },
  { name: 'defense',        label: 'Department of Defense' },
  { name: 'justice',        label: 'Department of Justice' },
  { name: 'interior',       label: 'Department of the Interior' },
  { name: 'agriculture',    label: 'Department of Agriculture' },
  { name: 'commerce',       label: 'Department of Commerce' },
  { name: 'labor',          label: 'Department of Labor' },
  { name: 'hhs',            label: 'Health and Human Services' },
  { name: 'hud',            label: 'Housing and Urban Development' },
  { name: 'transportation', label: 'Department of Transportation' },
  { name: 'energy',         label: 'Department of Energy' },
  { name: 'education',      label: 'Department of Education' },
  { name: 'veterans',       label: 'Veterans Affairs' },
  { name: 'dhs',            label: 'Homeland Security' },
  // Service branches (5)
  { name: 'airforce',       label: 'United States Air Force' },
  { name: 'navy',           label: 'United States Navy' },
  { name: 'army',           label: 'United States Army' },
  { name: 'spaceforce',     label: 'United States Space Force' },
  { name: 'marines',        label: 'United States Marine Corps' },
  // DOJ family
  { name: 'fbi',            label: 'Federal Bureau of Investigation' },
  { name: 'dea',            label: 'Drug Enforcement Administration' },
  { name: 'atf',            label: 'Bureau of Alcohol, Tobacco, Firearms and Explosives' },
  { name: 'usms',           label: 'United States Marshals Service' },
  // DHS family
  { name: 'fema',           label: 'Federal Emergency Management Agency' },
  { name: 'ice',            label: 'Immigration and Customs Enforcement' },
  { name: 'tsa',            label: 'Transportation Security Administration' },
  { name: 'uscg',           label: 'United States Coast Guard' },
  { name: 'secret-service', label: 'United States Secret Service' },
  // Treasury family
  { name: 'irs',            label: 'Internal Revenue Service' },
  // HHS family
  { name: 'fda',            label: 'Food and Drug Administration' },
  { name: 'cdc',            label: 'Centers for Disease Control and Prevention' },
  { name: 'nih',            label: 'National Institutes of Health' },
  { name: 'cms',            label: 'Centers for Medicare and Medicaid Services' },
  // Commerce family
  { name: 'noaa',           label: 'NOAA' },
  { name: 'census',         label: 'US Census Bureau' },
  { name: 'uspto',          label: 'US Patent and Trademark Office' },
  // Interior family
  { name: 'nps',            label: 'National Park Service' },
  { name: 'blm',            label: 'Bureau of Land Management' },
  { name: 'bia',            label: 'Bureau of Indian Affairs' },
  // DOT family
  { name: 'faa',            label: 'Federal Aviation Administration' },
  { name: 'fhwa',           label: 'Federal Highway Administration' },
  // Independent / executive-rank
  { name: 'epa',            label: 'Environmental Protection Agency' },
  { name: 'nasa',           label: 'NASA' },
  { name: 'sba',            label: 'Small Business Administration' },
  { name: 'nsf',            label: 'National Science Foundation' },
  { name: 'gsa',            label: 'General Services Administration' },
  { name: 'opm',            label: 'Office of Personnel Management' },
  { name: 'ssa',            label: 'Social Security Administration' },
  { name: 'fcc',            label: 'Federal Communications Commission' },
  { name: 'ftc',            label: 'Federal Trade Commission' },
  { name: 'sec',            label: 'Securities and Exchange Commission' },
  { name: 'fdic',           label: 'Federal Deposit Insurance Corporation' },
  { name: 'nrc',            label: 'Nuclear Regulatory Commission' },
  // Intel community
  { name: 'cia',            label: 'Central Intelligence Agency' },
  { name: 'nsa',            label: 'National Security Agency' },
  { name: 'odni',           label: 'Office of the Director of National Intelligence' },
];

// Seal name -> file extension (most are .png renders; a few are direct rasters)
const SEAL_EXT = {
  // Original raster fallbacks from the fetch
  'nrc': 'png',
};

const sealUrl = (name) => {
  const ext = SEAL_EXT[name] || 'png';
  return `assets/seals/png-render/${name}.${ext === 'png' ? 'png' : 'png'}`;
};

// Mode-specific defaults. Caller can override via props.
const MODE_DEFAULTS = {
  recognition: {
    visible: 30,
    sizeMin: 56, sizeMax: 88,
    opacity: 0.32,
    speedMin: 0.10, speedMax: 0.28,
    rotateMs: 9000,
    monochrome: false,
    fadeInMs: 2000, fadeOutMs: 2000,
    initialStaggerMs: 1500,
    edgePad: 30,
  },
  texture: {
    visible: 24,
    sizeMin: 38, sizeMax: 64,
    opacity: 0.10,
    speedMin: 0.04, speedMax: 0.10,  // slower drift — ambient
    rotateMs: 0,                      // no rotation; gentle continuous motion
    monochrome: true,
    fadeInMs: 3500, fadeOutMs: 3500,
    initialStaggerMs: 4000,
    edgePad: 20,
  },
};

const CURL_RATE = 0.0008;
const NOISE_AMP = 0.004;       // per-frame velocity nudge magnitude (organic feel)
const COLLISION_PAD = 6;       // px of empty buffer between seal edges
const SPAWN_MAX_TRIES = 12;    // max attempts to find a non-overlapping spawn spot

// Find a non-overlapping position for a new seal.
//   biasEdge=false  — fully random across the canvas (used for initial spawn)
//   biasEdge=true   — pin to one canvas edge (used for rotation respawns,
//                     so a new seal appears to drift in from off-screen)
// Returns {x, y} or null if no clear position was found in SPAWN_MAX_TRIES.
const pickNonOverlappingPosition = (state, size, edgePad, biasEdge = false) => {
  const w = state.width, h = state.height;
  const liveNodes = state.nodes.filter(n => n.phase !== 'out');

  for (let attempt = 0; attempt < SPAWN_MAX_TRIES; attempt++) {
    let x, y;
    if (biasEdge) {
      // Rotation behavior: pin to a random edge so the seal drifts inward
      const side = Math.floor(Math.random() * 4);
      if (side === 0)      { x = edgePad; }
      else if (side === 1) { x = w - size - edgePad; }
      else                 { x = edgePad + Math.random() * (w - 2*edgePad - size); }
      if (side === 2)      { y = edgePad; }
      else if (side === 3) { y = h - size - edgePad; }
      else                 { y = edgePad + Math.random() * (h - 2*edgePad - size); }
    } else {
      // Initial spawn: fully random over the whole canvas
      x = edgePad + Math.random() * (w - 2*edgePad - size);
      y = edgePad + Math.random() * (h - 2*edgePad - size);
    }

    // Check vs all live nodes
    let collides = false;
    const cx = x + size/2, cy = y + size/2, r = size/2;
    for (const o of liveNodes) {
      const ox = o.x + o.size/2, oy = o.y + o.size/2;
      const minDist = r + o.size/2 + COLLISION_PAD;
      if (Math.hypot(cx - ox, cy - oy) < minDist) { collides = true; break; }
    }
    if (!collides) return { x, y };
  }
  return null;
};

const LogoConstellation = ({ mode = 'recognition', sealsOverride = null }) => {
  const opts = { ...MODE_DEFAULTS[mode] };
  const sealList = sealsOverride || SEALS;

  const containerRef = React.useRef(null);
  const rafRef = React.useRef(null);
  const rotateTimerRef = React.useRef(null);

  const stateRef = React.useRef({
    nodes: [], width: 0, height: 0, inactive: [], reduced: false,
  });

  const [slots] = React.useState(() =>
    Array.from({ length: opts.visible }, (_, i) => ({ slotIdx: i }))
  );

  React.useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    const reduced = window.matchMedia &&
      window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    stateRef.current.reduced = reduced;

    const init = () => {
      const rect = container.getBoundingClientRect();
      stateRef.current.width = rect.width;
      stateRef.current.height = rect.height;

      const indices = sealList.map((_, i) => i);
      for (let i = indices.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [indices[i], indices[j]] = [indices[j], indices[i]];
      }
      const visibleCount = Math.min(opts.visible, sealList.length);
      const active = indices.slice(0, visibleCount);
      stateRef.current.inactive = indices.slice(visibleCount);

      // Place seals one-by-one, retrying if they would overlap an existing one.
      // Falls back to random placement after SPAWN_MAX_TRIES (collision response
      // will sort out any residual overlap on the first frames).
      stateRef.current.nodes = [];
      for (let slot = 0; slot < active.length; slot++) {
        const sealIdx = active[slot];
        const size = opts.sizeMin + Math.random() * (opts.sizeMax - opts.sizeMin);
        const angle = Math.random() * Math.PI * 2;
        const speed = opts.speedMin + Math.random() * (opts.speedMax - opts.speedMin);
        let x, y;
        const placed = pickNonOverlappingPosition(stateRef.current, size, opts.edgePad);
        if (placed) { x = placed.x; y = placed.y; }
        else {
          x = opts.edgePad + Math.random() * (rect.width - 2*opts.edgePad - size);
          y = opts.edgePad + Math.random() * (rect.height - 2*opts.edgePad - size);
        }
        stateRef.current.nodes.push({
          slot, sealIdx, x, y, size,
          vx: Math.cos(angle) * speed,
          vy: Math.sin(angle) * speed,
          phase: 'in',
          spawnAt: performance.now() + Math.random() * opts.initialStaggerMs,
          curlPhase: Math.random() * Math.PI * 2,
          curlFreq: 0.6 + Math.random() * 0.8,
        });
      }

      stateRef.current.nodes.forEach(n => {
        const el = container.querySelector(`[data-slot="${n.slot}"]`);
        if (el) {
          const seal = sealList[n.sealIdx];
          el.style.width = `${n.size}px`;
          el.style.height = `${n.size}px`;
          el.style.transform = `translate3d(${n.x}px, ${n.y}px, 0)`;
          el.style.backgroundImage = `url('${sealUrl(seal.name)}')`;
          el.setAttribute('aria-label', seal.label);
          el.style.opacity = '0';
        }
      });
    };

    init();

    if (reduced) return;

    const tick = () => {
      const s = stateRef.current;
      const w = s.width, h = s.height;
      const now = performance.now();

      for (const n of s.nodes) {
        let opacity = 0;
        if (n.phase === 'in') {
          if (now < n.spawnAt) {
            opacity = 0;
          } else {
            const t = Math.min(1, (now - n.spawnAt) / opts.fadeInMs);
            const eased = 1 - Math.pow(1 - t, 3);
            opacity = opts.opacity * eased;
            if (t >= 1) n.phase = 'live';
          }
        } else if (n.phase === 'live') {
          opacity = opts.opacity;
        } else if (n.phase === 'out') {
          const t = Math.min(1, (now - n.fadeOutAt) / opts.fadeOutMs);
          const eased = 1 - Math.pow(t, 3);
          opacity = opts.opacity * eased;
          if (t >= 1) n.opacity0Reached = true;
        }

        if (now >= n.spawnAt) {
          // Curl: slow, smooth direction rotation per node
          n.curlPhase += CURL_RATE * n.curlFreq;
          const dAngle = Math.sin(n.curlPhase) * CURL_RATE * n.curlFreq * 18;
          const cos = Math.cos(dAngle), sin = Math.sin(dAngle);
          const nvx = n.vx * cos - n.vy * sin;
          const nvy = n.vx * sin + n.vy * cos;
          n.vx = nvx; n.vy = nvy;

          // Tiny random velocity noise — hand-drawn feel, breaks perfect curves
          n.vx += (Math.random() - 0.5) * NOISE_AMP;
          n.vy += (Math.random() - 0.5) * NOISE_AMP;

          n.x += n.vx; n.y += n.vy;

          if (n.x < opts.edgePad || n.x + n.size > w - opts.edgePad) {
            n.vx *= -1;
            n.vy += (Math.random() - 0.5) * 0.04;
            n.x = Math.max(opts.edgePad, Math.min(w - opts.edgePad - n.size, n.x));
          }
          if (n.y < opts.edgePad || n.y + n.size > h - opts.edgePad) {
            n.vy *= -1;
            n.vx += (Math.random() - 0.5) * 0.04;
            n.y = Math.max(opts.edgePad, Math.min(h - opts.edgePad - n.size, n.y));
          }
        }

        const el = container.querySelector(`[data-slot="${n.slot}"]`);
        if (el) {
          el.style.transform = `translate3d(${n.x}px, ${n.y}px, 0)`;
          el.style.opacity = opacity.toFixed(3);
        }
      }

      // ── Pairwise collision avoidance ──
      // For every live pair, if they're closer than the sum of their radii
      // (+ COLLISION_PAD), separate them along the contact normal and swap
      // their normal-component velocities (elastic bounce, equal masses).
      // Tangential components are preserved — looks like a clean redirection.
      for (let i = 0; i < s.nodes.length; i++) {
        const a = s.nodes[i];
        if (a.phase === 'out' || performance.now() < a.spawnAt) continue;
        const ax = a.x + a.size/2, ay = a.y + a.size/2, ar = a.size/2;
        for (let j = i + 1; j < s.nodes.length; j++) {
          const b = s.nodes[j];
          if (b.phase === 'out' || performance.now() < b.spawnAt) continue;
          const bx = b.x + b.size/2, by = b.y + b.size/2, br = b.size/2;
          const dx = bx - ax, dy = by - ay;
          const dist = Math.hypot(dx, dy);
          const minDist = ar + br + COLLISION_PAD;
          if (dist > 0 && dist < minDist) {
            // Unit normal pointing from a to b
            const nx = dx / dist, ny = dy / dist;
            // Push apart so they no longer overlap (split the overlap evenly)
            const overlap = (minDist - dist) / 2;
            a.x -= nx * overlap;
            a.y -= ny * overlap;
            b.x += nx * overlap;
            b.y += ny * overlap;
            // Velocities along the collision normal
            const va_n = a.vx * nx + a.vy * ny;
            const vb_n = b.vx * nx + b.vy * ny;
            // Only respond if they're approaching (relative velocity along
            // normal is positive going b->a). Otherwise they're already
            // separating and bouncing again would whiplash them.
            if (va_n - vb_n > 0) {
              // Swap normal components (elastic, equal mass)
              a.vx += (vb_n - va_n) * nx;
              a.vy += (vb_n - va_n) * ny;
              b.vx += (va_n - vb_n) * nx;
              b.vy += (va_n - vb_n) * ny;
            }
          }
        }
      }

      // Re-normalize speeds toward target so noise + bounces don't accumulate
      const targetSpeed = (opts.speedMin + opts.speedMax) / 2;
      for (const n of s.nodes) {
        const sp = Math.hypot(n.vx, n.vy);
        if (sp > 0.001 && Math.abs(sp - targetSpeed) > 0.05) {
          n.vx = (n.vx / sp) * targetSpeed;
          n.vy = (n.vy / sp) * targetSpeed;
        }
      }

      rafRef.current = requestAnimationFrame(tick);
    };

    const onVisibility = () => {
      if (document.hidden) {
        if (rafRef.current) cancelAnimationFrame(rafRef.current);
        rafRef.current = null;
      } else if (!rafRef.current) {
        rafRef.current = requestAnimationFrame(tick);
      }
    };
    rafRef.current = requestAnimationFrame(tick);
    document.addEventListener('visibilitychange', onVisibility);

    // Rotation only if rotateMs > 0
    if (opts.rotateMs > 0) {
      const doRotate = () => {
        const s = stateRef.current;
        if (s.inactive.length === 0 || s.nodes.length === 0) return;
        const candidates = s.nodes.filter(n => n.phase === 'live');
        if (candidates.length === 0) return;

        let victim = candidates[0];
        let bestEdgeDist = Infinity;
        for (const n of candidates) {
          const ed = Math.min(n.x, n.y, s.width - n.x - n.size, s.height - n.y - n.size);
          if (ed < bestEdgeDist) { bestEdgeDist = ed; victim = n; }
        }
        victim.phase = 'out';
        victim.fadeOutAt = performance.now();
        victim.opacity0Reached = false;

        setTimeout(() => {
          if (!victim.opacity0Reached) return;
          s.inactive.push(victim.sealIdx);
          const incomingIdx = s.inactive.splice(
            Math.floor(Math.random() * s.inactive.length), 1
          )[0];
          victim.sealIdx = incomingIdx;

          // Rotation: spawn near an edge so the new seal drifts into view
          const pos = pickNonOverlappingPosition(s, victim.size, opts.edgePad, /* biasEdge */ true);
          if (pos) {
            victim.x = pos.x; victim.y = pos.y;
          } else {
            // Fallback: edge spawn (may overlap, collision system handles next frame)
            const side = Math.floor(Math.random() * 4);
            if (side === 0)      { victim.x = opts.edgePad; }
            else if (side === 1) { victim.x = s.width - victim.size - opts.edgePad; }
            else                 { victim.x = opts.edgePad + Math.random() * (s.width - 2*opts.edgePad - victim.size); }
            if (side === 2)      { victim.y = opts.edgePad; }
            else if (side === 3) { victim.y = s.height - victim.size - opts.edgePad; }
            else                 { victim.y = opts.edgePad + Math.random() * (s.height - 2*opts.edgePad - victim.size); }
          }

          const angle = Math.random() * Math.PI * 2;
          const speed = opts.speedMin + Math.random() * (opts.speedMax - opts.speedMin);
          victim.vx = Math.cos(angle) * speed;
          victim.vy = Math.sin(angle) * speed;
          victim.curlPhase = Math.random() * Math.PI * 2;

          const el = container.querySelector(`[data-slot="${victim.slot}"]`);
          if (el) {
            const seal = sealList[victim.sealIdx];
            el.style.backgroundImage = `url('${sealUrl(seal.name)}')`;
            el.setAttribute('aria-label', seal.label);
          }
          victim.phase = 'in';
          victim.spawnAt = performance.now();
        }, opts.fadeOutMs + 50);
      };
      rotateTimerRef.current = setInterval(doRotate, opts.rotateMs);
    }

    const onResize = () => {
      const rect = container.getBoundingClientRect();
      stateRef.current.width = rect.width;
      stateRef.current.height = rect.height;
      for (const n of stateRef.current.nodes) {
        n.x = Math.min(rect.width - n.size - opts.edgePad, Math.max(opts.edgePad, n.x));
        n.y = Math.min(rect.height - n.size - opts.edgePad, Math.max(opts.edgePad, n.y));
      }
    };
    window.addEventListener('resize', onResize);

    return () => {
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
      if (rotateTimerRef.current) clearInterval(rotateTimerRef.current);
      document.removeEventListener('visibilitychange', onVisibility);
      window.removeEventListener('resize', onResize);
    };
  }, [mode]);

  // CSS filter chain to convert PNG seals into a uniform navy silhouette for
  // the texture mode. Lossless and fully reversible — switching modes drops
  // the filter and the source PNG renders normally.
  // Produces a tone close to brand navy (#000080) with ~brightness(0.4).
  const sealFilter = mode === 'texture'
    ? 'brightness(0) saturate(100%) invert(8%) sepia(99%) saturate(7475%) hue-rotate(243deg) brightness(80%) contrast(143%)'
    : 'none';

  return (
    <div
      ref={containerRef}
      aria-hidden="true"
      style={{
        position: 'absolute',
        inset: 0,
        overflow: 'hidden',
        pointerEvents: 'none',
      }}
    >
      {slots.map((s) => (
        <div
          key={s.slotIdx}
          data-slot={s.slotIdx}
          style={{
            position: 'absolute',
            top: 0, left: 0,
            width: 80, height: 80,
            backgroundSize: 'contain',
            backgroundRepeat: 'no-repeat',
            backgroundPosition: 'center',
            opacity: 0,
            filter: sealFilter,
            willChange: 'transform, opacity',
            pointerEvents: 'none',
          }}
        />
      ))}
    </div>
  );
};

window.LogoConstellation = LogoConstellation;
