'use client';

/**
 * ParticleHero — public entry to the WebGL particle background.
 *
 * Responsibilities (kept OUT of ParticleField so that component can
 * assume happy-path WebGL2):
 *
 *   1. WebGL2 missing → static gradient.
 *   2. Mobile / low-power profile → either 16k particles or skip.
 *   3. prefers-reduced-motion → still WebGL + cursor tracking, but
 *      capped at 16k particles with halved drift and halved push
 *      velocity. The ring still follows the cursor at full fidelity
 *      because that's the interaction the user explicitly wants; we
 *      only damp the *ambient* motion so the field reads as calm.
 *   4. Lazy-load Three.js via next/dynamic so the hero LCP text isn't
 *      blocked by ~150kb of WebGL plumbing.
 *
 * The static fallback is a CSS-only radial gradient + faint dot mask.
 * It looks intentional, not broken — same color story as the live
 * particle field, just without motion.
 */

import dynamic from 'next/dynamic';
import { useEffect, useState } from 'react';

type Capability =
  | { kind: 'unknown' }
  | { kind: 'fallback' }
  | { kind: 'webgl'; textureSize: 128 | 256; motionScale: number };

// Dynamic import keeps three out of the initial bundle. ssr:false
// because there's no DOM/Canvas during SSR anyway.
const ParticleField = dynamic(
  () => import('./ParticleField').then((m) => ({ default: m.ParticleField })),
  {
    ssr: false,
    // No loading UI — the static gradient already lives at z-0 above
    // until this resolves, and the canvas paints into the same slot.
    loading: () => null,
  },
);

/**
 * Detect WebGL2 + float-render-target support without keeping the
 * context around. We create a throwaway canvas, ask for `webgl2`, and
 * probe `EXT_color_buffer_float`. If anything fails we fall back.
 *
 * Runs sync-only in the browser; never during SSR.
 */
function detectWebGL2(): boolean {
  try {
    const c = document.createElement('canvas');
    const gl = c.getContext('webgl2');
    if (!gl) return false;
    const ext = gl.getExtension('EXT_color_buffer_float');
    // Losing context to ensure we don't leak the probe.
    const loseExt = gl.getExtension('WEBGL_lose_context');
    loseExt?.loseContext();
    return Boolean(ext);
  } catch {
    return false;
  }
}

export function ParticleHero() {
  // Start in 'unknown' so SSR markup matches the first client render —
  // the fallback gradient is rendered until we resolve capability, so
  // there's no flash either way.
  const [cap, setCap] = useState<Capability>({ kind: 'unknown' });

  useEffect(() => {
    // 1. WebGL2 + float targets — hard gate. Without these the sim
    //    can't run at all, fall through to the static gradient.
    if (!detectWebGL2()) {
      setCap({ kind: 'fallback' });
      return;
    }

    const reduce = window.matchMedia('(prefers-reduced-motion: reduce)');

    /**
     * Pick the right particle tier for the device.
     *
     * Returns a non-fallback WebGL config OR null when the device is
     * too constrained to render the field at all (low-core phones).
     * Reduced-motion does NOT shrink the tier here — it's applied as
     * a separate motion scalar on top, because the user still wants
     * the cursor-tracking interaction.
     */
    const pickTier = (): Capability => {
      // Heuristic: small viewport OR an absurd DPR (low-DPI phones lying
      // about retina) with no high-end signal. hardwareConcurrency is a
      // rough but free proxy; logical cores <= 4 on a small viewport is
      // a strong hint we shouldn't push 65k particles.
      const isNarrow = window.matchMedia('(max-width: 768px)').matches;
      const dpr = window.devicePixelRatio || 1;
      const cores = navigator.hardwareConcurrency ?? 4;
      const reduced = reduce.matches;

      // Motion-reduce caps drift + ring-push velocity at 50% but keeps
      // pointer position fidelity at 100%. 1.0 means "default motion".
      const motionScale = reduced ? 0.5 : 1.0;

      if (isNarrow) {
        // Phones: drop to 16k. Going lower than that and the field
        // visibly thins out; going higher and we cook batteries.
        // 4-core phones get the static fallback — those are the
        // budget Androids most likely to thermal-throttle.
        if (cores <= 4) {
          return { kind: 'fallback' };
        }
        return { kind: 'webgl', textureSize: 128, motionScale };
      }

      if (dpr > 2.5 && cores <= 4) {
        // High-DPI low-core — likely a low-end tablet.
        return { kind: 'webgl', textureSize: 128, motionScale };
      }

      // Desktop / capable tablet. Reduced-motion users get the same 128
      // tier as mobile — fewer particles means less ambient activity in
      // peripheral vision, which is what the motion preference is for.
      if (reduced) {
        return { kind: 'webgl', textureSize: 128, motionScale };
      }
      return { kind: 'webgl', textureSize: 256, motionScale };
    };

    setCap(pickTier());

    // Respond to motion-preference changes mid-session — re-pick the
    // tier so toggling the OS setting takes effect without a reload.
    const onReduceChange = () => setCap(pickTier());
    reduce.addEventListener('change', onReduceChange);
    return () => reduce.removeEventListener('change', onReduceChange);
  }, []);

  // Static fallback: radial indigo glow + faint dotted mask.
  // Used both for 'unknown' (pre-hydration) and 'fallback'.
  if (cap.kind !== 'webgl') {
    return (
      <div
        aria-hidden="true"
        className="absolute inset-0 size-full overflow-hidden"
        style={{
          backgroundImage: [
            // Soft indigo glow centered on the hero
            'radial-gradient(60% 80% at 50% 45%, rgba(99,102,241,0.18), rgba(99,102,241,0) 70%)',
            // Very faint dotted texture — reads as "field of particles
            // at rest" rather than a flat gradient.
            'radial-gradient(circle at 1px 1px, rgba(255,255,255,0.05) 1px, transparent 1.5px)',
          ].join(', '),
          backgroundSize: '100% 100%, 24px 24px',
        }}
      />
    );
  }

  return <ParticleField textureSize={cap.textureSize} motionScale={cap.motionScale} />;
}

export default ParticleHero;
