import * as fs from 'fs';
import * as path from 'path';
import { v4 as uuid } from 'uuid';
import { RTCPeerConnection } from 'werift';
import { MediaRecorder } from 'werift/nonstandard';
import { log, LogLevel } from '../log';

type ServerAudioRecordingSession = {
  recordingId: string;
  meetingId: string;
  peerConnection: RTCPeerConnection;
  audioPath: string;
  createdAt: number;
  recorder?: MediaRecorder;
  audioTrackCount: number;
  localCandidates: any[];
};

type StartServerAudioRecordingOptions = {
  meetingId?: string;
  offerSdp: string;
  iceServers?: any[];
};

type StartServerAudioRecordingResult = {
  recordingId: string;
  meetingId: string;
  answerSdp: string;
  candidates: any[];
  audioPath: string;
};

type StoppedServerAudioRecording = {
  recordingId: string;
  meetingId: string;
  audioPath: string;
  hasAudio: boolean;
  audioTrackCount: number;
  createdAt: number;
  stoppedAt: number;
};

function waitForIceGatheringComplete(peerConnection: RTCPeerConnection, timeoutMs: number): Promise<void> {
  if (peerConnection.iceGatheringState === 'complete') {
    return Promise.resolve();
  }

  return new Promise((resolve) => {
    let done = false;
    const subscription = peerConnection.iceGatheringStateChange.subscribe((state) => {
      if (state === 'complete') {
        finish();
      }
    });
    const timer = setTimeout(finish, timeoutMs);

    function finish(): void {
      if (done) {
        return;
      }
      done = true;
      clearTimeout(timer);
      subscription.unSubscribe();
      resolve();
    }
  });
}

function toJsonCandidate(candidate: any): any {
  if (!candidate) {
    return candidate;
  }

  return typeof candidate.toJSON === 'function' ? candidate.toJSON() : candidate;
}

export class ServerAudioRecorderManager {
  private sessions: Map<string, ServerAudioRecordingSession> = new Map<string, ServerAudioRecordingSession>();

  constructor(private tempDir: string) {}

  async start(options: StartServerAudioRecordingOptions): Promise<StartServerAudioRecordingResult> {
    if (!options.offerSdp || typeof options.offerSdp !== 'string') {
      throw new Error('offerSdp is required');
    }

    if (!fs.existsSync(this.tempDir)) {
      fs.mkdirSync(this.tempDir, { recursive: true });
    }

    const recordingId = uuid();
    const meetingId = options.meetingId || 'unknown';
    const audioPath = path.join(this.tempDir, `${recordingId}.server-audio.webm`);
    const peerConnection = new RTCPeerConnection({
      iceServers: Array.isArray(options.iceServers) ? options.iceServers : []
    });
    const session: ServerAudioRecordingSession = {
      recordingId,
      meetingId,
      peerConnection,
      audioPath,
      createdAt: Date.now(),
      audioTrackCount: 0,
      localCandidates: []
    };

    peerConnection.onIceCandidate.subscribe((candidate) => {
      if (candidate) {
        session.localCandidates.push(toJsonCandidate(candidate));
      }
    });

    peerConnection.onTrack.subscribe((track) => {
      if (track.kind !== 'audio') {
        return;
      }

      session.audioTrackCount += 1;
      if (session.recorder) {
        log(LogLevel.warn, `Ignoring extra server audio track for recording ${recordingId}`);
        return;
      }

      session.recorder = new MediaRecorder({
        path: audioPath,
        tracks: [track]
      });
      session.recorder.onError.subscribe((error) => {
        log(LogLevel.error, `Server audio recorder error for ${recordingId}:`, error);
      });
      log(LogLevel.log, `Server audio track received for recording ${recordingId}`);
    });

    await peerConnection.setRemoteDescription({ type: 'offer', sdp: options.offerSdp });
    const answer = await peerConnection.createAnswer();
    await peerConnection.setLocalDescription(answer);
    await waitForIceGatheringComplete(peerConnection, 3000);

    this.sessions.set(recordingId, session);

    return {
      recordingId,
      meetingId,
      answerSdp: (peerConnection.localDescription && peerConnection.localDescription.sdp) || answer.sdp,
      candidates: session.localCandidates,
      audioPath
    };
  }

  async addCandidate(recordingId: string, candidate: any): Promise<boolean> {
    const session = this.sessions.get(recordingId);
    if (!session) {
      return false;
    }

    await session.peerConnection.addIceCandidate(candidate || {});
    return true;
  }

  async stop(recordingId: string): Promise<StoppedServerAudioRecording | null> {
    const session = this.sessions.get(recordingId);
    if (!session) {
      return null;
    }

    this.sessions.delete(recordingId);

    if (session.recorder) {
      await session.recorder.stop();
    }
    await session.peerConnection.close();

    const hasAudio = fs.existsSync(session.audioPath) && fs.statSync(session.audioPath).size > 0;
    return {
      recordingId: session.recordingId,
      meetingId: session.meetingId,
      audioPath: session.audioPath,
      hasAudio,
      audioTrackCount: session.audioTrackCount,
      createdAt: session.createdAt,
      stoppedAt: Date.now()
    };
  }

  async cancel(recordingId: string): Promise<boolean> {
    const stopped = await this.stop(recordingId);
    if (!stopped) {
      return false;
    }

    if (fs.existsSync(stopped.audioPath)) {
      fs.unlinkSync(stopped.audioPath);
    }
    return true;
  }
}

