/**
 * AudioRecorder - MediaRecorder wrapper with codec fallback and volume metering.
 */
class AudioRecorder {
  constructor() {
    this.mediaRecorder = null;
    this.stream = null;
    this.chunks = [];
    this.recordings = [];
    this.analyser = null;
    this.audioContext = null;
    this.mimeType = null;
    this.onVolumeChange = null;
    this._volumeAnimFrame = null;
  }

  /**
   * Request microphone access and set up audio context.
   * @param {Object} options
   * @param {string} options.deviceId - Preferred microphone device ID
   * @returns {Promise<MediaStream>}
   */
  async init(options = {}) {
    const constraints = {
      audio: options.deviceId
        ? { deviceId: { exact: options.deviceId } }
        : true,
    };

    this.stream = await navigator.mediaDevices.getUserMedia(constraints);

    // Set up audio context for volume metering
    this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
    const source = this.audioContext.createMediaStreamSource(this.stream);
    this.analyser = this.audioContext.createAnalyser();
    this.analyser.fftSize = 256;
    source.connect(this.analyser);

    // Determine best supported mime type
    this.mimeType = this._selectMimeType();

    return this.stream;
  }

  /**
   * Get available audio input devices.
   * @returns {Promise<MediaDeviceInfo[]>}
   */
  async getDevices() {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices.filter(d => d.kind === 'audioinput');
  }

  /**
   * Start volume metering (for mic check visualization).
   * @param {function} callback - Called with volume level 0-1
   */
  startMeter(callback) {
    this.onVolumeChange = callback;
    this._updateMeter();
  }

  /** Stop volume metering */
  stopMeter() {
    this.onVolumeChange = null;
    if (this._volumeAnimFrame) {
      cancelAnimationFrame(this._volumeAnimFrame);
      this._volumeAnimFrame = null;
    }
  }

  /** Get current volume level (0-1) */
  getVolume() {
    if (!this.analyser) return 0;
    const data = new Uint8Array(this.analyser.frequencyBinCount);
    this.analyser.getByteFrequencyData(data);
    const sum = data.reduce((a, b) => a + b, 0);
    return sum / (data.length * 255);
  }

  /**
   * Start recording.
   * @returns {Promise<void>}
   */
  async startRecording() {
    if (!this.stream) throw new Error('Call init() first');

    this.chunks = [];
    const options = this.mimeType ? { mimeType: this.mimeType } : {};
    this.mediaRecorder = new MediaRecorder(this.stream, options);

    this.mediaRecorder.ondataavailable = (e) => {
      if (e.data.size > 0) {
        this.chunks.push(e.data);
      }
    };

    this.mediaRecorder.start(100); // Collect data every 100ms
  }

  /**
   * Stop recording and return the blob.
   * @returns {Promise<{blob: Blob, duration: number, filename: string}>}
   */
  async stopRecording() {
    return new Promise((resolve) => {
      const startTime = this.mediaRecorder.startTime || Date.now();

      this.mediaRecorder.onstop = () => {
        const blob = new Blob(this.chunks, { type: this.mimeType || 'audio/webm' });
        const duration = Date.now() - startTime;
        const ext = this._getExtension();
        const filename = `recording_${Date.now()}.${ext}`;

        const recording = { blob, duration, filename };
        this.recordings.push(recording);
        resolve(recording);
      };

      this.mediaRecorder.stop();
    });
  }

  /** Check if currently recording */
  isRecording() {
    return this.mediaRecorder?.state === 'recording';
  }

  /**
   * Download a single recording.
   * @param {Object} recording - {blob, filename}
   */
  downloadRecording(recording) {
    const url = URL.createObjectURL(recording.blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = recording.filename;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }

  /**
   * Download all recordings as a ZIP (requires JSZip loaded on page).
   * @param {string} zipFilename
   */
  async downloadAllAsZip(zipFilename = 'recordings.zip') {
    if (typeof JSZip === 'undefined') {
      console.error('JSZip not loaded. Falling back to individual downloads.');
      this.recordings.forEach(r => this.downloadRecording(r));
      return;
    }

    const zip = new JSZip();
    for (const rec of this.recordings) {
      zip.file(rec.filename, rec.blob);
    }

    const zipBlob = await zip.generateAsync({ type: 'blob' });
    const url = URL.createObjectURL(zipBlob);
    const a = document.createElement('a');
    a.href = url;
    a.download = zipFilename;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }

  /** Release microphone and clean up */
  dispose() {
    this.stopMeter();
    if (this.stream) {
      this.stream.getTracks().forEach(t => t.stop());
      this.stream = null;
    }
    if (this.audioContext) {
      this.audioContext.close();
      this.audioContext = null;
    }
    this.mediaRecorder = null;
  }

  // Private

  _selectMimeType() {
    const candidates = [
      'audio/webm;codecs=opus',
      'audio/webm',
      'audio/ogg;codecs=opus',
      'audio/mp4',
    ];
    for (const type of candidates) {
      if (MediaRecorder.isTypeSupported(type)) {
        return type;
      }
    }
    return ''; // Let browser choose default
  }

  _getExtension() {
    if (!this.mimeType) return 'webm';
    if (this.mimeType.includes('webm')) return 'webm';
    if (this.mimeType.includes('ogg')) return 'ogg';
    if (this.mimeType.includes('mp4')) return 'm4a';
    return 'webm';
  }

  _updateMeter() {
    if (!this.onVolumeChange) return;
    this.onVolumeChange(this.getVolume());
    this._volumeAnimFrame = requestAnimationFrame(() => this._updateMeter());
  }
}
