/**
 * Users store effects (load, kick, ban, host election, profile persistence).
 */
/* eslint-disable @typescript-eslint/member-ordering */
import { Injectable, inject } from '@angular/core';
import {
  Actions,
  createEffect,
  ofType
} from '@ngrx/effects';
import { Store } from '@ngrx/store';
import {
  of,
  from,
  EMPTY
} from 'rxjs';
import {
  map,
  mergeMap,
  catchError,
  withLatestFrom,
  tap,
  switchMap
} from 'rxjs/operators';
import { v4 as uuidv4 } from 'uuid';
import { MessagesActions } from '../messages/messages.actions';
import { UsersActions } from './users.actions';
import { RoomsActions } from '../rooms/rooms.actions';
import {
  selectAllUsers,
  selectCurrentUser,
  selectCurrentUserId,
  selectHostId
} from './users.selectors';
import { selectCurrentRoom, selectSavedRooms } from '../rooms/rooms.selectors';
import { RealtimeSessionFacade } from '../../core/realtime';
import { DatabaseService } from '../../infrastructure/persistence';
import { ServerDirectoryFacade } from '../../domains/server-directory';
import {
  canManageMember,
  resolveLegacyRole,
  resolveRoomPermission
} from '../../domains/access-control';
import {
  BanEntry,
  ChatEvent,
  Room,
  User
} from '../../shared-kernel';
import { setStoredCurrentUserId } from '../../core/storage/current-user-storage';
import { findRoomMember, removeRoomMember } from '../rooms/room-members.helpers';

type IncomingModerationExtraAction =
  | ReturnType<typeof RoomsActions.forgetRoom>
  | ReturnType<typeof UsersActions.kickUserSuccess>
  | ReturnType<typeof UsersActions.banUserSuccess>;

type IncomingModerationAction =
  | ReturnType<typeof RoomsActions.updateRoom>
  | IncomingModerationExtraAction;

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

  /** Prepares persisted state for a successful login before exposing the user in-memory. */
  authenticateUser$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UsersActions.authenticateUser),
      switchMap(({ user }) =>
        from(this.prepareAuthenticatedUserStorage(user.id)).pipe(
          mergeMap(() => [
            MessagesActions.clearMessages(),
            UsersActions.resetUsersState(),
            RoomsActions.resetRoomsState(),
            UsersActions.setCurrentUser({ user }),
            RoomsActions.loadRooms()
          ]),
          catchError((error: Error) =>
            of(UsersActions.loadCurrentUserFailure({ error: error.message || 'Failed to prepare local user state.' }))
          )
        )
      )
    )
  );

  // Load current user from storage
  /** Loads the persisted current user from the local database on startup. */
  loadCurrentUser$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UsersActions.loadCurrentUser),
      switchMap(() =>
        from(this.db.getCurrentUser()).pipe(
          switchMap((user) => {
            if (!user) {
              return of(UsersActions.loadCurrentUserFailure({ error: 'No current user' }));
            }

            const sanitizedUser = this.clearStartupVoiceConnection(user);

            if (sanitizedUser === user) {
              return of(UsersActions.loadCurrentUserSuccess({ user }));
            }

            return from(this.db.updateUser(user.id, { voiceState: sanitizedUser.voiceState })).pipe(
              map(() => UsersActions.loadCurrentUserSuccess({ user: sanitizedUser })),
              // If persistence fails, still load a sanitized in-memory user to keep UI correct.
              catchError(() => of(UsersActions.loadCurrentUserSuccess({ user: sanitizedUser })))
            );
          }),
          catchError((error) =>
            of(UsersActions.loadCurrentUserFailure({ error: error.message }))
          )
        )
      )
    )
  );

  private clearStartupVoiceConnection(user: User): User {
    const voiceState = user.voiceState;

    if (!voiceState)
      return user;

    const hasStaleConnectionState =
      voiceState.isConnected ||
      voiceState.isSpeaking ||
      voiceState.roomId !== undefined ||
      voiceState.serverId !== undefined;

    if (!hasStaleConnectionState)
      return user;

    return {
      ...user,
      voiceState: {
        ...voiceState,
        isConnected: false,
        isSpeaking: false,
        roomId: undefined,
        serverId: undefined
      }
    };
  }

  private async prepareAuthenticatedUserStorage(userId: string): Promise<void> {
    setStoredCurrentUserId(userId);
    await this.db.initialize();
    await this.db.setCurrentUserId(userId);
  }

  /** Loads all users associated with a specific room from the local database. */
  loadRoomUsers$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UsersActions.loadRoomUsers),
      switchMap(({ roomId }) =>
        from(this.db.getUsersByRoom(roomId)).pipe(
          map((users) => UsersActions.loadRoomUsersSuccess({ users })),
          catchError((error) =>
            of(UsersActions.loadRoomUsersFailure({ error: error.message }))
          )
        )
      )
    )
  );

  /** Kicks a user from the room (requires moderator+ role). Broadcasts a kick signal. */
  kickUser$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UsersActions.kickUser),
      withLatestFrom(
        this.store.select(selectCurrentUser),
        this.store.select(selectCurrentRoom),
        this.store.select(selectSavedRooms)
      ),
      mergeMap(([
        { userId, roomId },
        currentUser,
        currentRoom,
        savedRooms
      ]) => {
        if (!currentUser)
          return EMPTY;

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

        if (!room)
          return EMPTY;

        const canKick = this.canKickInRoom(room, currentUser, currentRoom, userId);

        if (!canKick)
          return EMPTY;

        const nextMembers = removeRoomMember(room.members ?? [], userId, userId);

        return this.serverDirectory.kickServerMember(
          room.id,
          {
            actorUserId: currentUser.id,
            actorRole: currentUser.role,
            targetUserId: userId
          },
          this.toSourceSelector(room)
        ).pipe(
          catchError((error) => {
            console.error('Failed to revoke server membership on kick:', error);
            return of(void 0);
          }),
          mergeMap(() => {
            this.webrtc.broadcastMessage({
              type: 'kick',
              targetUserId: userId,
              roomId: room.id,
              kickedBy: currentUser.id
            });

            return currentRoom?.id === room.id
              ? [
                RoomsActions.updateRoom({ roomId: room.id,
                  changes: { members: nextMembers } }),
                UsersActions.kickUserSuccess({ userId,
                  roomId: room.id })
              ]
              : of(
                RoomsActions.updateRoom({ roomId: room.id,
                  changes: { members: nextMembers } })
              );
          })
        );
      })
    )
  );

  /** Bans a user, persists the ban locally, and broadcasts a ban signal to peers. */
  banUser$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UsersActions.banUser),
      withLatestFrom(
        this.store.select(selectCurrentUser),
        this.store.select(selectCurrentRoom),
        this.store.select(selectSavedRooms),
        this.store.select(selectAllUsers)
      ),
      mergeMap(([
        { userId, roomId, displayName, reason, expiresAt },
        currentUser,
        currentRoom,
        savedRooms,
        allUsers
      ]) => {
        if (!currentUser)
          return EMPTY;

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

        if (!room)
          return EMPTY;

        const canBan = this.canBanInRoom(room, currentUser, currentRoom, userId);

        if (!canBan)
          return EMPTY;

        const targetUser = allUsers.find((user) => user.id === userId || user.oderId === userId);
        const targetMember = findRoomMember(room.members ?? [], userId);
        const nextMembers = removeRoomMember(room.members ?? [], userId, userId);
        const ban: BanEntry = {
          oderId: uuidv4(),
          userId,
          roomId: room.id,
          bannedBy: currentUser.id,
          displayName: displayName || targetUser?.displayName || targetMember?.displayName,
          reason,
          expiresAt,
          timestamp: Date.now()
        };

        return this.serverDirectory.banServerMember(
          room.id,
          {
            actorUserId: currentUser.id,
            actorRole: currentUser.role,
            targetUserId: userId,
            banId: ban.oderId,
            displayName: ban.displayName,
            reason,
            expiresAt
          },
          this.toSourceSelector(room)
        ).pipe(
          catchError((error) => {
            console.error('Failed to persist server ban:', error);
            return of(void 0);
          }),
          switchMap(() =>
            from(this.db.saveBan(ban)).pipe(
              tap(() => {
                this.webrtc.broadcastMessage({
                  type: 'ban',
                  targetUserId: userId,
                  roomId: room.id,
                  bannedBy: currentUser.id,
                  ban
                });
              }),
              mergeMap(() => {
                const actions: (ReturnType<typeof RoomsActions.updateRoom>
                  | ReturnType<typeof UsersActions.banUserSuccess>)[] = [
                  RoomsActions.updateRoom({ roomId: room.id,
                    changes: { members: nextMembers } })
                ];

                if (currentRoom?.id === room.id) {
                  actions.push(UsersActions.banUserSuccess({ userId,
                    roomId: room.id,
                    ban }));
                }

                return actions;
              }),
              catchError(() => EMPTY)
            )
          )
        );
      })
    )
  );

  /** Removes a ban entry locally and broadcasts the change to peers in the same room. */
  unbanUser$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UsersActions.unbanUser),
      withLatestFrom(
        this.store.select(selectCurrentUser),
        this.store.select(selectCurrentRoom),
        this.store.select(selectSavedRooms)
      ),
      switchMap(([
        { roomId, oderId },
        currentUser,
        currentRoom,
        savedRooms
      ]) => {
        const room = this.resolveRoom(roomId, currentRoom, savedRooms);

        if (!currentUser || !room || !this.canModerateRoom(room, currentUser, currentRoom))
          return EMPTY;

        return this.serverDirectory.unbanServerMember(
          room.id,
          {
            actorUserId: currentUser.id,
            actorRole: currentUser.role,
            banId: oderId
          },
          this.toSourceSelector(room)
        ).pipe(
          catchError((error) => {
            console.error('Failed to remove server ban:', error);
            return of(void 0);
          }),
          switchMap(() =>
            from(this.db.removeBan(oderId)).pipe(
              tap(() => {
                this.webrtc.broadcastMessage({
                  type: 'unban',
                  roomId: room.id,
                  banOderId: oderId
                });
              }),
              map(() => UsersActions.unbanUserSuccess({ oderId })),
              catchError(() => EMPTY)
            )
          )
        );
      })
    )
  );

  /** Loads all active bans for the current room from the local database. */
  loadBans$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UsersActions.loadBans),
      withLatestFrom(this.store.select(selectCurrentRoom)),
      switchMap(([, currentRoom]) => {
        if (!currentRoom) {
          return of(UsersActions.loadBansSuccess({ bans: [] }));
        }

        return from(this.db.getBansForRoom(currentRoom.id)).pipe(
          map((bans) => UsersActions.loadBansSuccess({ bans })),
          catchError(() => of(UsersActions.loadBansSuccess({ bans: [] })))
        );
      })
    )
  );

  /** Applies incoming moderation events from peers to local persistence and UI state. */
  incomingModerationEvents$ = createEffect(() =>
    this.webrtc.onMessageReceived.pipe(
      withLatestFrom(
        this.store.select(selectCurrentUser),
        this.store.select(selectCurrentRoom),
        this.store.select(selectSavedRooms)
      ),
      mergeMap(([
        event,
        currentUser,
        currentRoom,
        savedRooms
      ]) => {
        switch (event.type) {
          case 'kick':
            return this.handleIncomingKick(event, currentUser ?? null, currentRoom, savedRooms);

          case 'ban':
            return this.handleIncomingBan(event, currentUser ?? null, currentRoom, savedRooms);

          case 'unban':
            return this.handleIncomingUnban(event, currentRoom, savedRooms);

          default:
            return EMPTY;
        }
      })
    )
  );

  /** Elects the current user as host if the previous host leaves. */
  handleHostLeave$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UsersActions.userLeft),
      withLatestFrom(
        this.store.select(selectHostId),
        this.store.select(selectCurrentUserId)
      ),
      mergeMap(([
        { userId },
        hostId,
        currentUserId
      ]) =>
        userId === hostId && currentUserId
          ? of(UsersActions.updateHost({ userId: currentUserId }))
          : EMPTY
      )
    )
  );

  /** Persists user profile changes to the local database whenever the current user is updated. */
  persistUser$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(
          UsersActions.setCurrentUser,
          UsersActions.loadCurrentUserSuccess,
          UsersActions.updateCurrentUser,
          UsersActions.updateCurrentUserProfile
        ),
        withLatestFrom(this.store.select(selectCurrentUser)),
        tap(([, user]) => {
          if (user) {
            this.db.saveUser(user);
            // Ensure current user ID is persisted when explicitly set
            this.db.setCurrentUserId(user.id);
          }
        })
      ),
    { dispatch: false }
  );

  /** Keep signaling identity aligned with the current profile to avoid stale fallback names. */
  syncSignalingIdentity$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(
          UsersActions.setCurrentUser,
          UsersActions.loadCurrentUserSuccess,
          UsersActions.updateCurrentUserProfile
        ),
        withLatestFrom(this.store.select(selectCurrentUser)),
        tap(([, user]) => {
          if (!user)
            return;

          this.webrtc.identify(user.oderId || user.id, this.resolveDisplayName(user), undefined, {
            description: user.description,
            profileUpdatedAt: user.profileUpdatedAt
          });
        })
      ),
    { dispatch: false }
  );

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

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

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

  private resolveDisplayName(user: Pick<User, 'displayName' | 'username'>): string {
    const displayName = user.displayName?.trim();

    if (displayName) {
      return displayName;
    }

    return user.username?.trim() || 'User';
  }

  private toSourceSelector(room: Room): { sourceId?: string; sourceUrl?: string } {
    return {
      sourceId: room.sourceId,
      sourceUrl: room.sourceUrl
    };
  }

  private canModerateRoom(room: Room, currentUser: User, currentRoom: Room | null): boolean {
    const role = this.getCurrentUserRoleForRoom(room, currentUser, currentRoom);

    return role === 'host' || resolveRoomPermission(room, currentUser, 'manageBans');
  }

  private canKickInRoom(room: Room, currentUser: User, currentRoom: Room | null, targetUserId: string): boolean {
    return canManageMember(room, currentUser, findRoomMember(room.members ?? [], targetUserId) ?? { id: targetUserId }, 'kickMembers');
  }

  private canBanInRoom(room: Room, currentUser: User, currentRoom: Room | null, targetUserId: string): boolean {
    return canManageMember(room, currentUser, findRoomMember(room.members ?? [], targetUserId) ?? { id: targetUserId }, 'banMembers');
  }

  private getCurrentUserRoleForRoom(room: Room, currentUser: User, currentRoom: Room | null): User['role'] | null {
    return resolveLegacyRole(currentRoom?.id === room.id ? currentRoom : room, currentUser);
  }

  private removeMemberFromRoom(room: Room, targetUserId: string): Partial<Room> {
    return {
      members: removeRoomMember(room.members ?? [], targetUserId, targetUserId)
    };
  }

  private resolveIncomingModerationActions(
    room: Room,
    targetUserId: string,
    currentRoom: Room | null,
    extra: IncomingModerationExtraAction[] = []
  ) {
    const actions: IncomingModerationAction[] = [
      RoomsActions.updateRoom({
        roomId: room.id,
        changes: this.removeMemberFromRoom(room, targetUserId)
      })
    ];

    if (currentRoom?.id === room.id) {
      actions.push(...extra);
    } else {
      actions.push(...extra.filter((action) => action.type === RoomsActions.forgetRoom.type));
    }

    return actions;
  }

  private shouldAffectVisibleUsers(room: Room, currentRoom: Room | null): boolean {
    return currentRoom?.id === room.id;
  }

  private canForgetForTarget(
    targetUserId: string,
    currentUser: User | null
  ): ReturnType<typeof RoomsActions.forgetRoom> | null {
    return this.isCurrentUserTarget(targetUserId, currentUser)
      ? RoomsActions.forgetRoom({ roomId: '' })
      : null;
  }

  private isCurrentUserTarget(targetUserId: string, currentUser: User | null): boolean {
    return !!currentUser && (targetUserId === currentUser.id || targetUserId === currentUser.oderId);
  }

  private buildIncomingBan(event: ChatEvent, targetUserId: string, roomId: string): BanEntry {
    const payloadBan = event.ban && typeof event.ban === 'object'
      ? event.ban as Partial<BanEntry>
      : null;

    return {
      oderId: typeof payloadBan?.oderId === 'string' ? payloadBan.oderId : uuidv4(),
      userId: typeof payloadBan?.userId === 'string' ? payloadBan.userId : targetUserId,
      roomId,
      bannedBy:
        typeof payloadBan?.bannedBy === 'string'
          ? payloadBan.bannedBy
          : (typeof event.bannedBy === 'string' ? event.bannedBy : 'unknown'),
      displayName:
        typeof payloadBan?.displayName === 'string'
          ? payloadBan.displayName
          : (typeof event.displayName === 'string' ? event.displayName : undefined),
      reason:
        typeof payloadBan?.reason === 'string'
          ? payloadBan.reason
          : (typeof event.reason === 'string' ? event.reason : undefined),
      expiresAt:
        typeof payloadBan?.expiresAt === 'number'
          ? payloadBan.expiresAt
          : (typeof event.expiresAt === 'number' ? event.expiresAt : undefined),
      timestamp: typeof payloadBan?.timestamp === 'number' ? payloadBan.timestamp : Date.now()
    };
  }

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

    if (!room || !targetUserId)
      return EMPTY;

    const actions = this.resolveIncomingModerationActions(
      room,
      targetUserId,
      currentRoom,
      this.isCurrentUserTarget(targetUserId, currentUser)
        ? [RoomsActions.forgetRoom({ roomId: room.id })]
        : [
          UsersActions.kickUserSuccess({ userId: targetUserId,
            roomId: room.id })
        ]
    );

    return actions;
  }

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

    if (!room || !targetUserId)
      return EMPTY;

    const ban = this.buildIncomingBan(event, targetUserId, room.id);
    const actions = this.resolveIncomingModerationActions(
      room,
      targetUserId,
      currentRoom,
      this.isCurrentUserTarget(targetUserId, currentUser)
        ? [RoomsActions.forgetRoom({ roomId: room.id })]
        : [
          UsersActions.banUserSuccess({ userId: targetUserId,
            roomId: room.id,
            ban })
        ]
    );

    return from(this.db.saveBan(ban)).pipe(
      mergeMap(() => (actions.length > 0 ? actions : EMPTY)),
      catchError(() => EMPTY)
    );
  }

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

    if (!room || !banOderId)
      return EMPTY;

    return from(this.db.removeBan(banOderId)).pipe(
      mergeMap(() => (currentRoom?.id === room.id
        ? of(UsersActions.unbanUserSuccess({ oderId: banOderId }))
        : EMPTY)),
      catchError(() => EMPTY)
    );
  }
}
