// ─────────────────────────────────────────────────────────────────
// TEMPO AUTO-DETECTION
//
// Real BPM detection that runs on uploaded audio files via Web Audio.
// No backend, no external service — pure browser DSP.
//
// Pipeline:
//   1. decodeAudioData  → Float32Array (mono mix)
//   2. downsample       → ~11 kHz (enough for tempo)
//   3. onset envelope   → energy-flux per ~10 ms hop (frame RMS difference)
//   4. autocorrelation  → ACF at BPM lags 60..200
//   5. peak picking     → primary BPM + confidence
//   6. octave check     → half / double candidates surfaced
//
// EXPORT:
//   window.TempoDetect.analyzeFile(File)         → { bpm, confidence, halfTime, candidates, alternates }
//   window.TempoDetect.analyzeBuffer(AudioBuffer)
//   <TempoDetectBadge result={…}/>               — small chip
//   <TempoDetectModal file={…} onAccept onReject onClose/>
//
// Listens:
//   astro-tempo-detect-request  {file, onResult}
//
// Emits:
//   astro-tempo-detect-progress {phase, pct}
//   astro-tempo-detect-done     {result, file}
// ─────────────────────────────────────────────────────────────────
(function(){
  const _S = React.useState;
  const _E = React.useEffect;

  const BPM_MIN = 60;
  const BPM_MAX = 200;
  const TARGET_SR = 11025;
  const HOP_MS = 10;       // 100 frames/sec
  const FRAME_MS = 40;     // RMS window

  // ─── Audio decode ────────────────────────────────────────────
  function getAudioCtx() {
    if (window.__tempoDetectCtx) return window.__tempoDetectCtx;
    const C = window.AudioContext || window.webkitAudioContext;
    if (!C) return null;
    window.__tempoDetectCtx = new C();
    return window.__tempoDetectCtx;
  }

  async function decodeFile(file) {
    const ctx = getAudioCtx();
    if (!ctx) throw new Error('Web Audio not supported');
    const buf = await file.arrayBuffer();
    return new Promise((res, rej) => {
      ctx.decodeAudioData(buf.slice(0), res, rej);
    });
  }

  // ─── Mono downmix + downsample ───────────────────────────────
  function monoDownsample(audioBuffer, targetSr) {
    const sr = audioBuffer.sampleRate;
    const ch = audioBuffer.numberOfChannels;
    const len = audioBuffer.length;
    // mono
    const mono = new Float32Array(len);
    for (let c = 0; c < ch; c++) {
      const data = audioBuffer.getChannelData(c);
      for (let i = 0; i < len; i++) mono[i] += data[i] / ch;
    }
    // downsample (linear)
    const ratio = sr / targetSr;
    const newLen = Math.floor(len / ratio);
    const out = new Float32Array(newLen);
    for (let i = 0; i < newLen; i++) {
      const idx = i * ratio;
      const i0 = Math.floor(idx);
      const f = idx - i0;
      out[i] = mono[i0] * (1 - f) + (mono[i0 + 1] || 0) * f;
    }
    return { samples: out, sampleRate: targetSr };
  }

  // ─── Energy-flux onset envelope ──────────────────────────────
  function onsetEnvelope(samples, sr) {
    const hop = Math.floor(sr * HOP_MS / 1000);
    const frame = Math.floor(sr * FRAME_MS / 1000);
    const n = Math.floor((samples.length - frame) / hop);
    const env = new Float32Array(Math.max(0, n));
    let prev = 0;
    for (let i = 0; i < n; i++) {
      const start = i * hop;
      let sum = 0;
      for (let j = 0; j < frame; j++) {
        const v = samples[start + j];
        sum += v * v;
      }
      const rms = Math.sqrt(sum / frame);
      env[i] = Math.max(0, rms - prev); // half-wave rectified flux
      prev = rms;
    }
    // Normalize
    let max = 0;
    for (let i = 0; i < env.length; i++) if (env[i] > max) max = env[i];
    if (max > 0) for (let i = 0; i < env.length; i++) env[i] /= max;
    return env;
  }

  // ─── Autocorrelation ─────────────────────────────────────────
  function autocorr(env, minLag, maxLag) {
    const out = new Float32Array(maxLag - minLag + 1);
    for (let lag = minLag; lag <= maxLag; lag++) {
      let sum = 0;
      const n = env.length - lag;
      for (let i = 0; i < n; i++) sum += env[i] * env[i + lag];
      out[lag - minLag] = sum / n;
    }
    return out;
  }

  // ─── BPM lag conversion ──────────────────────────────────────
  function lagToBpm(lag, framesPerSec) {
    return 60 * framesPerSec / lag;
  }
  function bpmToLag(bpm, framesPerSec) {
    return Math.round(60 * framesPerSec / bpm);
  }

  // ─── Peak picking ────────────────────────────────────────────
  function findPeaks(arr, prominenceFrac = 0.3) {
    let max = 0;
    for (let i = 0; i < arr.length; i++) if (arr[i] > max) max = arr[i];
    const peaks = [];
    for (let i = 1; i < arr.length - 1; i++) {
      if (arr[i] > arr[i - 1] && arr[i] > arr[i + 1] && arr[i] > max * prominenceFrac) {
        peaks.push({ idx: i, val: arr[i] });
      }
    }
    return peaks.sort((a, b) => b.val - a.val);
  }

  // ─── Main analyzer ───────────────────────────────────────────
  async function analyzeBuffer(audioBuffer, onProgress) {
    const prog = (phase, pct) => {
      onProgress && onProgress(phase, pct);
      window.dispatchEvent(new CustomEvent('astro-tempo-detect-progress', { detail: { phase, pct } }));
    };

    prog('decoding', 0.1);
    const { samples, sampleRate } = monoDownsample(audioBuffer, TARGET_SR);
    prog('onsets', 0.4);
    const env = onsetEnvelope(samples, sampleRate);
    const framesPerSec = sampleRate / Math.floor(sampleRate * HOP_MS / 1000);
    const minLag = bpmToLag(BPM_MAX, framesPerSec);
    const maxLag = bpmToLag(BPM_MIN, framesPerSec);
    prog('autocorr', 0.7);
    const acf = autocorr(env, minLag, maxLag);
    prog('peaks', 0.95);
    const peaks = findPeaks(acf, 0.45);
    if (peaks.length === 0) {
      prog('done', 1);
      return { bpm: null, confidence: 0, halfTime: false, candidates: [], alternates: [] };
    }
    const top = peaks[0];
    const topBpm = lagToBpm(top.idx + minLag, framesPerSec);
    // Confidence: ratio of top peak to second
    const second = peaks[1] ? peaks[1].val : top.val * 0.4;
    const conf = Math.max(0.3, Math.min(0.99, top.val / (second + 0.001) * 0.5));
    // Half/double-time check
    const halfBpm = topBpm / 2;
    const doubleBpm = topBpm * 2;
    const candidates = [
      { bpm: Math.round(topBpm), score: top.val, primary: true },
      halfBpm >= BPM_MIN ? { bpm: Math.round(halfBpm), score: top.val * 0.6, primary: false, kind: 'half' } : null,
      doubleBpm <= BPM_MAX ? { bpm: Math.round(doubleBpm), score: top.val * 0.6, primary: false, kind: 'double' } : null,
    ].filter(Boolean);
    const alternates = peaks.slice(1, 4).map(p => ({
      bpm: Math.round(lagToBpm(p.idx + minLag, framesPerSec)),
      score: p.val,
    }));
    // Heuristic: hip-hop / trap usually feels half-time; expose a hint
    const halfTime = topBpm > 130 && topBpm < 180;
    prog('done', 1);
    return {
      bpm: Math.round(topBpm),
      confidence: conf,
      halfTime,
      candidates,
      alternates,
      durationSec: audioBuffer.duration,
      sampleRate: audioBuffer.sampleRate,
      analyzedAt: Date.now(),
    };
  }

  async function analyzeFile(file, onProgress) {
    const audioBuffer = await decodeFile(file);
    return analyzeBuffer(audioBuffer, onProgress);
  }

  // ─── React: BADGE ────────────────────────────────────────────
  function TempoDetectBadge({ result }) {
    if (!result || result.bpm == null) return null;
    return (
      <div style={{
        display: 'inline-flex', alignItems: 'center', gap: 8,
        padding: '4px 10px', border: '1px solid var(--rule)', background: 'var(--bg-2)',
      }}>
        <span className="ff-mono num" style={{ fontSize: 16, fontWeight: 500, lineHeight: 1 }}>{result.bpm}</span>
        <span className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)' }}>BPM</span>
        <span className="ff-mono" style={{ fontSize: 9, color: 'var(--ink-3)' }}>· {Math.round(result.confidence * 100)}%</span>
      </div>
    );
  }

  // ─── React: MODAL (review on upload) ─────────────────────────
  function TempoDetectModal({ file, onAccept, onReject, onClose }) {
    const [phase, setPhase] = _S('init');
    const [pct, setPct] = _S(0);
    const [result, setResult] = _S(null);
    const [chosen, setChosen] = _S(null);
    const [err, setErr] = _S(null);

    _E(() => {
      let cancelled = false;
      (async () => {
        try {
          setPhase('decoding'); setPct(0.05);
          const r = await analyzeFile(file, (p, n) => {
            if (cancelled) return;
            setPhase(p); setPct(n);
          });
          if (cancelled) return;
          setResult(r);
          setChosen(r.bpm);
          setPhase('done');
        } catch (e) {
          if (!cancelled) { setErr(e.message || 'Analysis failed'); setPhase('error'); }
        }
      })();
      return () => { cancelled = true; };
    }, [file]);

    const accept = () => {
      onAccept && onAccept({ ...result, bpm: chosen });
      onClose && onClose();
    };
    const reject = () => {
      onReject && onReject();
      onClose && onClose();
    };

    return (
      <div onClick={onClose} style={{
        position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)',
        display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000,
      }}>
        <div onClick={e => e.stopPropagation()} style={{
          background: 'var(--paper)', border: '1px solid var(--rule)',
          width: 540, maxWidth: '92vw', padding: 28,
        }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 4 }}>
            <span className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.14em', color: 'var(--ink-3)' }}>TEMPO AUTO-DETECT</span>
            <button onClick={onClose} style={{ background: 'transparent', border: 0, color: 'var(--ink-3)', cursor: 'pointer', fontSize: 18, lineHeight: 1 }}>×</button>
          </div>
          <div style={{ fontSize: 18, fontWeight: 500, marginBottom: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{file.name}</div>
          <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginBottom: 22 }}>
            {(file.size / 1024 / 1024).toFixed(2)} MB · analyzing locally · no upload
          </div>

          {phase !== 'done' && phase !== 'error' && (
            <div style={{ marginBottom: 22 }}>
              <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)', marginBottom: 8 }}>
                {phase.toUpperCase()} · {Math.round(pct * 100)}%
              </div>
              <div style={{ width: '100%', height: 4, background: 'var(--bg-2)', position: 'relative' }}>
                <div style={{ position: 'absolute', top: 0, left: 0, height: '100%', width: `${pct * 100}%`, background: 'var(--ink)', transition: 'width 0.2s' }}/>
              </div>
              <div style={{ marginTop: 22, fontSize: 11, color: 'var(--ink-3)', lineHeight: 1.6 }}>
                Decoding waveform → measuring onset flux → autocorrelating energy peaks → picking dominant tempo and octave candidates.
              </div>
            </div>
          )}

          {phase === 'error' && (
            <div style={{ padding: 16, border: '1px solid var(--rule)', background: 'var(--bg-2)', marginBottom: 22 }}>
              <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: '#a32a18', marginBottom: 6 }}>ANALYSIS FAILED</div>
              <div style={{ fontSize: 12, color: 'var(--ink-2)' }}>{err}</div>
            </div>
          )}

          {phase === 'done' && result && result.bpm != null && (
            <>
              <div style={{ display: 'flex', alignItems: 'baseline', gap: 14, marginBottom: 4 }}>
                <span className="ff-mono num" style={{ fontSize: 56, fontWeight: 500, letterSpacing: '-0.02em', lineHeight: 1 }}>{chosen}</span>
                <span className="ff-mono upper" style={{ fontSize: 11, letterSpacing: '.14em', color: 'var(--ink-3)' }}>BPM</span>
              </div>
              <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginBottom: 18 }}>
                {Math.round(result.confidence * 100)}% confidence · {result.halfTime ? 'half-time feel suspected' : 'straight time'}
              </div>

              <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)', marginBottom: 8 }}>OCTAVE CANDIDATES</div>
              <div style={{ display: 'flex', gap: 8, marginBottom: 22 }}>
                {result.candidates.map((c, i) => (
                  <button key={i} onClick={() => setChosen(c.bpm)} style={{
                    flex: 1, padding: '10px 8px', cursor: 'pointer',
                    background: chosen === c.bpm ? 'var(--ink)' : 'transparent',
                    color: chosen === c.bpm ? 'var(--bg)' : 'var(--ink)',
                    border: '1px solid ' + (chosen === c.bpm ? 'var(--ink)' : 'var(--rule)'),
                    fontFamily: 'inherit',
                  }}>
                    <div className="ff-mono num" style={{ fontSize: 18, fontWeight: 500 }}>{c.bpm}</div>
                    <div className="ff-mono upper" style={{ fontSize: 8, letterSpacing: '.12em', opacity: 0.7, marginTop: 2 }}>
                      {c.primary ? 'PRIMARY' : c.kind === 'half' ? 'HALF-TIME' : 'DOUBLE-TIME'}
                    </div>
                  </button>
                ))}
              </div>

              {result.alternates && result.alternates.length > 0 && (
                <>
                  <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)', marginBottom: 8 }}>OTHER PEAKS</div>
                  <div style={{ display: 'flex', gap: 6, marginBottom: 22, flexWrap: 'wrap' }}>
                    {result.alternates.map((a, i) => (
                      <button key={i} onClick={() => setChosen(a.bpm)} className="ff-mono" style={{
                        fontSize: 11, padding: '4px 10px',
                        background: chosen === a.bpm ? 'var(--bg-2)' : 'transparent',
                        border: '1px solid var(--rule)', cursor: 'pointer', color: 'var(--ink-2)',
                      }}>
                        {a.bpm} <span style={{ color: 'var(--ink-3)' }}>· {Math.round(a.score / result.candidates[0].score * 100)}%</span>
                      </button>
                    ))}
                  </div>
                </>
              )}

              <div style={{ display: 'flex', gap: 8 }}>
                <button onClick={accept} className="ff-mono upper" style={{
                  flex: 1, fontSize: 11, padding: '12px 18px', letterSpacing: '.14em',
                  background: 'var(--ink)', color: 'var(--bg)', border: 0, cursor: 'pointer',
                }}>SAVE {chosen} BPM</button>
                <button onClick={reject} className="ff-mono upper" style={{
                  fontSize: 11, padding: '12px 18px', letterSpacing: '.14em',
                  background: 'transparent', color: 'var(--ink)', border: '1px solid var(--rule)', cursor: 'pointer',
                }}>SKIP</button>
              </div>
            </>
          )}
        </div>
      </div>
    );
  }

  // ─── Public ──────────────────────────────────────────────────
  window.TempoDetect = { analyzeFile, analyzeBuffer };
  window.TempoDetectBadge = TempoDetectBadge;
  window.TempoDetectModal = TempoDetectModal;

  console.log('[TempoDetect] loaded · web-audio autocorrelation · 60-200 BPM');
})();
