/* eslint-disable @typescript-eslint/member-ordering */
import {
  Injectable,
  computed,
  effect,
  inject,
  signal
} from '@angular/core';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { NotificationAudioService, AppSound } from '../../../../core/services/notification-audio.service';
import { ViewportService } from '../../../../core/platform';
import {
  VoiceActivityService,
  VoiceConnectionFacade,
  VoicePlaybackService
} from '../../../voice-connection';
import { VoiceSessionFacade } from '../../../voice-session';
import { DirectMessageService, PeerDeliveryService } from '../../../direct-message';
import type { DirectMessageConversation } from '../../../direct-message';
import { selectAllUsers, selectCurrentUser } from '../../../../store/users/users.selectors';
import { UsersActions } from '../../../../store/users/users.actions';
import {
  DirectCallEventPayload,
  DirectMessageParticipant,
  User
} from '../../../../shared-kernel';
import { DirectCallSession, participantToUser } from '../../domain/models/direct-call.model';
import { toDirectMessageParticipant } from '../../../direct-message';

@Injectable({ providedIn: 'root' })
export class DirectCallService {
  private readonly router = inject(Router);
  private readonly store = inject(Store);
  private readonly delivery = inject(PeerDeliveryService);
  private readonly directMessages = inject(DirectMessageService);
  private readonly audio = inject(NotificationAudioService);
  private readonly voice = inject(VoiceConnectionFacade);
  private readonly voiceSession = inject(VoiceSessionFacade);
  private readonly voiceActivity = inject(VoiceActivityService);
  private readonly playback = inject(VoicePlaybackService);
  private readonly viewport = inject(ViewportService);
  private readonly currentUser = this.store.selectSignal(selectCurrentUser);
  private readonly users = this.store.selectSignal(selectAllUsers);
  private readonly sessionsSignal = signal<DirectCallSession[]>([]);
  private readonly mobileOverlayCallId = signal<string | null>(null);

  readonly sessions = computed(() => this.sessionsSignal());
  readonly activeSessions = computed(() => this.sessions().filter((session) => session.status !== 'ended'));
  readonly visibleActiveSessions = computed(() => this.activeSessions().filter((session) => this.hasOngoingActivity(session)));
  readonly incomingCall = computed<DirectCallSession | null>(() => {
    if (this.isDoNotDisturb()) {
      return null;
    }

    const meId = this.currentUserId();

    if (!meId) {
      return null;
    }

    return [...this.activeSessions()]
      .sort((left, right) => right.createdAt - left.createdAt)
      .find((session) => session.status === 'ringing'
        && this.currentSession()?.callId !== session.callId
        && !session.participants[meId]?.joined
        && this.hasConnectedParticipant(session)) ?? null;
  });
  readonly currentSession = signal<DirectCallSession | null>(null);
  readonly hasActiveCall = computed(() => this.visibleActiveSessions().length > 0);
  readonly mobileOverlaySession = computed(() => {
    const callId = this.mobileOverlayCallId();

    if (!callId) {
      return null;
    }

    return this.visibleActiveSessions().find((session) => session.callId === callId) ?? null;
  });

  constructor() {
    this.delivery.directCallEvents$.subscribe((event) => {
      if (event.directCall) {
        void this.handleIncomingCallEvent(event.directCall);
      }
    });

    effect(() => {
      const session = this.currentSession();

      if (!session || session.status === 'ended') {
        return;
      }

      const peerIds = this.remoteParticipantIds(session);

      this.voice.syncOutgoingVoiceRouting(peerIds);
    });

    effect(() => {
      if (this.incomingCall() && !this.isDoNotDisturb()) {
        return;
      }

      this.audio.stop(AppSound.Call);
    });

    effect(() => {
      if (this.mobileOverlayCallId() && !this.mobileOverlaySession()) {
        this.mobileOverlayCallId.set(null);
      }
    });
  }

  sessionById(callId: string | null | undefined): DirectCallSession | null {
    if (!callId) {
      return null;
    }

    return this.sessionsSignal().find((session) => session.callId === callId) ?? null;
  }

  isCallingUser(user: User): boolean {
    const userId = this.userKey(user);

    return this.visibleActiveSessions().some((session) => session.participantIds.includes(userId));
  }

  isCallingConversation(conversationId: string | null | undefined): boolean {
    if (!conversationId) {
      return false;
    }

    return this.visibleActiveSessions().some((session) => session.callId === conversationId || session.conversationId === conversationId);
  }

  hasConnectedParticipant(session: DirectCallSession | null | undefined): boolean {
    if (!session || session.status === 'ended') {
      return false;
    }

    return Object.values(session.participants).some((participant) => participant.joined);
  }

  hasOngoingActivity(session: DirectCallSession | null | undefined): boolean {
    return this.hasConnectedParticipant(session);
  }

  async startCall(user: User): Promise<DirectCallSession> {
    const conversation = await this.directMessages.createConversation(user);
    const me = this.requireCurrentUser();
    const meParticipant = toDirectMessageParticipant(me);
    const peerParticipant = toDirectMessageParticipant(user);
    const participantIds = this.uniqueIds([meParticipant.userId, peerParticipant.userId]);
    const activeSession = this.findLiveSessionForParticipants(participantIds, conversation.id);

    if (activeSession) {
      return await this.rejoinLiveSession(activeSession);
    }

    const existing = this.sessionById(conversation.id);
    const session = existing ?? this.createSession({
      callId: conversation.id,
      conversationId: conversation.id,
      createdAt: Date.now(),
      initiatorId: meParticipant.userId,
      participantIds,
      participants: [meParticipant, peerParticipant],
      status: 'calling'
    });

    this.upsertSession(session);
    this.currentSession.set(session);
    await this.joinCall(session.callId, false);
    this.sendCallEvent(peerParticipant.userId, 'ring', session);
    await this.openCallView(session.callId);
    return session;
  }

  async startConversationCall(conversation: DirectMessageConversation): Promise<DirectCallSession> {
    if (this.isGroupConversation(conversation)) {
      return await this.startGroupCall(conversation);
    }

    const meId = this.currentUserId();
    const peerId = conversation.participants.find((participantId) => participantId !== meId);

    if (!peerId) {
      throw new Error('Direct message conversation has no recipient to call.');
    }

    const peer = this.userForParticipant(peerId) ?? participantToUser(this.participantFromConversation(conversation, peerId));

    return await this.startCall(peer);
  }

  async openCall(callId: string): Promise<void> {
    const session = this.sessionById(callId);

    if (session?.conversationId) {
      await this.directMessages.openConversation(session.conversationId);
    }

    this.currentSession.set(session);
  }

  async openCallView(callId: string): Promise<void> {
    if (this.viewport.isMobile()) {
      await this.openMobileCallOverlay(callId);
      return;
    }

    await this.router.navigate(['/call', callId]);
  }

  async openMobileCallOverlay(callId: string): Promise<void> {
    await this.openCall(callId);
    this.mobileOverlayCallId.set(callId);
  }

  closeMobileCallOverlay(): void {
    this.mobileOverlayCallId.set(null);
  }

  async answerIncomingCall(callId: string): Promise<void> {
    const session = this.sessionById(callId);

    if (!session || session.status === 'ended') {
      return;
    }

    this.audio.stop(AppSound.Call);
    this.currentSession.set(session);
    await this.joinCall(callId);
    await this.router.navigate(['/call', callId]);
  }

  declineIncomingCall(callId: string): void {
    const session = this.sessionById(callId);

    if (!session || session.status === 'ended') {
      return;
    }

    const meId = this.currentUserId();
    const nextSession = meId
      ? {
        ...this.markParticipantJoined(session, meId, false, 'ended'),
        status: 'ended' as const
      }
      : {
        ...session,
        status: 'ended' as const
      };

    this.audio.stop(AppSound.Call);

    if (meId) {
      this.broadcastCallEvent('leave', session);
    }

    this.upsertSession(nextSession);

    if (this.currentSession()?.callId === callId) {
      this.currentSession.set(null);
    }
  }

  async joinCall(callId: string, notifyPeers = true): Promise<void> {
    const session = this.sessionById(callId);
    const me = this.requireCurrentUser();
    const meId = this.userKey(me);

    if (!session) {
      return;
    }

    this.leaveOtherJoinedCalls(callId);
    this.leaveCurrentVoiceTargetForCall(callId);
    this.audio.stop(AppSound.Call);

    const ok = await this.voice.ensureSignalingConnected();

    if (!ok || !navigator.mediaDevices?.getUserMedia) {
      return;
    }

    const stream = await navigator.mediaDevices.getUserMedia({
      audio: {
        echoCancellation: true,
        noiseSuppression: false
      }
    });

    await this.voice.setLocalStream(stream);
    this.voiceActivity.trackLocalMic(meId, stream);
    this.voice.startVoiceHeartbeat(session.callId, session.callId);
    this.updateLocalVoiceState(session, true);
    this.playback.playPendingStreams({
      isConnected: true,
      outputVolume: 1,
      isDeafened: this.voice.isDeafened()
    });

    const nextSession = this.markParticipantJoined(session, meId, true, 'connected');

    this.upsertSession(nextSession);
    this.currentSession.set(nextSession);

    if (notifyPeers) {
      this.broadcastCallEvent('join', nextSession);
    }
  }

  leaveCall(callId: string, endForEveryone = false): void {
    const session = this.sessionById(callId);

    if (!session) {
      return;
    }

    this.leaveJoinedSession(session, endForEveryone);
  }

  leaveCurrentJoinedCall(exceptCallId?: string): void {
    for (const session of this.sessionsSignal()) {
      if (session.callId === exceptCallId || !this.isCurrentUserJoined(session)) {
        continue;
      }

      this.leaveJoinedSession(session);
    }
  }

  private leaveJoinedSession(session: DirectCallSession, endForEveryone = false): void {
    const action = endForEveryone ? 'end' : 'leave';
    const nextSession = this.markCurrentUserLeft(session, endForEveryone);

    this.audio.stop(AppSound.Call);
    this.broadcastCallEvent(action, nextSession);
    this.stopLocalMedia(nextSession);
    this.upsertSession(nextSession);

    this.currentSession.set(null);
  }

  async inviteUser(callId: string, user: User): Promise<void> {
    const session = this.sessionById(callId);

    if (!session) {
      return;
    }

    const participant = toDirectMessageParticipant(user);
    const nextSession = this.createSession({
      ...session,
      participantIds: this.uniqueIds([...session.participantIds, participant.userId]),
      participants: [...Object.values(session.participants).map((entry) => entry.profile), participant],
      status: session.status
    });
    const convertedSession = await this.convertToGroupConversationIfNeeded(this.preserveJoinedParticipants(session, nextSession));

    this.upsertSession(convertedSession);
    this.currentSession.set(convertedSession);
    this.broadcastCallEvent('update', convertedSession, [participant.userId]);
    this.sendCallEvent(participant.userId, 'ring', convertedSession);
  }

  remoteParticipantIds(session: DirectCallSession): string[] {
    const meId = this.currentUserId();

    return session.participantIds.filter((participantId) => participantId !== meId);
  }

  userForParticipant(participantId: string): User | null {
    const known = this.users().find((user) => user.id === participantId || user.oderId === participantId || user.peerId === participantId);

    if (known) {
      return known;
    }

    const participant = this.currentSession()?.participants[participantId]?.profile;

    return participant ? participantToUser(participant) : null;
  }

  private async handleIncomingCallEvent(payload: DirectCallEventPayload): Promise<void> {
    const meId = this.currentUserId();

    if (!meId || payload.sender.userId === meId) {
      return;
    }

    if (!this.callPayloadIncludesParticipant(payload, meId)) {
      return;
    }

    const participants = this.callParticipantsFromPayload(payload);
    const existing = this.sessionById(payload.callId);
    const incomingSession = this.createSession({
      callId: payload.callId,
      conversationId: payload.conversationId,
      createdAt: payload.createdAt,
      initiatorId: existing?.initiatorId ?? payload.sender.userId,
      participantIds: this.uniqueIds([
        ...payload.participantIds,
        meId,
        payload.sender.userId
      ]),
      participants,
      status: this.resolveIncomingStatus(payload.action, existing?.status)
    });
    const preservedSession = existing ? this.preserveJoinedParticipants(existing, incomingSession) : incomingSession;
    const session = this.applyIncomingParticipantState(preservedSession, payload);

    this.upsertSession(session);
    this.currentSession.set(this.currentSession()?.callId === session.callId ? session : this.currentSession());
    this.markRemoteVoiceState(payload.sender.userId, session, payload.action === 'join');

    if (payload.action === 'update') {
      await this.ensureCallConversation(session);
      return;
    }

    if (payload.action === 'ring') {
      await this.ensureCallConversation(session);

      if (this.shouldAlertIncomingCall(session)) {
        this.audio.playLoop(AppSound.Call);
      } else {
        this.audio.stop(AppSound.Call);
      }

      if (this.shouldAlertIncomingCall(session)) {
        await this.showIncomingNotification(payload.sender.displayName, payload.callId);
      }

      return;
    }

    this.audio.stop(AppSound.Call);

    if (payload.action === 'end') {
      if (this.currentSession()?.callId === payload.callId) {
        this.stopLocalMedia(session);
        this.currentSession.set(null);
      }
    }
  }

  private async startGroupCall(conversation: DirectMessageConversation): Promise<DirectCallSession> {
    const me = this.requireCurrentUser();
    const meParticipant = toDirectMessageParticipant(me);
    const participantIds = this.uniqueIds([...conversation.participants, meParticipant.userId]);
    const conversationParticipants = participantIds.map((participantId) => this.participantFromConversation(conversation, participantId));
    const participants = this.uniqueParticipants([meParticipant, ...conversationParticipants]);
    const activeSession = this.findLiveSessionForParticipants(participantIds, conversation.id);

    if (activeSession) {
      return await this.rejoinLiveSession(activeSession);
    }

    const existing = this.sessionById(conversation.id);
    const session = existing && existing.status !== 'ended'
      ? existing
      : this.createSession({
        callId: conversation.id,
        conversationId: conversation.id,
        createdAt: Date.now(),
        initiatorId: meParticipant.userId,
        participantIds,
        participants,
        status: 'calling'
      });

    this.upsertSession(session);
    this.currentSession.set(session);
    await this.joinCall(session.callId, false);
    this.broadcastCallEvent('ring', this.sessionById(session.callId) ?? session);
    await this.router.navigate(['/call', session.callId]);
    return this.sessionById(session.callId) ?? session;
  }

  private async rejoinLiveSession(session: DirectCallSession): Promise<DirectCallSession> {
    this.upsertSession(session);
    this.currentSession.set(session);

    if (!this.isCurrentUserJoined(session)) {
      await this.joinCall(session.callId);
    }

    const nextSession = this.sessionById(session.callId) ?? session;

    await this.router.navigate(['/call', nextSession.callId]);
    return nextSession;
  }

  private leaveOtherJoinedCalls(callId: string): void {
    this.leaveCurrentJoinedCall(callId);
  }

  private leaveCurrentVoiceTargetForCall(callId: string): void {
    const user = this.currentUser();
    const voiceState = user?.voiceState;

    if (!voiceState?.isConnected || (voiceState.roomId === callId && voiceState.serverId === callId)) {
      return;
    }

    const userId = user?.id;
    const userKey = user ? this.userKey(user) : undefined;

    this.voice.stopVoiceHeartbeat();

    if (userKey) {
      this.voiceActivity.untrackLocalMic(userKey);
    }

    this.voice.disableVoice();
    this.playback.teardownAll();
    this.voiceSession.endSession();

    if (userId) {
      this.store.dispatch(UsersActions.updateVoiceState({
        userId,
        voiceState: {
          isConnected: false,
          isMuted: false,
          isDeafened: false,
          roomId: undefined,
          serverId: undefined
        }
      }));
    }

    this.voice.broadcastMessage({
      type: 'voice-state',
      oderId: userKey,
      displayName: user?.displayName || 'User',
      voiceState: {
        isConnected: false,
        isMuted: false,
        isDeafened: false,
        roomId: voiceState.roomId,
        serverId: voiceState.serverId
      }
    });
  }

  private async ensureConversation(sender: DirectMessageParticipant): Promise<void> {
    await this.directMessages.createConversation(participantToUser(sender));
  }

  private isGroupConversation(conversation: DirectMessageConversation): boolean {
    return conversation.kind === 'group' || conversation.participants.length > 2;
  }

  private participantFromConversation(conversation: DirectMessageConversation, participantId: string): DirectMessageParticipant {
    const knownUser = this.userForParticipant(participantId);
    const profile = conversation.participantProfiles[participantId];

    if (knownUser) {
      return toDirectMessageParticipant(knownUser);
    }

    return profile ?? {
      userId: participantId,
      username: participantId,
      displayName: participantId
    };
  }

  private resolveIncomingStatus(action: DirectCallEventPayload['action'], currentStatus?: DirectCallSession['status']): DirectCallSession['status'] {
    if (action === 'ring') {
      return currentStatus === 'connected' ? 'connected' : 'ringing';
    }

    if (action === 'join') {
      return 'connected';
    }

    if (action === 'end') {
      return 'ended';
    }

    if (action === 'update') {
      return currentStatus ?? 'ringing';
    }

    if (action === 'leave') {
      return currentStatus === 'ringing' ? 'ringing' : 'connected';
    }

    return currentStatus ?? 'ringing';
  }

  private applyIncomingParticipantState(session: DirectCallSession, payload: DirectCallEventPayload): DirectCallSession {
    if (payload.action === 'ring' || payload.action === 'join') {
      return this.markParticipantJoined(session, payload.sender.userId, true, payload.action === 'join' ? 'connected' : session.status);
    }

    if (payload.action === 'leave') {
      const nextSession = this.markParticipantJoined(session, payload.sender.userId, false, session.status);

      return this.hasConnectedParticipant(nextSession)
        ? nextSession
        : {
          ...nextSession,
          status: 'ended'
        };
    }

    if (payload.action === 'end') {
      return {
        ...session,
        status: 'ended'
      };
    }

    return session;
  }

  private sendCallEvent(recipientId: string, action: DirectCallEventPayload['action'], session: DirectCallSession): void {
    const me = this.requireCurrentUser();

    this.delivery.sendCallEvent(recipientId, {
      type: 'direct-call',
      directCall: {
        action,
        callId: session.callId,
        conversationId: session.conversationId,
        createdAt: session.createdAt,
        sender: toDirectMessageParticipant(me),
        participantIds: session.participantIds,
        participants: Object.values(session.participants).map((participant) => participant.profile)
      }
    });
  }

  private broadcastCallEvent(action: DirectCallEventPayload['action'], session: DirectCallSession, excludedParticipantIds: string[] = []): void {
    const excluded = new Set(excludedParticipantIds);

    for (const participantId of this.remoteParticipantIds(session)) {
      if (excluded.has(participantId)) {
        continue;
      }

      this.sendCallEvent(participantId, action, session);
    }
  }

  private async convertToGroupConversationIfNeeded(session: DirectCallSession): Promise<DirectCallSession> {
    if (session.participantIds.length <= 2) {
      return session;
    }

    const conversation = await this.directMessages.createGroupConversation(
      Object.values(session.participants).map((participant) => participant.profile),
      this.groupConversationTitle(session),
      session.conversationId.startsWith('dm-group-') ? session.conversationId : undefined
    );

    return {
      ...session,
      conversationId: conversation.id
    };
  }

  private preserveJoinedParticipants(previousSession: DirectCallSession, nextSession: DirectCallSession): DirectCallSession {
    return {
      ...nextSession,
      participants: Object.fromEntries(Object.values(nextSession.participants).map((participant) => [
        participant.userId,
        {
          ...participant,
          joined: previousSession.participants[participant.userId]?.joined ?? participant.joined
        }
      ]))
    };
  }

  private async ensureCallConversation(session: DirectCallSession): Promise<void> {
    if (session.participantIds.length > 2) {
      await this.directMessages.createGroupConversation(
        Object.values(session.participants).map((participant) => participant.profile),
        this.groupConversationTitle(session),
        session.conversationId
      );

      return;
    }

    const sender = Object.values(session.participants)
      .map((participant) => participant.profile)
      .find((participant) => participant.userId !== this.currentUserId());

    if (sender) {
      await this.ensureConversation(sender);
    }
  }

  private callParticipantsFromPayload(payload: DirectCallEventPayload): DirectMessageParticipant[] {
    return this.uniqueParticipants([
      payload.sender,
      toDirectMessageParticipant(this.requireCurrentUser()),
      ...(payload.participants ?? []),
      ...payload.participantIds
        .map((participantId) => this.userForParticipant(participantId))
        .filter((user): user is User => !!user)
        .map((user) => toDirectMessageParticipant(user))
    ]);
  }

  private callPayloadIncludesParticipant(payload: DirectCallEventPayload, participantId: string): boolean {
    return payload.participantIds.includes(participantId)
      || (payload.participants ?? []).some((participant) => participant.userId === participantId);
  }

  private groupConversationTitle(session: DirectCallSession): string {
    const names = Object.values(session.participants)
      .map((participant) => participant.profile.displayName || participant.profile.username || participant.userId);

    if (names.length <= 3) {
      return names.join(', ');
    }

    return `${names.slice(0, 3).join(', ')} +${names.length - 3}`;
  }

  private createSession(input: {
    callId: string;
    conversationId: string;
    createdAt: number;
    initiatorId: string;
    participantIds: string[];
    participants: DirectMessageParticipant[];
    status: DirectCallSession['status'];
  }): DirectCallSession {
    const participants = Object.fromEntries(this.uniqueParticipants(input.participants).map((participant) => [
      participant.userId,
      {
        userId: participant.userId,
        profile: participant,
        joined: input.status === 'connected' && participant.userId === this.currentUserId()
      }
    ]));

    return {
      callId: input.callId,
      conversationId: input.conversationId,
      createdAt: input.createdAt,
      initiatorId: input.initiatorId,
      participantIds: this.uniqueIds(input.participantIds),
      participants,
      status: input.status
    };
  }

  private markParticipantJoined(
    session: DirectCallSession,
    participantId: string,
    joined: boolean,
    status: DirectCallSession['status']
  ): DirectCallSession {
    const participant = session.participants[participantId];

    return {
      ...session,
      status,
      participants: {
        ...session.participants,
        ...(participant
          ? {
            [participantId]: {
              ...participant,
              joined
            }
          }
          : {})
      }
    };
  }

  private upsertSession(session: DirectCallSession): void {
    this.sessionsSignal.update((sessions) => [...sessions.filter((entry) => entry.callId !== session.callId), session]);
  }

  private markCurrentUserLeft(session: DirectCallSession, endForEveryone: boolean): DirectCallSession {
    const meId = this.currentUserId();
    const locallyLeftSession = meId
      ? this.markParticipantJoined(session, meId, false, session.status)
      : session;

    return {
      ...locallyLeftSession,
      status: endForEveryone || !this.hasConnectedParticipant(locallyLeftSession) ? 'ended' as const : 'connected' as const
    };
  }

  private findLiveSessionForParticipants(participantIds: string[], conversationId?: string | null): DirectCallSession | null {
    const normalizedParticipantIds = this.uniqueIds(participantIds).sort();

    return this.visibleActiveSessions().find((session) => {
      if (conversationId && (session.callId === conversationId || session.conversationId === conversationId)) {
        return true;
      }

      const sessionParticipantIds = this.uniqueIds(session.participantIds).sort();

      return sessionParticipantIds.length === normalizedParticipantIds.length
        && sessionParticipantIds.every((participantId, index) => participantId === normalizedParticipantIds[index]);
    }) ?? null;
  }

  private isCurrentUserJoined(session: DirectCallSession): boolean {
    const meId = this.currentUserId();

    return !!meId && !!session.participants[meId]?.joined;
  }

  private stopLocalMedia(session: DirectCallSession): void {
    const meId = this.currentUserId();

    if (meId) {
      this.voiceActivity.untrackLocalMic(meId);
    }

    this.voice.stopVoiceHeartbeat();
    this.voice.disableVoice();
    this.playback.teardownAll();
    this.updateLocalVoiceState(session, false);
  }

  private updateLocalVoiceState(session: DirectCallSession, connected: boolean): void {
    const user = this.currentUser();

    if (!user?.id) {
      return;
    }

    this.store.dispatch(UsersActions.updateVoiceState({
      userId: user.id,
      voiceState: {
        isConnected: connected,
        isMuted: connected ? this.voice.isMuted() : false,
        isDeafened: connected ? this.voice.isDeafened() : false,
        roomId: connected ? session.callId : undefined,
        serverId: connected ? session.callId : undefined
      }
    }));
  }

  private markRemoteVoiceState(userId: string, session: DirectCallSession, connected: boolean): void {
    this.store.dispatch(UsersActions.updateVoiceState({
      userId,
      voiceState: {
        isConnected: connected,
        isMuted: false,
        isDeafened: false,
        roomId: connected ? session.callId : undefined,
        serverId: connected ? session.callId : undefined
      }
    }));
  }

  private shouldAlertIncomingCall(session: DirectCallSession): boolean {
    return session.status !== 'connected' && !this.isDoNotDisturb();
  }

  private isDoNotDisturb(): boolean {
    return this.currentUser()?.status === 'busy';
  }

  private async showIncomingNotification(displayName: string, callId: string): Promise<void> {
    if (typeof Notification === 'undefined') {
      return;
    }

    let permission = Notification.permission;

    if (permission === 'default') {
      permission = await Notification.requestPermission();
    }

    if (permission !== 'granted') {
      return;
    }

    const notification = new Notification('Incoming call', {
      body: `${displayName} is calling you`
    });

    notification.onclick = () => {
      window.focus();
      void this.router.navigate(['/call', callId]);
    };
  }

  private uniqueParticipants(participants: DirectMessageParticipant[]): DirectMessageParticipant[] {
    const seen = new Set<string>();

    return participants.filter((participant) => {
      if (seen.has(participant.userId)) {
        return false;
      }

      seen.add(participant.userId);
      return true;
    });
  }

  private uniqueIds(ids: string[]): string[] {
    return ids.filter((id, index) => !!id && ids.indexOf(id) === index);
  }

  private userKey(user: User): string {
    return user.oderId || user.id;
  }

  private currentUserId(): string | null {
    const user = this.currentUser();

    return user ? this.userKey(user) : null;
  }

  private requireCurrentUser(): User {
    const user = this.currentUser();

    if (!user) {
      throw new Error('Cannot use calls without a current user.');
    }

    return user;
  }
}
