/* eslint-disable @typescript-eslint/member-ordering */
import { Injectable, inject } from '@angular/core';
import {
  Actions,
  createEffect,
  ofType
} from '@ngrx/effects';
import { Store, type Action } from '@ngrx/store';
import { EMPTY } from 'rxjs';
import {
  mergeMap,
  tap,
  withLatestFrom
} from 'rxjs/operators';
import type {
  ChatEvent,
  Room,
  RoomMember,
  User
} from '../../shared-kernel';
import { RealtimeSessionFacade } from '../../core/realtime';
import { UsersActions } from '../users/users.actions';
import { selectAllUsers, selectCurrentUser } from '../users/users.selectors';
import { RoomsActions } from './rooms.actions';
import { selectCurrentRoom, selectSavedRooms } from './rooms.selectors';
import { normalizeRoomAccessControl, resolveLegacyRole } from '../../domains/access-control';
import {
  areRoomMembersEqual,
  findRoomMember,
  mergeRoomMembers,
  pruneRoomMembers,
  removeRoomMember,
  roomMemberFromUser,
  touchRoomMemberLastSeen,
  transferRoomOwnership,
  updateRoomMemberRole,
  upsertRoomMember
} from './room-members.helpers';

@Injectable()
export class RoomMembersSyncEffects {
  private readonly actions$ = inject(Actions);
  private readonly store = inject(Store);
  private readonly webrtc = inject(RealtimeSessionFacade);

  /** Ensure the local user is recorded in a room as soon as it becomes active. */
  ensureCurrentMemberOnRoomEntry$ = createEffect(() =>
    this.actions$.pipe(
      ofType(
        RoomsActions.createRoomSuccess,
        RoomsActions.joinRoomSuccess,
        RoomsActions.viewServerSuccess
      ),
      withLatestFrom(this.store.select(selectCurrentUser)),
      mergeMap(([{ room }, currentUser]) => {
        if (!currentUser)
          return EMPTY;

        const members = upsertRoomMember(
          room.members ?? [],
          this.buildCurrentUserMember(room, currentUser, true)
        );
        const actions = this.createRoomMemberUpdateActions(room, members);

        return actions.length > 0 ? actions : EMPTY;
      })
    )
  );

  /** Keep the viewed room's local member record aligned with the current profile. */
  syncCurrentUserIntoCurrentRoom$ = createEffect(() =>
    this.actions$.pipe(
      ofType(
        UsersActions.loadCurrentUserSuccess,
        UsersActions.setCurrentUser,
        UsersActions.updateCurrentUser
      ),
      withLatestFrom(
        this.store.select(selectCurrentUser),
        this.store.select(selectCurrentRoom)
      ),
      mergeMap(([
        , currentUser,
        currentRoom
      ]) => {
        if (!currentUser || !currentRoom)
          return EMPTY;

        const members = upsertRoomMember(
          currentRoom.members ?? [],
          this.buildCurrentUserMember(currentRoom, currentUser, true)
        );
        const actions = this.createRoomMemberUpdateActions(currentRoom, members);

        return actions.length > 0 ? actions : EMPTY;
      })
    )
  );

  /** Persist room-role changes into the stored member roster for the active room. */
  syncRoleChangesIntoCurrentRoom$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UsersActions.updateUserRole),
      withLatestFrom(this.store.select(selectCurrentRoom)),
      mergeMap(([{ userId, role }, currentRoom]) => {
        if (!currentRoom)
          return EMPTY;

        const members = updateRoomMemberRole(currentRoom.members ?? [], userId, role);
        const actions = this.createRoomMemberUpdateActions(currentRoom, members);

        return actions.length > 0 ? actions : EMPTY;
      })
    )
  );

  /** Keep active-room user roles derived from room access-control assignments. */
  syncAccessControlRolesIntoUsers$ = createEffect(() =>
    this.actions$.pipe(
      ofType(
        RoomsActions.createRoomSuccess,
        RoomsActions.joinRoomSuccess,
        RoomsActions.viewServerSuccess,
        RoomsActions.updateRoom
      ),
      withLatestFrom(
        this.store.select(selectCurrentRoom),
        this.store.select(selectSavedRooms),
        this.store.select(selectAllUsers),
        this.store.select(selectCurrentUser)
      ),
      mergeMap(([
        action,
        currentRoom,
        savedRooms,
        allUsers,
        currentUser
      ]) => {
        const room = this.resolveRoleSyncRoom(action, currentRoom, savedRooms);

        if (!room)
          return EMPTY;

        const actions = this.createUserRoleSyncActions(room, allUsers, currentUser ?? null);

        return actions.length > 0 ? actions : EMPTY;
      })
    )
  );

  /** Update persisted room rosters when signaling presence changes arrive. */
  signalingPresenceIntoRoomMembers$ = createEffect(() =>
    this.webrtc.onSignalingMessage.pipe(
      withLatestFrom(
        this.store.select(selectCurrentRoom),
        this.store.select(selectSavedRooms),
        this.store.select(selectCurrentUser)
      ),
      mergeMap(([
        message,
        currentRoom,
        savedRooms,
        currentUser
      ]) => {
        const signalingMessage: {
          type: string;
          serverId?: string;
          users?: { oderId: string; displayName: string }[];
          oderId?: string;
          displayName?: string;
        } = message;
        const roomId = typeof signalingMessage.serverId === 'string' ? signalingMessage.serverId : undefined;
        const room = this.resolveRoom(roomId, currentRoom, savedRooms);

        if (!room)
          return EMPTY;

        const myId = currentUser?.oderId || currentUser?.id;

        switch (signalingMessage.type) {
          case 'server_users': {
            if (!Array.isArray(signalingMessage.users))
              return EMPTY;

            let members = room.members ?? [];

            for (const user of signalingMessage.users as { oderId: string; displayName: string }[]) {
              if (!user?.oderId || user.oderId === myId)
                continue;

              members = upsertRoomMember(members, this.buildPresenceMember(room, user));
            }

            const actions = this.createRoomMemberUpdateActions(room, members);

            return actions.length > 0 ? actions : EMPTY;
          }

          case 'user_joined': {
            if (!signalingMessage.oderId || signalingMessage.oderId === myId)
              return EMPTY;

            const joinedUser = {
              oderId: signalingMessage.oderId,
              displayName: signalingMessage.displayName
            };
            const members = upsertRoomMember(
              room.members ?? [],
              this.buildPresenceMember(room, joinedUser)
            );
            const actions = this.createRoomMemberUpdateActions(room, members);

            return actions.length > 0 ? actions : EMPTY;
          }

          case 'user_left': {
            if (!signalingMessage.oderId)
              return EMPTY;

            const members = touchRoomMemberLastSeen(room.members ?? [], signalingMessage.oderId, Date.now());
            const actions = this.createRoomMemberUpdateActions(room, members);

            return actions.length > 0 ? actions : EMPTY;
          }

          default:
            return EMPTY;
        }
      })
    )
  );

  /** Request the latest member roster whenever a new peer data channel opens. */
  peerConnectedRosterSync$ = createEffect(
    () =>
      this.webrtc.onPeerConnected.pipe(
        withLatestFrom(this.store.select(selectCurrentRoom)),
        tap(([peerId, currentRoom]) => {
          if (!currentRoom)
            return;

          this.webrtc.sendToPeer(peerId, {
            type: 'member-roster-request',
            roomId: currentRoom.id
          });
        })
      ),
    { dispatch: false }
  );

  /** Kick off room-member sync when entering or switching to a room. */
  roomEntryRosterSync$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(
          RoomsActions.createRoomSuccess,
          RoomsActions.joinRoomSuccess,
          RoomsActions.viewServerSuccess
        ),
        tap(({ room }) => {
          for (const peerId of this.webrtc.getConnectedPeers()) {
            try {
              this.webrtc.sendToPeer(peerId, {
                type: 'member-roster-request',
                roomId: room.id
              });
            } catch {
              /* peer may have disconnected */
            }
          }
        })
      ),
    { dispatch: false }
  );

  /** Handle peer-to-peer member roster sync and explicit leave messages. */
  incomingRoomMemberEvents$ = createEffect(() =>
    this.webrtc.onMessageReceived.pipe(
      withLatestFrom(
        this.store.select(selectCurrentRoom),
        this.store.select(selectSavedRooms),
        this.store.select(selectCurrentUser)
      ),
      mergeMap(([
        event,
        currentRoom,
        savedRooms,
        currentUser
      ]) => {
        switch (event.type) {
          case 'member-roster-request': {
            const actions = this.handleMemberRosterRequest(event, currentRoom, savedRooms, currentUser ?? null);

            return actions.length > 0 ? actions : EMPTY;
          }

          case 'member-roster': {
            const actions = this.handleMemberRoster(event, currentRoom, savedRooms, currentUser ?? null);

            return actions.length > 0 ? actions : EMPTY;
          }

          case 'member-leave': {
            const actions = this.handleMemberLeave(event, currentRoom, savedRooms);

            return actions.length > 0 ? actions : EMPTY;
          }

          case 'host-change': {
            const actions = this.handleIncomingHostChange(event, currentRoom, savedRooms, currentUser ?? null);

            return actions.length > 0 ? actions : EMPTY;
          }

          case 'role-change': {
            const actions = this.handleIncomingRoleChange(event, currentRoom, savedRooms);

            return actions.length > 0 ? actions : EMPTY;
          }

          default:
            return EMPTY;
        }
      })
    )
  );

  private resolveRoom(roomId: string | undefined, currentRoom: Room | null, savedRooms: Room[]): Room | null {
    if (!roomId)
      return null;

    if (currentRoom?.id === roomId)
      return currentRoom;

    return savedRooms.find((room) => room.id === roomId) ?? null;
  }

  private buildCurrentUserMember(room: Room, currentUser: User, isCurrentRoom: boolean): RoomMember {
    const existingMember = findRoomMember(room.members ?? [], currentUser.oderId || currentUser.id);
    const role = room.hostId === currentUser.id
      ? 'host'
      : (isCurrentRoom ? currentUser.role : existingMember?.role ?? 'member');
    const seenAt = existingMember?.lastSeenAt ?? currentUser.joinedAt ?? Date.now();

    return {
      ...roomMemberFromUser(currentUser, seenAt, role),
      id: existingMember?.id ?? currentUser.id,
      joinedAt: existingMember?.joinedAt ?? currentUser.joinedAt ?? seenAt,
      avatarUrl: currentUser.avatarUrl ?? existingMember?.avatarUrl,
      role
    };
  }

  private buildPresenceMember(
    room: Room,
    data: { oderId: string; displayName?: string }
  ): RoomMember {
    const existingMember = findRoomMember(room.members ?? [], data.oderId);
    const now = Date.now();

    return {
      id: existingMember?.id ?? data.oderId,
      oderId: data.oderId,
      username:
        existingMember?.username ??
        (data.displayName || 'User').toLowerCase().replace(/\s+/g, '_'),
      displayName: data.displayName || existingMember?.displayName || 'User',
      avatarUrl: existingMember?.avatarUrl,
      role: existingMember?.role ?? 'member',
      joinedAt: existingMember?.joinedAt ?? now,
      lastSeenAt: now
    };
  }

  private createRoomMemberUpdateActions(room: Room, members: RoomMember[]): Action[] {
    return areRoomMembersEqual(room.members ?? [], members)
      ? []
      : [RoomsActions.updateRoom({ roomId: room.id, changes: { members } })];
  }

  private resolveRoleSyncRoom(
    action:
      | ReturnType<typeof RoomsActions.createRoomSuccess>
      | ReturnType<typeof RoomsActions.joinRoomSuccess>
      | ReturnType<typeof RoomsActions.viewServerSuccess>
      | ReturnType<typeof RoomsActions.updateRoom>,
    currentRoom: Room | null,
    savedRooms: Room[]
  ): Room | null {
    if ('room' in action) {
      return normalizeRoomAccessControl(action.room);
    }

    if (currentRoom?.id !== action.roomId || !this.hasRoleRelevantRoomChanges(action.changes)) {
      return null;
    }

    const room = this.resolveRoom(action.roomId, currentRoom, savedRooms);

    return room ? normalizeRoomAccessControl({ ...room, ...action.changes }) : null;
  }

  private hasRoleRelevantRoomChanges(changes: Partial<Room>): boolean {
    return (
      Object.prototype.hasOwnProperty.call(changes, 'hostId') ||
      Object.prototype.hasOwnProperty.call(changes, 'members') ||
      Object.prototype.hasOwnProperty.call(changes, 'permissions') ||
      Object.prototype.hasOwnProperty.call(changes, 'roles') ||
      Object.prototype.hasOwnProperty.call(changes, 'roleAssignments') ||
      Object.prototype.hasOwnProperty.call(changes, 'channelPermissions') ||
      Object.prototype.hasOwnProperty.call(changes, 'slowModeInterval')
    );
  }

  private createUserRoleSyncActions(room: Room, allUsers: User[], currentUser: User | null): Action[] {
    const usersById = new Map<string, User>();

    for (const user of allUsers) {
      if (this.shouldSyncUserRoleForRoom(room, user, currentUser)) {
        usersById.set(user.id, user);
      }
    }

    if (currentUser) {
      usersById.set(currentUser.id, currentUser);
    }

    return Array.from(usersById.values())
      .map((user) => ({
        user,
        role: resolveLegacyRole(room, user)
      }))
      .filter(({ user, role }) => user.role !== role)
      .map(({ user, role }) => UsersActions.updateUserRole({ userId: user.id,
        role }));
  }

  private shouldSyncUserRoleForRoom(room: Room, user: User, currentUser: User | null): boolean {
    if (currentUser && user.id === currentUser.id) {
      return true;
    }

    if (room.hostId === user.id || room.hostId === user.oderId) {
      return true;
    }

    if (Array.isArray(user.presenceServerIds) && user.presenceServerIds.includes(room.id)) {
      return true;
    }

    if (findRoomMember(room.members ?? [], user.oderId || user.id)) {
      return true;
    }

    return (room.roleAssignments ?? []).some((assignment) => (
      assignment.userId === user.id ||
      assignment.userId === user.oderId ||
      assignment.oderId === user.id ||
      assignment.oderId === user.oderId
    ));
  }

  private handleMemberRosterRequest(
    event: ChatEvent,
    currentRoom: Room | null,
    savedRooms: Room[],
    currentUser: User | null
  ): Action[] {
    const room = this.resolveRoom(event.roomId, currentRoom, savedRooms);

    if (!room || !event.fromPeerId)
      return [];

    const isCurrentRoom = currentRoom?.id === room.id;

    let members = room.members ?? [];

    if (currentUser) {
      members = upsertRoomMember(
        members,
        this.buildCurrentUserMember(room, currentUser, isCurrentRoom)
      );
    }

    this.webrtc.sendToPeer(event.fromPeerId, {
      type: 'member-roster',
      roomId: room.id,
      members
    });

    return this.createRoomMemberUpdateActions(room, members);
  }

  private handleMemberRoster(
    event: ChatEvent,
    currentRoom: Room | null,
    savedRooms: Room[],
    currentUser: User | null
  ): Action[] {
    const room = this.resolveRoom(event.roomId, currentRoom, savedRooms);

    if (!room || !Array.isArray(event.members))
      return [];

    let members = mergeRoomMembers(room.members ?? [], event.members);

    if (currentUser) {
      members = upsertRoomMember(
        members,
        this.buildCurrentUserMember(room, currentUser, currentRoom?.id === room.id)
      );
    }

    const actions = this.createRoomMemberUpdateActions(room, members);
    const currentUserId = currentUser?.oderId || currentUser?.id;

    for (const member of members) {
      const memberId = member.oderId || member.id;

      if (!member.avatarUrl || !memberId || memberId === currentUserId) {
        continue;
      }

      actions.push(UsersActions.upsertRemoteUserAvatar({
        user: {
          id: member.id,
          oderId: memberId,
          username: member.username,
          displayName: member.displayName,
          avatarUrl: member.avatarUrl
        }
      }));
    }

    return actions;
  }

  private handleMemberLeave(
    event: ChatEvent,
    currentRoom: Room | null,
    savedRooms: Room[]
  ): Action[] {
    const roomId = typeof event.roomId === 'string' ? event.roomId : currentRoom?.id;
    const room = this.resolveRoom(roomId, currentRoom, savedRooms);

    if (!room)
      return [];

    const actions = this.createRoomMemberUpdateActions(
      room,
      removeRoomMember(room.members ?? [], event.targetUserId, event.oderId)
    );
    const departedUserId = event.oderId ?? event.targetUserId;

    if (currentRoom?.id === room.id && departedUserId) {
      actions.push(
        UsersActions.userLeft({
          userId: departedUserId,
          serverId: room.id
        })
      );
    }

    return actions;
  }

  private handleIncomingHostChange(
    event: ChatEvent,
    currentRoom: Room | null,
    savedRooms: Room[],
    currentUser: User | null
  ): Action[] {
    const roomId = typeof event.roomId === 'string' ? event.roomId : currentRoom?.id;
    const room = this.resolveRoom(roomId, currentRoom, savedRooms);

    if (!room)
      return [];

    const members = Array.isArray(event.members)
      ? pruneRoomMembers(event.members)
      : transferRoomOwnership(
        room.members ?? [],
        event.hostId || event.hostOderId
          ? {
            id: event.hostId,
            oderId: event.hostOderId
          }
          : null,
        {
          id: event.previousHostId ?? event.previousHostOderId ?? '',
          oderId: event.previousHostOderId
        }
      );
    const hostId = typeof event.hostId === 'string' ? event.hostId : '';
    const actions: Action[] = [
      RoomsActions.updateRoom({
        roomId: room.id,
        changes: {
          hostId,
          members
        }
      })
    ];

    for (const previousHostKey of new Set([event.previousHostId, event.previousHostOderId].filter((value): value is string => !!value))) {
      actions.push(
        UsersActions.updateUserRole({
          userId: previousHostKey,
          role: 'member'
        })
      );
    }

    for (const nextHostKey of new Set([event.hostId, event.hostOderId].filter((value): value is string => !!value))) {
      actions.push(
        UsersActions.updateUserRole({
          userId: nextHostKey,
          role: 'host'
        })
      );
    }

    if (currentUser) {
      const isCurrentUserNextHost = event.hostId === currentUser.id || event.hostOderId === currentUser.oderId;
      const isCurrentUserPreviousHost = event.previousHostId === currentUser.id || event.previousHostOderId === currentUser.oderId;

      if (isCurrentUserPreviousHost && !isCurrentUserNextHost) {
        actions.push(UsersActions.updateCurrentUser({ updates: { role: 'member' } }));
      }

      if (isCurrentUserNextHost) {
        actions.push(UsersActions.updateCurrentUser({ updates: { role: 'host' } }));
      }
    }

    return actions;
  }

  private handleIncomingRoleChange(
    event: ChatEvent,
    currentRoom: Room | null,
    savedRooms: Room[]
  ): Action[] {
    const roomId = typeof event.roomId === 'string' ? event.roomId : currentRoom?.id;
    const room = this.resolveRoom(roomId, currentRoom, savedRooms);

    if (!room || !event.targetUserId || !event.role)
      return [];

    const actions = this.createRoomMemberUpdateActions(
      room,
      updateRoomMemberRole(room.members ?? [], event.targetUserId, event.role)
    );

    if (currentRoom?.id === room.id) {
      actions.push(
        UsersActions.updateUserRole({
          userId: event.targetUserId,
          role: event.role
        })
      );
    }

    return actions;
  }
}
