/**
 * MediaRecorder-based audio capture with live VU meter and post-hoc waveform.
 *
 * Usage:
 *   const rec = await createRecorder({ onLevel: (db) => ... });
 *   rec.start();
 *   ...
 *   const blob = await rec.stop();  // returns final Blob + waveform[]
 */

export type RecorderState = "idle" | "recording" | "paused" | "stopped";

export interface RecorderResult {
  blob: Blob;
  mimeType: string;
  fileExt: string;
  durationSec: number;
  waveform: number[]; // ~120 samples, 0..1
}

export interface RecorderOptions {
  /** Called every animation frame with current peak level 0..1 */
  onLevel?: (level: number) => void;
  /** Called every animation frame with elapsed time in seconds while recording */
  onTime?: (sec: number) => void;
}

interface ActiveRecorder {
  state: () => RecorderState;
  start: () => void;
  stop: () => Promise<RecorderResult>;
  cancel: () => void;
}

/**
 * Pick the best supported audio mimetype for MediaRecorder.
 * WebM/Opus is the most reliable across Chrome/Edge/Firefox/Android.
 * Safari iOS prefers MP4/AAC.
 */
function pickMime(): { mime: string; ext: string } {
  const candidates: { mime: string; ext: string }[] = [
    { mime: "audio/webm;codecs=opus", ext: "webm" },
    { mime: "audio/webm", ext: "webm" },
    { mime: "audio/ogg;codecs=opus", ext: "ogg" },
    { mime: "audio/mp4;codecs=mp4a.40.2", ext: "m4a" },
    { mime: "audio/mp4", ext: "m4a" },
  ];
  for (const c of candidates) {
    if (typeof MediaRecorder !== "undefined" && MediaRecorder.isTypeSupported(c.mime)) {
      return c;
    }
  }
  // Fallback — let the browser choose.
  return { mime: "", ext: "webm" };
}

export async function createRecorder(opts: RecorderOptions = {}): Promise<ActiveRecorder> {
  // 1. Request mic.
  const stream = await navigator.mediaDevices.getUserMedia({
    audio: {
      echoCancellation: true,
      noiseSuppression: true,
      autoGainControl: false,
    },
    video: false,
  });

  // 2. Set up AudioContext for VU + post-hoc waveform.
  const Ctx = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
  const audioCtx = new Ctx();
  const source = audioCtx.createMediaStreamSource(stream);
  const analyser = audioCtx.createAnalyser();
  analyser.fftSize = 1024;
  source.connect(analyser);
  const buf = new Uint8Array(analyser.fftSize);

  // 3. Pick mime + create MediaRecorder.
  const { mime, ext } = pickMime();
  const recorder = mime ? new MediaRecorder(stream, { mimeType: mime }) : new MediaRecorder(stream);
  const chunks: Blob[] = [];
  recorder.ondataavailable = (e) => {
    if (e.data && e.data.size > 0) chunks.push(e.data);
  };

  // 4. Sampling buffer for post-hoc waveform — collect peak per ~100ms window.
  const samples: number[] = [];
  let lastSampleAt = 0;
  let curPeak = 0;
  const SAMPLE_INTERVAL_MS = 80; // ~12 samples/sec → 120 samples = 10 sec of audio
  const MAX_SAMPLES = 200;

  let rafId = 0;
  let state: RecorderState = "idle";
  let startedAt = 0;

  const tick = (now: number) => {
    if (state !== "recording") return;
    analyser.getByteTimeDomainData(buf);
    let peak = 0;
    for (let i = 0; i < buf.length; i++) {
      const v = Math.abs(buf[i] - 128) / 128;
      if (v > peak) peak = v;
    }
    if (peak > curPeak) curPeak = peak;
    if (opts.onLevel) opts.onLevel(peak);
    if (opts.onTime) opts.onTime((now - startedAt) / 1000);

    if (now - lastSampleAt >= SAMPLE_INTERVAL_MS) {
      if (samples.length < MAX_SAMPLES) samples.push(curPeak);
      curPeak = 0;
      lastSampleAt = now;
    }

    rafId = requestAnimationFrame(tick);
  };

  const start = () => {
    if (state !== "idle") return;
    state = "recording";
    chunks.length = 0;
    samples.length = 0;
    curPeak = 0;
    lastSampleAt = performance.now();
    startedAt = performance.now();
    recorder.start(250); // emit dataavailable every 250ms
    rafId = requestAnimationFrame(tick);
  };

  const stop = (): Promise<RecorderResult> => {
    return new Promise((resolve) => {
      if (state === "stopped" || state === "idle") {
        cleanup();
        return resolve({
          blob: new Blob(chunks, { type: mime || "audio/webm" }),
          mimeType: mime || "audio/webm",
          fileExt: ext,
          durationSec: 0,
          waveform: [],
        });
      }
      const dur = (performance.now() - startedAt) / 1000;
      recorder.onstop = () => {
        const blob = new Blob(chunks, { type: mime || "audio/webm" });
        // Downsample / pad waveform to 120 buckets for display.
        const wf = resampleWaveform(samples, 120);
        cleanup();
        resolve({
          blob,
          mimeType: mime || "audio/webm",
          fileExt: ext,
          durationSec: dur,
          waveform: wf,
        });
      };
      state = "stopped";
      cancelAnimationFrame(rafId);
      try { recorder.stop(); } catch { /* already stopped */ }
    });
  };

  const cancel = () => {
    state = "stopped";
    cancelAnimationFrame(rafId);
    try { recorder.stop(); } catch {}
    cleanup();
  };

  const cleanup = () => {
    stream.getTracks().forEach((t) => t.stop());
    audioCtx.close().catch(() => {});
  };

  return {
    state: () => state,
    start,
    stop,
    cancel,
  };
}

/** Resample (or pad) a series of peak values into N evenly-spaced buckets. */
export function resampleWaveform(samples: number[], n: number): number[] {
  if (samples.length === 0) return Array.from({ length: n }, () => 0);
  if (samples.length === n) return samples.map((v) => Math.min(1, v));
  const out: number[] = [];
  for (let i = 0; i < n; i++) {
    const start = (i / n) * samples.length;
    const end = ((i + 1) / n) * samples.length;
    let peak = 0;
    for (let j = Math.floor(start); j < Math.ceil(end) && j < samples.length; j++) {
      if (samples[j] > peak) peak = samples[j];
    }
    out.push(Math.min(1, peak));
  }
  // Normalize so the loudest bucket is ~0.9 (visual contrast)
  const mx = Math.max(...out);
  if (mx > 0 && mx < 0.9) {
    const g = 0.9 / mx;
    return out.map((v) => Math.min(1, v * g));
  }
  return out;
}
