/* BioReveal — Unified BioScan (one capture, three signals + voice transcript).
 *
 * 25 seconds. Camera + mic on simultaneously.
 *  - Per-frame face RGB → rPPG vitals (HR, breathing, autonomic activation)
 *  - Final face frame → /api/skin (hydration, redness, texture, spots)
 *  - Mic captures voice → DSP features (pitch/jitter/energy)
 *  - Web Speech API transcribes voice in real-time (the user describes
 *    what they want to improve — that text becomes a primary AI input)
 *
 * Captured numbers are NOT shown to user (they're estimates, can be wrong).
 * Just "✓ Captured" — the AI synthesizer turns the raw signals into useful
 * analysis on the Reveal page.
 */

const StepBioscan = ({ value, onChange }) => {
  const videoRef = React.useRef(null);
  const canvasRef = React.useRef(null);
  const streamRef = React.useRef(null);
  const audioCtxRef = React.useRef(null);
  const sourceRef = React.useRef(null);
  const processorRef = React.useRef(null);
  const audioSamplesRef = React.useRef([]);
  const sampleRateRef = React.useRef(48000);
  const landmarkerRef = React.useRef(null);
  const captureBufRef = React.useRef({ r: [], g: [], b: [] });
  const startTsRef = React.useRef(0);
  const rafRef = React.useRef(0);
  const recognitionRef = React.useRef(null);
  const transcriptRef = React.useRef("");

  const [status, setStatus] = React.useState("idle"); // idle | loading | requesting | capturing | analyzing | done | error | denied
  const [progress, setProgress] = React.useState(0);
  const [usingMP, setUsingMP] = React.useState(false);
  const [errMsg, setErrMsg] = React.useState("");
  const [interimTranscript, setInterimTranscript] = React.useState("");
  const [captured, setCaptured] = React.useState(value?.captured || false);
  const [hadVitals, setHadVitals] = React.useState(false);
  const [hadSkin, setHadSkin] = React.useState(false);
  const [hadVoice, setHadVoice] = React.useState(false);
  const [hadTranscript, setHadTranscript] = React.useState(false);
  const CAPTURE_SECONDS = 25;
  const FS = 30;

  const cleanup = () => {
    cancelAnimationFrame(rafRef.current);
    try { processorRef.current?.disconnect(); } catch(_){}
    try { sourceRef.current?.disconnect(); } catch(_){}
    try { audioCtxRef.current?.close(); } catch(_){}
    try { recognitionRef.current?.stop(); } catch(_){}
    if (streamRef.current) {
      streamRef.current.getTracks().forEach(t => t.stop());
      streamRef.current = null;
    }
  };
  React.useEffect(() => () => cleanup(), []);

  const tryLoadFaceLM = async () => {
    if (landmarkerRef.current) return landmarkerRef.current;
    try {
      const mod = await import("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.18/+esm");
      const { FilesetResolver, FaceLandmarker } = mod;
      const fileset = await FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.18/wasm");
      const lm = await FaceLandmarker.createFromOptions(fileset, {
        baseOptions: { modelAssetPath: "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task" },
        runningMode: "VIDEO", numFaces: 1,
      });
      landmarkerRef.current = lm;
      return lm;
    } catch(_) { return null; }
  };

  const startTranscription = () => {
    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
    if (!SR) { console.warn("Web Speech API not supported"); return; }
    const rec = new SR();
    rec.continuous = true;
    rec.interimResults = true;
    rec.lang = "en-US";
    transcriptRef.current = "";
    rec.onresult = (e) => {
      let final = "", interim = "";
      for (let i = e.resultIndex; i < e.results.length; i++) {
        const t = e.results[i][0].transcript;
        if (e.results[i].isFinal) final += t + " "; else interim += t;
      }
      if (final) transcriptRef.current += final;
      setInterimTranscript((transcriptRef.current + " " + interim).trim());
    };
    rec.onerror = (e) => { console.warn("speech rec err", e.error); };
    try { rec.start(); recognitionRef.current = rec; } catch(e){ console.warn(e); }
  };

  const start = async () => {
    setErrMsg("");
    setInterimTranscript("");
    setCaptured(false);
    setStatus("loading");
    const lm = await tryLoadFaceLM();
    setUsingMP(!!lm);

    setStatus("requesting");
    let stream;
    try {
      stream = await navigator.mediaDevices.getUserMedia({
        video: { facingMode: "user", width: { ideal: 720 }, height: { ideal: 720 }, frameRate: { ideal: 30 } },
        audio: { echoCancellation: true, noiseSuppression: true },
      });
    } catch (err) {
      setErrMsg(err?.name === "NotAllowedError"
        ? "Camera + mic permission needed. Click the lock icon in the address bar to allow both, then retry."
        : err.message);
      setStatus(err?.name === "NotAllowedError" ? "denied" : "error");
      return;
    }
    streamRef.current = stream;
    const v = videoRef.current;
    v.srcObject = stream;
    try { await v.play(); } catch(_){}

    // Audio pipeline (DSP features)
    const audioStream = new MediaStream(stream.getAudioTracks());
    const ctx = new (window.AudioContext || window.webkitAudioContext)();
    audioCtxRef.current = ctx;
    sampleRateRef.current = ctx.sampleRate;
    const src = ctx.createMediaStreamSource(audioStream);
    sourceRef.current = src;
    const proc = ctx.createScriptProcessor(2048, 1, 1);
    processorRef.current = proc;
    audioSamplesRef.current = [];
    proc.onaudioprocess = (e) => {
      audioSamplesRef.current.push(new Float32Array(e.inputBuffer.getChannelData(0)));
    };
    src.connect(proc); proc.connect(ctx.destination);

    // Speech-to-text (free, browser-native)
    startTranscription();

    setStatus("capturing");
    startTsRef.current = performance.now();
    captureBufRef.current = { r: [], g: [], b: [] };
    const canvas = canvasRef.current;
    const cctx = canvas.getContext("2d", { willReadFrequently: true });
    canvas.width = v.videoWidth || 720;
    canvas.height = v.videoHeight || 720;

    const tick = async (ts) => {
      if (!streamRef.current) return;
      const elapsed = (ts - startTsRef.current) / 1000;
      if (elapsed >= CAPTURE_SECONDS) { await analyzeAll(canvas); return; }
      try {
        cctx.drawImage(v, 0, 0, canvas.width, canvas.height);
        const W = canvas.width, H = canvas.height;
        let rsum = 0, gsum = 0, bsum = 0, count = 0;
        if (lm) {
          let result;
          try { result = lm.detectForVideo(v, ts); } catch(_){}
          const lms = result?.faceLandmarks?.[0];
          if (lms && lms.length > 200) {
            const idxs = [50, 280, 10, 117, 346];
            const half = 14;
            for (const i of idxs) {
              const p = lms[i];
              const cx = Math.round(p.x * W), cy = Math.round(p.y * H);
              const x0 = Math.max(0, cx - half), y0 = Math.max(0, cy - half);
              const w = Math.min(W - x0, half * 2), h = Math.min(H - y0, half * 2);
              const data = cctx.getImageData(x0, y0, w, h).data;
              for (let j = 0; j < data.length; j += 4) {
                rsum += data[j]; gsum += data[j+1]; bsum += data[j+2]; count++;
              }
            }
          }
        }
        if (count === 0) {
          const w = Math.round(W * 0.4), h = Math.round(H * 0.4);
          const x0 = Math.max(0, Math.round(W/2 - w/2)), y0 = Math.max(0, Math.round(H * 0.45 - h/2));
          const data = cctx.getImageData(x0, y0, w, h).data;
          for (let j = 0; j < data.length; j += 64) {
            rsum += data[j]; gsum += data[j+1]; bsum += data[j+2]; count++;
          }
        }
        if (count > 0) {
          captureBufRef.current.r.push(rsum / count);
          captureBufRef.current.g.push(gsum / count);
          captureBufRef.current.b.push(bsum / count);
        }
        setProgress(Math.min(100, Math.round((elapsed / CAPTURE_SECONDS) * 100)));
      } catch(e){ console.warn(e); }
      rafRef.current = requestAnimationFrame(tick);
    };
    rafRef.current = requestAnimationFrame(tick);
  };

  const analyzeAll = async (canvas) => {
    setStatus("analyzing");
    cleanup();
    const buf = captureBufRef.current;
    const audio = audioSamplesRef.current;
    const transcript = transcriptRef.current.trim();

    let skinResult = null;
    try {
      const blob = await new Promise(res => canvas.toBlob(res, "image/jpeg", 0.85));
      if (blob) {
        const r = await fetch("/api/skin", { method: "POST", headers: { "Content-Type": "image/jpeg" }, body: blob });
        const d = await r.json();
        if (d.ok) skinResult = d;
      }
    } catch(e){ console.warn("skin failed", e); }

    let vitalsResult = null;
    if (buf.r.length >= 60) {
      try {
        const r = await fetch("/api/vitals", {
          method: "POST", headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ r: buf.r, g: buf.g, b: buf.b, fs: FS }),
        });
        const d = await r.json();
        if (d.ok) vitalsResult = d;
      } catch(e){ console.warn("vitals failed", e); }
    }

    let voiceResult = null;
    if (audio.length > 0) {
      try { voiceResult = computeVoiceFeatures(audio, sampleRateRef.current); }
      catch(e){ console.warn("voice failed", e); }
    }
    if (voiceResult && transcript) voiceResult.transcript = transcript;

    if (!vitalsResult && !skinResult && !voiceResult) {
      setStatus("error");
      setErrMsg("Couldn't extract any signals — try again with better light + a quieter room.");
      return;
    }

    setHadVitals(!!vitalsResult);
    setHadSkin(!!skinResult);
    setHadVoice(!!voiceResult);
    setHadTranscript(!!transcript);
    setCaptured(true);
    onChange({ captured: true, vitals: vitalsResult, skin: skinResult, voice: voiceResult });
    setStatus("done");
  };

  const skip = () => { cleanup(); onChange({ skipped: true }); setStatus("done"); setCaptured(false); };
  const retry = () => { setStatus("idle"); setProgress(0); setErrMsg(""); setCaptured(false); setInterimTranscript(""); transcriptRef.current = ""; };

  return (
    <>
      <div className="br-eyebrow">02 / 06 · The bioscan · Optional</div>
      <h2 className="br-question">25 seconds. Tell us what you want fixed.</h2>
      <p className="br-sub">Camera and mic come on. While the AI reads your face and voice, you talk about what you want to upgrade. That's the whole thing. The AI takes it from there.</p>

      {status === "idle" && (
        <div style={{ background: "var(--paper-2)", borderRadius: 8, padding: "16px 18px", marginBottom: 14, fontSize: 13, lineHeight: 1.6, color: "var(--ink)" }}>
          <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, letterSpacing: "0.18em", color: "var(--ink)", marginBottom: 10, textTransform: "uppercase" }}>When the scan starts, say out loud:</div>
          <div style={{ fontFamily: "var(--font-display)", fontSize: 17, fontStyle: "italic", lineHeight: 1.5 }}>
            "I want to fix my ___ and feel ___" — just talk. Pick from:
          </div>
          <div style={{ marginTop: 12, display: "flex", flexWrap: "wrap", gap: 8 }}>
            {["hair", "skin", "muscle", "weight", "energy", "sleep", "focus", "drive", "look younger", "joint pain"].map(g => (
              <span key={g} style={{ background: "var(--paper)", border: "1px solid var(--paper-3)", padding: "4px 10px", borderRadius: 999, fontSize: 12, fontFamily: "var(--font-mono)" }}>{g}</span>
            ))}
          </div>
          <div style={{ marginTop: 12, fontSize: 12, color: "var(--muted)" }}>Be honest. Tired? Say tired. Frustrated with your skin? Say it. The AI uses every word.</div>
        </div>
      )}

      <div className="br-selfie-frame" style={{ position: "relative", overflow: "hidden", background: "#0a0a0a" }}>
        <video ref={videoRef} muted playsInline autoPlay
          style={{ width: "100%", height: "100%", objectFit: "cover", transform: "scaleX(-1)", display: status === "capturing" || status === "analyzing" ? "block" : "none" }}/>
        <canvas ref={canvasRef} style={{ display: "none" }}/>
        <div className="br-corner tl"/><div className="br-corner tr"/><div className="br-corner bl"/><div className="br-corner br"/>

        {status === "idle" && (
          <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center", color: "#888", fontFamily: "var(--font-mono)", fontSize: 12, letterSpacing: "0.18em", textTransform: "uppercase" }}>
            Tap "Begin BioScan"
          </div>
        )}
        {status === "loading" && (
          <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center", color: "#bbb", fontFamily: "var(--font-mono)", fontSize: 12, letterSpacing: "0.18em", textTransform: "uppercase", textAlign: "center", padding: 16 }}>
            Loading face model<br/><span style={{ fontSize: 10, opacity: 0.6 }}>~3MB · first time only</span>
          </div>
        )}
        {status === "requesting" && (
          <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center", color: "#bbb", fontFamily: "var(--font-mono)", fontSize: 12, letterSpacing: "0.18em", textTransform: "uppercase", textAlign: "center", padding: 16 }}>
            Allow camera + mic
          </div>
        )}
        {status === "capturing" && (
          <>
            <div className="br-scan-line"/>
            <div className="br-selfie-stat" style={{ top: 16, left: 44 }}>● BIOSCAN · {progress}%</div>
            <div className="br-selfie-stat" style={{ top: 16, right: 44 }}>{usingMP ? "FACE-LOCKED" : "CENTER-ROI"}</div>
            <div style={{ position: "absolute", left: 0, right: 0, bottom: 60, padding: "0 24px", color: "#fff" }}>
              <div style={{ fontFamily: "var(--font-mono)", fontSize: 10, letterSpacing: "0.2em", opacity: 0.7, marginBottom: 6, textShadow: "0 1px 8px rgba(0,0,0,0.9)", textAlign: "center" }}>TELL ME WHAT YOU WANT FIXED</div>
              <div style={{ background: "rgba(0,0,0,0.55)", borderRadius: 8, padding: "10px 14px", fontSize: 14, lineHeight: 1.4, minHeight: 50, fontFamily: "var(--font-display)", fontWeight: 500, textAlign: "center" }}>
                {interimTranscript || <span style={{ opacity: 0.4 }}>(start speaking…)</span>}
              </div>
            </div>
            <div className="br-selfie-stat" style={{ bottom: 16, left: 44 }}>HOLD STILL · KEEP TALKING</div>
          </>
        )}
        {status === "analyzing" && (
          <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center", color: "#fff", fontFamily: "var(--font-mono)", fontSize: 12, letterSpacing: "0.18em", textTransform: "uppercase" }}>
            Processing all 3 signal channels…
          </div>
        )}
        {status === "done" && captured && (
          <div style={{ position: "absolute", inset: 0, background: "linear-gradient(180deg, rgba(0,0,0,0.9), #000)", display: "grid", placeItems: "center", padding: 24, textAlign: "center", color: "#fff" }}>
            <div style={{ maxWidth: 360 }}>
              <div style={{ fontSize: 56, marginBottom: 8 }}>✓</div>
              <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, letterSpacing: "0.22em", opacity: 0.55, marginBottom: 18 }}>BIOSCAN COMPLETE</div>
              <div style={{ display: "flex", flexDirection: "column", gap: 8, alignItems: "center" }}>
                <Pill on={hadVitals} label="We read your heart rate"/>
                <Pill on={hadSkin}   label="We read your skin"/>
                <Pill on={hadVoice}  label="We read your voice"/>
                <Pill on={hadTranscript} label="We heard what you want fixed"/>
              </div>
              <div style={{ marginTop: 18, fontSize: 12, opacity: 0.55, lineHeight: 1.5 }}>
                The AI puts it all together on the next page. Real read. No raw numbers.
              </div>
            </div>
          </div>
        )}
        {(status === "error" || status === "denied") && (
          <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center", background: "#111", color: "#fff", padding: 24, textAlign: "center" }}>
            <div>
              <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, letterSpacing: "0.2em", opacity: 0.7, marginBottom: 12 }}>BIOSCAN FAILED</div>
              <div style={{ fontSize: 13, opacity: 0.85, maxWidth: 380, lineHeight: 1.5 }}>{errMsg || "Unknown error"}</div>
            </div>
          </div>
        )}
      </div>

      <div style={{ display: "flex", gap: 10, marginTop: 16 }}>
        {status === "idle" && (
          <>
            <button className="br-option" style={{ flex: 1, justifyContent: "center" }} onClick={start}>● Begin BioScan</button>
            <button className="br-option" style={{ flex: 1, justifyContent: "center" }} onClick={skip}>Skip</button>
          </>
        )}
        {(status === "loading" || status === "requesting" || status === "capturing" || status === "analyzing") && (
          <button className="br-option" style={{ flex: 1, justifyContent: "center", opacity: 0.6 }} disabled>Working…</button>
        )}
        {status === "done" && (
          <>
            <button className="br-option selected" style={{ flex: 1, justifyContent: "center" }}>{captured ? "✓ BioScan saved" : "✓ Skipped"}</button>
            {captured && <button className="br-option" style={{ flex: 1, justifyContent: "center" }} onClick={retry}>Re-scan</button>}
          </>
        )}
        {(status === "error" || status === "denied") && (
          <>
            <button className="br-option" style={{ flex: 1, justifyContent: "center" }} onClick={retry}>Try again</button>
            <button className="br-option" style={{ flex: 1, justifyContent: "center" }} onClick={skip}>Skip</button>
          </>
        )}
      </div>

      <div style={{ marginTop: 14, fontSize: 11, color: "var(--muted)", lineHeight: 1.5, fontFamily: "var(--font-mono)", letterSpacing: "0.05em" }}>
        ⓘ <strong>WHAT THE AI READS</strong> · Your heart rate (from skin color), your skin, your voice, and what you say out loud. Raw numbers stay private. You get the read.
      </div>
    </>
  );
};

const Pill = ({ on, label }) => (
  <div style={{ display: "flex", alignItems: "center", gap: 10, fontSize: 13, fontFamily: "var(--font-mono)", letterSpacing: "0.04em", opacity: on ? 1 : 0.4 }}>
    <span style={{ width: 8, height: 8, borderRadius: "50%", background: on ? "#10b981" : "#444", flexShrink: 0 }}/>
    <span>{label}</span>
  </div>
);

// ── Voice DSP (client-side feature extraction) ──
function computeVoiceFeatures(samplesArrays, sr) {
  const total = samplesArrays.reduce((s,c)=>s+c.length, 0);
  const samples = new Float32Array(total);
  let off = 0;
  for (const c of samplesArrays) { samples.set(c, off); off += c.length; }
  if (samples.length < sr * 5) return null;

  const frameSize = Math.round(sr * 0.05);
  const hop = Math.round(sr * 0.025);

  const detectPitch = (frame) => {
    const minHz = 70, maxHz = 400;
    const minLag = Math.floor(sr / maxHz), maxLag = Math.floor(sr / minHz);
    let bestLag = 0, bestCorr = 0, sumE = 0;
    for (let i = 0; i < frame.length; i++) sumE += frame[i] * frame[i];
    if (sumE < 0.001) return 0;
    for (let lag = minLag; lag <= maxLag; lag++) {
      let corr = 0;
      for (let i = 0; i < frame.length - lag; i++) corr += frame[i] * frame[i+lag];
      if (corr > bestCorr) { bestCorr = corr; bestLag = lag; }
    }
    if (bestCorr / sumE < 0.3) return 0;
    return sr / bestLag;
  };
  const rms = (frame) => { let s=0; for (let i=0;i<frame.length;i++) s+=frame[i]*frame[i]; return Math.sqrt(s/frame.length); };
  const spectralCentroid = (frame) => {
    const N = 256;
    const sub = new Float32Array(N);
    const stride = Math.max(1, Math.floor(frame.length / N));
    for (let i = 0; i < N; i++) sub[i] = frame[i*stride] || 0;
    let num = 0, den = 0;
    for (let k = 1; k < N/2; k++) {
      let re = 0, im = 0;
      for (let n = 0; n < N; n++) {
        const a = -2 * Math.PI * k * n / N;
        re += sub[n] * Math.cos(a); im += sub[n] * Math.sin(a);
      }
      const mag = Math.sqrt(re*re + im*im);
      const f = (k * sr / stride) / N;
      num += f * mag; den += mag;
    }
    return den > 0 ? num / den : 0;
  };

  const pitches = [], energies = [], centroids = [];
  let voicedFrames = 0, totalFrames = 0;
  for (let i = 0; i + frameSize < samples.length; i += hop) {
    const frame = samples.subarray(i, i + frameSize);
    const e = rms(frame);
    energies.push(e); totalFrames++;
    if (e < 0.005) continue;
    const p = detectPitch(frame);
    if (p > 0) {
      pitches.push(p); voicedFrames++;
      if (totalFrames % 4 === 0) centroids.push(spectralCentroid(frame));
    }
  }
  if (pitches.length < 20) return null;
  const mean = (a) => a.reduce((s,x)=>s+x,0)/a.length;
  const std = (a) => { const m = mean(a); return Math.sqrt(mean(a.map(x=>(x-m)**2))); };
  const meanPitch = mean(pitches);
  const stdPitch = std(pitches);
  const meanEnergy = mean(energies);
  const meanCentroid = centroids.length ? mean(centroids) : 0;
  let jitter = 0;
  for (let i = 1; i < pitches.length; i++) jitter += Math.abs(pitches[i] - pitches[i-1]);
  jitter = (jitter / (pitches.length - 1)) / meanPitch;
  const stress_score = Math.round(Math.min(100, Math.max(0, jitter*1500 + (stdPitch < 8 ? 25 : 0) + (meanCentroid < 800 ? 15 : 0))));
  const fatigue_score = Math.round(Math.min(100, Math.max(0, (meanEnergy < 0.02 ? 40 : 20) + (meanCentroid < 700 ? 25 : 0) + (stdPitch < 10 ? 20 : 0))));
  const energy_score = Math.round(Math.min(100, Math.max(0, meanEnergy*2000 + stdPitch*0.8 + (meanCentroid > 1000 ? 15 : 0))));
  const mood_class = stdPitch > 15 && jitter < 0.04 ? "upbeat" : jitter > 0.06 ? "tense" : meanEnergy < 0.02 ? "flat" : "neutral";
  return {
    ok: true,
    voiced_ratio: Math.round((voicedFrames / totalFrames) * 100) / 100,
    mean_pitch_hz: Math.round(meanPitch),
    pitch_std_hz: Math.round(stdPitch * 10) / 10,
    mean_energy: Math.round(meanEnergy * 1000) / 1000,
    spectral_centroid_hz: Math.round(meanCentroid),
    jitter: Math.round(jitter * 1000) / 1000,
    stress_score, fatigue_score, energy_score, mood_class,
  };
}

window.BR_STEPS = window.BR_STEPS || {};
window.BR_STEPS.StepBioscan = StepBioscan;
