import { extendMicRelease, waitForMicRelease } from './micSession';

const PREFERRED_MIME_TYPES = [
  'audio/webm;codecs=opus',
  'audio/webm',
  'audio/mp4',
  'audio/ogg;codecs=opus',
  'audio/ogg',
];

export class MicCapture {
  private stream: MediaStream | null = null;
  private recorder: MediaRecorder | null = null;
  private chunks: Blob[] = [];
  private context: AudioContext | null = null;
  private analyser: AnalyserNode | null = null;
  private source: MediaStreamAudioSourceNode | null = null;

  static isSupported() {
    return (
      typeof navigator !== 'undefined' &&
      !!navigator.mediaDevices?.getUserMedia &&
      typeof MediaRecorder !== 'undefined'
    );
  }

  static pickMimeType() {
    for (const type of PREFERRED_MIME_TYPES) {
      if (MediaRecorder.isTypeSupported(type)) return type;
    }
    return undefined;
  }

  get recording() {
    return this.recorder?.state === 'recording';
  }

  get levelAnalyser() {
    return this.analyser;
  }

  /** 开始采集；后续 call 模式可改为 onChunk 流式回调 */
  async start(): Promise<void> {
    if (this.recording) return;
    await waitForMicRelease();
    this.chunks = [];
    this.stream = await navigator.mediaDevices.getUserMedia({
      audio: {
        echoCancellation: true,
        noiseSuppression: true,
        autoGainControl: true,
      },
    });
    const mimeType = MicCapture.pickMimeType();
    this.recorder = mimeType
      ? new MediaRecorder(this.stream, { mimeType })
      : new MediaRecorder(this.stream);
    this.recorder.ondataavailable = (event) => {
      if (event.data.size > 0) this.chunks.push(event.data);
    };
    this.context = new AudioContext();
    this.analyser = this.context.createAnalyser();
    this.analyser.fftSize = 256;
    this.analyser.smoothingTimeConstant = 0.75;
    this.source = this.context.createMediaStreamSource(this.stream);
    this.source.connect(this.analyser);
    if (this.context.state === 'suspended') {
      await this.context.resume();
    }
    this.recorder.start(250);
  }

  async stop(): Promise<Blob> {
    const recorder = this.recorder;
    if (!recorder || recorder.state === 'inactive') {
      await this.dispose();
      return new Blob();
    }
    const disposePromise = new Promise<Blob>((resolve) => {
      recorder.onstop = () => {
        const blob = new Blob(this.chunks, { type: recorder.mimeType || 'audio/webm' });
        void this.dispose().then(() => resolve(blob));
      };
      try {
        if (recorder.state === 'recording') {
          recorder.requestData();
        }
      } catch {
        // Some WebViews omit requestData(); stop() still flushes on stop.
      }
      recorder.stop();
    });
    extendMicRelease(disposePromise);
    return disposePromise;
  }

  async cancel(): Promise<void> {
    const recorder = this.recorder;
    if (recorder && recorder.state !== 'inactive') {
      recorder.onstop = null;
      try {
        if (recorder.state === 'recording') {
          recorder.requestData();
        }
      } catch {
        // ignore
      }
      recorder.stop();
    }
    const disposePromise = this.dispose();
    extendMicRelease(disposePromise);
    await disposePromise;
  }

  private async dispose() {
    this.source?.disconnect();
    this.source = null;
    this.analyser = null;
    const context = this.context;
    this.context = null;
    if (context) {
      try {
        await context.close();
      } catch {
        // ignore close races
      }
    }
    this.stream?.getTracks().forEach((track) => track.stop());
    this.stream = null;
    this.recorder = null;
    this.chunks = [];
  }
}

export function mapMicError(err: unknown) {
  const name = err instanceof DOMException ? err.name : '';
  if (name === 'NotAllowedError' || name === 'PermissionDeniedError') {
    return '请在浏览器设置中允许麦克风';
  }
  if (name === 'NotFoundError' || name === 'DevicesNotFoundError') {
    return '未检测到麦克风设备';
  }
  if (name === 'NotReadableError') {
    return '麦克风被其他应用占用';
  }
  return err instanceof Error ? err.message : '无法访问麦克风';
}
