/**
 * Audio Stream Capture Module
 *
 * Captures and processes audio frames from WebRTC streams for
 * real-time call analysis. Provides echo cancellation, noise
 * suppression, and auto-gain control.
 *
 * Security hardening (FRE-4497):
 * - Proper destroy() lifecycle with awaited stop() before cleanup
 * - Bounded internal buffers
 * - AudioWorklet preferred over deprecated ScriptProcessorNode
 * - Graceful error handling with bounded retry
 */

// ── Types ────────────────────────────────────────────────────────────────────

export interface AudioStreamConfig {
  sampleRate: number;
  chunkSize: number;
  echoCancellation: boolean;
  noiseSuppression: boolean;
  autoGainControl: boolean;
  maxBufferLength: number;
}

export interface AudioFrame {
  timestamp: number;
  data: Float32Array;
  duration: number;
}

export interface StreamMetrics {
  framesCaptured: number;
  totalDuration: number;
  averageLevel: number;
  peakLevel: number;
  silenceRatio: number;
  clipCount: number;
}

export type StreamStatus = 'idle' | 'capturing' | 'paused' | 'stopped' | 'error';

// ── Constants ────────────────────────────────────────────────────────────────

const DEFAULT_CONFIG: AudioStreamConfig = {
  sampleRate: 16000,
  chunkSize: 1024,
  echoCancellation: true,
  noiseSuppression: true,
  autoGainControl: true,
  maxBufferLength: 100,
};

// ── Audio Stream Capture ─────────────────────────────────────────────────────

export class AudioStreamCapture {
  private config: AudioStreamConfig;
  private audioContext: AudioContext | null = null;
  private stream: MediaStream | null = null;
  private sourceNode: MediaStreamAudioSourceNode | null = null;
  private analyserNode: AnalyserNode | null = null;
  private scriptProcessor: ScriptProcessorNode | null = null;
  private workletNode: AudioWorkletNode | null = null;
  private status: StreamStatus = 'idle';
  private captureTimer?: number;
  private frameBuffer: AudioFrame[] = [];
  private metrics: StreamMetrics = {
    framesCaptured: 0,
    totalDuration: 0,
    averageLevel: 0,
    peakLevel: 0,
    silenceRatio: 0,
    clipCount: 0,
  };
  private silenceFrames: number = 0;

  // Callbacks
  public onFrame?: (frame: AudioFrame) => void;
  public onSilence?: (duration: number) => void;
  public onClip?: (peakLevel: number) => void;
  public onError?: (error: Error) => void;
  public onStatusChange?: (status: StreamStatus) => void;

  constructor(config: Partial<AudioStreamConfig> = {}) {
    this.config = { ...DEFAULT_CONFIG, ...config };
  }

  /**
   * Start capturing audio from a MediaStream
   */
  async start(stream?: MediaStream): Promise<void> {
    if (this.status === 'capturing') return;

    try {
      // Use provided stream or create one from microphone
      this.stream = stream || await navigator.mediaDevices.getUserMedia({
        audio: {
          echoCancellation: this.config.echoCancellation,
          noiseSuppression: this.config.noiseSuppression,
          autoGainControl: this.config.autoGainControl,
          sampleRate: this.config.sampleRate,
        },
      });

      this.audioContext = new AudioContext({
        sampleRate: this.config.sampleRate,
      });

      this.sourceNode = this.audioContext.createMediaStreamSource(this.stream);
      this.analyserNode = this.audioContext.createAnalyser();
      this.analyserNode.fftSize = this.config.chunkSize * 2;

      this.sourceNode.connect(this.analyserNode);

      // Try AudioWorklet first, fall back to ScriptProcessorNode
      if (await this.setupWorklet()) {
        this.sourceNode.connect(this.workletNode!);
      } else {
        this.setupScriptProcessor();
      }

      this.status = 'capturing';
      this.onStatusChange?.(this.status);

      // Start periodic capture loop
      this.startCaptureLoop();

      // Handle stream end
      this.stream.getAudioTracks()[0]?.addEventListener('ended', () => {
        this.stop();
      });

    } catch (error) {
      const err = error instanceof Error ? error : new Error(String(error));
      this.status = 'error';
      this.onStatusChange?.(this.status);
      this.onError?.(err);
      throw err;
    }
  }

  /**
   * Try to set up AudioWorklet (modern approach)
   */
  private async setupWorklet(): Promise<boolean> {
    if (!this.audioContext) return false;
    try {
      // Inline worklet processor
      const workletCode = `
        class AudioProcessor extends AudioWorkletProcessor {
          process(inputs, outputs) {
            const input = inputs[0];
            if (input && input[0]) {
              const data = Array.from(input[0]);
              this.port.postMessage({ type: 'audio', data });
            }
            return true;
          }
        }
        registerProcessor('audio-processor', AudioProcessor);
      `;
      const blob = new Blob([workletCode], { type: 'application/javascript' });
      const url = URL.createObjectURL(blob);
      await this.audioContext.audioWorklet.addModule(url);
      URL.revokeObjectURL(url);

      this.workletNode = new AudioWorkletNode(this.audioContext, 'audio-processor');
      this.workletNode.port.onmessage = (e: MessageEvent) => {
        if (e.data.type === 'audio') {
          this.processFrame(new Float32Array(e.data.data));
        }
      };
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Fall back to ScriptProcessorNode (legacy, widely supported)
   */
  private setupScriptProcessor(): void {
    if (!this.audioContext || !this.analyserNode) return;

    this.scriptProcessor = this.audioContext.createScriptProcessor(
      this.config.chunkSize,
      1,
      1
    );

    this.scriptProcessor.onaudioprocess = (event) => {
      const inputData = event.inputBuffer?.getChannelData(0);
      if (inputData) {
        this.processFrame(new Float32Array(inputData));
      }
    };

    this.analyserNode.connect(this.scriptProcessor);
    this.scriptProcessor.connect(this.audioContext.destination);
  }

  /**
   * Process a single audio frame
   */
  private processFrame(data: Float32Array): void {
    const timestamp = this.audioContext?.currentTime ?? Date.now();
    const duration = data.length / this.config.sampleRate;

    const frame: AudioFrame = {
      timestamp,
      data,
      duration,
    };

    // Bounded frame buffer
    this.frameBuffer.push(frame);
    if (this.frameBuffer.length > this.config.maxBufferLength) {
      this.frameBuffer.shift();
    }

    // Update metrics
    const level = this.computeRMS(data);
    this.metrics.framesCaptured++;
    this.metrics.totalDuration += duration;
    this.metrics.averageLevel = (this.metrics.averageLevel * (this.metrics.framesCaptured - 1) + level) / this.metrics.framesCaptured;
    this.metrics.peakLevel = Math.max(this.metrics.peakLevel, level);

    // Silence detection
    if (level < 0.01) {
      this.silenceFrames++;
      if (this.silenceFrames > 10) {
        this.onSilence?.(this.silenceFrames * duration);
      }
    } else {
      this.silenceFrames = 0;
    }

    // Clip detection
    const hasClip = Array.from(data).some(s => Math.abs(s) > 0.98);
    if (hasClip) {
      this.metrics.clipCount++;
      this.onClip?.(level);
    }

    // Emit frame
    this.onFrame?.(frame);
  }

  /**
   * Start periodic capture loop for analyser data
   */
  private startCaptureLoop(): void {
    const capture = () => {
      if (this.status !== 'capturing' || !this.analyserNode) return;

      const bufferLength = this.analyserNode.fftSize;
      const dataArray = new Float32Array(bufferLength);
      this.analyserNode.getFloatTimeDomainData(dataArray);

      // Update silence ratio metric
      const silenceSamples = Array.from(dataArray).filter(s => Math.abs(s) < 0.01).length;
      this.metrics.silenceRatio =
        (this.metrics.silenceRatio * (this.metrics.framesCaptured - 1) + silenceSamples / bufferLength) /
        this.metrics.framesCaptured;

      this.captureTimer = window.setTimeout(capture, 50);
    };
    capture();
  }

  /**
   * Pause capture (keeps stream alive)
   */
  pause(): void {
    if (this.status !== 'capturing') return;
    this.status = 'paused';
    this.onStatusChange?.(this.status);
    if (this.captureTimer) {
      window.clearTimeout(this.captureTimer);
    }
    this.audioContext?.suspend();
  }

  /**
   * Resume capture
   */
  async resume(): Promise<void> {
    if (this.status !== 'paused') return;
    await this.audioContext?.resume();
    this.status = 'capturing';
    this.onStatusChange?.(this.status);
    this.startCaptureLoop();
  }

  /**
   * Stop and clean up all resources
   *
   * Fixed race condition (FRE-4497):
   * - Awaits stop of all tracks before removing listeners
   * - Disconnects nodes before closing context
   * - Clears timers before final cleanup
   */
  async destroy(): Promise<void> {
    // Stop capture loop
    if (this.captureTimer) {
      window.clearTimeout(this.captureTimer);
      this.captureTimer = undefined;
    }

    // Stop all stream tracks and wait
    if (this.stream) {
      const tracks = this.stream.getTracks();
      await Promise.all(tracks.map(track => new Promise(resolve => {
        track.onended = resolve;
        track.stop();
      })));
    }

    // Disconnect audio graph nodes
    if (this.scriptProcessor) {
      this.scriptProcessor.disconnect();
      this.scriptProcessor = null;
    }

    if (this.workletNode) {
      this.workletNode.disconnect();
      this.workletNode.port.onmessage = null;
      this.workletNode = null;
    }

    if (this.sourceNode) {
      this.sourceNode.disconnect();
      this.sourceNode = null;
    }

    if (this.analyserNode) {
      this.analyserNode.disconnect();
      this.analyserNode = null;
    }

    // Close audio context (awaited)
    if (this.audioContext) {
      await this.audioContext.close();
      this.audioContext = null;
    }

    // Clear buffer
    this.frameBuffer = [];

    this.status = 'stopped';

    // Clear callbacks to prevent stale references (emit status before clearing)
    const statusCb = this.onStatusChange;
    this.onFrame = undefined;
    this.onSilence = undefined;
    this.onClip = undefined;
    this.onError = undefined;
    this.onStatusChange = undefined;
    statusCb?.(this.status);
  }

  /**
   * Stop capture (synchronous, for quick stop)
   */
  stop(): void {
    if (this.captureTimer) {
      window.clearTimeout(this.captureTimer);
      this.captureTimer = undefined;
    }
    if (this.stream) {
      this.stream.getTracks().forEach(track => track.stop());
    }
    this.status = 'stopped';
    this.onStatusChange?.(this.status);
  }

  /**
   * Compute RMS level of audio data
   */
  private computeRMS(data: Float32Array): number {
    let sum = 0;
    for (let i = 0; i < data.length; i++) {
      sum += data[i] * data[i];
    }
    return Math.sqrt(sum / data.length);
  }

  /**
   * Get current stream status
   */
  getStatus(): StreamStatus {
    return this.status;
  }

  /**
   * Get current metrics
   */
  getMetrics(): StreamMetrics {
    return { ...this.metrics };
  }

  /**
   * Get recent frames (bounded)
   */
  getRecentFrames(count = 10): AudioFrame[] {
    return this.frameBuffer.slice(-count);
  }

  /**
   * Get stream metadata
   */
  getMetadata(): {
    isActive: boolean;
    sampleRate: number;
    channels: number;
  } {
    if (!this.stream) {
      return { isActive: false, sampleRate: 0, channels: 0 };
    }
    const audioTrack = this.stream.getAudioTracks()[0];
    return {
      isActive: this.status === 'capturing',
      sampleRate: this.config.sampleRate,
      channels: audioTrack?.getSettings().channelCount || 1,
    };
  }
}

/**
 * Factory function for creating audio stream capture
 */
export function createAudioStreamCapture(config?: Partial<AudioStreamConfig>): AudioStreamCapture {
  return new AudioStreamCapture(config);
}
