/**
 * RecorderService - Handles all MediaRecorder, audio monitoring, and video export logic
 */

export interface IRecorderEvents {
  onDurationUpdate?: (duration: number) => void;
  onRecordingComplete?: (blob: Blob) => void;
  onAudioLevelUpdate?: (level: number) => void;
  onError?: (error: Error) => void;
  onStreamEnded?: () => void;
}

export interface IRecordingOptions {
  mode: 'viewport' | 'screen';
  audioDeviceId?: string;
  viewportElement?: HTMLElement;
}

export class RecorderService {
  // Recording state
  private mediaRecorder: MediaRecorder | null = null;
  private recordedChunks: Blob[] = [];
  private durationInterval: number | null = null;
  private _duration: number = 0;
  private _recordedBlob: Blob | null = null;
  private _isRecording: boolean = false;

  // Audio monitoring state
  private audioContext: AudioContext | null = null;
  private audioAnalyser: AnalyserNode | null = null;
  private audioMonitoringInterval: number | null = null;
  private monitoringStream: MediaStream | null = null;

  // Current recording stream
  private currentStream: MediaStream | null = null;

  // Event callbacks
  private events: IRecorderEvents = {};

  constructor(events?: IRecorderEvents) {
    if (events) {
      this.events = events;
    }
  }

  // Public getters
  get isRecording(): boolean {
    return this._isRecording;
  }

  get duration(): number {
    return this._duration;
  }

  get recordedBlob(): Blob | null {
    return this._recordedBlob;
  }

  // Update event callbacks
  setEvents(events: IRecorderEvents): void {
    this.events = { ...this.events, ...events };
  }

  // ==================== Microphone Management ====================

  async loadMicrophones(requestPermission: boolean = false): Promise<MediaDeviceInfo[]> {
    try {
      if (requestPermission) {
        // Request permission by getting a temporary stream
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        stream.getTracks().forEach(track => track.stop());
      }

      const devices = await navigator.mediaDevices.enumerateDevices();
      return devices.filter(d => d.kind === 'audioinput');
    } catch (error) {
      console.error('Error loading microphones:', error);
      return [];
    }
  }

  async startAudioMonitoring(deviceId: string): Promise<void> {
    this.stopAudioMonitoring();

    if (!deviceId) return;

    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        audio: { deviceId: { exact: deviceId } }
      });

      this.monitoringStream = stream;
      this.audioContext = new AudioContext();
      const source = this.audioContext.createMediaStreamSource(stream);
      this.audioAnalyser = this.audioContext.createAnalyser();
      this.audioAnalyser.fftSize = 256;
      source.connect(this.audioAnalyser);

      const dataArray = new Uint8Array(this.audioAnalyser.frequencyBinCount);

      this.audioMonitoringInterval = window.setInterval(() => {
        if (this.audioAnalyser) {
          this.audioAnalyser.getByteFrequencyData(dataArray);
          const average = dataArray.reduce((a, b) => a + b) / dataArray.length;
          const level = Math.min(100, (average / 128) * 100);
          this.events.onAudioLevelUpdate?.(level);
        }
      }, 50);
    } catch (error) {
      console.error('Error starting audio monitoring:', error);
      this.events.onAudioLevelUpdate?.(0);
    }
  }

  stopAudioMonitoring(): void {
    if (this.audioMonitoringInterval) {
      clearInterval(this.audioMonitoringInterval);
      this.audioMonitoringInterval = null;
    }
    if (this.audioContext) {
      this.audioContext.close();
      this.audioContext = null;
    }
    if (this.monitoringStream) {
      this.monitoringStream.getTracks().forEach(track => track.stop());
      this.monitoringStream = null;
    }
    this.audioAnalyser = null;
  }

  // ==================== Recording Control ====================

  async startRecording(options: IRecordingOptions): Promise<void> {
    try {
      // Stop audio monitoring before recording
      this.stopAudioMonitoring();

      // Get video stream based on mode
      const displayMediaOptions: DisplayMediaStreamOptions = {
        video: {
          displaySurface: options.mode === 'viewport' ? 'browser' : 'monitor'
        } as MediaTrackConstraints,
        audio: false
      };

      // Add preferCurrentTab hint for viewport mode
      if (options.mode === 'viewport') {
        (displayMediaOptions as any).preferCurrentTab = true;
      }

      const videoStream = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);

      // If viewport mode, try to crop to viewport element using Element Capture API
      if (options.mode === 'viewport' && options.viewportElement) {
        try {
          if ('CropTarget' in window) {
            const cropTarget = await (window as any).CropTarget.fromElement(options.viewportElement);
            const [videoTrack] = videoStream.getVideoTracks();
            await (videoTrack as any).cropTo(cropTarget);
          }
        } catch (e) {
          console.warn('Element Capture not supported, recording full tab:', e);
        }
      }

      // Combine video with audio if enabled
      let combinedStream = videoStream;
      if (options.audioDeviceId) {
        try {
          const audioStream = await navigator.mediaDevices.getUserMedia({
            audio: { deviceId: { exact: options.audioDeviceId } }
          });
          combinedStream = new MediaStream([
            ...videoStream.getVideoTracks(),
            ...audioStream.getAudioTracks()
          ]);
        } catch (audioError) {
          console.warn('Could not add audio:', audioError);
        }
      }

      // Store stream for cleanup
      this.currentStream = combinedStream;

      // Create MediaRecorder
      const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
        ? 'video/webm;codecs=vp9'
        : 'video/webm';

      this.mediaRecorder = new MediaRecorder(combinedStream, { mimeType });
      this.recordedChunks = [];

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

      this.mediaRecorder.onstop = () => this.handleRecordingComplete();

      // Handle stream ending (user clicks "Stop sharing")
      videoStream.getVideoTracks()[0].onended = () => {
        if (this._isRecording) {
          this.stopRecording();
          this.events.onStreamEnded?.();
        }
      };

      this.mediaRecorder.start(1000); // Capture in 1-second chunks

      // Start duration timer
      this._duration = 0;
      this.durationInterval = window.setInterval(() => {
        this._duration++;
        this.events.onDurationUpdate?.(this._duration);
      }, 1000);

      this._isRecording = true;
    } catch (error) {
      console.error('Error starting recording:', error);
      this._isRecording = false;
      this.events.onError?.(error as Error);
      throw error;
    }
  }

  stopRecording(): void {
    if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
      this.mediaRecorder.stop();
    }

    if (this.durationInterval) {
      clearInterval(this.durationInterval);
      this.durationInterval = null;
    }
  }

  private async handleRecordingComplete(): Promise<void> {
    // Create blob from recorded chunks
    const blob = new Blob(this.recordedChunks, { type: 'video/webm' });

    this._recordedBlob = blob;

    // Stop all tracks
    if (this.currentStream) {
      this.currentStream.getTracks().forEach(track => track.stop());
      this.currentStream = null;
    }

    this._isRecording = false;
    this.events.onRecordingComplete?.(this._recordedBlob);
  }

  // ==================== Trim & Export ====================

  async exportTrimmedVideo(
    videoElement: HTMLVideoElement,
    trimStart: number,
    trimEnd: number
  ): Promise<Blob> {
    return new Promise((resolve, reject) => {
      // Create a canvas for capturing frames
      const canvas = document.createElement('canvas');
      canvas.width = videoElement.videoWidth || 1280;
      canvas.height = videoElement.videoHeight || 720;
      const ctx = canvas.getContext('2d');

      if (!ctx) {
        reject(new Error('Could not get canvas context'));
        return;
      }

      // Create canvas stream for video
      const canvasStream = canvas.captureStream(30);

      // Try to capture audio from video element
      let combinedStream: MediaStream;

      try {
        // Create audio context to capture video's audio
        const audioCtx = new AudioContext();
        const source = audioCtx.createMediaElementSource(videoElement);
        const destination = audioCtx.createMediaStreamDestination();
        source.connect(destination);
        source.connect(audioCtx.destination); // Also play through speakers

        // Combine video (from canvas) and audio (from video element)
        combinedStream = new MediaStream([
          ...canvasStream.getVideoTracks(),
          ...destination.stream.getAudioTracks()
        ]);

        // Store audioCtx for cleanup
        const cleanup = () => {
          audioCtx.close();
        };

        this.recordTrimmedStream(videoElement, canvas, ctx, combinedStream, trimStart, trimEnd, cleanup, resolve, reject);
      } catch (audioError) {
        console.warn('Could not capture audio, recording video only:', audioError);
        combinedStream = canvasStream;
        this.recordTrimmedStream(videoElement, canvas, ctx, combinedStream, trimStart, trimEnd, () => {}, resolve, reject);
      }
    });
  }

  private recordTrimmedStream(
    video: HTMLVideoElement,
    canvas: HTMLCanvasElement,
    ctx: CanvasRenderingContext2D,
    stream: MediaStream,
    trimStart: number,
    trimEnd: number,
    cleanup: () => void,
    resolve: (blob: Blob) => void,
    reject: (error: Error) => void
  ): void {
    const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
      ? 'video/webm;codecs=vp9'
      : 'video/webm';

    const recorder = new MediaRecorder(stream, { mimeType });
    const chunks: Blob[] = [];

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

    recorder.onstop = () => {
      cleanup();
      resolve(new Blob(chunks, { type: 'video/webm' }));
    };

    recorder.onerror = (e) => {
      cleanup();
      reject(new Error('Recording error: ' + e));
    };

    // Seek to trim start
    video.currentTime = trimStart;

    video.onseeked = () => {
      // Start recording
      recorder.start(100);

      // Start playing
      video.play();

      // Draw frames to canvas
      const drawFrame = () => {
        if (video.currentTime >= trimEnd || video.paused || video.ended) {
          video.pause();
          video.onseeked = null;

          // Give a small delay before stopping to ensure last frame is captured
          setTimeout(() => {
            if (recorder.state === 'recording') {
              recorder.stop();
            }
          }, 100);
          return;
        }

        ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
        requestAnimationFrame(drawFrame);
      };

      drawFrame();
    };
  }

  // ==================== Cleanup ====================

  reset(): void {
    this._recordedBlob = null;
    this.recordedChunks = [];
    this._duration = 0;
    this._isRecording = false;
  }

  dispose(): void {
    this.stopRecording();
    this.stopAudioMonitoring();
    this.reset();

    if (this.currentStream) {
      this.currentStream.getTracks().forEach(track => track.stop());
      this.currentStream = null;
    }
  }
}
