/* ============================================================
   Audio Insight · waveform rendering + playback
   - WaveformCanvas : bars from the amplitude envelope, colored by
                      per-frame severity of `colorKey` (default quality).
                      Playhead, click/drag seek, hovered read-out,
                      and problem-region markers beneath.
   - useAudioPlayback : real <audio> when a file is wired, else an
                        animated 1x playhead. Position persists.
   - Sparkline : compact per-dimension time-series for the breakdown.
   ============================================================ */
;(function(){
const { severityColor, severityRGB, severityAlpha, clamp, fmtTime } = window.AIData;

// resample an array to n points (mean-pooled)
function resample(arr, n) {
  const out = new Array(n);
  const step = arr.length / n;
  for (let i = 0; i < n; i++) {
    const a = Math.floor(i * step), b = Math.max(a + 1, Math.floor((i + 1) * step));
    let s = 0, c = 0;
    for (let j = a; j < b && j < arr.length; j++) { s += arr[j]; c++; }
    out[i] = c ? s / c : arr[Math.min(a, arr.length - 1)];
  }
  return out;
}

// contiguous regions where `series` exceeds threshold (for problem markers)
function regions(series, threshold) {
  const out = []; let start = -1;
  for (let i = 0; i < series.length; i++) {
    if (series[i] >= threshold && start < 0) start = i;
    else if (series[i] < threshold && start >= 0) { out.push([start, i - 1]); start = -1; }
  }
  if (start >= 0) out.push([start, series.length - 1]);
  return out.filter(([a, b]) => b - a >= 1);
}

function WaveformCanvas({ call, currentTime, duration, onSeek, colorKey = "risk_score", height = 168, markers = true, envelope = null }) {
  const wrapRef = React.useRef(null);
  const canvasRef = React.useRef(null);
  const [w, setW] = React.useState(800);
  const [hover, setHover] = React.useState(null); // {x, t, q}

  React.useEffect(() => {
    const el = wrapRef.current; if (!el) return;
    const ro = new ResizeObserver((entries) => setW(entries[0].contentRect.width));
    ro.observe(el); setW(el.clientWidth);
    return () => ro.disconnect();
  }, []);

  // bar layout
  const barW = 3, gap = 2;
  const nBars = Math.max(24, Math.floor(w / (barW + gap)));
  // bar heights: real decoded envelope when audio is wired, else the synthesized one
  const ampSource = envelope && envelope.length ? envelope : call.frames.amp;
  const amp = React.useMemo(() => resample(ampSource, nBars), [ampSource, nBars]);
  const sev = React.useMemo(() => resample(call.frames[colorKey], nBars), [call, nBars, colorKey]);
  const probRegions = React.useMemo(
    () => regions(resample(call.frames.risk_score, nBars), 0.55),
    [call, nBars]
  );

  React.useEffect(() => {
    const cv = canvasRef.current; if (!cv) return;
    const dpr = window.devicePixelRatio || 1;
    const H = height;
    cv.width = w * dpr; cv.height = H * dpr;
    cv.style.width = w + "px"; cv.style.height = H + "px";
    const ctx = cv.getContext("2d");
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctx.clearRect(0, 0, w, H);

    const waveBottom = H - 22;                 // leave room for marker strip
    const mid = waveBottom / 2 + 4;
    const maxBar = (waveBottom - 8) / 2;
    const playX = duration > 0 ? (currentTime / duration) * w : 0;

    for (let i = 0; i < nBars; i++) {
      const x = i * (barW + gap);
      const h = Math.max(2, amp[i] * maxBar * 1.7);
      const s = sev[i];
      const played = x <= playX;
      const a = severityAlpha(s) * (played ? 1 : 0.42);
      ctx.fillStyle = severityColor(s, a);
      // top + mirrored bottom
      ctx.fillRect(x, mid - h, barW, h);
      ctx.fillRect(x, mid + 1, barW, h * 0.8);
    }

    // problem-region marker strip
    if (markers) {
      const my = waveBottom + 9;
      for (const [a, b] of probRegions) {
        const x0 = a * (barW + gap);
        const x1 = b * (barW + gap) + barW;
        const peak = Math.max(...sev.slice(a, b + 1));
        ctx.fillStyle = severityColor(Math.max(0.6, peak), 0.9);
        ctx.beginPath();
        const r = 2, yy = my, hh = 4;
        ctx.roundRect ? ctx.roundRect(x0, yy, Math.max(6, x1 - x0), hh, r) : ctx.rect(x0, yy, Math.max(6, x1 - x0), hh);
        ctx.fill();
      }
    }

    // playhead
    if (currentTime > 0 || true) {
      ctx.strokeStyle = "rgba(249,249,249,0.92)";
      ctx.lineWidth = 1.5;
      ctx.beginPath(); ctx.moveTo(playX, 2); ctx.lineTo(playX, waveBottom + 2); ctx.stroke();
      ctx.fillStyle = "rgba(249,249,249,0.92)";
      ctx.beginPath(); ctx.arc(playX, 2, 3.5, 0, Math.PI * 2); ctx.fill();
    }

    // hover guide
    if (hover) {
      ctx.strokeStyle = "rgba(105,147,255,0.5)";
      ctx.lineWidth = 1;
      ctx.beginPath(); ctx.moveTo(hover.x, 2); ctx.lineTo(hover.x, waveBottom + 2); ctx.stroke();
    }
  }, [call, w, currentTime, duration, nBars, amp, sev, probRegions, height, hover, colorKey, markers]);

  const locate = (e) => {
    const rect = canvasRef.current.getBoundingClientRect();
    const x = clamp(e.clientX - rect.left, 0, rect.width);
    const t = (x / rect.width) * duration;
    const idx = Math.min(nBars - 1, Math.floor((x / rect.width) * nBars));
    return { x, t, q: call.frames.risk_score[Math.min(call.frames.risk_score.length - 1, Math.floor((idx / nBars) * call.frames.risk_score.length))] };
  };

  const dragging = React.useRef(false);
  return (
    <div ref={wrapRef} style={{ position: "relative", width: "100%", userSelect: "none" }}>
      <canvas
        ref={canvasRef}
        style={{ display: "block", cursor: "pointer" }}
        onMouseDown={(e) => { dragging.current = true; onSeek(locate(e).t); }}
        onMouseMove={(e) => { const l = locate(e); setHover(l); if (dragging.current) onSeek(l.t); }}
        onMouseUp={() => (dragging.current = false)}
        onMouseLeave={() => { dragging.current = false; setHover(null); }}
      />
      {hover && (
        <div style={{
          position: "absolute", top: 0, left: clamp(hover.x - 52, 0, w - 104), width: 104,
          transform: "translateY(-50%)", pointerEvents: "none",
          background: "var(--off-black)", border: "1px solid var(--border-subtle)",
          borderRadius: "var(--radius-sm)", padding: "5px 8px", fontFamily: "var(--font-mono)",
          fontSize: 11, color: "var(--text-secondary)", display: "flex", justifyContent: "space-between",
          boxShadow: "var(--shadow-md)",
        }}>
          <span>{fmtTime(hover.t)}</span>
          <span style={{ color: severityColor(Math.max(0.35, hover.q)) }}>{hover.q.toFixed(2)}</span>
        </div>
      )}
    </div>
  );
}

/* ---- playback: real audio if wired, else animated 1x playhead ---- */
function useAudioPlayback(call) {
  const [playing, setPlaying] = React.useState(false);
  const [currentTime, setCurrentTime] = React.useState(0);
  const audioRef = React.useRef(null);
  const rafRef = React.useRef(0);
  const lastRef = React.useRef(0);
  const duration = call.durationSec;

  // reset when call changes
  React.useEffect(() => {
    setPlaying(false); setCurrentTime(0);
    if (audioRef.current) { audioRef.current.pause(); audioRef.current = null; }
    if (call.audioUrl) {
      const a = new Audio(call.audioUrl);
      a.addEventListener("timeupdate", () => setCurrentTime(a.currentTime));
      a.addEventListener("ended", () => setPlaying(false));
      audioRef.current = a;
    }
    return () => { cancelAnimationFrame(rafRef.current); if (audioRef.current) audioRef.current.pause(); };
  }, [call]);

  // animated fallback loop
  React.useEffect(() => {
    if (!playing || call.audioUrl) return;
    lastRef.current = performance.now();
    const tick = (now) => {
      const dt = (now - lastRef.current) / 1000; lastRef.current = now;
      setCurrentTime((t) => {
        const nt = t + dt;
        if (nt >= duration) { setPlaying(false); return duration; }
        return nt;
      });
      rafRef.current = requestAnimationFrame(tick);
    };
    rafRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafRef.current);
  }, [playing, call, duration]);

  const toggle = () => {
    if (call.audioUrl && audioRef.current) {
      if (playing) audioRef.current.pause(); else audioRef.current.play();
    }
    setPlaying((p) => {
      const np = !p;
      if (np && currentTime >= duration) seek(0);
      return np;
    });
  };
  const seek = (t) => {
    const ct = clamp(t, 0, duration);
    setCurrentTime(ct);
    if (audioRef.current) audioRef.current.currentTime = ct;
  };
  return { playing, currentTime, duration, toggle, seek };
}

/* ---- amplitude envelope from the real audio file (Web Audio decode).
   Returns null until/unless an audio file is attached & decoded; the
   waveform falls back to the synthesized envelope in that case. ---- */
function useEnvelope(call) {
  const [env, setEnv] = React.useState(null);
  React.useEffect(() => {
    setEnv(null);
    if (!call.audioUrl) return;
    const AC = window.AudioContext || window.webkitAudioContext;
    if (!AC) return;
    let alive = true;
    const ctx = new AC();
    fetch(call.audioUrl)
      .then((r) => r.arrayBuffer())
      .then((buf) => ctx.decodeAudioData(buf))
      .then((audio) => {
        if (!alive) return;
        // mix all channels down to mono (per-sample average) so the envelope
        // reflects the whole signal, not just the left channel
        const nCh = audio.numberOfChannels || 1;
        const chans = [];
        for (let c = 0; c < nCh; c++) chans.push(audio.getChannelData(c));
        const len = audio.length;
        const N = 800;
        const block = Math.floor(len / N) || 1;
        const out = []; let peak = 0;
        for (let i = 0; i < N; i++) {
          let sum = 0;
          for (let j = 0; j < block; j++) {
            const idx = i * block + j;
            let s = 0;
            for (let c = 0; c < nCh; c++) s += chans[c][idx] || 0;
            s /= nCh;
            sum += s * s;
          }
          const rms = Math.sqrt(sum / block);
          out.push(rms); if (rms > peak) peak = rms;
        }
        setEnv(out.map((v) => (peak > 0 ? v / peak : 0)));
      })
      .catch(() => {})
      .finally(() => { try { ctx.close(); } catch (e) {} });
    return () => { alive = false; };
  }, [call]);
  return env;
}

/* ---- compact per-dimension sparkline ---- */
function Sparkline({ series, width = 132, height = 30 }) {
  const pts = React.useMemo(() => resample(series, 40), [series]);
  const avgS = pts.reduce((a, b) => a + b, 0) / pts.length;
  const path = pts.map((v, i) => {
    const x = (i / (pts.length - 1)) * width;
    const y = height - 2 - v * (height - 4);
    return `${i ? "L" : "M"}${x.toFixed(1)},${y.toFixed(1)}`;
  }).join(" ");
  const area = `${path} L${width},${height} L0,${height} Z`;
  const id = React.useId();
  return (
    <svg width={width} height={height} style={{ display: "block", overflow: "visible" }}>
      <defs>
        <linearGradient id={id} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor={severityColor(Math.max(0.4, avgS), 0.28)} />
          <stop offset="100%" stopColor={severityColor(avgS, 0)} />
        </linearGradient>
      </defs>
      <path d={area} fill={`url(#${id})`} />
      <path d={path} fill="none" stroke={severityColor(Math.max(0.32, avgS), 0.95)} strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" />
    </svg>
  );
}

window.AIWave = { WaveformCanvas, useAudioPlayback, useEnvelope, Sparkline, resample };
})();
