import { createReducer, on } from '@ngrx/store';
import {
  EntityState,
  EntityAdapter,
  createEntityAdapter
} from '@ngrx/entity';
import {
  User,
  BanEntry,
  UserStatus
} from '../../shared-kernel';
import { UsersActions } from './users.actions';

function normalizePresenceServerIds(serverIds: readonly string[] | undefined): string[] | undefined {
  if (!Array.isArray(serverIds)) {
    return undefined;
  }

  const normalized = Array.from(new Set(
    serverIds.filter((serverId): serverId is string => typeof serverId === 'string' && serverId.trim().length > 0)
  ));

  return normalized.length > 0 ? normalized : undefined;
}

function mergePresenceServerIds(
  existingServerIds: readonly string[] | undefined,
  incomingServerIds: readonly string[] | undefined
): string[] | undefined {
  return normalizePresenceServerIds([...(existingServerIds ?? []), ...(incomingServerIds ?? [])]);
}

function hasLivePeerTransport(user: User, connectedPeerIds: ReadonlySet<string>): boolean {
  if (connectedPeerIds.size === 0) {
    return false;
  }

  return connectedPeerIds.has(user.id)
    || connectedPeerIds.has(user.oderId)
    || (!!user.peerId && connectedPeerIds.has(user.peerId));
}

interface AvatarFields {
  avatarUrl?: string;
  avatarHash?: string;
  avatarMime?: string;
  avatarUpdatedAt?: number;
}

interface ProfileFields {
  displayName: string;
  description?: string;
  profileUpdatedAt?: number;
}

function hasOwnProperty(object: object, key: string): boolean {
  return Object.prototype.hasOwnProperty.call(object, key);
}

function normalizeProfileUpdatedAt(value: unknown): number | undefined {
  return typeof value === 'number' && Number.isFinite(value) && value > 0
    ? value
    : undefined;
}

function normalizeDisplayName(value: unknown): string | undefined {
  if (typeof value !== 'string') {
    return undefined;
  }

  const normalized = value.trim().replace(/\s+/g, ' ');

  return normalized || undefined;
}

function normalizeDescription(value: unknown): string | undefined {
  if (typeof value !== 'string') {
    return undefined;
  }

  const normalized = value.trim();

  return normalized || undefined;
}

function mergeProfileFields(
  existingValue: Partial<ProfileFields> | undefined,
  incomingValue: Partial<ProfileFields>,
  preferIncomingFallback = true
): ProfileFields {
  const existingUpdatedAt = normalizeProfileUpdatedAt(existingValue?.profileUpdatedAt) ?? 0;
  const incomingUpdatedAt = normalizeProfileUpdatedAt(incomingValue.profileUpdatedAt) ?? 0;
  const preferIncoming = incomingUpdatedAt === existingUpdatedAt
    ? preferIncomingFallback
    : incomingUpdatedAt > existingUpdatedAt;
  const existingDisplayName = normalizeDisplayName(existingValue?.displayName);
  const incomingDisplayName = normalizeDisplayName(incomingValue.displayName);
  const existingDescription = normalizeDescription(existingValue?.description);
  const incomingHasDescription = hasOwnProperty(incomingValue, 'description');
  const incomingDescription = normalizeDescription(incomingValue.description);

  return {
    displayName: preferIncoming
      ? (incomingDisplayName || existingDisplayName || 'User')
      : (existingDisplayName || incomingDisplayName || 'User'),
    description: preferIncoming
      ? (incomingHasDescription ? incomingDescription : existingDescription)
      : existingDescription,
    profileUpdatedAt: Math.max(existingUpdatedAt, incomingUpdatedAt) || undefined
  };
}

function mergeAvatarFields(
  existingValue: AvatarFields | undefined,
  incomingValue: AvatarFields,
  preferIncomingFallback = true
): AvatarFields {
  const existingUpdatedAt = existingValue?.avatarUpdatedAt ?? 0;
  const incomingUpdatedAt = incomingValue.avatarUpdatedAt ?? 0;
  const preferIncoming = incomingUpdatedAt === existingUpdatedAt
    ? preferIncomingFallback
    : incomingUpdatedAt > existingUpdatedAt;

  return {
    avatarUrl: preferIncoming
      ? (incomingValue.avatarUrl || existingValue?.avatarUrl)
      : (existingValue?.avatarUrl || incomingValue.avatarUrl),
    avatarHash: preferIncoming
      ? (incomingValue.avatarHash || existingValue?.avatarHash)
      : (existingValue?.avatarHash || incomingValue.avatarHash),
    avatarMime: preferIncoming
      ? (incomingValue.avatarMime || existingValue?.avatarMime)
      : (existingValue?.avatarMime || incomingValue.avatarMime),

    avatarUpdatedAt: Math.max(existingUpdatedAt, incomingUpdatedAt) || undefined
  };
}

function buildDisconnectedVoiceState(user: User): User['voiceState'] {
  if (!user.voiceState) {
    return undefined;
  }

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

function buildInactiveScreenShareState(user: User): User['screenShareState'] {
  if (!user.screenShareState) {
    return undefined;
  }

  return {
    ...user.screenShareState,
    isSharing: false,
    streamId: undefined,
    sourceId: undefined,
    sourceName: undefined
  };
}

function buildInactiveCameraState(user: User): User['cameraState'] {
  if (!user.cameraState) {
    return undefined;
  }

  return {
    ...user.cameraState,
    isEnabled: false
  };
}

function buildPresenceAwareUser(existingUser: User | undefined, incomingUser: User): User {
  const presenceServerIds = mergePresenceServerIds(existingUser?.presenceServerIds, incomingUser.presenceServerIds);
  const isOnline = (presenceServerIds?.length ?? 0) > 0 || incomingUser.isOnline === true;
  const status = isOnline
    ? (incomingUser.status !== 'offline'
      ? incomingUser.status
      : (existingUser?.status && existingUser.status !== 'offline' ? existingUser.status : 'online'))
    : 'offline';
  const profileFields = mergeProfileFields(existingUser, incomingUser, true);

  return {
    ...existingUser,
    ...incomingUser,
    ...profileFields,
    ...mergeAvatarFields(existingUser, incomingUser, true),
    presenceServerIds,
    isOnline,
    status
  };
}

function buildAvatarUser(existingUser: User | undefined, incomingUser: {
  id: string;
  oderId: string;
  username: string;
  displayName: string;
  description?: string;
  profileUpdatedAt?: number;
  avatarUrl?: string;
  avatarHash?: string;
  avatarMime?: string;
  avatarUpdatedAt?: number;
}): User {
  const profileFields = mergeProfileFields(existingUser, incomingUser, true);

  return {
    ...existingUser,
    id: incomingUser.id,
    oderId: incomingUser.oderId,
    username: incomingUser.username || existingUser?.username || 'user',
    ...profileFields,
    status: existingUser?.status ?? 'offline',
    role: existingUser?.role ?? 'member',
    joinedAt: existingUser?.joinedAt ?? Date.now(),
    ...mergeAvatarFields(existingUser, incomingUser, true)
  };
}

function buildPresenceRemovalChanges(
  user: User,
  update: { serverId?: string; serverIds?: readonly string[] }
): Partial<User> {
  const nextPresenceServerIds = update.serverIds !== undefined
    ? normalizePresenceServerIds(update.serverIds)
    : normalizePresenceServerIds((user.presenceServerIds ?? []).filter((serverId) => serverId !== update.serverId));
  const isOnline = (nextPresenceServerIds?.length ?? 0) > 0;
  const shouldClearLiveState = !isOnline
    || (!!user.voiceState?.serverId && !nextPresenceServerIds?.includes(user.voiceState.serverId));

  return {
    presenceServerIds: nextPresenceServerIds,
    isOnline,
    status: isOnline ? (user.status !== 'offline' ? user.status : 'online') : 'offline',
    voiceState: shouldClearLiveState ? buildDisconnectedVoiceState(user) : user.voiceState,
    screenShareState: shouldClearLiveState ? buildInactiveScreenShareState(user) : user.screenShareState,
    cameraState: shouldClearLiveState ? buildInactiveCameraState(user) : user.cameraState,
    gameActivity: isOnline ? user.gameActivity : undefined
  };
}

export interface UsersState extends EntityState<User> {
  currentUserId: string | null;
  hostId: string | null;
  loading: boolean;
  error: string | null;
  bans: BanEntry[];
  /** Manual status set by user (e.g. DND). `null` = automatic. */
  manualStatus: UserStatus | null;
}

export const usersAdapter: EntityAdapter<User> = createEntityAdapter<User>({
  selectId: (user) => user.id,
  sortComparer: (userA, userB) => userA.username.localeCompare(userB.username)
});

export const initialState: UsersState = usersAdapter.getInitialState({
  currentUserId: null,
  hostId: null,
  loading: false,
  error: null,
  bans: [],
  manualStatus: null
});

export const usersReducer = createReducer(
  initialState,
  on(UsersActions.resetUsersState, () => ({
    ...initialState
  })),

  on(UsersActions.loadCurrentUser, (state) => ({
    ...state,
    loading: true,
    error: null
  })),

  on(UsersActions.loadCurrentUserSuccess, (state, { user }) =>
    usersAdapter.upsertOne(user, {
      ...state,
      currentUserId: user.id,
      loading: false
    })
  ),

  on(UsersActions.loadCurrentUserFailure, (state, { error }) => ({
    ...state,
    loading: false,
    error
  })),
  on(UsersActions.setCurrentUser, (state, { user }) =>
    usersAdapter.upsertOne(user, {
      ...state,
      currentUserId: user.id
    })
  ),
  on(UsersActions.updateCurrentUser, (state, { updates }) => {
    if (!state.currentUserId)
      return state;

    return usersAdapter.updateOne(
      {
        id: state.currentUserId,
        changes: updates
      },
      state
    );
  }),
  on(UsersActions.updateCurrentUserProfile, (state, { profile }) => {
    if (!state.currentUserId)
      return state;

    return usersAdapter.updateOne(
      {
        id: state.currentUserId,
        changes: mergeProfileFields(state.entities[state.currentUserId], profile, true)
      },
      state
    );
  }),
  on(UsersActions.updateCurrentUserAvatar, (state, { avatar }) => {
    if (!state.currentUserId)
      return state;

    return usersAdapter.updateOne(
      {
        id: state.currentUserId,
        changes: mergeAvatarFields(state.entities[state.currentUserId], avatar, true)
      },
      state
    );
  }),
  on(UsersActions.loadRoomUsers, (state) => ({
    ...state,
    loading: true,
    error: null
  })),

  on(UsersActions.loadRoomUsersSuccess, (state, { users }) =>
    usersAdapter.upsertMany(users, {
      ...state,
      loading: false
    })
  ),

  on(UsersActions.loadRoomUsersFailure, (state, { error }) => ({
    ...state,
    loading: false,
    error
  })),
  on(UsersActions.userJoined, (state, { user }) =>
    usersAdapter.upsertOne(buildPresenceAwareUser(state.entities[user.id], user), state)
  ),
  on(UsersActions.syncServerPresence, (state, { roomId, users, connectedPeerIds }) => {
    let nextState = state;

    const seenUserIds = new Set<string>();
    const connectedPeerIdSet = new Set(connectedPeerIds ?? []);

    for (const user of users) {
      seenUserIds.add(user.id);
      nextState = usersAdapter.upsertOne(
        buildPresenceAwareUser(nextState.entities[user.id], user),
        nextState
      );
    }

    const stalePresenceUpdates = Object.values(nextState.entities)
      .filter((user): user is User =>
        !!user
        && user.id !== nextState.currentUserId
        && user.presenceServerIds?.includes(roomId) === true
        && !seenUserIds.has(user.id)
        && !hasLivePeerTransport(user, connectedPeerIdSet)
      )
      .map((user) => ({
        id: user.id,
        changes: buildPresenceRemovalChanges(user, { serverId: roomId })
      }));

    return stalePresenceUpdates.length > 0
      ? usersAdapter.updateMany(stalePresenceUpdates, nextState)
      : nextState;
  }),
  on(UsersActions.userLeft, (state, { userId, serverId, serverIds }) => {
    const existingUser = state.entities[userId];

    if (!existingUser) {
      return (!serverId && !serverIds)
        ? usersAdapter.removeOne(userId, state)
        : state;
    }

    if (!serverId && !serverIds) {
      return usersAdapter.removeOne(userId, state);
    }

    return usersAdapter.updateOne(
      {
        id: userId,
        changes: buildPresenceRemovalChanges(existingUser, {
          serverId,
          serverIds
        })
      },
      state
    );
  }),
  on(UsersActions.updateUser, (state, { userId, updates }) =>
    usersAdapter.updateOne(
      {
        id: userId,
        changes: updates
      },
      state
    )
  ),
  on(UsersActions.upsertRemoteUserAvatar, (state, { user }) =>
    usersAdapter.upsertOne(buildAvatarUser(state.entities[user.id], user), state)
  ),
  on(UsersActions.updateUserRole, (state, { userId, role }) =>
    usersAdapter.updateOne(
      {
        id: userId,
        changes: { role }
      },
      state
    )
  ),
  on(UsersActions.kickUserSuccess, (state, { userId }) =>
    usersAdapter.removeOne(userId, state)
  ),
  on(UsersActions.banUserSuccess, (state, { userId, ban }) => {
    const newState = usersAdapter.removeOne(userId, state);

    return {
      ...newState,
      bans: [...state.bans, ban]
    };
  }),
  on(UsersActions.unbanUserSuccess, (state, { oderId }) => ({
    ...state,
    bans: state.bans.filter((ban) => ban.oderId !== oderId)
  })),
  on(UsersActions.loadBansSuccess, (state, { bans }) => ({
    ...state,
    bans
  })),
  on(UsersActions.adminMuteUser, (state, { userId }) =>
    usersAdapter.updateOne(
      {
        id: userId,
        changes: {
          voiceState: {
            ...state.entities[userId]?.voiceState,
            isConnected: state.entities[userId]?.voiceState?.isConnected ?? false,
            isMuted: true,
            isDeafened: state.entities[userId]?.voiceState?.isDeafened ?? false,
            isSpeaking: false,
            isMutedByAdmin: true
          }
        }
      },
      state
    )
  ),
  on(UsersActions.adminUnmuteUser, (state, { userId }) =>
    usersAdapter.updateOne(
      {
        id: userId,
        changes: {
          voiceState: {
            ...state.entities[userId]?.voiceState,
            isConnected: state.entities[userId]?.voiceState?.isConnected ?? false,
            isMuted: state.entities[userId]?.voiceState?.isMuted ?? false,
            isDeafened: state.entities[userId]?.voiceState?.isDeafened ?? false,
            isSpeaking: state.entities[userId]?.voiceState?.isSpeaking ?? false,
            isMutedByAdmin: false
          }
        }
      },
      state
    )
  ),
  on(UsersActions.updateVoiceState, (state, { userId, voiceState }) => {
    const prev = state.entities[userId]?.voiceState || {
      isConnected: false,
      isMuted: false,
      isDeafened: false,
      isSpeaking: false
    };
    const hasRoomId = Object.prototype.hasOwnProperty.call(voiceState, 'roomId');
    const hasServerId = Object.prototype.hasOwnProperty.call(voiceState, 'serverId');

    return usersAdapter.updateOne(
      {
        id: userId,
        changes: {
          voiceState: {
            isConnected: voiceState.isConnected ?? prev.isConnected,
            isMuted: voiceState.isMuted ?? prev.isMuted,
            isDeafened: voiceState.isDeafened ?? prev.isDeafened,
            isSpeaking: voiceState.isSpeaking ?? prev.isSpeaking,
            isMutedByAdmin: voiceState.isMutedByAdmin ?? prev.isMutedByAdmin,
            volume: voiceState.volume ?? prev.volume,
            roomId: hasRoomId ? voiceState.roomId : prev.roomId,
            serverId: hasServerId ? voiceState.serverId : prev.serverId
          }
        }
      },
      state
    );
  }),
  on(UsersActions.updateScreenShareState, (state, { userId, screenShareState }) => {
    const prev = state.entities[userId]?.screenShareState || {
      isSharing: false
    };

    return usersAdapter.updateOne(
      {
        id: userId,
        changes: {
          screenShareState: {
            isSharing: screenShareState.isSharing ?? prev.isSharing,
            streamId: screenShareState.streamId ?? prev.streamId,
            sourceId: screenShareState.sourceId ?? prev.sourceId,
            sourceName: screenShareState.sourceName ?? prev.sourceName
          }
        }
      },
      state
    );
  }),
  on(UsersActions.updateCameraState, (state, { userId, cameraState }) => {
    const prev = state.entities[userId]?.cameraState || {
      isEnabled: false
    };

    return usersAdapter.updateOne(
      {
        id: userId,
        changes: {
          cameraState: {
            isEnabled: cameraState.isEnabled ?? prev.isEnabled
          }
        }
      },
      state
    );
  }),
  on(UsersActions.updateGameActivity, (state, { userId, gameActivity }) => {
    const existingUser = state.entities[userId];

    if (!existingUser) {
      return state;
    }

    return usersAdapter.updateOne(
      {
        id: userId,
        changes: {
          gameActivity: gameActivity ?? undefined
        }
      },
      state
    );
  }),
  on(UsersActions.syncUsers, (state, { users }) =>
    usersAdapter.upsertMany(users, state)
  ),
  on(UsersActions.clearUsers, (state) => {
    const idsToRemove = Object.keys(state.entities).filter((id) => id !== state.currentUserId);

    return usersAdapter.removeMany(idsToRemove, {
      ...state,
      hostId: null
    });
  }),
  on(UsersActions.updateHost, (state, { userId }) => {
    let newState = state;

    if (state.hostId && state.hostId !== userId) {
      newState = usersAdapter.updateOne(
        {
          id: state.hostId,
          changes: { role: 'member' }
        },
        state
      );
    }

    return usersAdapter.updateOne(
      {
        id: userId,
        changes: { role: 'host' }
      },
      {
        ...newState,
        hostId: userId
      }
    );
  }),
  on(UsersActions.setManualStatus, (state, { status }) => {
    const manualStatus = status;
    const effectiveStatus = manualStatus ?? 'online';

    if (!state.currentUserId)
      return { ...state, manualStatus };

    return usersAdapter.updateOne(
      {
        id: state.currentUserId,
        changes: { status: effectiveStatus }
      },
      { ...state, manualStatus }
    );
  }),
  on(UsersActions.updateRemoteUserStatus, (state, { userId, status }) => {
    const existingUser = state.entities[userId];

    if (!existingUser)
      return state;

    return usersAdapter.updateOne(
      {
        id: userId,
        changes: { status }
      },
      state
    );
  })
);
