export interface VadObservation {
  speechStarted: boolean;
  turnEnded: boolean;
  silenceDurationMs: number;
}

export class AdaptiveVad {
  private noiseFloor = 0.008;
  private speaking = false;
  private lastSpeechAt = 0;

  get silenceWindowMs(): number {
    return Math.max(400, Math.min(600, Math.round(400 + this.noiseFloor * 5000)));
  }

  observe(rms: number, nowMs: number): VadObservation {
    const level = Math.max(0, Number.isFinite(rms) ? rms : 0);
    if (!this.speaking) {
      this.noiseFloor = this.noiseFloor * 0.9 + Math.min(level, 0.06) * 0.1;
    }
    const speechThreshold = Math.max(0.018, this.noiseFloor * 2.8);
    const isSpeech = level >= speechThreshold;
    let speechStarted = false;
    let turnEnded = false;
    let silenceDurationMs = 0;
    if (isSpeech) {
      speechStarted = !this.speaking;
      this.speaking = true;
      this.lastSpeechAt = nowMs;
    } else if (this.speaking && nowMs - this.lastSpeechAt >= this.silenceWindowMs) {
      this.speaking = false;
      turnEnded = true;
      silenceDurationMs = nowMs - this.lastSpeechAt;
    }
    return { speechStarted, turnEnded, silenceDurationMs };
  }

  reset(): void {
    this.speaking = false;
    this.lastSpeechAt = 0;
  }
}

export interface VoiceCaptureCallbacks {
  onChunk: (chunk: ArrayBuffer) => void;
  onSpeechStart?: () => void;
  onTurnEnd: (vadLatencyMs: number) => void;
  onError: (error: Error) => void;
}

export class VoiceCallAudioCapture {
  private stream: MediaStream | null = null;
  private audioContext: AudioContext | null = null;
  private analyser: AnalyserNode | null = null;
  private recorder: MediaRecorder | null = null;
  private vadTimer: number | null = null;
  private pendingChunkReads: Promise<void> = Promise.resolve();
  private readonly vad = new AdaptiveVad();

  get mimeType(): string {
    if (typeof MediaRecorder === "undefined") return "audio/webm;codecs=opus";
    if (MediaRecorder.isTypeSupported("audio/webm;codecs=opus")) return "audio/webm;codecs=opus";
    if (MediaRecorder.isTypeSupported("audio/ogg;codecs=opus")) return "audio/ogg;codecs=opus";
    return "audio/webm";
  }

  get sampleRate(): number {
    return this.audioContext?.sampleRate ?? 48000;
  }

  async prepare(): Promise<void> {
    if (this.stream) return;
    this.stream = await navigator.mediaDevices.getUserMedia({
      audio: {
        echoCancellation: true,
        noiseSuppression: true,
        autoGainControl: true,
        channelCount: 1,
      },
    });
    this.audioContext = new AudioContext({ latencyHint: "interactive" });
    await this.audioContext.resume();
    this.analyser = this.audioContext.createAnalyser();
    this.analyser.fftSize = 1024;
    this.audioContext.createMediaStreamSource(this.stream).connect(this.analyser);
  }

  startTurn(callbacks: VoiceCaptureCallbacks): void {
    if (!this.stream || !this.analyser) throw new Error("Microphone is not prepared");
    if (this.recorder?.state === "recording") return;
    this.vad.reset();
    this.pendingChunkReads = Promise.resolve();
    const recorder = new MediaRecorder(this.stream, { mimeType: this.mimeType });
    this.recorder = recorder;
    recorder.ondataavailable = (event) => {
      if (event.data.size === 0) return;
      this.pendingChunkReads = this.pendingChunkReads
        .then(() => event.data.arrayBuffer())
        .then(callbacks.onChunk)
        .catch((error: unknown) => {
          callbacks.onError(error instanceof Error ? error : new Error(String(error)));
        });
    };
    recorder.onerror = () => callbacks.onError(new Error("Microphone recorder failed"));
    recorder.start(120);
    const samples = new Float32Array(this.analyser.fftSize);
    this.vadTimer = window.setInterval(() => {
      this.analyser?.getFloatTimeDomainData(samples);
      let squareSum = 0;
      for (const sample of samples) squareSum += sample * sample;
      const rms = Math.sqrt(squareSum / samples.length);
      const observation = this.vad.observe(rms, performance.now());
      if (observation.speechStarted) callbacks.onSpeechStart?.();
      if (observation.turnEnded) {
        this.stopTurn(() => callbacks.onTurnEnd(observation.silenceDurationMs));
      }
    }, 50);
  }

  stopTurn(onStopped?: () => void): void {
    if (this.vadTimer !== null) window.clearInterval(this.vadTimer);
    this.vadTimer = null;
    if (this.recorder?.state === "recording") {
      this.recorder.onstop = () => {
        void this.pendingChunkReads.then(() => onStopped?.());
      };
      this.recorder.stop();
    } else {
      onStopped?.();
    }
    this.recorder = null;
  }

  dispose(): void {
    this.stopTurn();
    this.stream?.getTracks().forEach((track) => track.stop());
    this.stream = null;
    void this.audioContext?.close();
    this.audioContext = null;
    this.analyser = null;
  }
}
