// mograndom — Presence engine
// Live face-landmark overlay + a Presence score (1.0–10.0) computed from
// EXPRESSION + MOTION + ENGAGEMENT + COMPOSURE — i.e. what you DO on camera,
// not the geometry you were born with. Uses MediaPipe Face Landmarker (CDN);
// runs locally in-browser, no uploads. Gracefully degrades to a simulated
// score if the model fails to load (offline preview, etc).

// Globals exposed: usePresenceEngine, PresenceMeter, MogCallout, simulatedPresence

const FACE_LANDMARKER_URL = 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14/wasm';
const FACE_LANDMARKER_MODEL = 'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task';

// Cached singleton across renders
let _landmarkerPromise = null;
async function getFaceLandmarker() {
  if (_landmarkerPromise) return _landmarkerPromise;
  _landmarkerPromise = (async () => {
    // Use Function() to bypass Babel's import transform — we want a true
    // dynamic ESM import in the browser.
    const dynamicImport = new Function('u', 'return import(u)');
    const mod = await dynamicImport('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14/vision_bundle.mjs');
    const { FaceLandmarker, FilesetResolver } = mod;
    const fileset = await FilesetResolver.forVisionTasks(FACE_LANDMARKER_URL);
    return await FaceLandmarker.createFromOptions(fileset, {
      baseOptions: { modelAssetPath: FACE_LANDMARKER_MODEL, delegate: 'GPU' },
      runningMode: 'VIDEO',
      numFaces: 1,
      outputFaceBlendshapes: true,
      outputFacialTransformationMatrixes: true,
    });
  })();
  return _landmarkerPromise;
}

// ── Presence math ──────────────────────────────────────────────────────────
// Each sub-score is 0..1; final Presence is 1.0..10.0.
function computePresence(prev, blendshapes, transform, mesh, dt) {
  const bs = {};
  if (blendshapes) for (const c of blendshapes) bs[c.categoryName] = c.score;

  // EXPRESSION — favor intentional, dynamic expressions: smile, brow, eyes open
  const smile = ((bs.mouthSmileLeft || 0) + (bs.mouthSmileRight || 0)) / 2;
  const brow  = ((bs.browInnerUp || 0) + (bs.browOuterUpLeft || 0) + (bs.browOuterUpRight || 0)) / 3;
  const eyesOpen = 1 - Math.min(1, ((bs.eyeBlinkLeft || 0) + (bs.eyeBlinkRight || 0)) / 2);
  const jaw = (bs.jawOpen || 0);
  const expression = clamp(smile * 1.0 + brow * 0.6 + eyesOpen * 0.25 + jaw * 0.4, 0, 1);

  // MOTION — head movement amplitude, smoothed (low-passed)
  const cur = transform ? transformOrigin(transform) : null;
  let motion = prev.motion ?? 0;
  if (cur && prev.lastOrigin) {
    const dx = cur[0] - prev.lastOrigin[0];
    const dy = cur[1] - prev.lastOrigin[1];
    const dz = cur[2] - prev.lastOrigin[2];
    const inst = Math.sqrt(dx * dx + dy * dy + dz * dz);
    motion = motion * 0.86 + Math.min(1, inst * 14) * 0.14;
  }

  // ENGAGEMENT — looking at camera. yaw + pitch from transform; small angles = high engagement
  let engagement = 0.5;
  if (transform) {
    const { yaw, pitch } = matrixYawPitch(transform);
    const a = Math.sqrt(yaw * yaw + pitch * pitch);     // radians
    engagement = clamp(1 - a / 0.6, 0, 1);              // ±34° → 0
  }

  // COMPOSURE — face well-framed (centered + reasonable size)
  let composure = 0.6;
  if (mesh) {
    const xs = mesh.map(p => p.x), ys = mesh.map(p => p.y);
    const cx = avg(xs), cy = avg(ys);
    const w = Math.max(...xs) - Math.min(...xs);
    const h = Math.max(...ys) - Math.min(...ys);
    const centerPenalty = Math.hypot(cx - 0.5, cy - 0.5) * 1.6;
    const size = Math.max(w, h);                         // ~0.3–0.7 ideal
    const sizePenalty = Math.abs(size - 0.5) * 1.4;
    composure = clamp(1 - centerPenalty - sizePenalty, 0, 1);
  }

  // Combine. Weighted, then mapped to [1.0, 10.0] with a gentle floor so it
  // doesn't sit at 1 when you're just being still.
  const raw = expression * 0.38 + motion * 0.22 + engagement * 0.28 + composure * 0.12;
  const target = 1.0 + raw * 9.0;

  // Smooth ticker so the number doesn't jitter
  const ticker = prev.ticker == null ? target : (prev.ticker * 0.78 + target * 0.22);

  return {
    score: ticker,
    breakdown: { expression, motion, engagement, composure },
    motion,
    lastOrigin: cur || prev.lastOrigin,
    ticker,
    hasFace: !!mesh,
  };
}

function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function avg(a) { return a.reduce((x, y) => x + y, 0) / a.length; }
function transformOrigin(m) {
  // 4x4 column-major from MediaPipe; translation = [m[12], m[13], m[14]]
  // We use the rows here defensively
  if (m.length >= 16) return [m[12] || 0, m[13] || 0, m[14] || 0];
  return [0, 0, 0];
}
function matrixYawPitch(m) {
  // Extract yaw/pitch from rotation portion (column-major).
  // R = [[m0 m4 m8],[m1 m5 m9],[m2 m6 m10]]
  const m0 = m[0], m1 = m[1], m2 = m[2], m6 = m[6], m10 = m[10];
  const yaw = Math.atan2(m2, m10);
  const pitch = Math.atan2(-m6, Math.sqrt(m0 * m0 + m1 * m1));
  return { yaw, pitch };
}

// ── usePresenceEngine ──────────────────────────────────────────────────────
// Runs Face Landmarker against a <video>. Draws mesh into a sibling <canvas>.
// Returns { score, breakdown, hasFace, status }.
function usePresenceEngine({ videoRef, canvasRef, active, theme, accentColor }) {
  const [score, setScore] = React.useState(1.0);
  const [breakdown, setBreakdown] = React.useState({ expression: 0, motion: 0, engagement: 0, composure: 0 });
  const [hasFace, setHasFace] = React.useState(false);
  const [status, setStatus] = React.useState('idle'); // idle | loading | running | fallback | error
  const stateRef = React.useRef({ score: 1, ticker: null, motion: 0, lastOrigin: null });

  React.useEffect(() => {
    if (!active) return;
    let cancelled = false;
    let raf;
    let landmarker;
    let lastT = 0;

    (async () => {
      setStatus('loading');
      try {
        landmarker = await getFaceLandmarker();
      } catch (e) {
        if (cancelled) return;
        // graceful fallback — keep meter HIDDEN; we cannot detect face presence
        // without the model, so we should not pretend to have a presence score.
        setStatus('fallback');
        setHasFace(false);
        return;
      }
      if (cancelled) return;
      setStatus('running');

      const loop = (t) => {
        if (cancelled) return;
        raf = requestAnimationFrame(loop);
        const v = videoRef.current;
        const c = canvasRef.current;
        if (!v || !c || v.readyState < 2 || v.videoWidth === 0) return;
        if (t - lastT < 33) return;     // ~30 fps
        lastT = t;

        let result;
        try { result = landmarker.detectForVideo(v, t); }
        catch (e) { return; }

        // size canvas to video display rect
        const rect = v.getBoundingClientRect();
        if (c.width !== Math.floor(rect.width) || c.height !== Math.floor(rect.height)) {
          c.width = Math.floor(rect.width);
          c.height = Math.floor(rect.height);
        }
        const ctx = c.getContext('2d');
        ctx.clearRect(0, 0, c.width, c.height);

        const mesh = result?.faceLandmarks?.[0];
        const blendshapes = result?.faceBlendshapes?.[0]?.categories;
        const transform = result?.facialTransformationMatrixes?.[0]?.data;
        const dt = 1 / 30;

        if (mesh) {
          drawMesh(ctx, mesh, c.width, c.height, accentColor || '#fbbf24');
          const next = computePresence(stateRef.current, blendshapes, transform, mesh, dt);
          stateRef.current = next;
          setScore(next.score);
          setBreakdown(next.breakdown);
          setHasFace(true);
        } else {
          // decay toward 1 when no face
          stateRef.current.ticker = (stateRef.current.ticker || 1) * 0.95 + 1.0 * 0.05;
          setScore(stateRef.current.ticker);
          setHasFace(false);
        }
      };
      raf = requestAnimationFrame(loop);
    })();

    return () => {
      cancelled = true;
      if (raf) cancelAnimationFrame(raf);
    };
  }, [active, videoRef, canvasRef, accentColor]);

  return { score, breakdown, hasFace, status };
}

// ── Mesh drawing ───────────────────────────────────────────────────────────
// We don't ship the full ~3k tesselation here; we draw landmark dots + a
// curated set of contour rings that read clearly as a "face mesh".
// Coordinates are normalized 0..1; video is mirrored, so x flips.
const FACE_OVAL = [10,338,297,332,284,251,389,356,454,323,361,288,397,365,379,378,400,377,152,148,176,149,150,136,172,58,132,93,234,127,162,21,54,103,67,109];
const LIPS_OUTER = [61,146,91,181,84,17,314,405,321,375,291,409,270,269,267,0,37,39,40,185];
const LEFT_EYE = [33,7,163,144,145,153,154,155,133,173,157,158,159,160,161,246];
const RIGHT_EYE = [263,249,390,373,374,380,381,382,362,398,384,385,386,387,388,466];
const LEFT_BROW = [70,63,105,66,107];
const RIGHT_BROW = [336,296,334,293,300];

function drawMesh(ctx, lm, W, H, color) {
  // Project the 3D landmarks (x,y,z) to 2D with a tiny perspective skew
  // based on z so the cloud reads as a tracked 3D point cloud, not a flat mask.
  // z is roughly in [-0.1, 0.1] for a head; negative = closer to camera.
  const project = (p) => {
    const z = p.z || 0;
    const k = 1 - z * 1.6;                  // closer points push outward slightly
    const cx = 0.5, cy = 0.5;
    const x = cx + (1 - p.x - cx) * k;       // mirrored
    const y = cy + (p.y - cy) * k;
    return { x: x * W, y: y * H, z };
  };

  const pts = lm.map(project);

  // Depth-cued point cloud — every landmark, sized + faded by z
  for (let i = 0; i < pts.length; i++) {
    const p = pts[i];
    const depth = clamp((p.z + 0.08) / 0.16, 0, 1); // 0=near, 1=far
    const r = 1.6 - depth * 1.0;                    // 1.6→0.6 px
    const a = 0.85 - depth * 0.55;                  // 0.85→0.30
    ctx.fillStyle = withAlpha(color, a);
    ctx.beginPath();
    ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
    ctx.fill();
  }

  // Highlight key tracked points (eyes, brows, lips, nose tip, jaw) with
  // small ring "tracker" markers — gives the 3D-tracked feel.
  const KEY_POINTS = [
    1,        // nose tip
    33, 263,  // outer eye corners
    133, 362, // inner eye corners
    61, 291,  // mouth corners
    13, 14,   // upper/lower lip center
    10,       // forehead
    152,      // chin
    234, 454, // cheek/jaw outer
    70, 300,  // brow outer
  ];
  ctx.lineWidth = 1.1;
  for (const i of KEY_POINTS) {
    const p = pts[i];
    if (!p) continue;
    const depth = clamp((p.z + 0.08) / 0.16, 0, 1);
    const r = 4 - depth * 1.5;
    ctx.strokeStyle = withAlpha(color, 0.95 - depth * 0.4);
    ctx.beginPath();
    ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
    ctx.stroke();
    // tiny crosshair tick
    ctx.beginPath();
    ctx.moveTo(p.x - r - 2, p.y);
    ctx.lineTo(p.x - r + 0.5, p.y);
    ctx.moveTo(p.x + r - 0.5, p.y);
    ctx.lineTo(p.x + r + 2, p.y);
    ctx.stroke();
  }

  // A few soft connector lines between symmetric key points to imply
  // a 3D rig without drawing the full tesselation.
  const connectors = [[33, 263], [133, 362], [61, 291], [70, 300], [10, 152]];
  ctx.lineWidth = 0.6;
  ctx.strokeStyle = withAlpha(color, 0.18);
  for (const [a, b] of connectors) {
    const p = pts[a], q = pts[b];
    if (!p || !q) continue;
    ctx.beginPath();
    ctx.moveTo(p.x, p.y);
    ctx.lineTo(q.x, q.y);
    ctx.stroke();
  }
}

function withAlpha(hex, a) {
  // accepts #rrggbb, returns rgba
  const h = hex.replace('#', '');
  const r = parseInt(h.slice(0, 2), 16);
  const g = parseInt(h.slice(2, 4), 16);
  const b = parseInt(h.slice(4, 6), 16);
  return `rgba(${r},${g},${b},${a})`;
}

// ── Simulated opponent presence ────────────────────────────────────────────
function useSimulatedPresence({ targetMu = 3500, hot = false }) {
  const [score, setScore] = React.useState(5.0);
  const [breakdown, setBreakdown] = React.useState({ expression: 0.5, motion: 0.4, engagement: 0.6, composure: 0.6 });
  React.useEffect(() => {
    const base = clamp(2 + (targetMu / 5000) * 7, 2, 9.4);
    let v = base;
    const id = setInterval(() => {
      const wander = (Math.random() - 0.5) * (hot ? 0.9 : 0.4);
      v = v * 0.9 + (base + wander) * 0.1;
      setScore(clamp(v, 1.2, 9.8));
      setBreakdown({
        expression: clamp(0.4 + Math.sin(performance.now() / 1500) * 0.25, 0, 1),
        motion:     clamp(0.4 + Math.sin(performance.now() / 1100 + 1) * 0.25, 0, 1),
        engagement: clamp(0.6 + Math.sin(performance.now() / 1900 + 2) * 0.2, 0, 1),
        composure:  clamp(0.7 + Math.sin(performance.now() / 2300 + 3) * 0.15, 0, 1),
      });
    }, 110);
    return () => clearInterval(id);
  }, [targetMu, hot]);
  return { score, breakdown, hasFace: true, status: 'sim' };
}

// ── PresenceMeter component ────────────────────────────────────────────────
function PresenceMeter({ score, breakdown, color, label, side, mirror }) {
  const pct = clamp((score - 1) / 9, 0, 1);
  return (
    <div style={{
      position: 'absolute', top: 16, [side === 'right' ? 'right' : 'left']: 16,
      width: 240, padding: 12,
      background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(8px)',
      border: `1px solid ${color}66`, borderRadius: 8,
      fontFamily: 'Geist, sans-serif',
    }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontFamily: 'JetBrains Mono, monospace', fontSize: 10, letterSpacing: '0.14em', color: '#fff9', textTransform: 'uppercase' }}>
        <span>{label}</span>
        <span>CAMERA GAME</span>
      </div>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 6, marginTop: 4 }}>
        <span style={{ fontSize: 38, fontWeight: 700, fontFamily: 'JetBrains Mono, monospace', color: '#fff', letterSpacing: '-0.02em', lineHeight: 1, fontVariantNumeric: 'tabular-nums' }}>
          {score.toFixed(1)}
        </span>
        <span style={{ fontSize: 12, color: '#fff7' }}>/ 10.0</span>
      </div>
      <div style={{ height: 4, background: '#fff1', borderRadius: 999, overflow: 'hidden', marginTop: 8 }}>
        <div style={{ width: `${pct * 100}%`, height: '100%', background: `linear-gradient(90deg, ${color}, ${color}cc)`, transition: 'width 120ms linear' }} />
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6, marginTop: 10 }}>
        <Sub label="expr"  v={breakdown.expression}  c={color} />
        <Sub label="motion" v={breakdown.motion}     c={color} />
        <Sub label="eyes"  v={breakdown.engagement}  c={color} />
        <Sub label="frame" v={breakdown.composure}   c={color} />
      </div>
      <div style={{ marginTop: 10, paddingTop: 8, borderTop: '1px solid #fff1', fontFamily: 'JetBrains Mono, monospace', fontSize: 9, letterSpacing: '0.08em', color: '#fff5', lineHeight: 1.5 }}>
        coaching only · peers decide the match
      </div>
    </div>
  );
}
function Sub({ label, v, c }) {
  return (
    <div>
      <div style={{ display: 'flex', justifyContent: 'space-between', fontFamily: 'JetBrains Mono, monospace', fontSize: 9.5, color: '#fff7', letterSpacing: '0.08em', textTransform: 'uppercase' }}>
        <span>{label}</span>
        <span>{Math.round(v * 100)}</span>
      </div>
      <div style={{ height: 2, background: '#fff1', marginTop: 3, borderRadius: 999, overflow: 'hidden' }}>
        <div style={{ width: `${v * 100}%`, height: '100%', background: c, transition: 'width 200ms linear' }} />
      </div>
    </div>
  );
}

// ── MogCallout — flashes when gap widens ────────────────────────────────────
function MogCallout({ delta }) {
  const [pulse, setPulse] = React.useState(null);
  const lastSign = React.useRef(0);
  React.useEffect(() => {
    if (Math.abs(delta) < 1.5) return;
    const sign = delta > 0 ? 1 : -1;
    if (sign !== lastSign.current) {
      lastSign.current = sign;
      setPulse({ id: Math.random(), text: sign > 0 ? 'DOMINANT' : 'OUTPLAYED', color: sign > 0 ? '#fbbf24' : '#ef4444' });
      const id = setTimeout(() => setPulse(null), 1400);
      return () => clearTimeout(id);
    }
  }, [delta]);
  if (!pulse) return null;
  return (
    <div key={pulse.id} style={{
      position: 'absolute', left: '50%', top: '38%', transform: 'translate(-50%, -50%)',
      fontFamily: 'Geist, sans-serif', fontWeight: 700, fontSize: 56,
      color: pulse.color, letterSpacing: '-0.02em',
      textShadow: `0 0 30px ${pulse.color}aa`,
      animation: 'mg-fade-up 280ms ease-out',
      pointerEvents: 'none', zIndex: 5,
    }}>{pulse.text}</div>
  );
}

Object.assign(window, {
  usePresenceEngine, useSimulatedPresence, PresenceMeter, MogCallout,
});
