import { ScreenShareQualityPreset, ScreenShareStartOptions } from '../../screen-share.config';
import { WebRTCLogger } from '../../logging/webrtc-logger';
import {
  LinuxScreenShareAudioRoutingInfo,
  LinuxScreenShareMonitorAudioChunkPayload,
  LinuxScreenShareMonitorAudioEndedPayload,
  LinuxScreenShareMonitorCaptureInfo,
  ScreenShareElectronApi
} from './shared';

interface LinuxScreenShareMonitorAudioPipeline {
  audioContext: AudioContext;
  audioTrack: MediaStreamTrack;
  bitsPerSample: number;
  captureId: string;
  channelCount: number;
  mediaDestination: MediaStreamAudioDestinationNode;
  nextStartTime: number;
  pendingBytes: Uint8Array;
  sampleRate: number;
  unsubscribeChunk: () => void;
  unsubscribeEnded: () => void;
}

interface LinuxElectronScreenShareCaptureDependencies {
  getElectronApi(): ScreenShareElectronApi | null;
  onCaptureEnded(): void;
  startDisplayMedia(options: ScreenShareStartOptions, preset: ScreenShareQualityPreset): Promise<MediaStream>;
}

export class LinuxElectronScreenShareCapture {
  private audioRoutingActive = false;
  private audioRoutingResetPromise: Promise<void> | null = null;
  private monitorAudioPipeline: LinuxScreenShareMonitorAudioPipeline | null = null;

  constructor(
    private readonly logger: WebRTCLogger,
    private readonly dependencies: LinuxElectronScreenShareCaptureDependencies
  ) {}

  isSupported(): boolean {
    if (typeof window === 'undefined' || typeof navigator === 'undefined') {
      return false;
    }

    const electronApi = this.dependencies.getElectronApi();
    const platformHint = `${navigator.userAgent} ${navigator.platform}`;

    return !!electronApi?.prepareLinuxScreenShareAudioRouting
      && !!electronApi?.activateLinuxScreenShareAudioRouting
      && !!electronApi?.deactivateLinuxScreenShareAudioRouting
      && !!electronApi?.startLinuxScreenShareMonitorCapture
      && !!electronApi?.stopLinuxScreenShareMonitorCapture
      && !!electronApi?.onLinuxScreenShareMonitorAudioChunk
      && !!electronApi?.onLinuxScreenShareMonitorAudioEnded
      && /linux/i.test(platformHint);
  }

  async awaitPendingReset(): Promise<void> {
    if (!this.audioRoutingResetPromise) {
      return;
    }

    await this.audioRoutingResetPromise;
  }

  scheduleReset(): void {
    if (!this.audioRoutingActive || this.audioRoutingResetPromise) {
      return;
    }

    this.audioRoutingResetPromise = this.resetAudioRouting()
      .catch((error) => {
        this.logger.warn('Failed to reset Linux Electron audio routing', error);
      })
      .finally(() => {
        this.audioRoutingResetPromise = null;
      });
  }

  async startCapture(
    options: ScreenShareStartOptions,
    preset: ScreenShareQualityPreset
  ): Promise<MediaStream> {
    const electronApi = this.getRequiredElectronApi();
    const routingInfo = await electronApi.prepareLinuxScreenShareAudioRouting();

    this.assertAudioRoutingReady(routingInfo, 'Linux Electron audio routing is unavailable.');

    let desktopStream: MediaStream | null = null;

    try {
      const activation = await electronApi.activateLinuxScreenShareAudioRouting();

      this.assertAudioRoutingReady(activation, 'Failed to activate Linux Electron audio routing.');

      if (!activation.active) {
        throw new Error(activation.reason || 'Failed to activate Linux Electron audio routing.');
      }

      desktopStream = await this.dependencies.startDisplayMedia({
        ...options,
        includeSystemAudio: false
      }, preset);

      const { audioTrack, captureInfo } = await this.startMonitorTrack();
      const stream = new MediaStream([...desktopStream.getVideoTracks(), audioTrack]);

      desktopStream.getAudioTracks().forEach((track) => track.stop());

      this.audioRoutingActive = true;
      this.logger.info('Linux Electron screen-share audio routing enabled', {
        screenShareMonitorSourceName: captureInfo.sourceName,
        voiceSinkName: activation.voiceSinkName
      });

      return stream;
    } catch (error) {
      desktopStream?.getTracks().forEach((track) => track.stop());
      await this.resetAudioRouting();
      throw error;
    }
  }

  private getRequiredElectronApi(): Required<Pick<
    ScreenShareElectronApi,
    | 'prepareLinuxScreenShareAudioRouting'
    | 'activateLinuxScreenShareAudioRouting'
    | 'deactivateLinuxScreenShareAudioRouting'
    | 'startLinuxScreenShareMonitorCapture'
    | 'stopLinuxScreenShareMonitorCapture'
    | 'onLinuxScreenShareMonitorAudioChunk'
    | 'onLinuxScreenShareMonitorAudioEnded'
  >> {
    const electronApi = this.dependencies.getElectronApi();

    if (!electronApi?.prepareLinuxScreenShareAudioRouting
      || !electronApi.activateLinuxScreenShareAudioRouting
      || !electronApi.deactivateLinuxScreenShareAudioRouting
      || !electronApi.startLinuxScreenShareMonitorCapture
      || !electronApi.stopLinuxScreenShareMonitorCapture
      || !electronApi.onLinuxScreenShareMonitorAudioChunk
      || !electronApi.onLinuxScreenShareMonitorAudioEnded) {
      throw new Error('Linux Electron audio routing is unavailable.');
    }

    return {
      prepareLinuxScreenShareAudioRouting: electronApi.prepareLinuxScreenShareAudioRouting,
      activateLinuxScreenShareAudioRouting: electronApi.activateLinuxScreenShareAudioRouting,
      deactivateLinuxScreenShareAudioRouting: electronApi.deactivateLinuxScreenShareAudioRouting,
      startLinuxScreenShareMonitorCapture: electronApi.startLinuxScreenShareMonitorCapture,
      stopLinuxScreenShareMonitorCapture: electronApi.stopLinuxScreenShareMonitorCapture,
      onLinuxScreenShareMonitorAudioChunk: electronApi.onLinuxScreenShareMonitorAudioChunk,
      onLinuxScreenShareMonitorAudioEnded: electronApi.onLinuxScreenShareMonitorAudioEnded
    };
  }

  private assertAudioRoutingReady(
    routingInfo: LinuxScreenShareAudioRoutingInfo,
    unavailableReason: string
  ): void {
    if (!routingInfo.available) {
      throw new Error(routingInfo.reason || unavailableReason);
    }

    if (!routingInfo.monitorCaptureSupported) {
      throw new Error('Linux screen-share monitor capture requires restarting the desktop app so the new Electron main process can load.');
    }
  }

  private async resetAudioRouting(): Promise<void> {
    const electronApi = this.dependencies.getElectronApi();
    const captureId = this.monitorAudioPipeline?.captureId;

    this.audioRoutingActive = false;

    this.disposeMonitorAudioPipeline();

    try {
      if (captureId && electronApi?.stopLinuxScreenShareMonitorCapture) {
        await electronApi.stopLinuxScreenShareMonitorCapture(captureId);
      }
    } catch (error) {
      this.logger.warn('Failed to stop Linux screen-share monitor capture', error);
    }

    try {
      if (electronApi?.deactivateLinuxScreenShareAudioRouting) {
        await electronApi.deactivateLinuxScreenShareAudioRouting();
      }
    } catch (error) {
      this.logger.warn('Failed to deactivate Linux Electron audio routing', error);
    }
  }

  private async startMonitorTrack(): Promise<{
    audioTrack: MediaStreamTrack;
    captureInfo: LinuxScreenShareMonitorCaptureInfo;
  }> {
    const electronApi = this.dependencies.getElectronApi();

    if (!electronApi?.startLinuxScreenShareMonitorCapture
      || !electronApi?.stopLinuxScreenShareMonitorCapture
      || !electronApi?.onLinuxScreenShareMonitorAudioChunk
      || !electronApi?.onLinuxScreenShareMonitorAudioEnded) {
      throw new Error('Linux screen-share monitor capture is unavailable.');
    }

    const queuedChunksByCaptureId = new Map<string, Uint8Array[]>();
    const queuedEndedReasons = new Map<string, string | undefined>();

    let pipeline: LinuxScreenShareMonitorAudioPipeline | null = null;
    let captureInfo: LinuxScreenShareMonitorCaptureInfo | null = null;

    const queueChunk = (captureId: string, chunk: Uint8Array): void => {
      const queuedChunks = queuedChunksByCaptureId.get(captureId) || [];

      queuedChunks.push(this.copyBytes(chunk));
      queuedChunksByCaptureId.set(captureId, queuedChunks);
    };
    const onChunk = (payload: LinuxScreenShareMonitorAudioChunkPayload): void => {
      if (!pipeline || payload.captureId !== pipeline.captureId) {
        queueChunk(payload.captureId, payload.chunk);
        return;
      }

      this.handleMonitorAudioChunk(pipeline, payload.chunk);
    };
    const onEnded = (payload: LinuxScreenShareMonitorAudioEndedPayload): void => {
      if (!pipeline || payload.captureId !== pipeline.captureId) {
        queuedEndedReasons.set(payload.captureId, payload.reason);
        return;
      }

      this.logger.warn('Linux screen-share monitor capture ended', payload);
      this.dependencies.onCaptureEnded();
    };
    const unsubscribeChunk = electronApi.onLinuxScreenShareMonitorAudioChunk(onChunk) as () => void;
    const unsubscribeEnded = electronApi.onLinuxScreenShareMonitorAudioEnded(onEnded) as () => void;

    try {
      captureInfo = await electronApi.startLinuxScreenShareMonitorCapture() as LinuxScreenShareMonitorCaptureInfo;

      const audioContext = new AudioContext({ sampleRate: captureInfo.sampleRate });
      const mediaDestination = audioContext.createMediaStreamDestination();

      await audioContext.resume();

      const audioTrack = mediaDestination.stream.getAudioTracks()[0];

      if (!audioTrack) {
        throw new Error('Renderer audio pipeline did not produce a screen-share monitor track.');
      }

      pipeline = {
        audioContext,
        audioTrack,
        bitsPerSample: captureInfo.bitsPerSample,
        captureId: captureInfo.captureId,
        channelCount: captureInfo.channelCount,
        mediaDestination,
        nextStartTime: audioContext.currentTime + 0.05,
        pendingBytes: new Uint8Array(0),
        sampleRate: captureInfo.sampleRate,
        unsubscribeChunk,
        unsubscribeEnded
      };

      this.monitorAudioPipeline = pipeline;
      const activeCaptureId = captureInfo.captureId;

      audioTrack.addEventListener('ended', () => {
        if (this.monitorAudioPipeline?.captureId === activeCaptureId) {
          this.dependencies.onCaptureEnded();
        }
      }, { once: true });

      const queuedChunks = queuedChunksByCaptureId.get(captureInfo.captureId) || [];
      const activePipeline = pipeline;

      queuedChunks.forEach((chunk) => {
        this.handleMonitorAudioChunk(activePipeline, chunk);
      });

      queuedChunksByCaptureId.delete(captureInfo.captureId);

      if (queuedEndedReasons.has(captureInfo.captureId)) {
        throw new Error(queuedEndedReasons.get(captureInfo.captureId)
          || 'Linux screen-share monitor capture ended before audio initialisation completed.');
      }

      return {
        audioTrack,
        captureInfo
      };
    } catch (error) {
      if (pipeline) {
        this.disposeMonitorAudioPipeline(pipeline.captureId);
      } else {
        unsubscribeChunk();
        unsubscribeEnded();
      }

      try {
        await electronApi.stopLinuxScreenShareMonitorCapture(captureInfo?.captureId);
      } catch (stopError) {
        this.logger.warn('Failed to stop Linux screen-share monitor capture after startup failure', stopError);
      }

      throw error;
    }
  }

  private disposeMonitorAudioPipeline(captureId?: string): void {
    if (!this.monitorAudioPipeline) {
      return;
    }

    if (captureId && captureId !== this.monitorAudioPipeline.captureId) {
      return;
    }

    const pipeline = this.monitorAudioPipeline;

    this.monitorAudioPipeline = null;
    pipeline.unsubscribeChunk();
    pipeline.unsubscribeEnded();
    pipeline.audioTrack.stop();
    pipeline.pendingBytes = new Uint8Array(0);

    void pipeline.audioContext.close().catch((error) => {
      this.logger.warn('Failed to close Linux screen-share monitor audio context', error);
    });
  }

  private handleMonitorAudioChunk(
    pipeline: LinuxScreenShareMonitorAudioPipeline,
    chunk: Uint8Array
  ): void {
    if (pipeline.bitsPerSample !== 16) {
      this.logger.warn('Unsupported Linux screen-share monitor capture sample size', {
        bitsPerSample: pipeline.bitsPerSample,
        captureId: pipeline.captureId
      });

      return;
    }

    const bytesPerSample = pipeline.bitsPerSample / 8;
    const bytesPerFrame = bytesPerSample * pipeline.channelCount;

    if (!Number.isFinite(bytesPerFrame) || bytesPerFrame <= 0) {
      return;
    }

    const combinedBytes = this.concatBytes(pipeline.pendingBytes, chunk);
    const completeByteLength = combinedBytes.byteLength - (combinedBytes.byteLength % bytesPerFrame);

    if (completeByteLength <= 0) {
      pipeline.pendingBytes = combinedBytes;
      return;
    }

    const completeBytes = combinedBytes.subarray(0, completeByteLength);

    pipeline.pendingBytes = this.copyBytes(combinedBytes.subarray(completeByteLength));

    if (pipeline.audioContext.state !== 'running') {
      void pipeline.audioContext.resume().catch((error) => {
        this.logger.warn('Failed to resume Linux screen-share monitor audio context', error);
      });
    }

    const frameCount = completeByteLength / bytesPerFrame;
    const audioBuffer = this.createAudioBuffer(pipeline, completeBytes, frameCount);
    const source = pipeline.audioContext.createBufferSource();

    source.buffer = audioBuffer;
    source.connect(pipeline.mediaDestination);

    source.onended = () => {
      source.disconnect();
    };

    const now = pipeline.audioContext.currentTime;
    const startTime = Math.max(pipeline.nextStartTime, now + 0.02);

    source.start(startTime);
    pipeline.nextStartTime = startTime + audioBuffer.duration;
  }

  private createAudioBuffer(
    pipeline: LinuxScreenShareMonitorAudioPipeline,
    bytes: Uint8Array,
    frameCount: number
  ): AudioBuffer {
    const audioBuffer = pipeline.audioContext.createBuffer(pipeline.channelCount, frameCount, pipeline.sampleRate);
    const sampleData = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
    const channelData = Array.from(
      { length: pipeline.channelCount },
      (_, channelIndex) => audioBuffer.getChannelData(channelIndex)
    );
    const bytesPerSample = pipeline.bitsPerSample / 8;
    const bytesPerFrame = bytesPerSample * pipeline.channelCount;

    for (let frameIndex = 0; frameIndex < frameCount; frameIndex += 1) {
      const frameOffset = frameIndex * bytesPerFrame;

      for (let channelIndex = 0; channelIndex < pipeline.channelCount; channelIndex += 1) {
        const sampleOffset = frameOffset + (channelIndex * bytesPerSample);

        channelData[channelIndex][frameIndex] = sampleData.getInt16(sampleOffset, true) / 32768;
      }
    }

    return audioBuffer;
  }

  private concatBytes(first: Uint8Array, second: Uint8Array): Uint8Array {
    if (first.byteLength === 0) {
      return this.copyBytes(second);
    }

    if (second.byteLength === 0) {
      return this.copyBytes(first);
    }

    const combined = new Uint8Array(first.byteLength + second.byteLength);

    combined.set(first, 0);
    combined.set(second, first.byteLength);

    return combined;
  }

  private copyBytes(bytes: Uint8Array): Uint8Array {
    return bytes.byteLength > 0 ? new Uint8Array(bytes) : new Uint8Array(0);
  }
}
