/* eslint-disable, @typescript-eslint/no-non-null-assertion, @typescript-eslint/member-ordering, id-denylist */
/**
 * Manages screen sharing: getDisplayMedia / Electron desktop capturer,
 * system-audio capture, and attaching screen tracks to peers.
 */
import { WebRTCLogger } from '../logging/webrtc-logger';
import { ScreenShareQualityPreset, ScreenShareStartOptions } from '../screen-share.config';
import { PeerData } from '../realtime.types';
import {
  TRACK_KIND_AUDIO,
  TRACK_KIND_VIDEO,
  TRANSCEIVER_SEND_RECV,
  TRANSCEIVER_RECV_ONLY
} from '../realtime.constants';
import { DEFAULT_SCREEN_SHARE_START_OPTIONS, SCREEN_SHARE_QUALITY_PRESETS } from '../screen-share.config';
import { BrowserScreenShareCapture } from './screen-share-platforms/browser-screen-share.capture';
import { DesktopElectronScreenShareCapture } from './screen-share-platforms/desktop-electron-screen-share.capture';
import { LinuxElectronScreenShareCapture } from './screen-share-platforms/linux-electron-screen-share.capture';
import { getElectronApi } from '../../../core/platform/electron/get-electron-api';
import { ScreenShareElectronApi } from './screen-share-platforms/shared';

/**
 * Callbacks the ScreenShareManager needs from the owning service.
 */
export interface ScreenShareCallbacks {
  getActivePeers(): Map<string, PeerData>;
  getLocalMediaStream(): MediaStream | null;
  renegotiate(peerId: string): Promise<void>;
  broadcastCurrentStates(): void;
  selectDesktopSource?(
    sources: readonly { id: string; name: string; thumbnail: string }[],
    options: { includeSystemAudio: boolean }
  ): Promise<{
    includeSystemAudio: boolean;
    source: { id: string; name: string; thumbnail: string };
  }>;
  updateLocalScreenShareState?(state: LocalScreenShareState): void;
}

type ScreenShareCaptureMethod = 'display-media' | 'electron-desktop' | 'linux-electron';

export interface LocalScreenShareState {
  active: boolean;
  captureMethod: ScreenShareCaptureMethod | null;
  includeSystemAudio: boolean;
  stream: MediaStream | null;
  suppressRemotePlayback: boolean;
  forceDefaultRemotePlaybackOutput: boolean;
}

export class ScreenShareManager {
  /** The active screen-capture stream. */
  private activeScreenStream: MediaStream | null = null;

  /** Optional system-audio stream captured alongside the screen. */
  private screenAudioStream: MediaStream | null = null;

  /** The quality preset currently applied to the active share. */
  private activeScreenPreset: ScreenShareQualityPreset | null = null;

  /** Remote peers that explicitly requested screen-share video. */
  private readonly requestedViewerPeerIds = new Set<string>();

  /** Browser `getDisplayMedia` capture path. */
  private readonly browserScreenShareCapture: BrowserScreenShareCapture;

  /** Desktop Electron capture path for non-Linux desktop builds. */
  private readonly desktopElectronScreenShareCapture: DesktopElectronScreenShareCapture;

  /** Linux Electron screen/audio capture path with isolated audio routing. */
  private readonly linuxElectronScreenShareCapture: LinuxElectronScreenShareCapture;

  /** Whether screen sharing is currently active. */
  private isScreenActive = false;

  constructor(
    private readonly logger: WebRTCLogger,
    private callbacks: ScreenShareCallbacks
  ) {
    this.browserScreenShareCapture = new BrowserScreenShareCapture(this.logger);
    this.desktopElectronScreenShareCapture = new DesktopElectronScreenShareCapture(this.logger, {
      getElectronApi: () => this.getElectronApi(),
      getSelectDesktopSource: () => this.callbacks.selectDesktopSource
    });

    this.linuxElectronScreenShareCapture = new LinuxElectronScreenShareCapture(this.logger, {
      getElectronApi: () => this.getElectronApi(),
      onCaptureEnded: () => {
        if (this.isScreenActive) {
          this.stopScreenShare();
        }
      },
      startDisplayMedia: async (options, preset) =>
        await this.browserScreenShareCapture.startCapture(options, preset)
    });
  }

  /**
   * Replace the callback set at runtime.
   * Needed because of circular initialisation between managers.
   *
   * @param nextCallbacks - The new callback interface to wire into this manager.
   */
  setCallbacks(nextCallbacks: ScreenShareCallbacks): void {
    this.callbacks = nextCallbacks;
  }

  /** Returns the current screen-capture stream, or `null` if inactive. */
  getScreenStream(): MediaStream | null { return this.activeScreenStream; }
  /** Whether screen sharing is currently active. */
  getIsScreenActive(): boolean { return this.isScreenActive; }

  /**
   * Begin screen sharing.
   *
   * On Linux Electron builds, prefers a dedicated PulseAudio/PipeWire routing
   * path so remote voice playback is kept out of captured system audio.
   * On Windows Electron builds, prefers `getDisplayMedia` with system audio
   * so the separate mic `getUserMedia` stream is not disrupted; falls back to
   * Electron desktop capture only when `getDisplayMedia` fails entirely.
   * In browser contexts, uses `getDisplayMedia`.
   *
   * @param options - Screen-share capture options.
   * @returns The captured screen {@link MediaStream}.
   * @throws If both Electron and browser screen capture fail.
   */
  async startScreenShare(options: ScreenShareStartOptions = DEFAULT_SCREEN_SHARE_START_OPTIONS): Promise<MediaStream> {
    const shareOptions = {
      ...DEFAULT_SCREEN_SHARE_START_OPTIONS,
      ...options
    };
    const preset = SCREEN_SHARE_QUALITY_PRESETS[shareOptions.quality];
    const electronDesktopCaptureAvailable = this.desktopElectronScreenShareCapture.isAvailable();

    let captureMethod: ScreenShareCaptureMethod | null = null;

    try {
      this.logger.info('startScreenShare invoked', shareOptions);

      if (this.activeScreenStream) {
        this.stopScreenShare();
      }

      await this.linuxElectronScreenShareCapture.awaitPendingReset();

      this.activeScreenStream = null;

      if (shareOptions.includeSystemAudio && this.linuxElectronScreenShareCapture.isSupported()) {
        try {
          this.activeScreenStream = await this.linuxElectronScreenShareCapture.startCapture(shareOptions, preset);
          captureMethod = 'linux-electron';
        } catch (error) {
          this.rethrowIfScreenShareAborted(error);
          this.logger.warn('Linux Electron audio routing failed; falling back to standard capture', error);
        }
      }

      if (!this.activeScreenStream && shareOptions.includeSystemAudio) {
        try {
          this.activeScreenStream = await this.browserScreenShareCapture.startCapture(shareOptions, preset);
          captureMethod = 'display-media';

          if (this.activeScreenStream.getAudioTracks().length === 0) {
            if (electronDesktopCaptureAvailable) {
              // On Windows Electron, keep the getDisplayMedia stream for video
              // rather than falling through to getUserMedia desktop audio which
              // can replace or kill the active mic stream.
              this.logger.warn(
                'getDisplayMedia did not provide system audio; '
                + 'continuing without system audio to preserve mic stream'
              );

              shareOptions.includeSystemAudio = false;
            } else {
              this.logger.warn('getDisplayMedia did not provide system audio; trying next capture method');
              this.activeScreenStream.getTracks().forEach((track) => track.stop());
              this.activeScreenStream = null;
              captureMethod = null;
            }
          }
        } catch (error) {
          this.rethrowIfScreenShareAborted(error);
          this.logger.warn('getDisplayMedia with system audio failed; falling back to Electron desktop capture', error);
        }
      }

      if (!this.activeScreenStream && electronDesktopCaptureAvailable) {
        try {
          const electronCapture = await this.desktopElectronScreenShareCapture.startCapture(shareOptions, preset);

          this.activeScreenStream = electronCapture.stream;
          shareOptions.includeSystemAudio = electronCapture.includeSystemAudio;
          captureMethod = 'electron-desktop';
        } catch (error) {
          this.rethrowIfScreenShareAborted(error);
          this.logger.warn('Electron desktop capture failed; falling back to getDisplayMedia', error);
        }
      }

      if (!this.activeScreenStream) {
        this.activeScreenStream = await this.browserScreenShareCapture.startCapture(shareOptions, preset);
        captureMethod = 'display-media';
      }

      this.configureScreenStream(preset);
      this.prepareScreenAudio(shareOptions.includeSystemAudio);
      this.activeScreenPreset = preset;
      this.attachScreenTracksToPeers(preset);

      this.isScreenActive = true;
      this.publishLocalScreenShareState(shareOptions.includeSystemAudio, captureMethod);
      this.callbacks.broadcastCurrentStates();

      const activeScreenStream = this.activeScreenStream;

      if (!activeScreenStream) {
        throw new Error('Screen sharing did not produce an active stream.');
      }

      const screenVideoTrack = activeScreenStream.getVideoTracks()[0];

      if (screenVideoTrack) {
        screenVideoTrack.onended = () => {
          this.logger.warn('Screen video track ended');
          this.stopScreenShare();
        };
      }

      return activeScreenStream;
    } catch (error) {
      this.logger.error('Failed to start screen share', error);
      throw error;
    }
  }

  /**
   * Stop screen sharing and remove screen-share tracks on all peers.
   *
   * Stops all screen-capture tracks, resets screen transceivers to receive-only,
   * and triggers renegotiation.
   */
  stopScreenShare(): void {
    if (this.activeScreenStream) {
      this.activeScreenStream.getTracks().forEach((track) => track.stop());
      this.activeScreenStream = null;
    }

    this.linuxElectronScreenShareCapture.scheduleReset();

    this.screenAudioStream = null;
    this.activeScreenPreset = null;
    this.isScreenActive = false;
    this.publishLocalScreenShareState(false, null);
    this.callbacks.broadcastCurrentStates();

    this.callbacks.getActivePeers().forEach((peerData, peerId) => {
      this.detachScreenTracksFromPeer(peerData, peerId);
    });
  }

  requestScreenShareForPeer(peerId: string): void {
    this.requestedViewerPeerIds.add(peerId);

    if (!this.isScreenActive || !this.activeScreenPreset) {
      return;
    }

    const peerData = this.callbacks.getActivePeers().get(peerId);

    if (!peerData) {
      return;
    }

    this.attachScreenTracksToPeer(peerData, peerId, this.activeScreenPreset);
  }

  stopScreenShareForPeer(peerId: string): void {
    this.requestedViewerPeerIds.delete(peerId);

    const peerData = this.callbacks.getActivePeers().get(peerId);

    if (!peerData) {
      return;
    }

    this.detachScreenTracksFromPeer(peerData, peerId);
  }

  clearScreenShareRequest(peerId: string): void {
    this.requestedViewerPeerIds.delete(peerId);
  }

  /**
   * Attach the current screen-share tracks to a newly-connected peer.
   *
   * This is needed when a peer connects after screen sharing already started,
   * because `startScreenShare()` only pushes tracks to peers that existed at
   * the time sharing began.
   */
  syncScreenShareToPeer(peerId: string): void {
    if (
      !this.requestedViewerPeerIds.has(peerId)
      || !this.isScreenActive
      || !this.activeScreenStream
      || !this.activeScreenPreset
    ) {
      return;
    }

    const peerData = this.callbacks.getActivePeers().get(peerId);

    if (!peerData) {
      return;
    }

    this.attachScreenTracksToPeer(peerData, peerId, this.activeScreenPreset);
  }

  /** Clean up all resources. */
  destroy(): void {
    this.stopScreenShare();
  }

  private getElectronApi(): ScreenShareElectronApi | null {
    return getElectronApi();
  }

  private publishLocalScreenShareState(
    includeSystemAudio: boolean,
    captureMethod: ScreenShareCaptureMethod | null
  ): void {
    this.callbacks.updateLocalScreenShareState?.({
      active: this.isScreenActive,
      captureMethod: this.isScreenActive ? captureMethod : null,
      includeSystemAudio: this.isScreenActive ? includeSystemAudio : false,
      stream: this.isScreenActive ? this.activeScreenStream : null,
      suppressRemotePlayback: this.isScreenActive
        && this.desktopElectronScreenShareCapture.shouldSuppressRemotePlaybackDuringShare(
          includeSystemAudio,
          captureMethod === 'electron-desktop'
        ),
      forceDefaultRemotePlaybackOutput: this.isScreenActive
        && includeSystemAudio
        && captureMethod === 'linux-electron'
    });
  }

  /**
   * Create a dedicated stream for system audio captured alongside the screen.
   *
   * @param includeSystemAudio - Whether system audio should be sent.
   */
  private prepareScreenAudio(includeSystemAudio: boolean): void {
    const screenAudioTrack = includeSystemAudio ? (this.activeScreenStream?.getAudioTracks()[0] || null) : null;

    if (!screenAudioTrack) {
      if (includeSystemAudio) {
        this.logger.warn('System audio was requested, but no screen audio track was captured');
      }

      this.screenAudioStream = null;
      return;
    }

    this.screenAudioStream = new MediaStream([screenAudioTrack]);
    this.logger.attachTrackDiagnostics(screenAudioTrack, 'screenAudio');
    this.logger.logStream('screenAudio', this.screenAudioStream);
  }

  /**
   * Attach screen video and optional system-audio tracks to all
   * active peer connections, then trigger SDP renegotiation.
   *
   * @param options - Screen-share capture options.
   * @param preset - Selected quality preset for sender tuning.
   */
  private attachScreenTracksToPeers(
    preset: ScreenShareQualityPreset
  ): void {
    this.callbacks.getActivePeers().forEach((peerData, peerId) => {
      if (!this.requestedViewerPeerIds.has(peerId)) {
        return;
      }

      this.attachScreenTracksToPeer(peerData, peerId, preset);
    });
  }

  private attachScreenTracksToPeer(
    peerData: PeerData,
    peerId: string,
    preset: ScreenShareQualityPreset
  ): void {
    if (!this.activeScreenStream) {
      return;
    }

    const screenVideoTrack = this.activeScreenStream.getVideoTracks()[0];

    if (!screenVideoTrack) {
      return;
    }

    this.logger.attachTrackDiagnostics(screenVideoTrack, `screenVideo:${peerId}`);

    let screenVideoSender = peerData.screenVideoSender;

    if (!screenVideoSender) {
      const videoTransceiver = peerData.connection.addTransceiver(TRACK_KIND_VIDEO, {
        direction: TRANSCEIVER_SEND_RECV
      });

      screenVideoSender = videoTransceiver.sender;
    } else {
      const videoTransceiver = peerData.connection.getTransceivers().find(
        (transceiver) => transceiver.sender === screenVideoSender
      );

      if (videoTransceiver?.direction === TRANSCEIVER_RECV_ONLY) {
        videoTransceiver.direction = TRANSCEIVER_SEND_RECV;
      }
    }

    peerData.screenVideoSender = screenVideoSender;

    if (typeof screenVideoSender.setStreams === 'function') {
      screenVideoSender.setStreams(this.activeScreenStream);
    }

    screenVideoSender.replaceTrack(screenVideoTrack)
      .then(() => {
        this.logger.info('screen video replaceTrack ok', { peerId });
        void this.applyScreenShareVideoParameters(screenVideoSender, preset, peerId);
      })
      .catch((error) => this.logger.error('screen video replaceTrack failed', error));

    const screenAudioTrack = this.screenAudioStream?.getAudioTracks()[0] || null;

    if (screenAudioTrack) {
      this.logger.attachTrackDiagnostics(screenAudioTrack, `screenAudio:${peerId}`);
      let screenAudioSender = peerData.screenAudioSender;

      if (!screenAudioSender) {
        const screenAudioTransceiver = peerData.connection.addTransceiver(TRACK_KIND_AUDIO, {
          direction: TRANSCEIVER_SEND_RECV
        });

        screenAudioSender = screenAudioTransceiver.sender;
      } else {
        const screenAudioTransceiver = peerData.connection.getTransceivers().find(
          (transceiver) => transceiver.sender === screenAudioSender
        );

        if (screenAudioTransceiver?.direction === TRANSCEIVER_RECV_ONLY) {
          screenAudioTransceiver.direction = TRANSCEIVER_SEND_RECV;
        }
      }

      peerData.screenAudioSender = screenAudioSender;

      if (typeof screenAudioSender.setStreams === 'function') {
        screenAudioSender.setStreams(this.activeScreenStream);
      }

      screenAudioSender.replaceTrack(screenAudioTrack)
        .then(() => this.logger.info('screen audio replaceTrack ok', { peerId }))
        .catch((error) => this.logger.error('screen audio replaceTrack failed', error));
    }

    this.callbacks.renegotiate(peerId);
  }

  private detachScreenTracksFromPeer(peerData: PeerData, peerId: string): void {
    const transceivers = peerData.connection.getTransceivers();
    const videoTransceiver = transceivers.find(
      (transceiver) => transceiver.sender === peerData.screenVideoSender
    );
    const screenAudioTransceiver = transceivers.find(
      (transceiver) => transceiver.sender === peerData.screenAudioSender
    );

    if (videoTransceiver) {
      videoTransceiver.sender.replaceTrack(null).catch((error) => {
        this.logger.error('Failed to clear screen video sender track', error, { peerId });
      });

      if (videoTransceiver.direction === TRANSCEIVER_SEND_RECV) {
        videoTransceiver.direction = TRANSCEIVER_RECV_ONLY;
      }
    }

    if (screenAudioTransceiver) {
      screenAudioTransceiver.sender.replaceTrack(null).catch((error) => {
        this.logger.error('Failed to clear screen audio sender track', error, { peerId });
      });

      if (screenAudioTransceiver.direction === TRANSCEIVER_SEND_RECV) {
        screenAudioTransceiver.direction = TRANSCEIVER_RECV_ONLY;
      }
    }

    peerData.screenVideoSender = undefined;
    peerData.screenAudioSender = undefined;

    this.callbacks.renegotiate(peerId);
  }

  private isScreenShareSelectionAborted(error: unknown): boolean {
    return error instanceof Error
      && (error.name === 'AbortError' || error.name === 'NotAllowedError');
  }

  private rethrowIfScreenShareAborted(error: unknown): void {
    if (this.isScreenShareSelectionAborted(error)) {
      throw error;
    }
  }

  private configureScreenStream(preset: ScreenShareQualityPreset): void {
    const screenVideoTrack = this.activeScreenStream?.getVideoTracks()[0];

    if (!screenVideoTrack) {
      throw new Error('Screen capture returned no video track.');
    }

    if ('contentHint' in screenVideoTrack) {
      screenVideoTrack.contentHint = preset.contentHint;
    }

    this.logger.attachTrackDiagnostics(screenVideoTrack, 'screenVideo');
    this.logger.logStream('screen', this.activeScreenStream);

    if (typeof screenVideoTrack.applyConstraints === 'function') {
      screenVideoTrack.applyConstraints({
        width: { ideal: preset.width, max: preset.width },
        height: { ideal: preset.height, max: preset.height },
        frameRate: { ideal: preset.frameRate, max: preset.frameRate }
      }).catch((error) => {
        this.logger.warn('Failed to re-apply screen video constraints', error);
      });
    }
  }

  private async applyScreenShareVideoParameters(
    sender: RTCRtpSender,
    preset: ScreenShareQualityPreset,
    peerId: string
  ): Promise<void> {
    try {
      const params = sender.getParameters();
      const encodings = params.encodings?.length ? params.encodings : [{} as RTCRtpEncodingParameters];

      params.encodings = encodings.map((encoding, index) => index === 0
        ? {
          ...encoding,
          maxBitrate: preset.maxBitrateBps,
          maxFramerate: preset.frameRate,
          scaleResolutionDownBy: preset.scaleResolutionDownBy ?? encoding.scaleResolutionDownBy ?? 1
        }
        : encoding);

      (params as RTCRtpSendParameters & { degradationPreference?: string }).degradationPreference = preset.degradationPreference;

      await sender.setParameters(params);
      this.logger.info('Applied screen-share sender parameters', {
        peerId,
        maxBitrate: preset.maxBitrateBps,
        maxFramerate: preset.frameRate
      });
    } catch (error) {
      this.logger.warn('Failed to apply screen-share sender parameters', error, { peerId });
    }
  }
}
