/* eslint-disable @typescript-eslint/member-ordering */

import { Injectable, inject } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import {
  Actions,
  createEffect,
  ofType
} from '@ngrx/effects';
import { Store } from '@ngrx/store';
import {
  of,
  from,
  EMPTY,
  merge,
  timer
} from 'rxjs';
import {
  map,
  mergeMap,
  catchError,
  withLatestFrom,
  tap,
  switchMap,
  filter
} from 'rxjs/operators';
import { v4 as uuidv4 } from 'uuid';
import { RoomsActions } from './rooms.actions';
import { UsersActions } from '../users/users.actions';
import { MessagesActions } from '../messages/messages.actions';
import { selectCurrentUser } from '../users/users.selectors';
import {
  selectActiveChannelId,
  selectCurrentRoom,
  selectSavedRooms
} from './rooms.selectors';
import { RealtimeSessionFacade } from '../../core/realtime';
import {
  clearLastViewedChatFromStorage,
  DatabaseService,
  loadLastViewedChatFromStorage,
  saveLastViewedChatToStorage
} from '../../infrastructure/persistence';
import { ServerDirectoryFacade } from '../../domains/server-directory';
import { hasRoomBanForUser } from '../../domains/access-control';
import { Room } from '../../shared-kernel';
import {
  removeRoomMember,
  transferRoomOwnership,
  findRoomMember
} from './room-members.helpers';
import { defaultChannels } from './room-channels.defaults';
import { RoomSignalingConnection } from './room-signaling-connection';
import {
  resolveRoomChannels,
  resolveTextChannelId,
  extractRoomIdFromUrl
} from './rooms.helpers';

type BlockedRoomAccessAction =
  | ReturnType<typeof RoomsActions.forgetRoom>
  | ReturnType<typeof RoomsActions.joinRoomFailure>;

const VIEW_SERVER_LOAD_DELAY_MS = 75;

@Injectable()
export class RoomsEffects {
  private actions$ = inject(Actions);
  private store = inject(Store);
  private router = inject(Router);
  private db = inject(DatabaseService);
  private webrtc = inject(RealtimeSessionFacade);
  private serverDirectory = inject(ServerDirectoryFacade);

  private readonly signalingConnection = new RoomSignalingConnection(
    this.webrtc,
    this.serverDirectory,
    this.store
  );

  /** Loads all saved rooms from the local database. */
  loadRooms$ = createEffect(() =>
    this.actions$.pipe(
      ofType(RoomsActions.loadRooms),
      switchMap(() =>
        from(this.db.getAllRooms()).pipe(
          map((rooms) => RoomsActions.loadRoomsSuccess({ rooms })),
          catchError((error) => of(RoomsActions.loadRoomsFailure({ error: error.message })))
        )
      )
    )
  );

  /** Opens the routed room after login/refresh once saved rooms are available. */
  syncViewedRoomToRoute$ = createEffect(() =>
    merge(
      this.actions$.pipe(
        ofType(
          RoomsActions.loadRoomsSuccess,
          UsersActions.loadCurrentUserSuccess,
          UsersActions.setCurrentUser
        )
      ),
      this.router.events.pipe(
        filter((event): event is NavigationEnd => event instanceof NavigationEnd)
      )
    ).pipe(
      withLatestFrom(
        this.store.select(selectCurrentUser),
        this.store.select(selectCurrentRoom),
        this.store.select(selectSavedRooms)
      ),
      mergeMap(([
        , currentUser,
        currentRoom,
        savedRooms
      ]) => {
        if (!currentUser) {
          return EMPTY;
        }

        const roomId = extractRoomIdFromUrl(this.router.url);

        if (!roomId || currentRoom?.id === roomId) {
          return EMPTY;
        }

        const room = savedRooms.find((savedRoom) => savedRoom.id === roomId) ?? null;

        if (!room) {
          return EMPTY;
        }

        return of(RoomsActions.viewServer({ room }));
      })
    )
  );

  /** Searches the server directory with debounced input. */
  searchServers$ = createEffect(() =>
    this.actions$.pipe(
      ofType(RoomsActions.searchServers),
      switchMap(({ query }) =>
        this.serverDirectory.searchServers(query).pipe(
          map((servers) => RoomsActions.searchServersSuccess({ servers })),
          catchError((error) => of(RoomsActions.searchServersFailure({ error: error.message })))
        )
      )
    )
  );

  /** Reconnects saved rooms so joined servers stay online while the app is running. */
  keepSavedRoomsConnected$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(
          RoomsActions.loadRoomsSuccess,
          RoomsActions.forgetRoomSuccess,
          RoomsActions.deleteRoomSuccess,
          UsersActions.loadCurrentUserSuccess,
          UsersActions.setCurrentUser
        ),
        withLatestFrom(
          this.store.select(selectCurrentUser),
          this.store.select(selectCurrentRoom),
          this.store.select(selectSavedRooms)
        ),
        tap(([
          , user,
          currentRoom,
          savedRooms
        ]) => {
          this.signalingConnection.syncSavedRoomConnections(user ?? null, currentRoom, savedRooms, this.router.url);
        })
      ),
    { dispatch: false }
  );

  /** Creates a new room, saves it locally, and registers it with the server directory. */
  createRoom$ = createEffect(() =>
    this.actions$.pipe(
      ofType(RoomsActions.createRoom),
      withLatestFrom(this.store.select(selectCurrentUser)),
      switchMap(([{ name, description, topic, isPrivate, password, sourceId, sourceUrl }, currentUser]) => {
        if (!currentUser) {
          return of(RoomsActions.createRoomFailure({ error: 'Not logged in' }));
        }

        const allEndpoints = this.serverDirectory.servers();
        const activeEndpoints = this.serverDirectory.activeServers();
        const selectedEndpoint = allEndpoints.find((endpoint) =>
          (sourceId && endpoint.id === sourceId)
          || (!!sourceUrl && endpoint.url === sourceUrl)
        );
        const endpoint = selectedEndpoint
          ?? activeEndpoints[0]
          ?? allEndpoints[0]
          ?? null;
        const normalizedPassword = typeof password === 'string' ? password.trim() : '';
        const room: Room = {
          id: uuidv4(),
          name,
          description,
          topic,
          hostId: currentUser.id,
          password: normalizedPassword || undefined,
          hasPassword: normalizedPassword.length > 0,
          isPrivate: isPrivate ?? false,
          createdAt: Date.now(),
          userCount: 1,
          maxUsers: 50,
          channels: defaultChannels(),
          sourceId: endpoint?.id,
          sourceName: endpoint?.name,
          sourceUrl: endpoint?.url
        };

        // Save to local DB
        this.db.saveRoom(room);

        // Register with central server (using the same room ID for discoverability)
        this.serverDirectory
          .registerServer({
            id: room.id, // Use the same ID as the local room
            name: room.name,
            description: room.description,
            ownerId: currentUser.id,
            ownerPublicKey: currentUser.oderId,
            hostName: currentUser.displayName,
            password: normalizedPassword || null,
            hasPassword: normalizedPassword.length > 0,
            isPrivate: room.isPrivate,
            userCount: 1,
            maxUsers: room.maxUsers || 50,
            icon: room.icon,
            iconUpdatedAt: room.iconUpdatedAt,
            tags: [],
            channels: room.channels ?? defaultChannels()
          }, endpoint ? {
            sourceId: endpoint.id,
            sourceUrl: endpoint.url
          } : undefined
          )
          .subscribe();

        return of(RoomsActions.createRoomSuccess({ room }));
      }),
      catchError((error) => of(RoomsActions.createRoomFailure({ error: error.message })))
    )
  );

  /** Joins an existing room by ID, resolving room data from local DB or server directory. */
  joinRoom$ = createEffect(() =>
    this.actions$.pipe(
      ofType(RoomsActions.joinRoom),
      withLatestFrom(this.store.select(selectCurrentUser)),
      switchMap(([{ roomId, password: _password, serverInfo }, currentUser]) => {
        if (!currentUser) {
          return of(RoomsActions.joinRoomFailure({ error: 'Not logged in' }));
        }

        return from(this.getBlockedRoomAccessActions(roomId, currentUser)).pipe(
          switchMap((blockedActions) => {
            if (blockedActions.length > 0) {
              return from(blockedActions);
            }

            // First check local DB
            return from(this.db.getRoom(roomId)).pipe(
              switchMap((room) => {
                const sourceSelector = serverInfo
                  ? this.serverDirectory.buildRoomSignalSelector({
                    sourceId: serverInfo.sourceId,
                    sourceName: serverInfo.sourceName,
                    sourceUrl: serverInfo.sourceUrl,
                    signalingUrl: serverInfo.signalingUrl,
                    fallbackName: serverInfo.sourceName ?? serverInfo.name
                  }, {
                    ensureEndpoint: !!(serverInfo.sourceUrl ?? serverInfo.signalingUrl)
                  })
                  : undefined;

                if (room) {
                  const resolvedSource = this.serverDirectory.normaliseRoomSignalSource({
                    sourceId: serverInfo?.sourceId ?? room.sourceId,
                    sourceName: serverInfo?.sourceName ?? room.sourceName,
                    sourceUrl: serverInfo?.sourceUrl ?? room.sourceUrl,
                    signalingUrl: serverInfo?.signalingUrl,
                    fallbackName: serverInfo?.sourceName ?? room.sourceName ?? serverInfo?.name ?? room.name
                  }, {
                    ensureEndpoint: !!(serverInfo?.sourceUrl ?? room.sourceUrl ?? serverInfo?.signalingUrl)
                  });
                  const resolvedRoom: Room = {
                    ...room,
                    isPrivate: typeof serverInfo?.isPrivate === 'boolean' ? serverInfo.isPrivate : room.isPrivate,
                    icon: serverInfo?.icon ?? room.icon,
                    iconUpdatedAt: serverInfo?.iconUpdatedAt ?? room.iconUpdatedAt,
                    channels: resolveRoomChannels(room.channels, serverInfo?.channels),
                    slowModeInterval: serverInfo?.slowModeInterval ?? room.slowModeInterval,
                    roles: serverInfo?.roles ?? room.roles,
                    roleAssignments: serverInfo?.roleAssignments ?? room.roleAssignments,
                    channelPermissions: serverInfo?.channelPermissions ?? room.channelPermissions,
                    ...resolvedSource,
                    hasPassword:
                      typeof serverInfo?.hasPassword === 'boolean'
                        ? serverInfo.hasPassword
                        : (typeof room.hasPassword === 'boolean' ? room.hasPassword : !!room.password)
                  };

                  this.db.updateRoom(room.id, {
                    sourceId: resolvedRoom.sourceId,
                    sourceName: resolvedRoom.sourceName,
                    sourceUrl: resolvedRoom.sourceUrl,
                    channels: resolvedRoom.channels,
                    slowModeInterval: resolvedRoom.slowModeInterval,
                    roles: resolvedRoom.roles,
                    roleAssignments: resolvedRoom.roleAssignments,
                    channelPermissions: resolvedRoom.channelPermissions,
                    icon: resolvedRoom.icon,
                    iconUpdatedAt: resolvedRoom.iconUpdatedAt,
                    hasPassword: resolvedRoom.hasPassword,
                    isPrivate: resolvedRoom.isPrivate
                  });

                  return of(RoomsActions.joinRoomSuccess({ room: resolvedRoom }));
                }

                // If not in local DB but we have server info from search, create a room entry
                if (serverInfo) {
                  const resolvedSource = this.serverDirectory.normaliseRoomSignalSource({
                    sourceId: serverInfo.sourceId,
                    sourceName: serverInfo.sourceName,
                    sourceUrl: serverInfo.sourceUrl,
                    signalingUrl: serverInfo.signalingUrl,
                    fallbackName: serverInfo.sourceName ?? serverInfo.name
                  }, {
                    ensureEndpoint: !!(serverInfo.sourceUrl ?? serverInfo.signalingUrl)
                  });
                  const newRoom: Room = {
                    id: roomId,
                    name: serverInfo.name,
                    description: serverInfo.description,
                    hostId: '', // Unknown, will be determined via signaling
                    hasPassword: !!serverInfo.hasPassword,
                    isPrivate: !!serverInfo.isPrivate,
                    createdAt: Date.now(),
                    userCount: 1,
                    maxUsers: 50,
                    icon: serverInfo.icon,
                    iconUpdatedAt: serverInfo.iconUpdatedAt,
                    channels: resolveRoomChannels(undefined, serverInfo.channels),
                    slowModeInterval: serverInfo.slowModeInterval,
                    roles: serverInfo.roles,
                    roleAssignments: serverInfo.roleAssignments,
                    channelPermissions: serverInfo.channelPermissions,
                    ...resolvedSource
                  };

                  // Save to local DB for future reference
                  this.db.saveRoom(newRoom);
                  return of(RoomsActions.joinRoomSuccess({ room: newRoom }));
                }

                // Try to get room info from server
                return this.serverDirectory.getServer(roomId, sourceSelector).pipe(
                  switchMap((serverData) => {
                    if (serverData) {
                      const resolvedSource = this.serverDirectory.normaliseRoomSignalSource({
                        sourceId: serverData.sourceId,
                        sourceName: serverData.sourceName,
                        sourceUrl: serverData.sourceUrl,
                        fallbackName: serverData.sourceName ?? serverData.name
                      }, {
                        ensureEndpoint: !!serverData.sourceUrl
                      });
                      const newRoom: Room = {
                        id: serverData.id,
                        name: serverData.name,
                        description: serverData.description,
                        hostId: serverData.ownerId || '',
                        hasPassword: !!serverData.hasPassword,
                        isPrivate: serverData.isPrivate,
                        createdAt: serverData.createdAt || Date.now(),
                        userCount: serverData.userCount,
                        maxUsers: serverData.maxUsers,
                        icon: serverData.icon,
                        iconUpdatedAt: serverData.iconUpdatedAt,
                        channels: resolveRoomChannels(undefined, serverData.channels),
                        slowModeInterval: serverData.slowModeInterval,
                        roles: serverData.roles,
                        roleAssignments: serverData.roleAssignments,
                        channelPermissions: serverData.channelPermissions,
                        ...resolvedSource
                      };

                      this.db.saveRoom(newRoom);
                      return of(RoomsActions.joinRoomSuccess({ room: newRoom }));
                    }

                    return of(RoomsActions.joinRoomFailure({ error: 'Room not found' }));
                  }),
                  catchError(() => of(RoomsActions.joinRoomFailure({ error: 'Room not found' })))
                );
              }),
              catchError((error) => of(RoomsActions.joinRoomFailure({ error: error.message })))
            );
          }),
          catchError((error) => of(RoomsActions.joinRoomFailure({ error: error.message })))
        );
      })
    )
  );

  /** Navigates to the room view and establishes or reuses a signaling connection. */
  navigateToRoom$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(RoomsActions.createRoomSuccess, RoomsActions.joinRoomSuccess),
        withLatestFrom(this.store.select(selectCurrentUser), this.store.select(selectSavedRooms)),
        tap(([
          { room },
          user,
          savedRooms
        ]) => {
          const navigationRequestVersion = this.signalingConnection.beginRoomNavigation(room.id);

          void this.signalingConnection.connectToRoomSignaling(room, user ?? null, undefined, savedRooms, {
            showCompatibilityError: true,
            navigationRequestVersion
          });

          this.router.navigate(['/room', room.id]);
        })
      ),
    { dispatch: false }
  );

  /** Remembers the viewed room whenever a room becomes active. */
  persistLastViewedChatOnRoomActivation$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(RoomsActions.createRoomSuccess, RoomsActions.joinRoomSuccess, RoomsActions.viewServerSuccess),
        withLatestFrom(this.store.select(selectCurrentUser)),
        tap(([{ room }, currentUser]) => {
          if (!currentUser) {
            return;
          }

          const persisted = loadLastViewedChatFromStorage(currentUser.id);
          const channelId = persisted?.roomId === room.id
            ? resolveTextChannelId(room.channels, persisted.channelId)
            : resolveTextChannelId(room.channels);

          saveLastViewedChatToStorage({
            userId: currentUser.id,
            roomId: room.id,
            channelId
          });
        })
      ),
    { dispatch: false }
  );

  /** Remembers the currently selected text channel for the active room. */
  persistLastViewedChatOnChannelSelection$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(RoomsActions.selectChannel),
        withLatestFrom(this.store.select(selectCurrentRoom), this.store.select(selectCurrentUser)),
        tap(([
          { channelId },
          currentRoom,
          currentUser
        ]) => {
          if (!currentRoom || !currentUser) {
            return;
          }

          const resolvedChannelId = resolveTextChannelId(currentRoom.channels, channelId);

          if (!resolvedChannelId || resolvedChannelId !== channelId) {
            return;
          }

          saveLastViewedChatToStorage({
            userId: currentUser.id,
            roomId: currentRoom.id,
            channelId
          });
        })
      ),
    { dispatch: false }
  );

  /** Restores the last viewed text channel once the active room's channels are known. */
  restoreLastViewedTextChannel$ = createEffect(() =>
    this.actions$.pipe(
      ofType(
        RoomsActions.createRoomSuccess,
        RoomsActions.joinRoomSuccess,
        RoomsActions.viewServerSuccess,
        RoomsActions.updateRoom
      ),
      withLatestFrom(
        this.store.select(selectCurrentUser),
        this.store.select(selectCurrentRoom),
        this.store.select(selectActiveChannelId)
      ),
      mergeMap(([
        , currentUser,
        currentRoom,
        activeChannelId
      ]) => {
        if (!currentUser || !currentRoom) {
          return EMPTY;
        }

        const persisted = loadLastViewedChatFromStorage(currentUser.id);

        if (!persisted || persisted.roomId !== currentRoom.id) {
          return EMPTY;
        }

        const channelId = resolveTextChannelId(currentRoom.channels, persisted.channelId);

        if (!channelId || channelId === activeChannelId) {
          return EMPTY;
        }

        return of(RoomsActions.selectChannel({ channelId }));
      })
    )
  );

  refreshServerOwnedRoomMetadata$ = createEffect(() =>
    this.actions$.pipe(
      ofType(RoomsActions.joinRoomSuccess, RoomsActions.viewServerSuccess),
      switchMap(({ room }) => {
        const source = this.signalingConnection.resolveRoomSignalSource(room);
        const selector = this.signalingConnection.resolveRoomSignalSelector(source, room.name);
        const roomRequest$ = selector
          ? this.serverDirectory.getServer(room.id, selector).pipe(
            switchMap((serverData) => serverData
              ? of(serverData)
              : this.serverDirectory.findServerAcrossActiveEndpoints(room.id, source))
          )
          : this.serverDirectory.findServerAcrossActiveEndpoints(room.id, source);

        return roomRequest$.pipe(
          map((serverData) => {
            if (!serverData) {
              return null;
            }

            const resolvedSource = this.serverDirectory.normaliseRoomSignalSource({
              sourceId: serverData.sourceId ?? room.sourceId,
              sourceName: serverData.sourceName ?? room.sourceName,
              sourceUrl: serverData.sourceUrl ?? room.sourceUrl,
              fallbackName: serverData.sourceName ?? room.sourceName ?? room.name
            }, {
              ensureEndpoint: !!(serverData.sourceUrl ?? room.sourceUrl)
            });

            return RoomsActions.updateRoom({
              roomId: room.id,
              changes: {
                name: serverData.name,
                description: serverData.description,
                hostId: serverData.ownerId || room.hostId,
                hasPassword: !!serverData.hasPassword,
                isPrivate: serverData.isPrivate,
                maxUsers: serverData.maxUsers,
                icon: serverData.icon ?? room.icon,
                iconUpdatedAt: serverData.iconUpdatedAt ?? room.iconUpdatedAt,
                channels: resolveRoomChannels(room.channels, serverData.channels),
                slowModeInterval: serverData.slowModeInterval ?? room.slowModeInterval,
                roles: serverData.roles ?? room.roles,
                roleAssignments: serverData.roleAssignments ?? room.roleAssignments,
                channelPermissions: serverData.channelPermissions ?? room.channelPermissions,
                ...resolvedSource
              }
            });
          }),
          filter((action): action is ReturnType<typeof RoomsActions.updateRoom> => !!action),
          catchError(() => EMPTY)
        );
      })
    )
  );

  /** Switches the UI view to an already-joined server without leaving others. */
  viewServer$ = createEffect(() =>
    this.actions$.pipe(
      ofType(RoomsActions.viewServer),
      withLatestFrom(this.store.select(selectCurrentUser), this.store.select(selectSavedRooms)),
      switchMap(([
        { room, skipBanCheck },
        user,
        savedRooms
      ]) => {
        if (!user) {
          return of(RoomsActions.joinRoomFailure({ error: 'Not logged in' }));
        }

        const activateViewedRoom = () => {
          const oderId = user.oderId || this.webrtc.peerId();
          const navigationRequestVersion = this.signalingConnection.beginRoomNavigation(room.id);

          void this.signalingConnection.connectToRoomSignaling(room, user, oderId, savedRooms, {
            showCompatibilityError: true,
            navigationRequestVersion
          });

          window.setTimeout(() => {
            if (this.signalingConnection.isCurrentRoomNavigation(room.id, navigationRequestVersion)) {
              void this.router.navigate(['/room', room.id]);
            }
          }, 0);

          return of(RoomsActions.viewServerSuccess({ room }));
        };

        if (skipBanCheck) {
          return activateViewedRoom();
        }

        return from(this.getBlockedRoomAccessActions(room.id, user)).pipe(
          switchMap((blockedActions) => {
            if (blockedActions.length > 0) {
              return from(blockedActions);
            }

            return activateViewedRoom();
          }),
          catchError((error) => of(RoomsActions.joinRoomFailure({ error: error.message })))
        );
      })
    )
  );

  /** Reloads messages and bans when the viewed server changes. */
  onViewServerSuccess$ = createEffect(() =>
    this.actions$.pipe(
      ofType(RoomsActions.viewServerSuccess),
      switchMap(({ room }) => timer(VIEW_SERVER_LOAD_DELAY_MS).pipe(
        mergeMap(() => [MessagesActions.loadMessages({ roomId: room.id }), UsersActions.loadBans()])
      ))
    )
  );

  /** Handles leave-room dispatches (navigation only, peers stay connected). */
  leaveRoom$ = createEffect(() =>
    this.actions$.pipe(
      ofType(RoomsActions.leaveRoom),
      map(() => RoomsActions.leaveRoomSuccess())
    )
  );

  /** Deletes a room (host-only): removes from DB, notifies peers, and disconnects. */
  deleteRoom$ = createEffect(() =>
    this.actions$.pipe(
      ofType(RoomsActions.deleteRoom),
      withLatestFrom(this.store.select(selectCurrentUser), this.store.select(selectCurrentRoom)),
      filter(
        ([
          , currentUser,
          currentRoom
        ]) => !!currentUser && currentRoom?.hostId === currentUser.id
      ),
      switchMap(([{ roomId }]) => {
        this.db.deleteRoom(roomId);
        this.signalingConnection.deleteRoomFallbackSource(roomId);
        this.webrtc.broadcastMessage({ type: 'room-deleted',
          roomId });

        this.webrtc.disconnectAll();
        return of(RoomsActions.deleteRoomSuccess({ roomId }));
      })
    )
  );

  /** Leaves a server, optionally transfers ownership, and removes it locally. */
  forgetRoom$ = createEffect(() =>
    this.actions$.pipe(
      ofType(RoomsActions.forgetRoom),
      withLatestFrom(
        this.store.select(selectCurrentUser),
        this.store.select(selectCurrentRoom),
        this.store.select(selectSavedRooms)
      ),
      switchMap(([
        { roomId, nextOwnerKey },
        currentUser,
        currentRoom,
        savedRooms
      ]) => {
        const room = currentRoom?.id === roomId
          ? currentRoom
          : (savedRooms.find((savedRoom) => savedRoom.id === roomId) ?? null);
        const isRoomOwner = !!currentUser && !!room && (room.hostId === currentUser.id || room.hostId === currentUser.oderId);

        if (currentUser && room && isRoomOwner) {
          const nextOwner = nextOwnerKey
            ? (findRoomMember(room.members ?? [], nextOwnerKey) ?? null)
            : null;
          const updatedMembers = removeRoomMember(
            transferRoomOwnership(
              room.members ?? [],
              nextOwner,
              {
                id: room.hostId,
                oderId: currentUser.oderId
              }
            ),
            currentUser.id,
            currentUser.oderId
          );
          const nextHostId = nextOwner?.id || nextOwner?.oderId || '';
          const nextHostOderId = nextOwner?.oderId || '';

          this.webrtc.broadcastMessage({
            type: 'host-change',
            roomId,
            hostId: nextHostId,
            hostOderId: nextHostOderId,
            previousHostId: room.hostId,
            previousHostOderId: currentUser.oderId,
            members: updatedMembers
          });

          this.serverDirectory.updateServer(roomId, {
            currentOwnerId: currentUser.id,
            actingRole: 'host',
            ownerId: nextHostId,
            ownerPublicKey: nextHostOderId
          }, {
            sourceId: room.sourceId,
            sourceUrl: room.sourceUrl
          }).subscribe({
            error: () => {}
          });
        }

        if (currentUser) {
          this.webrtc.broadcastMessage({
            type: 'member-leave',
            roomId,
            targetUserId: currentUser.id,
            oderId: currentUser.oderId,
            displayName: currentUser.displayName
          });
        }

        if (currentUser && room) {
          this.serverDirectory.notifyLeave(roomId, currentUser.id, {
            sourceId: room.sourceId,
            sourceUrl: room.sourceUrl
          }).subscribe({
            error: () => {}
          });
        }

        // Delete from local DB
        this.db.deleteRoom(roomId);
        this.signalingConnection.deleteRoomFallbackSource(roomId);

        // Leave this specific server (doesn't affect other servers)
        this.webrtc.leaveRoom(roomId);

        return currentRoom?.id === roomId
          ? [RoomsActions.leaveRoomSuccess(), RoomsActions.forgetRoomSuccess({ roomId })]
          : of(RoomsActions.forgetRoomSuccess({ roomId }));
      })
    )
  );

  /** Clears stale resume state when the remembered room is removed locally. */
  clearLastViewedChatOnRoomRemoval$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(RoomsActions.deleteRoomSuccess, RoomsActions.forgetRoomSuccess),
        tap(({ roomId }) => {
          const persisted = loadLastViewedChatFromStorage();

          if (persisted?.roomId === roomId) {
            clearLastViewedChatFromStorage();
          }
        })
      ),
    { dispatch: false }
  );

  /** Persists newly created room to the local database. */
  persistRoomCreation$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(RoomsActions.createRoomSuccess),
        tap(({ room }) => {
          this.db.saveRoom(room);
        })
      ),
    { dispatch: false }
  );

  /** Set the creator's role to 'host' after creating a room. */
  setHostRoleOnCreate$ = createEffect(() =>
    this.actions$.pipe(
      ofType(RoomsActions.createRoomSuccess),
      map(() => UsersActions.updateCurrentUser({ updates: { role: 'host' } }))
    )
  );

  /** Set the user's role to 'host' when rejoining a room they own. */
  setHostRoleOnJoin$ = createEffect(() =>
    this.actions$.pipe(
      ofType(RoomsActions.joinRoomSuccess),
      withLatestFrom(this.store.select(selectCurrentUser)),
      filter(([{ room }, user]) => !!user && !!room.hostId && room.hostId === user.id),
      map(() => UsersActions.updateCurrentUser({ updates: { role: 'host' } }))
    )
  );

  /** Loads messages and bans when joining a room. */
  onJoinRoomSuccess$ = createEffect(() =>
    this.actions$.pipe(
      ofType(RoomsActions.joinRoomSuccess),
      // Don't load users from database - they come from signaling server.
      mergeMap(({ room }) => [MessagesActions.loadMessages({ roomId: room.id }), UsersActions.loadBans()])
    )
  );

  /** Clears viewed messages when leaving a room. */
  onLeaveRoom$ = createEffect(() =>
    this.actions$.pipe(
      ofType(RoomsActions.leaveRoomSuccess),
      mergeMap(() => [MessagesActions.clearMessages()])
    )
  );

  /** Persists room field changes to the local database. */
  updateRoom$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(RoomsActions.updateRoom),
        tap(({ roomId, changes }) => {
          this.db.updateRoom(roomId, changes);
        })
      ),
    { dispatch: false }
  );

  // ── Private helpers ────────────────────────────────────────────

  private async getBlockedRoomAccessActions(
    roomId: string,
    currentUser: { id: string; oderId: string } | null
  ): Promise<BlockedRoomAccessAction[]> {
    const bans = await this.db.getBansForRoom(roomId);

    if (!hasRoomBanForUser(bans, currentUser, this.getPersistedCurrentUserId())) {
      return [];
    }

    const blockedActions: BlockedRoomAccessAction[] = [RoomsActions.joinRoomFailure({ error: 'You are banned from this server' })];
    const storedRoom = await this.db.getRoom(roomId);

    if (storedRoom) {
      blockedActions.unshift(RoomsActions.forgetRoom({ roomId }));
    }

    return blockedActions;
  }

  private getPersistedCurrentUserId(): string | null {
    return localStorage.getItem('metoyou_currentUserId');
  }
}
