/* eslint-disable @typescript-eslint/member-ordering */
import {
  Component,
  DestroyRef,
  inject,
  computed,
  effect,
  input,
  OnDestroy,
  output,
  signal
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
  lucideMessageSquare,
  lucideMic,
  lucideMicOff,
  lucideChevronLeft,
  lucideAlertTriangle,
  lucideMonitor,
  lucideVideo,
  lucideHash,
  lucideUsers,
  lucidePlus,
  lucideVolumeX,
  lucideGamepad2,
  lucidePackage
} from '@ng-icons/lucide';
import { selectOnlineUsers, selectCurrentUser } from '../../../store/users/users.selectors';
import { SignalServerAuthService } from '../../../domains/authentication/application/services/signal-server-auth.service';
import { isSelfPresenceUserId } from '../../../domains/authentication/domain/logic/self-presence-identity.rules';
import {
  selectCurrentRoom,
  selectActiveChannelId,
  selectTextChannels,
  selectVoiceChannels,
  selectIsConnecting
} from '../../../store/rooms/rooms.selectors';
import { selectMessagesLoading } from '../../../store/messages/messages.selectors';
import { UsersActions } from '../../../store/users/users.actions';
import { RoomsActions } from '../../../store/rooms/rooms.actions';
import { MessagesActions } from '../../../store/messages/messages.actions';
import { RealtimeSessionFacade } from '../../../core/realtime';
import { ScreenShareFacade } from '../../../domains/screen-share';
import { NotificationsFacade } from '../../../domains/notifications';
import { ThemeNodeDirective } from '../../../domains/theme';
import {
  VoiceActivityService,
  VoiceConnectionFacade,
  VoiceConnectivityHealthService
} from '../../../domains/voice-connection';
import {
  VoiceSessionFacade,
  VoiceWorkspaceService,
  isLocalVoiceOwner,
  isVoiceOnAnotherClient
} from '../../../domains/voice-session';
import { DirectMessageService } from '../../../domains/direct-message';
import { DirectCallService } from '../../../domains/direct-call';
import { VoicePlaybackService } from '../../../domains/voice-connection';
import { formatGameActivityElapsed } from '../../../domains/game-activity';
import { ExternalLinkService } from '../../../core/platform/external-link.service';
import { VoiceControlsComponent } from '../../../domains/voice-session/feature/voice-controls/voice-controls.component';
import { PluginRenderHostComponent } from '../../../domains/plugins/feature/plugin-render-host/plugin-render-host.component';
import { PluginActionMenuService, PluginUiRegistryService } from '../../../domains/plugins';
import { isChannelNameTaken, normalizeChannelName } from '../../../store/rooms/room-channels.rules';
import {
  canManageMember,
  resolveRoomPermission,
  setRoleAssignmentsForMember,
  SYSTEM_ROLE_IDS
} from '../../../domains/access-control';
import {
  ContextMenuComponent,
  UserAvatarComponent,
  ConfirmDialogComponent,
  UserVolumeMenuComponent,
  ProfileCardService,
  SkeletonComponent,
  SkeletonListComponent
} from '../../../shared';
import {
  Channel,
  ChatEvent,
  GameActivity,
  RoomMember,
  Room,
  User
} from '../../../shared-kernel';
import { v4 as uuidv4 } from 'uuid';
import { visibilityAwareInterval$ } from '../../../shared/rxjs';
import { APP_TRANSLATE_IMPORTS, AppI18nService } from '../../../core/i18n';
import { AutoFocusDirective, SelectOnFocusDirective } from '../../../shared/directives';

type PanelMode = 'channels' | 'users';

const SKELETON_REVEAL_DELAY_MS = 180;

@Component({
  selector: 'app-rooms-side-panel',
  standalone: true,
  imports: [
    CommonModule,
    FormsModule,
    NgIcon,
    VoiceControlsComponent,
    ContextMenuComponent,
    UserVolumeMenuComponent,
    UserAvatarComponent,
    ConfirmDialogComponent,
    PluginRenderHostComponent,
    ThemeNodeDirective,
    SkeletonComponent,
    SkeletonListComponent,
    AutoFocusDirective,
    SelectOnFocusDirective,
    ...APP_TRANSLATE_IMPORTS
  ],
  viewProviders: [
    provideIcons({
      lucideMessageSquare,
      lucideMic,
      lucideMicOff,
      lucideChevronLeft,
      lucideAlertTriangle,
      lucideMonitor,
      lucideVideo,
      lucideHash,
      lucideUsers,
      lucidePlus,
      lucideVolumeX,
      lucideGamepad2,
      lucidePackage
    })
  ],
  templateUrl: './rooms-side-panel.component.html'
})
export class RoomsSidePanelComponent implements OnDestroy {
  private store = inject(Store);
  private signalServerAuth = inject(SignalServerAuthService);
  private router = inject(Router);
  private realtime = inject(RealtimeSessionFacade);
  private voiceConnection = inject(VoiceConnectionFacade);
  private screenShare = inject(ScreenShareFacade);
  private notifications = inject(NotificationsFacade);
  private voiceSessionService = inject(VoiceSessionFacade);
  private voiceWorkspace = inject(VoiceWorkspaceService);
  private voicePlayback = inject(VoicePlaybackService);
  private directCalls = inject(DirectCallService);
  private profileCard = inject(ProfileCardService);
  private directMessages = inject(DirectMessageService);
  private readonly externalLinks = inject(ExternalLinkService);
  private readonly pluginActionMenu = inject(PluginActionMenuService);
  private readonly voiceActivity = inject(VoiceActivityService);
  private readonly voiceConnectivity = inject(VoiceConnectivityHealthService);
  private readonly pluginUi = inject(PluginUiRegistryService);
  private readonly appI18n = inject(AppI18nService);
  private profileCardOpenTimer: ReturnType<typeof setTimeout> | null = null;
  private skeletonRevealTimer: ReturnType<typeof setTimeout> | null = null;
  private readonly destroyRef = inject(DestroyRef);

  readonly panelMode = input<PanelMode>('channels');
  readonly showVoiceControls = input(true);
  readonly textChannelSelected = output<string>();
  showFloatingControls = this.voiceSessionService.showFloatingControls;
  isVoiceWorkspaceExpanded = this.voiceWorkspace.isExpanded;
  onlineUsers = this.store.selectSignal(selectOnlineUsers);
  currentUser = this.store.selectSignal(selectCurrentUser);
  currentRoom = this.store.selectSignal(selectCurrentRoom);
  isConnecting = this.store.selectSignal(selectIsConnecting);
  messagesLoading = this.store.selectSignal(selectMessagesLoading);
  activeChannelId = this.store.selectSignal(selectActiveChannelId);
  textChannels = this.store.selectSignal(selectTextChannels);
  voiceChannels = this.store.selectSignal(selectVoiceChannels);
  pluginChannelSections = this.pluginUi.channelSectionRecords;
  pluginMenuActions = this.pluginUi.toolbarActionRecords;
  pluginSidePanels = this.pluginUi.sidePanelRecords;
  panelHydrating = computed(() => this.panelMode() === 'channels' && (this.isConnecting() || this.messagesLoading()));
  delayedPanelHydrating = signal(false);
  showTextChannelSkeleton = computed(() => this.delayedPanelHydrating() && this.textChannels().length === 0);
  showVoiceChannelSkeleton = computed(() => this.delayedPanelHydrating() && this.voiceEnabled() && this.voiceChannels().length === 0);
  showPluginSkeleton = computed(() => this.delayedPanelHydrating() && !this.hasPluginPanelContent());
  localUserHasDesync = this.voiceConnectivity.localUserHasDesync;
  roomMembers = computed(() => this.currentRoom()?.members ?? []);
  roomMemberIdentifiers = computed(() => {
    const identifiers = new Set<string>();

    for (const member of this.roomMembers()) {
      this.addIdentifiers(identifiers, member);
    }

    return identifiers;
  });
  onlineRoomUsers = computed(() => {
    const memberIdentifiers = this.roomMemberIdentifiers();
    const roomId = this.currentRoom()?.id;

    return this.onlineUsers().filter(
      (user) => !this.isCurrentUserIdentity(user) && this.matchesIdentifiers(memberIdentifiers, user) && this.isUserPresentInRoom(user, roomId)
    );
  });
  offlineRoomMembers = computed(() => {
    const onlineIdentifiers = new Set<string>();

    for (const user of this.onlineRoomUsers()) {
      this.addIdentifiers(onlineIdentifiers, user);
    }

    this.addSelfPresenceIdentifiers(onlineIdentifiers);

    return this.roomMembers().filter((member) => !this.matchesIdentifiers(onlineIdentifiers, member));
  });
  knownUserCount = computed(() => {
    const memberIds = new Set(
      this.roomMembers()
        .map((member) => this.roomMemberKey(member))
        .filter(Boolean)
    );
    const current = this.currentUser();

    if (current) {
      memberIds.add(current.oderId || current.id);
    }

    return memberIds.size;
  });

  private hasPluginPanelContent(): boolean {
    return this.pluginChannelSections().length > 0 || this.pluginMenuActions().length > 0 || this.pluginSidePanels().length > 0;
  }

  showChannelMenu = signal(false);
  channelMenuX = signal(0);
  channelMenuY = signal(0);
  contextChannel = signal<Channel | null>(null);

  renamingChannelId = signal<string | null>(null);
  channelNameError = signal<string | null>(null);

  showCreateChannelDialog = signal(false);
  createChannelType = signal<'text' | 'voice'>('text');
  newChannelName = '';

  showUserMenu = signal(false);
  userMenuX = signal(0);
  userMenuY = signal(0);
  contextMenuUser = signal<User | null>(null);

  showVolumeMenu = signal(false);
  volumeMenuX = signal(0);
  volumeMenuY = signal(0);
  volumeMenuPeerId = signal('');
  volumeMenuDisplayName = signal('');
  draggedVoiceUserId = signal<string | null>(null);
  dragTargetVoiceChannelId = signal<string | null>(null);
  activityNow = signal(Date.now());

  constructor() {
    effect(() => {
      if (!this.panelHydrating()) {
        this.clearSkeletonRevealTimer();
        this.delayedPanelHydrating.set(false);
        return;
      }

      if (this.delayedPanelHydrating() || this.skeletonRevealTimer) {
        return;
      }

      this.skeletonRevealTimer = setTimeout(() => {
        this.skeletonRevealTimer = null;

        if (this.panelHydrating()) {
          this.delayedPanelHydrating.set(true);
        }
      }, SKELETON_REVEAL_DELAY_MS);
    });

    visibilityAwareInterval$(1_000)
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => this.activityNow.set(Date.now()));
  }

  ngOnDestroy(): void {
    this.clearSkeletonRevealTimer();
    this.cancelQueuedProfileCardOpen();
    this.pluginActionMenu.close();
  }

  private clearSkeletonRevealTimer(): void {
    if (!this.skeletonRevealTimer) {
      return;
    }

    clearTimeout(this.skeletonRevealTimer);
    this.skeletonRevealTimer = null;
  }

  gameActivityElapsed(user: User | null | undefined): string {
    const activity = user?.gameActivity;

    return activity ? formatGameActivityElapsed(activity.startedAt, this.activityNow()) : '';
  }

  openGameStore(event: Event, activity: GameActivity): void {
    event.stopPropagation();

    if (activity.store?.url) {
      this.externalLinks.open(activity.store.url);
    }
  }

  openProfileCard(event: Event, user: User, editable: boolean): void {
    event.stopPropagation();
    const el = event.currentTarget as HTMLElement;

    this.profileCard.open(el, user, { placement: 'left', editable });
  }

  openUserCard(event: Event, user: User): void {
    event.stopPropagation();
    this.queueProfileCardOpen(event.currentTarget as HTMLElement, user, false);
  }

  openProfileCardForMember(event: Event, member: RoomMember): void {
    const user = this.roomMemberToUser(member);

    this.openProfileCard(event, user, false);
  }

  openMemberCard(event: Event, member: RoomMember): void {
    event.stopPropagation();
    this.queueProfileCardOpen(event.currentTarget as HTMLElement, this.roomMemberToUser(member), false);
  }

  openPluginActionMenu(event: Event): void {
    event.stopPropagation();
    this.cancelQueuedProfileCardOpen();
    this.pluginActionMenu.open(event.currentTarget as HTMLElement);
  }

  async openDirectMessage(event: Event, user: User): Promise<void> {
    event.stopPropagation();
    this.cancelQueuedProfileCardOpen();

    if (this.isCurrentUserIdentity(user)) {
      return;
    }

    const conversation = await this.directMessages.createConversation(user);

    await this.router.navigate(['/dm', conversation.id]);
  }

  async openDirectMessageForMember(event: Event, member: RoomMember): Promise<void> {
    await this.openDirectMessage(event, this.roomMemberToUser(member));
  }

  private roomMemberToUser(member: RoomMember): User {
    return {
      id: member.id,
      oderId: member.oderId || member.id,
      username: member.username,
      displayName: member.displayName,
      description: member.description,
      profileUpdatedAt: member.profileUpdatedAt,
      avatarUrl: member.avatarUrl,
      avatarHash: member.avatarHash,
      avatarMime: member.avatarMime,
      avatarUpdatedAt: member.avatarUpdatedAt,
      status: 'disconnected',
      role: member.role,
      joinedAt: member.joinedAt
    };
  }

  private roomMemberKey(member: RoomMember): string {
    return member.oderId || member.id;
  }

  private addIdentifiers(identifiers: Set<string>, entity: { id?: string; oderId?: string } | null | undefined): void {
    if (!entity)
      return;

    if (entity.id) {
      identifiers.add(entity.id);
    }

    if (entity.oderId) {
      identifiers.add(entity.oderId);
    }
  }

  private matchesIdentifiers(identifiers: Set<string>, entity: { id?: string; oderId?: string }): boolean {
    return !!((entity.id && identifiers.has(entity.id)) || (entity.oderId && identifiers.has(entity.oderId)));
  }

  private isUserPresentInRoom(entity: { presenceServerIds?: string[] }, roomId: string | undefined): boolean {
    if (!roomId || !Array.isArray(entity.presenceServerIds) || entity.presenceServerIds.length === 0) {
      return true;
    }

    return entity.presenceServerIds.includes(roomId);
  }

  private isCurrentUserIdentity(entity: { id?: string; oderId?: string }): boolean {
    const current = this.currentUser();

    if (!current) {
      return false;
    }

    const selfIds = this.signalServerAuth.resolveSelfPresenceUserIdsForRoom(
      current,
      this.currentRoom()?.sourceUrl
    );

    return isSelfPresenceUserId(entity.oderId, selfIds) || isSelfPresenceUserId(entity.id, selfIds);
  }

  private addSelfPresenceIdentifiers(identifiers: Set<string>): void {
    const current = this.currentUser();

    if (!current) {
      return;
    }

    this.addIdentifiers(identifiers, current);

    for (const selfId of this.signalServerAuth.resolveSelfPresenceUserIdsForRoom(
      current,
      this.currentRoom()?.sourceUrl
    )) {
      identifiers.add(selfId);
    }
  }

  private queueProfileCardOpen(anchor: HTMLElement, user: User, editable: boolean): void {
    this.cancelQueuedProfileCardOpen();
    this.profileCardOpenTimer = setTimeout(() => {
      this.profileCardOpenTimer = null;
      this.profileCard.open(anchor, user, { placement: 'left', editable });
    }, 180);
  }

  private cancelQueuedProfileCardOpen(): void {
    if (!this.profileCardOpenTimer) {
      return;
    }

    clearTimeout(this.profileCardOpenTimer);
    this.profileCardOpenTimer = null;
  }

  hasConnectivityIssue(user: User): boolean {
    return this.voiceConnectivity.hasPeerDesync(user.oderId || user.id);
  }

  canManageChannels(): boolean {
    const room = this.currentRoom();
    const user = this.currentUser();

    if (!room || !user)
      return false;

    return resolveRoomPermission(room, user, 'manageChannels');
  }

  selectTextChannel(channelId: string) {
    if (this.renamingChannelId())
      return;

    this.voiceWorkspace.showChat();
    this.store.dispatch(RoomsActions.selectChannel({ channelId }));
    this.textChannelSelected.emit(channelId);
  }

  openChannelContextMenu(evt: MouseEvent, channel: Channel) {
    evt.preventDefault();
    this.contextChannel.set(channel);
    this.channelMenuX.set(evt.clientX);
    this.channelMenuY.set(evt.clientY);
    this.showChannelMenu.set(true);
  }

  closeChannelMenu() {
    this.showChannelMenu.set(false);
  }

  startRename() {
    const ch = this.contextChannel();

    this.closeChannelMenu();
    this.channelNameError.set(null);

    if (ch) {
      this.renamingChannelId.set(ch.id);
    }
  }

  confirmRename(event: Event) {
    const input = event.target as HTMLInputElement;
    const name = normalizeChannelName(input.value);
    const channelId = this.renamingChannelId();

    if (!channelId) {
      return;
    }

    const validationError = this.getChannelNameError(name, channelId);

    if (validationError) {
      this.channelNameError.set(validationError);
      requestAnimationFrame(() => {
        input.focus();
        input.select();
      });

      return;
    }

    this.channelNameError.set(null);

    const currentName = this.currentRoom()?.channels?.find((channel) => channel.id === channelId)?.name;

    if (currentName !== name) {
      this.store.dispatch(RoomsActions.renameChannel({ channelId, name }));
    }

    this.renamingChannelId.set(null);
  }

  cancelRename() {
    this.channelNameError.set(null);
    this.renamingChannelId.set(null);
  }

  deleteChannel() {
    const ch = this.contextChannel();

    this.closeChannelMenu();

    if (ch) {
      this.store.dispatch(RoomsActions.removeChannel({ channelId: ch.id }));
    }
  }

  toggleChannelNotifications(): void {
    const channel = this.contextChannel();
    const roomId = this.currentRoom()?.id;

    this.closeChannelMenu();

    if (!channel || channel.type !== 'text' || !roomId) {
      return;
    }

    this.notifications.setChannelMuted(roomId, channel.id, !this.notifications.isChannelMuted(roomId, channel.id));
  }

  isContextChannelMuted(): boolean {
    const channel = this.contextChannel();
    const roomId = this.currentRoom()?.id;

    return !!channel && channel.type === 'text' && !!roomId && this.notifications.isChannelMuted(roomId, channel.id);
  }

  channelUnreadCount(channelId: string): number {
    const roomId = this.currentRoom()?.id;

    return roomId ? this.notifications.channelUnreadCount(roomId, channelId) : 0;
  }

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

  resyncMessages() {
    this.closeChannelMenu();
    const room = this.currentRoom();

    if (!room) {
      return;
    }

    this.store.dispatch(MessagesActions.startSync());

    const peers = this.realtime.getConnectedPeers();
    const inventoryRequest: ChatEvent = { type: 'chat-inventory-request', roomId: room.id };

    peers.forEach((pid) => {
      try {
        this.realtime.sendToPeer(pid, inventoryRequest);
      } catch {
        return;
      }
    });
  }

  createChannel(type: 'text' | 'voice') {
    this.createChannelType.set(type);
    this.newChannelName = '';
    this.channelNameError.set(null);
    this.showCreateChannelDialog.set(true);
  }

  confirmCreateChannel() {
    const name = normalizeChannelName(this.newChannelName);
    const validationError = this.getChannelNameError(name);

    if (validationError) {
      this.channelNameError.set(validationError);
      return;
    }

    const type = this.createChannelType();
    const existing = type === 'text' ? this.textChannels() : this.voiceChannels();
    const channel: Channel = {
      id: type === 'voice' ? `vc-${uuidv4().slice(0, 8)}` : uuidv4().slice(0, 8),
      name,
      type,
      position: existing.length
    };

    this.store.dispatch(RoomsActions.addChannel({ channel }));
    this.channelNameError.set(null);
    this.showCreateChannelDialog.set(false);
  }

  cancelCreateChannel() {
    this.channelNameError.set(null);
    this.showCreateChannelDialog.set(false);
  }

  clearChannelNameError(): void {
    if (this.channelNameError()) {
      this.channelNameError.set(null);
    }
  }

  private getChannelNameError(name: string, excludeChannelId?: string): string | null {
    if (!name) {
      return 'room.channel.nameRequired';
    }

    const channels = this.currentRoom()?.channels ?? [];
    const channelType = excludeChannelId ? channels.find((channel) => channel.id === excludeChannelId)?.type : this.createChannelType();

    if (!channelType) {
      return null;
    }

    if (isChannelNameTaken(channels, name, channelType, excludeChannelId)) {
      return 'room.channel.nameUnique';
    }

    return null;
  }

  openUserContextMenu(evt: MouseEvent, user: User) {
    evt.preventDefault();

    if (!this.canManageContextUser(user))
      return;

    this.contextMenuUser.set(user);
    this.userMenuX.set(evt.clientX);
    this.userMenuY.set(evt.clientY);
    this.showUserMenu.set(true);
  }

  closeUserMenu() {
    this.showUserMenu.set(false);
  }

  openVoiceUserVolumeMenu(evt: MouseEvent, user: User) {
    evt.preventDefault();
    const me = this.currentUser();

    if (user.id === me?.id || user.oderId === me?.oderId)
      return;

    this.volumeMenuPeerId.set(user.oderId || user.id);
    this.volumeMenuDisplayName.set(user.displayName);
    this.volumeMenuX.set(evt.clientX);
    this.volumeMenuY.set(evt.clientY);
    this.showVolumeMenu.set(true);
  }

  changeUserRole(role: 'admin' | 'moderator' | 'member') {
    const user = this.contextMenuUser();
    const room = this.currentRoom();

    this.closeUserMenu();

    if (!user || !room)
      return;

    const roleIds = role === 'admin' ? [SYSTEM_ROLE_IDS.admin] : role === 'moderator' ? [SYSTEM_ROLE_IDS.moderator] : [];
    const roleAssignments = setRoleAssignmentsForMember(room.roleAssignments, user, roleIds);

    this.store.dispatch(
      RoomsActions.updateRoomAccessControl({
        roomId: room.id,
        changes: { roleAssignments }
      })
    );
  }

  kickUserAction() {
    const user = this.contextMenuUser();

    this.closeUserMenu();

    if (user) {
      this.store.dispatch(UsersActions.kickUser({ userId: user.id }));
    }
  }

  private openExistingVoiceWorkspace(room: Room | null, current: User | null, roomId: string): boolean {
    if (
      !room
      || !current?.voiceState?.isConnected
      || current.voiceState.roomId !== roomId
      || current.voiceState.serverId !== room.id
      || !isLocalVoiceOwner(current.voiceState, this.realtime.getClientInstanceId())
    ) {
      return false;
    }

    this.voiceWorkspace.open(null, { connectRemoteShares: true });
    return true;
  }

  isPassiveInVoiceRoom(roomId: string): boolean {
    const current = this.currentUser();
    const room = this.currentRoom();

    return !!current?.voiceState?.isConnected
      && current.voiceState.roomId === roomId
      && current.voiceState.serverId === room?.id
      && isVoiceOnAnotherClient(current.voiceState, this.realtime.getClientInstanceId());
  }

  isPassiveVoiceClient(): boolean {
    const current = this.currentUser();

    return isVoiceOnAnotherClient(current?.voiceState, this.realtime.getClientInstanceId());
  }

  isPassiveVoiceUser(user: User | null): boolean {
    const current = this.currentUser();

    if (!user || !current) {
      return false;
    }

    return (user.id === current.id || user.oderId === current.oderId)
      && this.isPassiveVoiceClient();
  }

  voiceChannelActionLabel(roomId: string): string {
    if (this.isCurrentRoom(roomId)) {
      return this.isVoiceWorkspaceExpanded()
        ? this.appI18n.instant('room.panel.open')
        : this.appI18n.instant('room.panel.view');
    }

    if (this.isPassiveInVoiceRoom(roomId)) {
      return this.appI18n.instant('room.panel.takeOverVoice');
    }

    return this.appI18n.instant('room.panel.joinVoiceChannel');
  }

  private canJoinRequestedVoiceRoom(room: Room, current: User | null, roomId: string): boolean {
    return !current || resolveRoomPermission(room, current, 'joinVoice', roomId);
  }

  private prepareVoiceJoin(room: Room, current: User | null): void {
    if (!current?.voiceState?.isConnected || current.voiceState.serverId === room.id) {
      return;
    }

    this.disconnectCurrentVoiceTarget(current);
  }

  private enableVoiceForJoin(room: Room, current: User | null, roomId: string): Promise<void> {
    const isSwitchingChannels = !!current?.voiceState?.isConnected && current.voiceState.serverId === room.id && current.voiceState.roomId !== roomId;

    return isSwitchingChannels ? Promise.resolve() : this.voiceConnection.enableVoice().then(() => undefined);
  }

  joinVoice(roomId: string) {
    const room = this.currentRoom();
    const current = this.currentUser();

    if (this.openExistingVoiceWorkspace(room, current ?? null, roomId)) {
      this.voiceConnection.clearConnectionError();
      return;
    }

    if (!room) {
      this.voiceConnection.reportConnectionError('room.voiceJoin.noActiveRoom');
      return;
    }

    if (!this.canJoinRequestedVoiceRoom(room, current ?? null, roomId)) {
      this.voiceConnection.reportConnectionError('room.voiceJoin.noPermission');
      return;
    }

    this.directCalls.leaveCurrentJoinedCall();
    this.prepareVoiceJoin(room, current ?? null);

    const startJoin = () => {
      this.enableVoiceForJoin(room, current ?? null, roomId)
        .then(() => this.onVoiceJoinSucceeded(roomId, room, current ?? null))
        .catch((error) => this.handleVoiceJoinFailure(error));
    };

    if (this.isPassiveInVoiceRoom(roomId) || this.isPassiveVoiceClient()) {
      this.realtime.requestVoiceClientTakeover();
      window.setTimeout(startJoin, 300);
      return;
    }

    startJoin();
  }

  private onVoiceJoinSucceeded(roomId: string, room: Room, current: User | null): void {
    this.voiceConnection.clearConnectionError();
    this.updateVoiceStateStore(roomId, room, current);
    this.trackCurrentUserMic();
    this.startVoiceHeartbeat(roomId, room);
    this.broadcastVoiceConnected(roomId, room, current);
    this.startVoiceSession(roomId, room);
  }

  private handleVoiceJoinFailure(error: unknown): void {
    const message = error instanceof Error ? error.message : 'room.voiceJoin.failed';

    this.voiceConnection.reportConnectionError(message);
  }

  private trackCurrentUserMic(): void {
    const userId = this.currentUser()?.oderId || this.currentUser()?.id;
    const micStream = this.voiceConnection.getRawMicStream();

    if (userId && micStream) {
      this.voiceActivity.trackLocalMic(userId, micStream);
    }
  }

  private untrackCurrentUserMic(): void {
    const userId = this.currentUser()?.oderId || this.currentUser()?.id;

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

  private updateVoiceStateStore(roomId: string, room: Room, current: User | null): void {
    if (!current?.id)
      return;

    this.store.dispatch(
      UsersActions.updateVoiceState({
        userId: current.id,
        voiceState: {
          isConnected: true,
          isMuted: current.voiceState?.isMuted ?? false,
          isDeafened: current.voiceState?.isDeafened ?? false,
          roomId,
          serverId: room.id,
          clientInstanceId: this.realtime.getClientInstanceId()
        }
      })
    );
  }

  private startVoiceHeartbeat(roomId: string, room: Room): void {
    this.voiceConnection.startVoiceHeartbeat(roomId, room.id);
  }

  private broadcastVoiceConnected(roomId: string, room: Room, current: User | null): void {
    const clientInstanceId = this.realtime.getClientInstanceId();

    this.voiceConnection.broadcastMessage({
      type: 'voice-state',
      oderId: current?.oderId || current?.id,
      displayName: current?.displayName || 'User',
      voiceState: {
        isConnected: true,
        isMuted: current?.voiceState?.isMuted ?? false,
        isDeafened: current?.voiceState?.isDeafened ?? false,
        roomId,
        serverId: room.id,
        clientInstanceId
      }
    });
  }

  private startVoiceSession(roomId: string, room: Room): void {
    const voiceChannel = this.voiceChannels().find((channel) => channel.id === roomId);
    const voiceRoomName = voiceChannel ? `🔊 ${voiceChannel.name}` : roomId;

    this.voiceSessionService.startSession({
      serverId: room.id,
      serverName: room.name,
      roomId,
      roomName: voiceRoomName,
      serverIcon: room.icon,
      serverDescription: room.description,
      serverRoute: `/room/${room.id}`
    });
  }

  leaveVoice(roomId: string) {
    const current = this.currentUser();

    if (!(current?.voiceState?.isConnected && current.voiceState.roomId === roomId))
      return;

    this.disconnectCurrentVoiceTarget(current);
  }

  private disconnectCurrentVoiceTarget(current: User | null): void {
    const previousVoiceState = current?.voiceState;

    this.voiceConnection.stopVoiceHeartbeat();
    this.untrackCurrentUserMic();
    this.voiceConnection.disableVoice();

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

      this.store.dispatch(
        UsersActions.updateCameraState({
          userId: current.id,
          cameraState: { isEnabled: false }
        })
      );
    }

    this.voiceConnection.broadcastMessage({
      type: 'voice-state',
      oderId: current?.oderId || current?.id,
      displayName: current?.displayName || 'User',
      voiceState: {
        isConnected: false,
        isMuted: false,
        isDeafened: false,
        roomId: previousVoiceState?.roomId,
        serverId: previousVoiceState?.serverId,
        clientInstanceId: undefined
      }
    });

    this.voiceSessionService.endSession();
  }

  voiceOccupancy(roomId: string): number {
    return this.voiceUsersInRoom(roomId).length;
  }

  viewShare(userId: string) {
    this.voiceWorkspace.focusStream(`screen:${userId}`, { connectRemoteShares: true });
  }

  viewStream(userId: string) {
    const focusTarget = this.isUserSharing(userId) ? `screen:${userId}` : `camera:${userId}`;

    this.voiceWorkspace.focusStream(focusTarget, { connectRemoteShares: true });
  }

  canMoveVoiceUsers(): boolean {
    return this.canManageChannels();
  }

  canDragVoiceUser(user: User): boolean {
    return this.canMoveVoiceUsers() && !this.isCurrentUserIdentity(user) && !!user.voiceState?.isConnected;
  }

  onVoiceUserDragStart(event: DragEvent, user: User): void {
    if (!this.canDragVoiceUser(user)) {
      event.preventDefault();
      return;
    }

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

    if (!dragId) {
      event.preventDefault();
      return;
    }

    this.draggedVoiceUserId.set(dragId);
    event.dataTransfer?.setData('text/plain', dragId);

    if (event.dataTransfer) {
      event.dataTransfer.effectAllowed = 'move';
    }
  }

  onVoiceUserDragEnd(): void {
    this.draggedVoiceUserId.set(null);
    this.dragTargetVoiceChannelId.set(null);
  }

  onVoiceChannelDragOver(event: DragEvent, channelId: string): void {
    if (!this.draggedVoiceUserId()) {
      return;
    }

    event.preventDefault();
    this.dragTargetVoiceChannelId.set(channelId);

    if (event.dataTransfer) {
      event.dataTransfer.dropEffect = 'move';
    }
  }

  onVoiceChannelDragLeave(channelId: string): void {
    if (this.dragTargetVoiceChannelId() === channelId) {
      this.dragTargetVoiceChannelId.set(null);
    }
  }

  onVoiceChannelDrop(event: DragEvent, channelId: string): void {
    event.preventDefault();

    const draggedUserId = this.draggedVoiceUserId() || event.dataTransfer?.getData('text/plain') || null;

    this.draggedVoiceUserId.set(null);
    this.dragTargetVoiceChannelId.set(null);

    if (!draggedUserId) {
      return;
    }

    this.moveVoiceUserToChannel(draggedUserId, channelId);
  }

  private moveVoiceUserToChannel(draggedUserId: string, channelId: string): void {
    const room = this.currentRoom();
    const actor = this.currentUser();

    if (!room || !actor || !this.canMoveVoiceUsers()) {
      return;
    }

    const targetUser = this.onlineUsers().find((user) => user.id === draggedUserId || user.oderId === draggedUserId);

    if (!targetUser?.voiceState?.isConnected || targetUser.voiceState.serverId !== room.id || targetUser.voiceState.roomId === channelId) {
      return;
    }

    const movedVoiceState: Partial<User['voiceState']> = {
      isConnected: true,
      isMuted: targetUser.voiceState.isMuted,
      isDeafened: targetUser.voiceState.isDeafened,
      isSpeaking: targetUser.voiceState.isSpeaking,
      isMutedByAdmin: targetUser.voiceState.isMutedByAdmin,
      volume: targetUser.voiceState.volume,
      roomId: channelId,
      serverId: room.id
    };

    this.store.dispatch(
      UsersActions.updateVoiceState({
        userId: targetUser.id,
        voiceState: movedVoiceState
      })
    );

    this.realtime.broadcastMessage({
      type: 'voice-channel-move',
      roomId: room.id,
      targetUserId: targetUser.oderId || targetUser.id,
      voiceState: movedVoiceState,
      displayName: targetUser.displayName
    });
  }

  isUserLocallyMuted(user: User): boolean {
    const peerId = user.oderId || user.id;

    return this.voicePlayback.isUserMuted(peerId);
  }

  isUserOnCamera(userId: string): boolean {
    const user = this.findKnownUser(userId);

    if (!this.isUserInCurrentVoiceRoom(userId, user)) {
      return false;
    }

    const current = this.currentUser();

    if (current && (current.id === userId || current.oderId === userId)) {
      return this.voiceConnection.isCameraEnabled();
    }

    if (user?.cameraState?.isEnabled === true) {
      return true;
    }

    if (user?.cameraState?.isEnabled === false) {
      return false;
    }

    return this.getPeerKeysForUser(user, userId).some((peerKey) => this.hasActiveVideoStream(this.voiceConnection.getRemoteCameraStream(peerKey)));
  }

  isUserSharing(userId: string): boolean {
    const user = this.findKnownUser(userId);

    if (!this.isUserInCurrentVoiceRoom(userId, user)) {
      return false;
    }

    const current = this.currentUser();

    if (current && (current.id === userId || current.oderId === userId)) {
      return this.screenShare.isScreenSharing();
    }

    if (user?.screenShareState?.isSharing === true) {
      return true;
    }

    if (user?.screenShareState?.isSharing === false) {
      return false;
    }

    const stream =
      this.getPeerKeysForUser(user, userId)
        .map((peerKey) => this.screenShare.getRemoteScreenShareStream(peerKey))
        .find((candidate) => this.hasActiveVideoStream(candidate)) || null;

    return this.hasActiveVideoStream(stream);
  }

  isUserStreaming(userId: string): boolean {
    return this.isUserSharing(userId) || this.isUserOnCamera(userId);
  }

  getVoiceUserRingClass(user: User): string {
    if (user.voiceState?.isDeafened) {
      return 'ring-2 ring-red-500';
    }

    if (user.voiceState?.isMuted) {
      return 'ring-2 ring-yellow-500';
    }

    if (this.isVoiceUserSpeaking(user)) {
      return 'ring-2 ring-green-400 shadow-[0_0_8px_2px_rgba(74,222,128,0.6)]';
    }

    return 'ring-2 ring-green-500/40';
  }

  getUserLiveIconName(userId: string): string {
    return this.isUserSharing(userId) ? 'lucideMonitor' : 'lucideVideo';
  }

  voiceUsersInRoom(roomId: string) {
    const room = this.currentRoom();
    const me = this.currentUser();
    const remoteUsers = this.onlineUsers().filter(
      (user) => !!user.voiceState?.isConnected && user.voiceState?.roomId === roomId && user.voiceState?.serverId === room?.id
    );

    if (me?.voiceState?.isConnected && me.voiceState?.roomId === roomId && me.voiceState?.serverId === room?.id) {
      const meId = me.id;
      const meOderId = me.oderId;
      const alreadyIncluded = remoteUsers.some((user) => user.id === meId || user.oderId === meOderId);

      if (!alreadyIncluded) {
        return [me, ...remoteUsers];
      }
    }

    return remoteUsers;
  }

  isCurrentRoom(roomId: string): boolean {
    const me = this.currentUser();
    const room = this.currentRoom();

    return !!(
      me?.voiceState?.isConnected
      && me.voiceState.roomId === roomId
      && me.voiceState.serverId === room?.id
      && isLocalVoiceOwner(me.voiceState, this.realtime.getClientInstanceId())
    );
  }

  voiceEnabled(): boolean {
    const room = this.currentRoom();
    const user = this.currentUser();

    return !!room && !!user && resolveRoomPermission(room, user, 'joinVoice');
  }

  canManageContextUser(user: User | null): boolean {
    const room = this.currentRoom();
    const currentUser = this.currentUser();

    if (!room || !currentUser || !user) {
      return false;
    }

    return this.canChangeUserRole(user) || this.canKickUser(user);
  }

  canChangeUserRole(user: User | null): boolean {
    const room = this.currentRoom();
    const currentUser = this.currentUser();

    if (!room || !currentUser || !user) {
      return false;
    }

    return canManageMember(room, currentUser, user, 'manageRoles');
  }

  canKickUser(user: User | null): boolean {
    const room = this.currentRoom();
    const currentUser = this.currentUser();

    if (!room || !currentUser || !user) {
      return false;
    }

    return canManageMember(room, currentUser, user, 'kickMembers');
  }

  getPeerLatency(user: User): number | null {
    const latencies = this.voiceConnection.peerLatencies();

    return latencies.get(user.oderId ?? '') ?? latencies.get(user.id) ?? null;
  }

  getPingColorClass(user: User): string {
    const ms = this.getPeerLatency(user);

    if (ms === null)
      return 'bg-gray-500';

    if (ms < 100)
      return 'bg-green-500';

    if (ms < 200)
      return 'bg-yellow-500';

    if (ms < 350)
      return 'bg-orange-500';

    return 'bg-red-500';
  }

  createChannelDialogTitle(): string {
    const key = this.createChannelType() === 'text' ? 'room.channel.createText' : 'room.channel.createVoice';

    return this.appI18n.instant(key);
  }

  peerLatencyTitle(user: User): string {
    const latencyMs = this.getPeerLatency(user);

    if (latencyMs !== null) {
      return this.appI18n.instant('room.panel.latencyMs', { ms: latencyMs });
    }

    return this.appI18n.instant('room.panel.measuringLatency');
  }

  messageAriaLabel(displayName: string): string {
    return this.appI18n.instant('room.panel.messageUser', { name: displayName });
  }

  private isVoiceUserSpeaking(user: User): boolean {
    const userKey = user.oderId || user.id;

    return !!userKey && this.voiceActivity.speakingMap().get(userKey) === true;
  }

  private findKnownUser(userId: string): User | null {
    const current = this.currentUser();

    if (current && (current.id === userId || current.oderId === userId)) {
      return current;
    }

    return this.onlineUsers().find((onlineUser) => onlineUser.id === userId || onlineUser.oderId === userId) ?? null;
  }

  private isUserInCurrentVoiceRoom(userId: string, user: User | null): boolean {
    const currentVoiceState = this.currentUser()?.voiceState;
    const current = this.currentUser();

    if (!currentVoiceState?.isConnected || !currentVoiceState.roomId || !currentVoiceState.serverId) {
      return false;
    }

    if (current && (current.id === userId || current.oderId === userId)) {
      return true;
    }

    return (
      !!user?.voiceState?.isConnected &&
      user.voiceState.roomId === currentVoiceState.roomId &&
      user.voiceState.serverId === currentVoiceState.serverId
    );
  }

  private getPeerKeysForUser(user: User | null, userId: string): string[] {
    return [
      user?.oderId,
      user?.id,
      userId
    ].filter((candidate): candidate is string => !!candidate);
  }

  private hasActiveVideoStream(stream: MediaStream | null): boolean {
    return !!stream && stream.getVideoTracks().some((track) => track.readyState === 'live');
  }
}
