/**
 * WebRTCService - thin Angular service that composes specialised managers.
 *
 * Each concern lives in its own file under `./`:
 *   - SignalingManager      - WebSocket lifecycle & reconnection
 *   - PeerConnectionManager - RTCPeerConnection, offers/answers, ICE, data channels
 *   - MediaManager          - mic voice, mute, deafen, bitrate
 *   - ScreenShareManager    - screen capture & mixed audio
 *   - WebRTCLogger          - debug / diagnostic logging
 *
 * This file wires them together and exposes a public API that is
 * identical to the old monolithic service so consumers don't change.
 */
/* eslint-disable @typescript-eslint/member-ordering, @typescript-eslint/no-non-null-assertion */
import {
  Injectable,
  inject,
  OnDestroy
} from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { ChatEvent } from '../../shared-kernel';
import type { SignalingMessage } from '../../shared-kernel';
import { TimeSyncService } from '../../core/services/time-sync.service';
import { DebuggingService } from '../../core/services/debugging';
import { ScreenShareSourcePickerService } from '../../domains/screen-share';
import { MediaManager } from './media/media.manager';
import { ScreenShareManager } from './media/screen-share.manager';
import { VoiceSessionController } from './media/voice-session-controller';
import { IceServerSettingsService } from './ice-server-settings.service';
import type { PeerData, VoiceStateSnapshot } from './realtime.types';
import { LatencyProfile } from './realtime.constants';
import { ScreenShareStartOptions } from './screen-share.config';
import { WebRTCLogger } from './logging/webrtc-logger';
import { PeerConnectionManager } from './peer-connection-manager/peer-connection.manager';
import { PeerMediaFacade } from './streams/peer-media-facade';
import { RemoteScreenShareRequestController } from './streams/remote-screen-share-request-controller';
import { IncomingSignalingMessage, IncomingSignalingMessageHandler } from './signaling/signaling-message-handler';
import { ServerMembershipSignalingHandler } from './signaling/server-membership-signaling-handler';
import { ServerSignalingCoordinator } from './signaling/server-signaling-coordinator';
import { SignalingManager } from './signaling/signaling.manager';
import { SignalingTransportHandler } from './signaling/signaling-transport-handler';
import { WebRtcStateController } from './state/webrtc-state-controller';
import { SignalServerAuthService } from '../../domains/authentication/application/services/signal-server-auth.service';
import { ClientInstanceService } from '../../core/platform/client-instance.service';
import { Store } from '@ngrx/store';
import { UsersActions } from '../../store/users/users.actions';
import { selectCurrentUser } from '../../store/users/users.selectors';

@Injectable({
  providedIn: 'root'
})
export class WebRTCService implements OnDestroy {
  private readonly timeSync = inject(TimeSyncService);
  private readonly debugging = inject(DebuggingService);
  private readonly screenShareSourcePicker = inject(ScreenShareSourcePickerService);
  private readonly iceServerSettings = inject(IceServerSettingsService);
  private readonly signalServerAuth = inject(SignalServerAuthService);
  private readonly store = inject(Store);
  private readonly clientInstance = inject(ClientInstanceService);
  private currentHomeUser: { id: string; homeSignalServerUrl?: string; displayName: string } | null = null;

  private readonly logger = new WebRTCLogger(() => this.debugging.enabled());
  private readonly state = new WebRtcStateController();

  readonly peerId = this.state.peerId;
  readonly isConnected = this.state.isConnected;
  readonly hasEverConnected = this.state.hasEverConnected;
  readonly isVoiceConnected = this.state.isVoiceConnected;
  readonly connectedPeers = this.state.connectedPeers;
  readonly isMuted = this.state.isMuted;
  readonly isDeafened = this.state.isDeafened;
  readonly isCameraEnabled = this.state.isCameraEnabled;
  readonly isScreenSharing = this.state.isScreenSharing;
  readonly isNoiseReductionEnabled = this.state.isNoiseReductionEnabled;
  readonly screenStream = this.state.screenStream;
  readonly isScreenShareRemotePlaybackSuppressed = this.state.isScreenShareRemotePlaybackSuppressed;
  readonly forceDefaultRemotePlaybackOutput = this.state.forceDefaultRemotePlaybackOutput;
  readonly hasConnectionError = this.state.hasConnectionError;
  readonly connectionErrorMessage = this.state.connectionErrorMessage;
  readonly shouldShowConnectionError = this.state.shouldShowConnectionError;
  readonly peerLatencies = this.state.peerLatencies;

  private readonly signalingMessage$ = new Subject<IncomingSignalingMessage>();
  readonly onSignalingMessage = this.signalingMessage$.asObservable();

  private readonly signalingReconnectedSubject$ = new Subject<string>();
  readonly signalingReconnected$ = this.signalingReconnectedSubject$.asObservable();

  // Delegates to managers
  get onMessageReceived(): Observable<ChatEvent> {
    return this.peerMediaFacade.onMessageReceived;
  }
  get onPeerConnected(): Observable<string> {
    return this.peerMediaFacade.onPeerConnected;
  }
  get onPeerDisconnected(): Observable<string> {
    return this.peerMediaFacade.onPeerDisconnected;
  }
  get onRemoteStream(): Observable<{ peerId: string; stream: MediaStream }> {
    return this.peerMediaFacade.onRemoteStream;
  }
  get onVoiceConnected(): Observable<void> {
    return this.peerMediaFacade.onVoiceConnected;
  }

  private readonly peerManager: PeerConnectionManager;
  private readonly mediaManager: MediaManager;
  private readonly screenShareManager: ScreenShareManager;
  private readonly peerMediaFacade: PeerMediaFacade;
  private readonly voiceSessionController: VoiceSessionController;
  private readonly signalingCoordinator: ServerSignalingCoordinator<IncomingSignalingMessage>;
  private readonly signalingTransportHandler: SignalingTransportHandler<IncomingSignalingMessage>;
  private readonly signalingMessageHandler: IncomingSignalingMessageHandler;
  private readonly serverMembershipSignalingHandler: ServerMembershipSignalingHandler<IncomingSignalingMessage>;
  private readonly remoteScreenShareRequestController: RemoteScreenShareRequestController;

  constructor() {
    this.store.select(selectCurrentUser).subscribe((user) => {
      this.currentHomeUser = user
        ? {
          id: user.id,
          homeSignalServerUrl: user.homeSignalServerUrl,
          displayName: user.displayName
        }
        : null;
    });

    // Create managers with null callbacks first to break circular initialization
    this.peerManager = new PeerConnectionManager(this.logger, null!);

    this.mediaManager = new MediaManager(this.logger, null!);

    this.screenShareManager = new ScreenShareManager(this.logger, null!);

    this.peerMediaFacade = new PeerMediaFacade({
      peerManager: this.peerManager,
      mediaManager: this.mediaManager,
      screenShareManager: this.screenShareManager
    });

    this.voiceSessionController = new VoiceSessionController({
      mediaManager: this.mediaManager,
      getIsScreenSharing: () => this.state.isScreenSharingActive(),
      setVoiceConnected: (connected) => this.state.setVoiceConnected(connected),
      setMuted: (muted) => this.state.setMuted(muted),
      setDeafened: (deafened) => this.state.setDeafened(deafened),
      setNoiseReductionEnabled: (enabled) => this.state.setNoiseReductionEnabled(enabled)
    });

    this.signalingCoordinator = new ServerSignalingCoordinator({
      createManager: (signalUrl, getLastJoinedServer, getMemberServerIds) => new SignalingManager(
        this.logger,
        () => this.signalingTransportHandler.getIdentifyCredentialsForSignalUrl(signalUrl),
        getLastJoinedServer,
        getMemberServerIds
      ),
      handleConnectionStatus: (signalUrl, connected, errorMessage) =>
        this.handleSignalingConnectionStatus(signalUrl, connected, errorMessage),
      handleHeartbeatTick: () => this.peerMediaFacade.broadcastCurrentStates(),
      handleMessage: (message, signalUrl) => this.handleSignalingMessage(message, signalUrl)
    });

    this.signalingTransportHandler = new SignalingTransportHandler({
      signalingCoordinator: this.signalingCoordinator,
      logger: this.logger,
      getLocalPeerId: () => this.state.getLocalPeerId(),
      resolveCredential: (signalUrl) => {
        if (!signalUrl) {
          return null;
        }

        return this.signalServerAuth.resolveCredentialForSignalUrl(signalUrl, this.currentHomeUser);
      },
      getHomeCredential: () => {
        const homeSignalServerUrl = this.currentHomeUser?.homeSignalServerUrl;

        if (!homeSignalServerUrl) {
          return null;
        }

        return this.signalServerAuth.resolveCredentialForSignalUrl(homeSignalServerUrl, this.currentHomeUser);
      },
      getClientInstanceId: () => this.clientInstance.getClientInstanceId()
    });

    // Now wire up cross-references (all managers are instantiated)
    this.peerManager.setCallbacks({
      sendRawMessage: (msg: Record<string, unknown>) => this.signalingTransportHandler.sendRawMessage(msg),
      getLocalMediaStream: (): MediaStream | null => this.peerMediaFacade.getLocalStream(),
      isSignalingConnected: (): boolean => this.state.isSignalingConnected(),
      getVoiceStateSnapshot: (): VoiceStateSnapshot => this.voiceSessionController.getCurrentVoiceState(),
      getIdentifyCredentials: () => this.signalingTransportHandler.getIdentifyCredentials(),
      getLocalPeerId: (): string => this.state.getLocalPeerId(),
      isScreenSharingActive: (): boolean => this.state.isScreenSharingActive(),
      isCameraEnabled: (): boolean => this.state.isCameraEnabledActive(),
      getIceServers: (): RTCIceServer[] => this.iceServerSettings.rtcIceServers()
    });

    this.mediaManager.setCallbacks({
      getActivePeers: (): Map<string, PeerData> => this.peerMediaFacade.getActivePeers(),
      renegotiate: (peerId: string): Promise<void> => this.peerMediaFacade.renegotiate(peerId),
      broadcastMessage: (event: ChatEvent): void => this.peerMediaFacade.broadcastMessage(event),
      getIdentifyOderId: (): string => this.signalingTransportHandler.getIdentifyOderId(),
      getIdentifyDisplayName: (): string => this.signalingTransportHandler.getIdentifyDisplayName(),
      setCameraEnabled: (enabled: boolean): void => this.state.setCameraEnabled(enabled)
    });

    this.screenShareManager.setCallbacks({
      getActivePeers: (): Map<string, PeerData> => this.peerMediaFacade.getActivePeers(),
      getLocalMediaStream: (): MediaStream | null => this.peerMediaFacade.getLocalStream(),
      renegotiate: (peerId: string): Promise<void> => this.peerMediaFacade.renegotiate(peerId),
      broadcastCurrentStates: (): void => this.peerMediaFacade.broadcastCurrentStates(),
      selectDesktopSource: async (sources, options) => await this.screenShareSourcePicker.open(
        sources,
        options.includeSystemAudio
      ),
      updateLocalScreenShareState: (state): void => this.state.applyLocalScreenShareState(state)
    });

    this.signalingMessageHandler = new IncomingSignalingMessageHandler({
      getLocalOderId: () => this.signalingTransportHandler.getIdentifyCredentials()?.oderId ?? null,
      getEffectiveServerId: () => this.voiceSessionController.getEffectiveServerId(this.state.currentServerId),
      isVoiceConnected: () => this.state.isVoiceConnectedActive(),
      peerManager: this.peerManager,
      setServerTime: (serverTime) => this.timeSync.setFromServerTime(serverTime),
      signalingCoordinator: this.signalingCoordinator,
      logger: this.logger
    });

    this.serverMembershipSignalingHandler = new ServerMembershipSignalingHandler({
      signalingCoordinator: this.signalingCoordinator,
      signalingTransport: this.signalingTransportHandler,
      logger: this.logger,
      getActiveServerId: () => this.state.currentServerId,
      isVoiceConnected: () => this.state.isVoiceConnectedActive(),
      runFullCleanup: () => this.fullCleanup()
    });

    this.remoteScreenShareRequestController = new RemoteScreenShareRequestController({
      getConnectedPeerIds: () => this.peerMediaFacade.getConnectedPeerIds(),
      sendToPeer: (peerId, event) => this.peerMediaFacade.sendToPeer(peerId, event),
      clearRemoteScreenShareStream: (peerId) => this.peerMediaFacade.clearRemoteScreenShareStream(peerId),
      requestScreenShareForPeer: (peerId) => this.peerMediaFacade.requestScreenShareForPeer(peerId),
      stopScreenShareForPeer: (peerId) => this.peerMediaFacade.stopScreenShareForPeer(peerId),
      clearScreenShareRequest: (peerId) => this.peerMediaFacade.clearScreenShareRequest(peerId)
    });

    this.wireManagerEvents();
  }

  private wireManagerEvents(): void {
    // Internal control-plane messages for on-demand screen-share delivery.
    this.peerManager.messageReceived$.subscribe((event) =>
      this.remoteScreenShareRequestController.handlePeerControlMessage(event)
    );

    // Peer manager -> connected peers signal
    this.peerManager.connectedPeersChanged$.subscribe((peers: string[]) =>
      this.state.setConnectedPeers(peers)
    );

    // If we are already sharing when a new peer connection finishes, push the
    // current screen-share tracks to that peer and renegotiate.
    this.peerManager.peerConnected$.subscribe((peerId) => {
      if (this.peerMediaFacade.isScreenShareActive()) {
        this.peerMediaFacade.syncScreenShareToPeer(peerId);
      }

      this.mediaManager.refreshVoiceRouting();

      this.remoteScreenShareRequestController.handlePeerConnected(peerId);
    });

    this.peerManager.peerDisconnected$.subscribe((peerId) => {
      this.remoteScreenShareRequestController.handlePeerDisconnected(peerId);
    });

    // Media manager -> voice connected signal
    this.mediaManager.voiceConnected$.subscribe(() => {
      this.voiceSessionController.handleVoiceConnected();
    });

    // Peer manager -> latency updates
    this.peerManager.peerLatencyChanged$.subscribe(() =>
      this.state.syncPeerLatencies(this.peerManager.peerLatencies)
    );
  }

  private handleSignalingConnectionStatus(signalUrl: string, connected: boolean, errorMessage?: string): void {
    this.state.updateSignalingConnectionStatus(
      connected ? true : this.signalingCoordinator.isAnySignalingConnected(),
      connected,
      errorMessage
    );

    if (connected) {
      this.signalingReconnectedSubject$.next(signalUrl);
    }
  }

  private handleSignalingMessage(message: IncomingSignalingMessage, signalUrl: string): void {
    if (message.type === 'auth_required' || message.type === 'auth_error') {
      this.store.dispatch(UsersActions.signalServerAuthFailed({ signalUrl }));
      return;
    }

    this.signalingMessage$.next(message);
    this.signalingMessageHandler.handleMessage(message, signalUrl);
  }

  //  PUBLIC API - matches the old monolithic service's interface

  /**
   * Connect to a signaling server via WebSocket.
   *
   * @param serverUrl - The WebSocket URL of the signaling server.
   * @returns An observable that emits `true` once connected.
   */
  connectToSignalingServer(serverUrl: string): Observable<boolean> {
    return this.signalingTransportHandler.connectToSignalingServer(serverUrl);
  }

  /** Returns true when the signaling socket for a given URL is currently open. */
  isSignalingConnectedTo(serverUrl: string): boolean {
    return this.signalingTransportHandler.isSignalingConnectedTo(serverUrl);
  }

  /**
   * Ensure the signaling WebSocket is connected, reconnecting if needed.
   *
   * @param timeoutMs - Maximum time (ms) to wait for the connection.
   * @returns `true` if connected within the timeout.
   */
  async ensureSignalingConnected(timeoutMs?: number): Promise<boolean> {
    return await this.signalingTransportHandler.ensureSignalingConnected(timeoutMs);
  }

  /**
   * Send a signaling-level message (with `from` and `timestamp` auto-populated).
   *
   * @param message - The signaling message payload (excluding `from` / `timestamp`).
   */
  sendSignalingMessage(message: Omit<SignalingMessage, 'from' | 'timestamp'>): void {
    this.signalingTransportHandler.sendSignalingMessage(message);
  }

  /**
   * Send a raw JSON payload through the signaling WebSocket.
   *
   * @param message - Arbitrary JSON message.
   */
  sendRawMessage(message: Record<string, unknown>): void {
    this.signalingTransportHandler.sendRawMessage(message);
  }

  /** Send a raw JSON payload through a specific signaling WebSocket. */
  sendRawMessageToSignalUrl(signalUrl: string, message: Record<string, unknown>): boolean {
    return this.signalingTransportHandler.sendRawMessageToSignalUrl(signalUrl, message);
  }

  /**
   * Track the currently-active server ID (for server-scoped operations).
   *
   * @param serverId - The server to mark as active.
   */
  setCurrentServer(serverId: string): void {
    this.state.setCurrentServer(serverId);
  }

  /** The server ID currently being viewed / active, or `null`. */
  get currentServerId(): string | null {
    return this.state.currentServerId;
  }

  /** The last signaling URL used by the client, if any. */
  getCurrentSignalingUrl(): string | null {
    return this.signalingTransportHandler.getCurrentSignalingUrl(this.state.currentServerId);
  }

  /**
   * Send an identify message to the signaling server.
   *
   * The credentials are cached so they can be replayed after a reconnect.
   *
   * @param oderId      - The user's unique order/peer ID.
   * @param displayName - The user's display name.
   */
  identify(
    oderId: string,
    displayName: string,
    signalUrl?: string,
    profile?: { description?: string; profileUpdatedAt?: number; homeSignalServerUrl?: string }
  ): void {
    this.signalingTransportHandler.identify(oderId, displayName, signalUrl, profile);
  }

  /**
   * Join a server (room) on the signaling server.
   *
   * @param roomId - The server / room ID to join.
   * @param userId - The local user ID.
   */
  joinRoom(roomId: string, userId: string, signalUrl?: string): void {
    this.serverMembershipSignalingHandler.joinRoom(roomId, userId, signalUrl);
  }

  /**
   * Switch to a different server. If already a member, sends a view event;
   * otherwise joins the server.
   *
   * @param serverId - The target server ID.
   * @param userId   - The local user ID.
   */
  switchServer(serverId: string, userId: string, signalUrl?: string): void {
    this.serverMembershipSignalingHandler.switchServer(serverId, userId, signalUrl);
  }

  /**
   * Leave one or all servers.
   *
   * If `serverId` is provided, leaves only that server.
   * Otherwise leaves every joined server and performs a full cleanup.
   *
   * @param serverId - Optional server to leave; omit to leave all.
   */
  leaveRoom(serverId?: string): void {
    this.serverMembershipSignalingHandler.leaveRoom(serverId);
  }

  /**
   * Check whether the local client has joined a given server.
   *
   * @param serverId - The server to check.
   */
  hasJoinedServer(serverId: string): boolean {
    return this.signalingCoordinator.hasJoinedServer(serverId);
  }

  /** Returns a read-only set of all currently-joined server IDs. */
  getJoinedServerIds(): ReadonlySet<string> {
    return this.signalingCoordinator.getJoinedServerIds();
  }

  /**
   * Broadcast a {@link ChatEvent} to every connected peer.
   *
   * @param event - The chat event to send.
   */
  broadcastMessage(event: ChatEvent): void {
    this.peerMediaFacade.broadcastMessage(event);
    this.relayBroadcastEvent(event);
  }

  /**
   * Send a {@link ChatEvent} to a specific peer.
   *
   * @param peerId - The target peer ID.
   * @param event  - The chat event to send.
   */
  sendToPeer(peerId: string, event: ChatEvent): void {
    this.peerMediaFacade.sendToPeer(peerId, event);
  }

  syncRemoteScreenShareRequests(peerIds: string[], enabled: boolean): void {
    this.remoteScreenShareRequestController.syncRemoteScreenShareRequests(peerIds, enabled);
  }

  /**
   * Send a {@link ChatEvent} to a peer with back-pressure awareness.
   *
   * @param peerId - The target peer ID.
   * @param event  - The chat event to send.
   */
  async sendToPeerBuffered(peerId: string, event: ChatEvent): Promise<void> {
    return await this.peerMediaFacade.sendToPeerBuffered(peerId, event);
  }

  /** Returns an array of currently-connected peer IDs. */
  getConnectedPeers(): string[] {
    return this.peerMediaFacade.getConnectedPeerIds();
  }

  hasSignalingRouteForPeer(peerId: string): boolean {
    const signalUrl = this.signalingCoordinator.getPeerSignalUrl(peerId);

    return !!signalUrl && this.signalingCoordinator.isSignalingConnectedTo(signalUrl);
  }

  /**
   * Get the composite remote {@link MediaStream} for a connected peer.
   *
   * @param peerId - The remote peer whose stream to retrieve.
   * @returns The stream, or `null` if the peer has no active stream.
   */
  getRemoteStream(peerId: string): MediaStream | null {
    return this.peerMediaFacade.getRemoteStream(peerId);
  }

  /**
   * Get the remote voice-only stream for a connected peer.
   *
   * @param peerId - The remote peer whose voice stream to retrieve.
   * @returns The stream, or `null` if the peer has no active voice audio.
   */
  getRemoteVoiceStream(peerId: string): MediaStream | null {
    return this.peerMediaFacade.getRemoteVoiceStream(peerId);
  }

  /**
   * Get the remote camera stream for a connected peer.
   *
   * @param peerId - The remote peer whose camera stream to retrieve.
   * @returns The stream, or `null` if the peer has no active camera video.
   */
  getRemoteCameraStream(peerId: string): MediaStream | null {
    return this.peerMediaFacade.getRemoteCameraStream(peerId);
  }

  /**
   * Get the remote screen-share stream for a connected peer.
   *
   * This contains the screen video track and any audio track that belongs to
   * the screen share itself, not the peer's normal voice-chat audio.
   *
   * @param peerId - The remote peer whose screen-share stream to retrieve.
   * @returns The stream, or `null` if the peer has no active screen share.
   */
  getRemoteScreenShareStream(peerId: string): MediaStream | null {
    return this.peerMediaFacade.getRemoteScreenShareStream(peerId);
  }

  /**
   * Get the current local media stream (microphone audio).
   *
   * @returns The local {@link MediaStream}, or `null` if voice is not active.
   */
  getLocalStream(): MediaStream | null {
    return this.peerMediaFacade.getLocalStream();
  }

  /**
   * Get the current local camera stream.
   *
   * @returns The local camera {@link MediaStream}, or `null` if the camera is disabled.
   */
  getLocalCameraStream(): MediaStream | null {
    return this.peerMediaFacade.getLocalCameraStream();
  }

  /**
   * Get the raw local microphone stream before gain / RNNoise processing.
   *
   * @returns The raw microphone {@link MediaStream}, or `null` if voice is not active.
   */
  getRawMicStream(): MediaStream | null {
    return this.peerMediaFacade.getRawMicStream();
  }

  reportConnectionError(message: string): void {
    this.state.setConnectionError(message);
  }

  clearConnectionError(): void {
    this.state.clearConnectionError();
  }

  /**
   * Request microphone access and start sending audio to all peers.
   *
   * @returns The captured local {@link MediaStream}.
   */
  async enableVoice(): Promise<MediaStream> {
    return await this.voiceSessionController.enableVoice();
  }

  /** Stop local voice capture and remove audio senders from peers. */
  disableVoice(): void {
    this.voiceSessionController.disableVoice();
    this.state.setCameraEnabled(false);
  }

  /**
   * Start sharing the local camera video with peers in the active voice channel.
   *
   * @returns The camera {@link MediaStream}.
   */
  async enableCamera(): Promise<MediaStream> {
    const stream = await this.mediaManager.enableCamera();

    this.state.setCameraEnabled(this.mediaManager.getIsCameraActive());
    return stream;
  }

  /** Stop local camera capture and remove camera tracks from peers. */
  disableCamera(): void {
    this.mediaManager.disableCamera();
    this.state.setCameraEnabled(this.mediaManager.getIsCameraActive());
  }

  /**
   * Inject an externally-obtained media stream as the local voice source.
   *
   * @param stream - The media stream to use.
   */
  async setLocalStream(stream: MediaStream): Promise<void> {
    await this.voiceSessionController.setLocalStream(stream);
  }

  /**
   * Toggle the local microphone mute state.
   *
   * @param muted - Explicit state; if omitted, the current state is toggled.
   */
  toggleMute(muted?: boolean): void {
    this.voiceSessionController.toggleMute(muted);
  }

  /**
   * Toggle self-deafen (suppress incoming audio playback).
   *
   * @param deafened - Explicit state; if omitted, the current state is toggled.
   */
  toggleDeafen(deafened?: boolean): void {
    this.voiceSessionController.toggleDeafen(deafened);
  }

  /**
   * Toggle RNNoise noise reduction on the local microphone.
   *
   * When enabled, the raw mic audio is routed through an AudioWorklet
   * that applies neural-network noise suppression before being sent
   * to peers.
   *
   * @param enabled - Explicit state; if omitted, the current state is toggled.
   */
  async toggleNoiseReduction(enabled?: boolean): Promise<void> {
    await this.voiceSessionController.toggleNoiseReduction(enabled);
  }

  /**
   * Set the output volume for remote audio playback.
   *
   * @param volume - Normalised volume (0-1).
   */
  setOutputVolume(volume: number): void {
    this.voiceSessionController.setOutputVolume(volume);
  }

  /**
   * Set the input (microphone) volume.
   *
   * Adjusts a Web Audio GainNode on the local mic stream so the level
   * sent to peers changes in real time without renegotiation.
   *
   * @param volume - Normalised volume (0-1).
   */
  setInputVolume(volume: number): void {
    this.voiceSessionController.setInputVolume(volume);
  }

  /**
   * Set the maximum audio bitrate for all peer connections.
   *
   * @param kbps - Target bitrate in kilobits per second.
   */
  async setAudioBitrate(kbps: number): Promise<void> {
    return await this.voiceSessionController.setAudioBitrate(kbps);
  }

  /**
   * Apply a predefined latency profile that maps to a specific bitrate.
   *
   * @param profile - One of `'low'`, `'balanced'`, or `'high'`.
   */
  async setLatencyProfile(profile: LatencyProfile): Promise<void> {
    return await this.voiceSessionController.setLatencyProfile(profile);
  }

  /**
   * Start broadcasting voice-presence heartbeats to all peers.
   *
   * Also marks the given server as the active voice server and closes
   * any peer connections that belong to other servers so that audio
   * is isolated to the correct voice channel.
   *
   * @param roomId   - The voice channel room ID.
   * @param serverId - The voice channel server ID.
   */
  startVoiceHeartbeat(roomId?: string, serverId?: string): void {
    this.voiceSessionController.startVoiceHeartbeat(roomId, serverId);
  }

  /** Stop the voice-presence heartbeat. */
  stopVoiceHeartbeat(): void {
    this.voiceSessionController.stopVoiceHeartbeat();
  }

  syncOutgoingVoiceRouting(allowedPeerIds: string[]): void {
    this.mediaManager.setAllowedVoicePeerIds(allowedPeerIds);
  }

  /**
   * Start sharing the screen (or a window) with all connected peers.
   *
   * @param options - Screen-share capture options.
   * @returns The screen-capture {@link MediaStream}.
   */
  async startScreenShare(options: ScreenShareStartOptions): Promise<MediaStream> {
    return await this.peerMediaFacade.startScreenShare(options);
  }

  /** Stop screen sharing and restore microphone audio on all peers. */
  stopScreenShare(): void {
    this.peerMediaFacade.stopScreenShare();
  }

  private relayBroadcastEvent(event: ChatEvent): void {
    const clientInstanceId = this.clientInstance.getClientInstanceId();

    if (event.type === 'chat-message' && event.message?.roomId) {
      this.signalingTransportHandler.sendRawMessage({
        type: 'chat_message',
        serverId: event.message.roomId,
        message: {
          ...event.message,
          clientInstanceId
        },
        clientInstanceId
      });

      return;
    }

    if (event.type === 'voice-state' && event.voiceState?.serverId) {
      this.signalingTransportHandler.sendRawMessage({
        ...event,
        type: 'voice_state',
        serverId: event.voiceState.serverId,
        voiceState: {
          ...event.voiceState,
          clientInstanceId
        },
        clientInstanceId
      });
    }
  }

  requestVoiceClientTakeover(): void {
    this.signalingTransportHandler.sendRawMessage({
      type: 'voice_client_takeover',
      clientInstanceId: this.clientInstance.getClientInstanceId()
    });
  }

  getClientInstanceId(): string {
    return this.clientInstance.getClientInstanceId();
  }

  /** Disconnect from the signaling server and clean up all state. */
  disconnect(): void {
    this.leaveRoom();
    this.destroyAllSignalingManagers();
    this.state.resetConnectionState();
  }

  /** Alias for {@link disconnect}. */
  disconnectAll(): void {
    this.disconnect();
  }

  private fullCleanup(): void {
    this.signalingCoordinator.clearPeerTracking();
    this.remoteScreenShareRequestController.clear();
    this.peerMediaFacade.closeAllPeers();
    this.state.clearPeerViewState();
    this.voiceSessionController.resetVoiceSession();
    this.state.setCameraEnabled(false);
    this.peerMediaFacade.stopScreenShare();
    this.state.clearScreenShareState();
  }

  private destroyAllSignalingManagers(): void {
    this.signalingCoordinator.destroy();
  }

  ngOnDestroy(): void {
    this.disconnect();
    this.peerMediaFacade.destroy();
  }
}
