/* eslint-disable @typescript-eslint/member-ordering */
import {
  Component,
  DestroyRef,
  computed,
  effect,
  inject,
  signal
} from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Store } from '@ngrx/store';
import { NavigationEnd, Router } from '@angular/router';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucidePhone, lucidePlus } from '@ng-icons/lucide';
import {
  EMPTY,
  Subject,
  catchError,
  filter,
  firstValueFrom,
  from,
  map,
  switchMap,
  tap
} from 'rxjs';

import { Room, User } from '../../../shared-kernel';
import { buildLoginReturnQueryParams } from '../../../domains/authentication/domain/logic/auth-navigation.rules';
import { UserBarComponent } from '../../../domains/authentication/feature/user-bar/user-bar.component';
import { VoiceSessionFacade } from '../../../domains/voice-session';
import { selectSavedRooms, selectCurrentRoom } from '../../../store/rooms/rooms.selectors';
import { selectCurrentUser, selectOnlineUsers } from '../../../store/users/users.selectors';
import { RoomsActions } from '../../../store/rooms/rooms.actions';
import { DatabaseService } from '../../../infrastructure/persistence';
import { NotificationsFacade } from '../../../domains/notifications';
import { DirectCallService, DirectCallSession } from '../../../domains/direct-call';
import { DmRailComponent } from '../../../domains/direct-message/feature/dm-rail/dm-rail.component';
import { type ServerInfo, ServerDirectoryFacade } from '../../../domains/server-directory';
import { CreateServerDialogComponent } from '../../../domains/server-directory/feature/create-server-dialog/create-server-dialog.component';
import { ThemeNodeDirective } from '../../../domains/theme';
import { hasRoomBanForUser } from '../../../domains/access-control';
import {
  ConfirmDialogComponent,
  ContextMenuComponent,
  LeaveServerDialogComponent
} from '../../../shared';
import { AppI18nService, APP_TRANSLATE_IMPORTS } from '../../../core/i18n';

const ACTIVATION_DEBOUNCE_MS = 150;

@Component({
  selector: 'app-servers-rail',
  standalone: true,
  imports: [
    CommonModule,
    FormsModule,
    NgIcon,
    ConfirmDialogComponent,
    ContextMenuComponent,
    CreateServerDialogComponent,
    DmRailComponent,
    LeaveServerDialogComponent,
    ThemeNodeDirective,
    UserBarComponent,
    ...APP_TRANSLATE_IMPORTS
  ],
  viewProviders: [provideIcons({ lucidePhone, lucidePlus })],
  templateUrl: './servers-rail.component.html'
})
export class ServersRailComponent {
  private readonly appI18n = inject(AppI18nService);
  private store = inject(Store);
  private router = inject(Router);
  private voiceSession = inject(VoiceSessionFacade);
  private db = inject(DatabaseService);
  private notifications = inject(NotificationsFacade);
  readonly directCalls = inject(DirectCallService);
  private serverDirectory = inject(ServerDirectoryFacade);
  private destroyRef = inject(DestroyRef);
  private banLookupRequestVersion = 0;
  private bannedLookupUserKey: string | null = null;
  private activationRequestVersion = 0;
  private activationTimer: ReturnType<typeof window.setTimeout> | null = null;
  private joinRequestVersion = 0;
  private joinRequestTimer: ReturnType<typeof window.setTimeout> | null = null;
  private visibleSavedRoomCache: Room[] = [];
  private savedRoomJoinRequests = new Subject<{ room: Room; password?: string }>();
  savedRooms = this.store.selectSignal(selectSavedRooms);
  currentRoom = this.store.selectSignal(selectCurrentRoom);

  showMenu = signal(false);
  menuX = signal(72);
  menuY = signal(100);
  contextRoom = signal<Room | null>(null);
  optimisticSelectedRoomId = signal<string | null>(null);
  showLeaveConfirm = signal(false);
  showCreateDialog = signal(false);
  currentUser = this.store.selectSignal(selectCurrentUser);
  onlineUsers = this.store.selectSignal(selectOnlineUsers);
  bannedRoomLookup = signal<Record<string, boolean>>({});
  isOnSearch = toSignal(
    this.router.events.pipe(
      filter((navigationEvent): navigationEvent is NavigationEnd => navigationEvent instanceof NavigationEnd),
      map((navigationEvent) => navigationEvent.urlAfterRedirects.startsWith('/search'))
    ),
    { initialValue: this.router.url.startsWith('/search') }
  );
  isOnDirectMessage = toSignal(
    this.router.events.pipe(
      filter((navigationEvent): navigationEvent is NavigationEnd => navigationEvent instanceof NavigationEnd),
      map((navigationEvent) => this.isDirectMessageUrl(navigationEvent.urlAfterRedirects))
    ),
    { initialValue: this.isDirectMessageUrl(this.router.url) }
  );
  isOnDashboard = toSignal(
    this.router.events.pipe(
      filter((navigationEvent): navigationEvent is NavigationEnd => navigationEvent instanceof NavigationEnd),
      map((navigationEvent) => navigationEvent.urlAfterRedirects.startsWith('/dashboard'))
    ),
    { initialValue: this.router.url.startsWith('/dashboard') }
  );
  isOnCall = toSignal(
    this.router.events.pipe(
      filter((navigationEvent): navigationEvent is NavigationEnd => navigationEvent instanceof NavigationEnd),
      map((navigationEvent) => navigationEvent.urlAfterRedirects.startsWith('/call/'))
    ),
    { initialValue: this.router.url.startsWith('/call/') }
  );
  currentCallId = toSignal(
    this.router.events.pipe(
      filter((navigationEvent): navigationEvent is NavigationEnd => navigationEvent instanceof NavigationEnd),
      map((navigationEvent) => this.callIdFromUrl(navigationEvent.urlAfterRedirects))
    ),
    { initialValue: this.callIdFromUrl(this.router.url) }
  );
  selectedCallIndex = computed(() => {
    const routeCallId = this.currentCallId();
    const visibleCalls = this.directCalls.visibleActiveSessions();

    if (routeCallId) {
      const routeMatchIndex = visibleCalls.findIndex((call) => call.callId === routeCallId || call.conversationId === routeCallId);

      if (routeMatchIndex >= 0) {
        return routeMatchIndex;
      }
    }

    const currentSession = this.directCalls.currentSession();

    if (!currentSession) {
      return -1;
    }

    return visibleCalls.findIndex((call) => call.callId === currentSession.callId);
  });
  bannedServerName = signal('');
  showBannedDialog = signal(false);
  showPasswordDialog = signal(false);
  passwordPromptRoom = signal<Room | null>(null);
  joinPassword = signal('');
  joinPasswordError = signal<string | null>(null);
  visibleSavedRooms = computed(() => this.stabilizeVisibleSavedRooms(this.savedRooms().filter((room) => !this.isRoomMarkedBanned(room))));
  voicePresenceByRoom = computed(() => {
    const presence: Record<string, number> = {};
    const seenByRoom = new Map<string, Set<string>>();
    const addVoicePresence = (user: User | null | undefined): void => {
      if (!user) {
        return;
      }

      const voiceState = user?.voiceState;
      const roomId = voiceState?.serverId;

      if (!voiceState?.isConnected || !roomId) {
        return;
      }

      const userKey = user.oderId || user.id;

      let seenUsers = seenByRoom.get(roomId);

      if (!seenUsers) {
        seenUsers = new Set<string>();
        seenByRoom.set(roomId, seenUsers);
      }

      if (seenUsers.has(userKey)) {
        return;
      }

      seenUsers.add(userKey);
      presence[roomId] = (presence[roomId] ?? 0) + 1;
    };

    for (const user of this.onlineUsers()) {
      addVoicePresence(user);
    }

    addVoicePresence(this.currentUser());

    return presence;
  });

  constructor() {
    effect(() => {
      const rooms = this.savedRooms();
      const currentUser = this.currentUser();

      void this.refreshBannedLookup(rooms, currentUser ?? null);
    });

    effect(() => {
      const optimisticRoomId = this.optimisticSelectedRoomId();

      if (!optimisticRoomId) {
        return;
      }

      if (this.currentRoom()?.id === optimisticRoomId && !this.isOnDirectMessage() && !this.isOnCall()) {
        this.optimisticSelectedRoomId.set(null);
      }
    });

    this.savedRoomJoinRequests
      .pipe(
        switchMap(({ room, password }) => this.requestJoinInBackground(room, password)),
        takeUntilDestroyed(this.destroyRef)
      )
      .subscribe();

    this.destroyRef.onDestroy(() => {
      if (this.activationTimer) {
        window.clearTimeout(this.activationTimer);
      }

      if (this.joinRequestTimer) {
        window.clearTimeout(this.joinRequestTimer);
      }
    });
  }

  initial(name?: string): string {
    if (!name)
      return '?';

    const ch = name.trim()[0]?.toUpperCase();

    return ch || '?';
  }

  trackRoomId = (index: number, room: Room) => room.id;

  goToDashboard(): void {
    const voiceServerId = this.voiceSession.getVoiceServerId();

    this.optimisticSelectedRoomId.set(null);

    if (voiceServerId) {
      this.voiceSession.setViewingVoiceServer(false);
    }

    this.router.navigate(['/dashboard']);
  }

  openCreateDialog(): void {
    this.showCreateDialog.set(true);
  }

  closeCreateDialog(): void {
    this.showCreateDialog.set(false);
  }

  joinSavedRoom(room: Room): void {
    const targetRoom = this.savedRooms().find((savedRoom) => savedRoom.id === room.id) ?? room;
    const currentUserId = localStorage.getItem('metoyou_currentUserId');

    if (!currentUserId) {
      this.router.navigate(['/login'], {
        queryParams: buildLoginReturnQueryParams(this.router.url)
      });

      return;
    }

    if (this.isRoomMarkedBanned(targetRoom)) {
      this.bannedServerName.set(targetRoom.name);
      this.showBannedDialog.set(true);
      return;
    }

    this.optimisticSelectedRoomId.set(targetRoom.id);
    this.queueSavedRoomActivation(targetRoom);
    this.queueSavedRoomJoin(targetRoom);
  }

  openCall(callId: string): void {
    this.optimisticSelectedRoomId.set(null);
    void this.directCalls.openCallView(callId);
  }

  isSelectedCall(callIndex: number): boolean {
    return this.selectedCallIndex() === callIndex;
  }

  callAvatarUrls(call: DirectCallSession): string[] {
    if (call.participantIds.length <= 2) {
      return [];
    }

    return Object.values(call.participants)
      .filter((participant) => participant.joined)
      .map((participant) => this.directCalls.userForParticipant(participant.userId)?.avatarUrl || participant.profile.avatarUrl)
      .filter((avatarUrl): avatarUrl is string => !!avatarUrl)
      .slice(0, 3);
  }

  closeBannedDialog(): void {
    this.showBannedDialog.set(false);
    this.bannedServerName.set('');
  }

  closePasswordDialog(): void {
    this.showPasswordDialog.set(false);
    this.passwordPromptRoom.set(null);
    this.joinPassword.set('');
    this.joinPasswordError.set(null);
  }

  confirmPasswordJoin(): void {
    const room = this.passwordPromptRoom();

    if (!room)
      return;

    this.joinPasswordError.set(null);
    this.savedRoomJoinRequests.next({ room, password: this.joinPassword() });
  }

  isRoomMarkedBanned(room: Room): boolean {
    return !!this.bannedRoomLookup()[room.id];
  }

  private callIdFromUrl(url: string): string | null {
    const path = url.split(/[?#]/, 1)[0];
    const match = path.match(/^\/call\/([^/]+)/);

    return match?.[1] ? decodeURIComponent(match[1]) : null;
  }

  openContextMenu(evt: MouseEvent, room: Room): void {
    evt.preventDefault();
    this.contextRoom.set(room);
    this.menuX.set(Math.max(evt.clientX + 8, 72));
    this.menuY.set(evt.clientY);
    this.showMenu.set(true);
  }

  closeMenu(): void {
    this.showMenu.set(false);
  }

  isCurrentContextRoom(): boolean {
    const ctx = this.contextRoom();
    const cur = this.currentRoom();

    return !!ctx && !!cur && ctx.id === cur.id;
  }

  openLeaveConfirm(): void {
    this.closeMenu();

    if (this.contextRoom()) {
      this.showLeaveConfirm.set(true);
    }
  }

  confirmLeave(result: { nextOwnerKey?: string }): void {
    const ctx = this.contextRoom();

    if (!ctx)
      return;

    const isCurrentRoom = this.currentRoom()?.id === ctx.id;

    this.store.dispatch(
      RoomsActions.forgetRoom({
        roomId: ctx.id,
        nextOwnerKey: result.nextOwnerKey
      })
    );

    if (isCurrentRoom) {
      this.optimisticSelectedRoomId.set(null);
      this.router.navigate(['/search']);
    }

    this.showLeaveConfirm.set(false);
    this.contextRoom.set(null);
  }

  cancelLeave(): void {
    this.showLeaveConfirm.set(false);
  }

  toggleRoomNotifications(): void {
    const room = this.contextRoom();

    if (!room) {
      return;
    }

    this.notifications.setRoomMuted(room.id, !this.notifications.isRoomMuted(room.id));
    this.closeMenu();
  }

  isRoomNotificationsMuted(roomId: string): boolean {
    return this.notifications.isRoomMuted(roomId);
  }

  roomUnreadCount(roomId: string): number {
    return this.notifications.roomUnreadCount(roomId);
  }

  voicePresenceCount(roomId: string): number {
    return this.voicePresenceByRoom()[roomId] ?? 0;
  }

  voicePresenceTitle(roomId: string): string {
    const count = this.voicePresenceCount(roomId);

    return this.appI18n.instant(
      count === 1 ? 'serversRail.voicePresence.oneUser' : 'serversRail.voicePresence.manyUsers',
      { count }
    );
  }

  formatUnreadCount(count: number): string {
    return count > 99 ? '99+' : String(count);
  }

  isSelectedRoom(room: Room): boolean {
    if (this.isOnDirectMessage() || this.isOnCall() || this.isOnDashboard()) {
      return false;
    }

    const optimisticRoomId = this.optimisticSelectedRoomId();

    if (optimisticRoomId) {
      return optimisticRoomId === room.id;
    }

    return this.currentRoom()?.id === room.id;
  }

  private stabilizeVisibleSavedRooms(nextRooms: Room[]): Room[] {
    const previousById = new Map(this.visibleSavedRoomCache.map((room) => [room.id, room]));
    const stabilizedRooms = nextRooms.map((room) => {
      const previousRoom = previousById.get(room.id);

      return previousRoom && this.hasSameRailRoomView(previousRoom, room) ? previousRoom : room;
    });

    if (
      stabilizedRooms.length === this.visibleSavedRoomCache.length
      && stabilizedRooms.every((room, index) => room === this.visibleSavedRoomCache[index])
    ) {
      return this.visibleSavedRoomCache;
    }

    this.visibleSavedRoomCache = stabilizedRooms;
    return stabilizedRooms;
  }

  private hasSameRailRoomView(previousRoom: Room, nextRoom: Room): boolean {
    return previousRoom.id === nextRoom.id && previousRoom.name === nextRoom.name && previousRoom.icon === nextRoom.icon;
  }

  private isDirectMessageUrl(url: string): boolean {
    const path = url.split(/[?#]/, 1)[0];

    return path === '/dm' || path.startsWith('/dm/') || path === '/pm' || path.startsWith('/pm/');
  }

  private async refreshBannedLookup(rooms: Room[], currentUser: User | null): Promise<void> {
    const requestVersion = ++this.banLookupRequestVersion;

    if (!currentUser || rooms.length === 0) {
      this.bannedLookupUserKey = null;
      this.bannedRoomLookup.set({});
      return;
    }

    const persistedUserId = localStorage.getItem('metoyou_currentUserId');
    const userKey = `${currentUser.id}:${currentUser.oderId}:${persistedUserId ?? ''}`;
    const roomIds = new Set(rooms.map((room) => room.id));

    if (this.bannedLookupUserKey !== userKey) {
      this.bannedLookupUserKey = userKey;
      this.bannedRoomLookup.set({});
    }

    const currentLookup = this.bannedRoomLookup();
    const nextLookup = Object.fromEntries(
      Object.entries(currentLookup).filter(([roomId]) => roomIds.has(roomId))
    );
    const roomsToLookup = rooms.filter((room) => nextLookup[room.id] === undefined);

    if (roomsToLookup.length === 0) {
      if (Object.keys(nextLookup).length !== Object.keys(currentLookup).length) {
        this.bannedRoomLookup.set(nextLookup);
      }

      return;
    }

    const entries = await Promise.all(
      roomsToLookup.map(async (room) => {
        const bans = await this.db.getBansForRoom(room.id);

        return [room.id, hasRoomBanForUser(bans, currentUser, persistedUserId)] as const;
      })
    );

    if (requestVersion !== this.banLookupRequestVersion) {
      return;
    }

    this.bannedRoomLookup.set({
      ...nextLookup,
      ...Object.fromEntries(entries)
    });
  }

  private prepareVoiceContext(room: Room): void {
    const voiceServerId = this.voiceSession.getVoiceServerId();

    if (voiceServerId && voiceServerId !== room.id) {
      this.voiceSession.setViewingVoiceServer(false);
    } else if (voiceServerId === room.id) {
      this.voiceSession.setViewingVoiceServer(true);
    }
  }

  private activateSavedRoom(room: Room): void {
    this.prepareVoiceContext(room);
    this.closePasswordDialog();
    this.store.dispatch(RoomsActions.setSignalServerReconnecting({ isReconnecting: false }));
    this.store.dispatch(RoomsActions.viewServer({ room, skipBanCheck: true }));
  }

  private queueSavedRoomActivation(room: Room): void {
    const requestVersion = ++this.activationRequestVersion;

    if (this.activationTimer) {
      window.clearTimeout(this.activationTimer);
    }

    this.activationTimer = window.setTimeout(() => {
      if (requestVersion !== this.activationRequestVersion) {
        return;
      }

      this.activationTimer = null;
      this.activateSavedRoom(room);
    }, ACTIVATION_DEBOUNCE_MS);
  }

  private queueSavedRoomJoin(room: Room): void {
    const requestVersion = ++this.joinRequestVersion;

    if (this.joinRequestTimer) {
      window.clearTimeout(this.joinRequestTimer);
    }

    this.joinRequestTimer = window.setTimeout(() => {
      if (requestVersion !== this.joinRequestVersion) {
        return;
      }

      this.joinRequestTimer = null;
      this.savedRoomJoinRequests.next({ room });
    }, ACTIVATION_DEBOUNCE_MS);
  }

  private requestJoinInBackground(room: Room, password?: string) {
    const currentUserId = localStorage.getItem('metoyou_currentUserId');
    const currentUser = this.currentUser();

    if (!currentUserId)
      return EMPTY;

    this.joinPasswordError.set(null);

    return from(this.resolveRoomJoinTarget(room)).pipe(
      switchMap((joinTarget) => {
        if (!joinTarget.selector) {
          if (this.currentRoom()?.id === room.id) {
            this.store.dispatch(RoomsActions.setSignalServerReconnecting({ isReconnecting: true }));
          }

          return EMPTY;
        }

        return this.serverDirectory
          .requestJoin(
            {
              roomId: room.id,
              userId: currentUserId,
              userPublicKey: currentUser?.oderId || currentUserId,
              displayName: currentUser?.displayName || 'Anonymous',
              password: password?.trim() || undefined
            },
            joinTarget.selector
          )
          .pipe(
            tap((response) => {
              this.closePasswordDialog();
              this.store.dispatch(
                RoomsActions.updateRoom({
                  roomId: room.id,
                  changes: this.toRoomRefreshChanges(joinTarget.room, response.server, response.signalingUrl)
                })
              );

              if (this.currentRoom()?.id === room.id) {
                this.store.dispatch(RoomsActions.setSignalServerReconnecting({ isReconnecting: false }));
              }
            })
          );
      }),
      catchError((error: unknown) => {
        this.handleBackgroundJoinError(room, error);
        return EMPTY;
      })
    );
  }

  private handleBackgroundJoinError(room: Room, error: unknown): void {
    const serverError = error as {
      error?: { error?: string; errorCode?: string };
      status?: number;
    };
    const errorCode = serverError?.error?.errorCode;
    const message = serverError?.error?.error || this.appI18n.instant('serversRail.joinFailed');

    if (errorCode === 'PASSWORD_REQUIRED') {
      this.passwordPromptRoom.set(room);
      this.showPasswordDialog.set(true);
      this.joinPasswordError.set(message);
      return;
    }

    if (errorCode === 'BANNED') {
      this.closePasswordDialog();
      this.optimisticSelectedRoomId.set(null);
      this.bannedRoomLookup.update((lookup) => ({
        ...lookup,
        [room.id]: true
      }));

      this.bannedServerName.set(room.name);
      this.showBannedDialog.set(true);
      return;
    }

    if (this.shouldFallbackToOfflineView(error) && this.currentRoom()?.id === room.id) {
      this.store.dispatch(RoomsActions.setSignalServerReconnecting({ isReconnecting: true }));
    }
  }

  private shouldFallbackToOfflineView(error: unknown): boolean {
    const serverError = error as {
      error?: { errorCode?: string };
      status?: number;
    };
    const errorCode = serverError?.error?.errorCode;
    const status = serverError?.status;

    return errorCode === 'SERVER_NOT_FOUND' || status === 0 || status === 404 || (typeof status === 'number' && status >= 500);
  }

  private toRoomRefreshChanges(room: Room, server: ServerInfo, signalingUrl?: string): Partial<Room> {
    const resolvedSource = this.serverDirectory.normaliseRoomSignalSource(
      {
        sourceId: server.sourceId ?? room.sourceId,
        sourceName: server.sourceName ?? room.sourceName,
        sourceUrl: server.sourceUrl ?? room.sourceUrl,
        signalingUrl,
        fallbackName: server.sourceName ?? room.sourceName ?? room.name
      },
      {
        ensureEndpoint: true
      }
    );

    return {
      name: server.name,
      description: server.description,
      topic: server.topic ?? room.topic,
      hostId: server.ownerId || room.hostId,
      userCount: server.userCount,
      maxUsers: server.maxUsers,
      hasPassword:
        typeof server.hasPassword === 'boolean' ? server.hasPassword : typeof room.hasPassword === 'boolean' ? room.hasPassword : !!room.password,
      isPrivate: server.isPrivate,
      createdAt: server.createdAt,
      channels: Array.isArray(server.channels) && server.channels.length > 0 ? server.channels : room.channels,
      ...resolvedSource
    };
  }

  private async resolveRoomJoinTarget(room: Room): Promise<{
    room: Room;
    selector: ReturnType<ServerDirectoryFacade['buildRoomSignalSelector']>;
  }> {
    let resolvedRoom = this.applyResolvedRoomSource(
      room,
      this.serverDirectory.normaliseRoomSignalSource(
        {
          sourceId: room.sourceId,
          sourceName: room.sourceName,
          sourceUrl: room.sourceUrl,
          fallbackName: room.sourceName ?? room.name
        },
        {
          ensureEndpoint: !!room.sourceUrl
        }
      )
    );
    let selector = this.serverDirectory.buildRoomSignalSelector(
      {
        sourceId: resolvedRoom.sourceId,
        sourceName: resolvedRoom.sourceName,
        sourceUrl: resolvedRoom.sourceUrl,
        fallbackName: resolvedRoom.sourceName ?? resolvedRoom.name
      },
      {
        ensureEndpoint: !!resolvedRoom.sourceUrl
      }
    );

    const authoritativeServer = selector ? await firstValueFrom(this.serverDirectory.getServer(room.id, selector)) : null;

    if (!authoritativeServer) {
      return {
        room: resolvedRoom,
        selector
      };
    }

    const authoritativeSource = this.serverDirectory.normaliseRoomSignalSource(
      {
        sourceId: authoritativeServer.sourceId ?? resolvedRoom.sourceId,
        sourceName: authoritativeServer.sourceName ?? resolvedRoom.sourceName,
        sourceUrl: authoritativeServer.sourceUrl ?? resolvedRoom.sourceUrl,
        fallbackName: authoritativeServer.sourceName ?? resolvedRoom.sourceName ?? resolvedRoom.name
      },
      {
        ensureEndpoint: !!(authoritativeServer.sourceUrl ?? resolvedRoom.sourceUrl)
      }
    );

    resolvedRoom = this.applyResolvedRoomSource(resolvedRoom, authoritativeSource);
    selector = this.serverDirectory.buildRoomSignalSelector(
      {
        sourceId: resolvedRoom.sourceId,
        sourceName: resolvedRoom.sourceName,
        sourceUrl: resolvedRoom.sourceUrl,
        fallbackName: resolvedRoom.sourceName ?? resolvedRoom.name
      },
      {
        ensureEndpoint: !!resolvedRoom.sourceUrl
      }
    );

    return {
      room: resolvedRoom,
      selector
    };
  }

  private applyResolvedRoomSource(room: Room, source: Pick<Room, 'sourceId' | 'sourceName' | 'sourceUrl'>): Room {
    const nextRoom: Room = {
      ...room,
      sourceId: source.sourceId,
      sourceName: source.sourceName,
      sourceUrl: source.sourceUrl
    };

    if (room.sourceId === nextRoom.sourceId && room.sourceName === nextRoom.sourceName && room.sourceUrl === nextRoom.sourceUrl) {
      return room;
    }

    this.store.dispatch(
      RoomsActions.updateRoom({
        roomId: room.id,
        changes: {
          sourceId: nextRoom.sourceId,
          sourceName: nextRoom.sourceName,
          sourceUrl: nextRoom.sourceUrl
        }
      })
    );

    return nextRoom;
  }
}
