/*
 * Copyright (C) 2026 Fluxer Contributors
 *
 * This file is part of Fluxer.
 *
 * Fluxer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Fluxer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
 */

import {createHash} from 'node:crypto';
import type {AlertService} from '@fluxer/api/src/alert/AlertService';
import type {AuthService} from '@fluxer/api/src/auth/AuthService';
import type {ChannelID, GuildID, UserID} from '@fluxer/api/src/BrandedTypes';
import {
	createChannelID,
	createGuildID,
	createMessageID,
	createUserID,
	userIdToChannelId,
	vanityCodeToInviteCode,
} from '@fluxer/api/src/BrandedTypes';
import {Config} from '@fluxer/api/src/Config';
import {mapChannelToResponse} from '@fluxer/api/src/channel/ChannelMappers';
import type {IChannelRepository} from '@fluxer/api/src/channel/IChannelRepository';
import {mapMessageToResponse} from '@fluxer/api/src/channel/MessageMappers';
import {mapFavoriteMemeToResponse} from '@fluxer/api/src/favorite_meme/FavoriteMemeModel';
import type {IFavoriteMemeRepository} from '@fluxer/api/src/favorite_meme/IFavoriteMemeRepository';
import {
	mapGuildEmojiToResponse,
	mapGuildMemberToResponse,
	mapGuildRoleToResponse,
	mapGuildStickerToResponse,
	mapGuildToGuildResponse,
} from '@fluxer/api/src/guild/GuildModel';
import type {IGuildRepositoryAggregate} from '@fluxer/api/src/guild/repositories/IGuildRepositoryAggregate';
import type {AvatarService} from '@fluxer/api/src/infrastructure/AvatarService';
import type {IDiscriminatorService} from '@fluxer/api/src/infrastructure/DiscriminatorService';
import type {IGatewayService} from '@fluxer/api/src/infrastructure/IGatewayService';
import type {IMediaService} from '@fluxer/api/src/infrastructure/IMediaService';
import type {IStorageService} from '@fluxer/api/src/infrastructure/IStorageService';
import type {UserCacheService} from '@fluxer/api/src/infrastructure/UserCacheService';
import type {IInviteRepository} from '@fluxer/api/src/invite/IInviteRepository';
import {Logger} from '@fluxer/api/src/Logger';
import type {LimitConfigService} from '@fluxer/api/src/limits/LimitConfigService';
import {resolveLimitSafe} from '@fluxer/api/src/limits/LimitConfigUtils';
import {createLimitMatchContext} from '@fluxer/api/src/limits/LimitMatchContextBuilder';
import type {RequestCache} from '@fluxer/api/src/middleware/RequestCacheMiddleware';
import type {AuthSession} from '@fluxer/api/src/models/AuthSession';
import type {Channel} from '@fluxer/api/src/models/Channel';
import type {FavoriteMeme} from '@fluxer/api/src/models/FavoriteMeme';
import type {Guild} from '@fluxer/api/src/models/Guild';
import type {GuildEmoji} from '@fluxer/api/src/models/GuildEmoji';
import type {GuildMember} from '@fluxer/api/src/models/GuildMember';
import type {GuildRole} from '@fluxer/api/src/models/GuildRole';
import type {GuildSticker} from '@fluxer/api/src/models/GuildSticker';
import type {ReadState} from '@fluxer/api/src/models/ReadState';
import type {Relationship} from '@fluxer/api/src/models/Relationship';
import type {User} from '@fluxer/api/src/models/User';
import type {UserGuildSettings} from '@fluxer/api/src/models/UserGuildSettings';
import {UserSettings} from '@fluxer/api/src/models/UserSettings';
import type {BotAuthService} from '@fluxer/api/src/oauth/BotAuthService';
import type {IApplicationRepository} from '@fluxer/api/src/oauth/repositories/IApplicationRepository';
import type {PackService} from '@fluxer/api/src/pack/PackService';
import type {ReadStateService} from '@fluxer/api/src/read_state/ReadStateService';
import type {IUserRepository} from '@fluxer/api/src/user/IUserRepository';
import {CustomStatusValidator} from '@fluxer/api/src/user/services/CustomStatusValidator';
import {getCachedUserPartialResponse} from '@fluxer/api/src/user/UserCacheHelpers';
import {createPremiumClearPatch, shouldStripExpiredPremium} from '@fluxer/api/src/user/UserHelpers';
import {
	mapRelationshipToResponse,
	mapUserGuildSettingsToResponse,
	mapUserSettingsToResponse,
	mapUserToPrivateResponse,
} from '@fluxer/api/src/user/UserMappers';
import {isUserAdult} from '@fluxer/api/src/utils/AgeUtils';
import {deriveDominantAvatarColor} from '@fluxer/api/src/utils/AvatarColorUtils';
import {calculateDistance, parseCoordinate} from '@fluxer/api/src/utils/GeoUtils';
import {lookupGeoip} from '@fluxer/api/src/utils/IpUtils';
import type {VoiceAccessContext, VoiceAvailabilityService} from '@fluxer/api/src/voice/VoiceAvailabilityService';
import type {VoiceService} from '@fluxer/api/src/voice/VoiceService';
import type {IWebhookRepository} from '@fluxer/api/src/webhook/IWebhookRepository';
import {ChannelTypes, MessageTypes} from '@fluxer/constants/src/ChannelConstants';
import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
import {MAX_PRIVATE_CHANNELS_PER_USER} from '@fluxer/constants/src/LimitConstants';
import {
	GroupDmAddPermissionFlags,
	IncomingCallFlags,
	UserFlags,
	UserPremiumTypes,
} from '@fluxer/constants/src/UserConstants';
import {RateLimitError} from '@fluxer/errors/src/domains/core/RateLimitError';
import {UnauthorizedError} from '@fluxer/errors/src/domains/core/UnauthorizedError';
import {UnknownGuildError} from '@fluxer/errors/src/domains/guild/UnknownGuildError';
import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError';
import type {IRateLimitService} from '@fluxer/rate_limit/src/IRateLimitService';
import type {ChannelResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
import type {GuildMemberResponse} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
import type {
	RpcRequest,
	RpcResponse,
	RpcResponseGuildData,
	RpcResponseSessionData,
} from '@fluxer/schema/src/domains/rpc/RpcSchemas';
import type {RelationshipResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
import {ms} from 'itty-time';
import sharp from 'sharp';
import {uint8ArrayToBase64} from 'uint8array-extras';

interface HandleRpcRequestParams {
	request: RpcRequest;
	requestCache: RequestCache;
}

interface HandleSessionRequestParams {
	token: string;
	version: number;
	requestCache: RequestCache;
	ip?: string;
	latitude?: string;
	longitude?: string;
}

interface HandleGuildRequestParams {
	guildId: GuildID;
	requestCache: RequestCache;
}

interface GetGuildDataParams {
	guildId: GuildID;
}

interface GetUserDataParams {
	userId: UserID;
	includePrivateChannels?: boolean;
}

interface GuildData {
	guild: Guild;
	channels: Array<Channel>;
	emojis: Array<GuildEmoji>;
	stickers: Array<GuildSticker>;
	members: Array<GuildMember>;
	roles: Array<GuildRole>;
}

interface UserData {
	user: User;
	settings: UserSettings | null;
	guildSettings: Array<UserGuildSettings>;
	notes: Map<UserID, string>;
	readStates: Array<ReadState>;
	guildIds: Array<GuildID>;
	privateChannels: Array<Channel>;
	relationships: Array<Relationship>;
	favoriteMemes: Array<FavoriteMeme>;
	pinnedDMs: Array<ChannelID>;
}

export class RpcService {
	private readonly customStatusValidator: CustomStatusValidator;

	constructor(
		private userRepository: IUserRepository,
		private applicationRepository: IApplicationRepository,
		private guildRepository: IGuildRepositoryAggregate,
		private channelRepository: IChannelRepository,
		private userCacheService: UserCacheService,
		private readStateService: ReadStateService,
		private authService: AuthService,
		private gatewayService: IGatewayService,
		private alertService: AlertService,
		private discriminatorService: IDiscriminatorService,
		private favoriteMemeRepository: IFavoriteMemeRepository,
		private packService: PackService,
		private botAuthService: BotAuthService,
		private inviteRepository: IInviteRepository,
		private webhookRepository: IWebhookRepository,
		private storageService: IStorageService,
		private avatarService: AvatarService,
		private rateLimitService: IRateLimitService,
		private mediaService: IMediaService,
		private readonly limitConfigService: LimitConfigService,
		private voiceService?: VoiceService,
		private voiceAvailabilityService?: VoiceAvailabilityService,
	) {
		this.customStatusValidator = new CustomStatusValidator(
			this.userRepository,
			this.guildRepository,
			this.packService,
			this.limitConfigService,
		);
	}

	private async sanitizeOwnedBotDiscriminators(owner: User): Promise<boolean> {
		// Only reroll bot discriminators that are invalid (#0000). This is a one-time migration guarded by UserFlags.BOT_SANITIZED.
		const applications = await this.applicationRepository.listApplicationsByOwner(owner.id);
		const botUserIds: Array<UserID> = [];

		for (const app of applications) {
			if (!app.hasBotUser()) continue;
			const botUserId = app.getBotUserId();
			if (botUserId) {
				botUserIds.push(botUserId);
			}
		}

		const uniqueBotUserIds = Array.from(new Set(botUserIds));
		let allSucceeded = true;
		let rerolledCount = 0;

		for (const botUserId of uniqueBotUserIds) {
			const botUser = await this.userRepository.findUnique(botUserId);
			if (!botUser) {
				Logger.warn(
					{ownerUserId: owner.id.toString(), botUserId: botUserId.toString()},
					'Owned bot user missing during discriminator sanitization',
				);
				allSucceeded = false;
				continue;
			}

			if (!botUser.isBot) {
				Logger.warn(
					{ownerUserId: owner.id.toString(), botUserId: botUserId.toString()},
					'Owned bot user record is not marked as bot during discriminator sanitization',
				);
				allSucceeded = false;
				continue;
			}

			const isDeleted = (botUser.flags & UserFlags.DELETED) === UserFlags.DELETED;
			if (isDeleted) {
				continue;
			}

			if (botUser.discriminator !== 0) {
				continue;
			}

			try {
				const discriminatorResult = await this.discriminatorService.generateDiscriminator({
					username: botUser.username,
				});

				if (
					!discriminatorResult.available ||
					discriminatorResult.discriminator === -1 ||
					discriminatorResult.discriminator === 0
				) {
					Logger.warn(
						{
							ownerUserId: owner.id.toString(),
							botUserId: botUserId.toString(),
							username: botUser.username,
							discriminatorResult,
						},
						'Failed to reroll invalid bot discriminator during sanitization',
					);
					allSucceeded = false;
					continue;
				}

				const updatedBotUser = await this.userRepository.patchUpsert(
					botUserId,
					{
						discriminator: discriminatorResult.discriminator,
					},
					botUser.toRow(),
				);
				await this.userCacheService.setUserPartialResponseFromUser(updatedBotUser);
				rerolledCount += 1;
			} catch (error) {
				Logger.warn(
					{ownerUserId: owner.id.toString(), botUserId: botUserId.toString(), error},
					'Failed to sanitize owned bot discriminator',
				);
				allSucceeded = false;
			}
		}

		Logger.info(
			{
				ownerUserId: owner.id.toString(),
				botCount: uniqueBotUserIds.length,
				rerolledCount,
				allSucceeded,
			},
			'Completed owned bot discriminator sanitization pass',
		);

		return allSucceeded;
	}

	private async ensurePersonalNotesChannel(user: User): Promise<void> {
		const personalNotesChannelId = userIdToChannelId(user.id);
		const existingChannel = await this.channelRepository.findUnique(personalNotesChannelId);
		if (existingChannel) {
			if (existingChannel.type !== ChannelTypes.DM_PERSONAL_NOTES) {
				Logger.warn(
					{channelId: personalNotesChannelId.toString(), type: existingChannel.type},
					'Unexpected channel type already exists for personal notes channel',
				);
			}
			return;
		}

		await this.channelRepository.upsert({
			channel_id: personalNotesChannelId,
			guild_id: null,
			type: ChannelTypes.DM_PERSONAL_NOTES,
			name: '',
			topic: null,
			icon_hash: null,
			url: null,
			parent_id: null,
			position: 0,
			owner_id: null,
			recipient_ids: new Set(),
			nsfw: false,
			rate_limit_per_user: 0,
			bitrate: null,
			user_limit: null,
			rtc_region: null,
			last_message_id: null,
			last_pin_timestamp: null,
			permission_overwrites: null,
			nicks: null,
			soft_deleted: false,
			indexed_at: null,
			version: 1,
		});
	}

	private async updateGuildMemberCount(guild: Guild, actualMemberCount: number): Promise<Guild> {
		if (guild.memberCount === actualMemberCount) {
			return guild;
		}
		const guildRow = guild.toRow();
		return await this.guildRepository.upsert({
			...guildRow,
			member_count: actualMemberCount,
		});
	}

	private async migrateStickerAnimated(sticker: GuildSticker): Promise<GuildSticker> {
		if (sticker.animated !== null && sticker.animated !== undefined) {
			return sticker;
		}

		try {
			const animated = await this.avatarService.checkStickerAnimated(sticker.id);

			if (animated !== null) {
				const updatedSticker = await this.guildRepository.upsertSticker({
					guild_id: sticker.guildId,
					sticker_id: sticker.id,
					name: sticker.name,
					description: sticker.description,
					animated,
					tags: sticker.tags,
					creator_id: sticker.creatorId,
					version: sticker.version,
				});

				Logger.debug({stickerId: sticker.id, animated}, 'Migrated sticker animated field');
				return updatedSticker;
			}
		} catch (error) {
			Logger.warn({stickerId: sticker.id, error}, 'Failed to migrate sticker animated field');
		}

		return sticker;
	}

	async handleRpcRequest({request, requestCache}: HandleRpcRequestParams): Promise<RpcResponse> {
		switch (request.type) {
			case 'session':
				return {
					type: 'session',
					data: await this.handleSessionRequest({
						token: request.token,
						version: request.version,
						requestCache,
						ip: request.ip,
						latitude: request.latitude,
						longitude: request.longitude,
					}),
				};
			case 'log_guild_crash': {
				await this.alertService.logGuildCrash({
					guildId: request.guild_id.toString(),
					stacktrace: request.stacktrace,
				});
				return {
					type: 'log_guild_crash',
					data: {success: true},
				};
			}
			case 'guild':
				return {
					type: 'guild',
					data: await this.handleGuildRequest({
						guildId: createGuildID(request.guild_id),
						requestCache,
					}),
				};
			case 'get_user_guild_settings': {
				const result = await this.getUserGuildSettings({
					userIds: request.user_ids.map(createUserID),
					guildId: createGuildID(request.guild_id),
				});
				return {
					type: 'get_user_guild_settings',
					data: {
						user_guild_settings: result.user_guild_settings.map((settings) =>
							settings ? mapUserGuildSettingsToResponse(settings) : null,
						),
					},
				};
			}
			case 'get_push_subscriptions':
				return {
					type: 'get_push_subscriptions',
					data: await this.getPushSubscriptions({
						userIds: request.user_ids.map(createUserID),
					}),
				};
			case 'get_badge_counts': {
				const badgeCounts = await this.getBadgeCounts({
					userIds: request.user_ids.map(createUserID),
				});
				return {
					type: 'get_badge_counts',
					data: {
						badge_counts: badgeCounts,
					},
				};
			}
			case 'geoip_lookup': {
				const geoip = await lookupGeoip(request.ip);
				return {
					type: 'geoip_lookup',
					data: {
						country_code: geoip.countryCode ?? 'US',
					},
				};
			}
			case 'delete_push_subscriptions':
				return {
					type: 'delete_push_subscriptions',
					data: await this.deletePushSubscriptions({
						subscriptions: request.subscriptions.map((sub) => ({
							userId: createUserID(sub.user_id),
							subscriptionId: sub.subscription_id,
						})),
					}),
				};
			case 'get_user_blocked_ids':
				return {
					type: 'get_user_blocked_ids',
					data: await this.getUserBlockedIds({
						userIds: request.user_ids.map(createUserID),
					}),
				};
			case 'voice_get_token': {
				Logger.debug(
					{type: 'voice_get_token', guildId: request.guild_id, channelId: request.channel_id, userId: request.user_id},
					'RPC voice_get_token received',
				);
				if (!this.voiceService) {
					throw new Error('Voice is not enabled on this server');
				}
				const result = await this.voiceService.getVoiceToken({
					guildId: request.guild_id !== undefined ? createGuildID(request.guild_id) : undefined,
					channelId: createChannelID(request.channel_id),
					userId: createUserID(request.user_id),
					connectionId: request.connection_id,
					region: request.rtc_region,
					latitude: request.latitude,
					longitude: request.longitude,
					canSpeak: request.can_speak,
					canStream: request.can_stream,
					canVideo: request.can_video,
					tokenNonce: request.token_nonce,
				});
				return {
					type: 'voice_get_token',
					data: result,
				};
			}
			case 'voice_force_disconnect_participant': {
				if (!this.voiceService) {
					throw new Error('Voice is not enabled on this server');
				}
				await this.voiceService.disconnectParticipant({
					guildId: request.guild_id !== undefined ? createGuildID(request.guild_id) : undefined,
					channelId: createChannelID(request.channel_id),
					userId: createUserID(request.user_id),
					connectionId: request.connection_id,
				});
				return {
					type: 'voice_force_disconnect_participant',
					data: {success: true},
				};
			}
			case 'voice_update_participant': {
				if (!this.voiceService) {
					throw new Error('Voice is not enabled on this server');
				}
				await this.voiceService.updateParticipant({
					guildId: request.guild_id !== undefined ? createGuildID(request.guild_id) : undefined,
					channelId: createChannelID(request.channel_id),
					userId: createUserID(request.user_id),
					mute: request.mute,
					deaf: request.deaf,
				});
				return {
					type: 'voice_update_participant',
					data: {success: true},
				};
			}
			case 'voice_force_disconnect_channel': {
				if (!this.voiceService) {
					throw new Error('Voice is not enabled on this server');
				}
				const result = await this.voiceService.disconnectChannel({
					guildId: request.guild_id !== undefined ? createGuildID(request.guild_id) : undefined,
					channelId: createChannelID(request.channel_id),
				});
				return {
					type: 'voice_force_disconnect_channel',
					data: {
						success: result.success,
						disconnected_count: result.disconnectedCount,
						message: result.message,
					},
				};
			}
			case 'voice_update_participant_permissions': {
				if (!this.voiceService) {
					throw new Error('Voice is not enabled on this server');
				}
				await this.voiceService.updateParticipantPermissions({
					guildId: request.guild_id !== undefined ? createGuildID(request.guild_id) : undefined,
					channelId: createChannelID(request.channel_id),
					userId: createUserID(request.user_id),
					connectionId: request.connection_id,
					canSpeak: request.can_speak,
					canStream: request.can_stream,
					canVideo: request.can_video,
				});
				return {
					type: 'voice_update_participant_permissions',
					data: {success: true},
				};
			}
			case 'kick_temporary_member': {
				const success = await this.kickTemporaryMember({
					userId: createUserID(request.user_id),
					guildIds: request.guild_ids.map(createGuildID),
				});
				return {
					type: 'kick_temporary_member',
					data: {success},
				};
			}
			case 'call_ended': {
				await this.handleCallEnded({
					channelId: createChannelID(request.channel_id),
					messageId: createMessageID(request.message_id),
					participants: request.participants.map(createUserID),
					endedTimestamp: new Date(request.ended_timestamp),
					requestCache,
				});
				return {
					type: 'call_ended',
					data: {success: true},
				};
			}
			case 'validate_custom_status': {
				const userId = createUserID(request.user_id);
				const validatedCustomStatus =
					request.custom_status === null || request.custom_status === undefined
						? null
						: await this.customStatusValidator.validate(userId, request.custom_status);
				return {
					type: 'validate_custom_status',
					data: {
						custom_status: validatedCustomStatus
							? {
									text: validatedCustomStatus.text,
									expires_at: validatedCustomStatus.expiresAt?.toISOString() ?? null,
									emoji_id: validatedCustomStatus.emojiId?.toString() ?? null,
									emoji_name: validatedCustomStatus.emojiName,
									emoji_animated: validatedCustomStatus.emojiAnimated,
								}
							: null,
					},
				};
			}
			case 'get_dm_channel': {
				const channel = await this.getDmChannel({
					channelId: createChannelID(request.channel_id),
					userId: createUserID(request.user_id),
					requestCache,
				});
				return {
					type: 'get_dm_channel',
					data: {channel},
				};
			}
			default: {
				const exhaustiveCheck: never = request;
				throw new Error(`Unknown RPC request type: ${String((exhaustiveCheck as {type?: string}).type ?? 'unknown')}`);
			}
		}
	}

	private parseTokenType(token: string): 'user' | 'bot' | 'unknown' {
		if (token.startsWith('flx_')) {
			return 'user';
		}

		const dotIndex = token.indexOf('.');
		if (dotIndex > 0 && dotIndex < token.length - 1) {
			const beforeDot = token.slice(0, dotIndex);
			if (/^\d+$/.test(beforeDot)) {
				return 'bot';
			}
		}

		return 'unknown';
	}

	private normalizeSessionToken(token: string): string {
		if (token.startsWith('Bot ')) {
			return token.slice('Bot '.length);
		}
		return token;
	}

	private isUnknownUserError(error: unknown): error is UnknownUserError {
		return error instanceof UnknownUserError;
	}

	private async mapRpcSessionPrivateChannels(params: {
		channels: Array<Channel>;
		userId: UserID;
		requestCache: RequestCache;
	}): Promise<Array<ChannelResponse>> {
		const {channels, userId, requestCache} = params;
		const mappedChannels = await Promise.allSettled(
			channels.map((channel) =>
				mapChannelToResponse({
					channel,
					currentUserId: userId,
					userCacheService: this.userCacheService,
					requestCache,
				}),
			),
		);
		const validChannels: Array<ChannelResponse> = [];
		for (const [index, mappedChannel] of mappedChannels.entries()) {
			if (mappedChannel.status === 'fulfilled') {
				validChannels.push(mappedChannel.value);
				continue;
			}
			const channel = channels[index];
			if (this.isUnknownUserError(mappedChannel.reason)) {
				Logger.warn(
					{
						userId: userId.toString(),
						channelId: channel?.id.toString(),
					},
					'Skipping RPC session private channel with unknown user reference',
				);
				continue;
			}
			throw mappedChannel.reason;
		}
		return validChannels;
	}

	private async mapRpcSessionRelationships(params: {
		relationships: Array<Relationship>;
		userId: UserID;
		requestCache: RequestCache;
	}): Promise<Array<RelationshipResponse>> {
		const {relationships, userId, requestCache} = params;
		const userPartialResolver = (targetUserId: UserID) =>
			getCachedUserPartialResponse({
				userId: targetUserId,
				userCacheService: this.userCacheService,
				requestCache,
			});
		const mappedRelationships = await Promise.allSettled(
			relationships.map((relationship) => mapRelationshipToResponse({relationship, userPartialResolver})),
		);
		const validRelationships: Array<RelationshipResponse> = [];
		for (const [index, mappedRelationship] of mappedRelationships.entries()) {
			if (mappedRelationship.status === 'fulfilled') {
				validRelationships.push(mappedRelationship.value);
				continue;
			}
			const relationship = relationships[index];
			if (this.isUnknownUserError(mappedRelationship.reason)) {
				Logger.warn(
					{
						userId: userId.toString(),
						targetUserId: relationship?.targetUserId.toString(),
						type: relationship?.type,
					},
					'Skipping RPC session relationship with unknown user reference',
				);
				continue;
			}
			throw mappedRelationship.reason;
		}
		return validRelationships;
	}

	private async mapRpcGuildMembers(params: {
		guildId: GuildID;
		members: Array<GuildMember>;
		requestCache: RequestCache;
	}): Promise<Array<GuildMemberResponse>> {
		const {guildId, members, requestCache} = params;
		const mappedMembers = await Promise.allSettled(
			members.map((member) => mapGuildMemberToResponse(member, this.userCacheService, requestCache)),
		);
		const validMembers: Array<GuildMemberResponse> = [];
		for (const [index, mappedMember] of mappedMembers.entries()) {
			if (mappedMember.status === 'fulfilled') {
				validMembers.push(mappedMember.value);
				continue;
			}
			const member = members[index];
			if (this.isUnknownUserError(mappedMember.reason)) {
				Logger.warn(
					{
						guildId: guildId.toString(),
						userId: member?.userId.toString(),
					},
					'Skipping RPC guild member with unknown user reference',
				);
				continue;
			}
			throw mappedMember.reason;
		}
		return validMembers;
	}

	private async handleSessionRequest({
		token,
		version,
		requestCache,
		ip,
		latitude,
		longitude,
	}: HandleSessionRequestParams): Promise<RpcResponseSessionData> {
		const normalizedToken = this.normalizeSessionToken(token);
		const tokenHash = createHash('sha256').update(normalizedToken).digest('hex');
		const tokenType = this.parseTokenType(normalizedToken);
		const tokenHashPrefix = tokenHash.slice(0, 12);
		Logger.debug(
			{
				tokenType,
				tokenHashPrefix,
				version,
				hasIp: ip !== undefined,
				hasLatitude: latitude !== undefined,
				hasLongitude: longitude !== undefined,
			},
			'RPC session handling started',
		);
		const bucketKey = `gateway:rpc:session:${tokenType}:${tokenHash}`;
		const rateLimitResult = await this.rateLimitService.checkLimit({
			identifier: bucketKey,
			maxAttempts: 20,
			windowMs: ms('1 minute'),
		});
		if (!rateLimitResult.allowed) {
			Logger.warn(
				{
					tokenType,
					tokenHashPrefix,
					retryAfter: rateLimitResult.retryAfter,
					limit: rateLimitResult.limit,
					resetTime: rateLimitResult.resetTime,
				},
				'RPC session request rate limited',
			);
			throw new RateLimitError({
				retryAfter: rateLimitResult.retryAfter!,
				limit: rateLimitResult.limit,
				resetTime: rateLimitResult.resetTime,
			});
		}

		let userId: UserID | null = null;
		let authSession: AuthSession | null = null;

		if (tokenType === 'user') {
			authSession = await this.authService.getAuthSessionByToken(normalizedToken);
			if (authSession) {
				userId = authSession.userId;
			}
		} else if (tokenType === 'bot') {
			userId = await this.botAuthService.validateBotToken(normalizedToken);
		}

		if (!userId) {
			Logger.warn(
				{
					tokenType,
					tokenHashPrefix,
				},
				'RPC session token validation failed',
			);
			throw new UnauthorizedError();
		}
		const userData = await this.getUserData({userId, includePrivateChannels: false});

		if (!userData || !userData.user) {
			Logger.warn(
				{
					tokenType,
					tokenHashPrefix,
					userId: userId.toString(),
				},
				'RPC session user lookup failed',
			);
			throw new UnauthorizedError();
		}

		let user = userData.user;

		if (user.avatarHash && user.avatarColor == null) {
			try {
				const avatarKey = `avatars/${user.id}/${this.stripAnimationPrefix(user.avatarHash)}`;
				const object = await this.storageService.readObject(Config.s3.buckets.cdn, avatarKey);
				const avatarColor = await deriveDominantAvatarColor(object);
				const updatedUser = await this.userRepository.patchUpsert(user.id, {avatar_color: avatarColor}, user.toRow());
				if (updatedUser) {
					user = updatedUser;
					this.userCacheService.setUserPartialResponseFromUserInBackground(updatedUser, requestCache);
				}
			} catch (error) {
				Logger.warn({userId: user.id, error}, 'Failed to repair user avatar color');
			}
		}

		if (user.bannerHash && user.bannerColor == null) {
			try {
				const bannerKey = `banners/${user.id}/${this.stripAnimationPrefix(user.bannerHash)}`;
				const object = await this.storageService.readObject(Config.s3.buckets.cdn, bannerKey);
				const bannerColor = await deriveDominantAvatarColor(object);
				const updatedUser = await this.userRepository.patchUpsert(user.id, {banner_color: bannerColor}, user.toRow());
				if (updatedUser) {
					user = updatedUser;
				}
			} catch (error) {
				Logger.warn({userId: user.id, error}, 'Failed to repair user banner color');
			}
		}

		userData.user = user;

		let userSettings = userData.settings;
		if (userSettings?.customStatus?.isExpired()) {
			userSettings = Object.assign(Object.create(Object.getPrototypeOf(userSettings)), userSettings, {
				customStatus: null,
			});
			userData.settings = userSettings;
		}

		let flagsToUpdate: bigint | null = null;

		if (!(user.flags & UserFlags.HAS_SESSION_STARTED)) {
			flagsToUpdate = (flagsToUpdate ?? user.flags) | UserFlags.HAS_SESSION_STARTED;
		}

		const hadPremium = user.premiumType != null && user.premiumType > 0;
		const isPremium = user.isPremium();
		const needsPremiumStrip = shouldStripExpiredPremium(user);
		const hasBeenSanitized = !!(user.flags & UserFlags.PREMIUM_PERKS_SANITIZED);
		const hasInvalidNonLifetimeDiscriminator =
			user.discriminator === 0 && user.premiumType !== UserPremiumTypes.LIFETIME;

		if (needsPremiumStrip) {
			try {
				const strippedUser = await this.userRepository.patchUpsert(user.id, createPremiumClearPatch(), user.toRow());
				if (strippedUser) {
					user = strippedUser;
					userData.user = strippedUser;
					this.userCacheService.setUserPartialResponseFromUserInBackground(strippedUser, requestCache);
				}
			} catch (error) {
				Logger.warn({userId: user.id.toString(), error}, 'Failed to strip expired premium on RPC session start');
			}
		}

		if (hasInvalidNonLifetimeDiscriminator) {
			try {
				const discriminatorResult = await this.discriminatorService.generateDiscriminator({
					username: user.username,
					user,
				});

				if (discriminatorResult.available && discriminatorResult.discriminator !== -1) {
					const updatedUser = await this.userRepository.patchUpsert(
						user.id,
						{
							discriminator: discriminatorResult.discriminator,
						},
						user.toRow(),
					);
					if (updatedUser) {
						Object.assign(user, updatedUser);
						userData.user = user;
						this.userCacheService.setUserPartialResponseFromUserInBackground(user, requestCache);
					}
				}
			} catch (error) {
				Logger.error(
					{userId: user.id.toString(), error},
					'Failed to reset invalid non-lifetime discriminator on RPC session start',
				);
			}

			flagsToUpdate = (flagsToUpdate ?? user.flags) & ~UserFlags.PREMIUM_DISCRIMINATOR;
		}

		if (hadPremium && !isPremium && !hasBeenSanitized) {
			if (user.flags & UserFlags.PREMIUM_DISCRIMINATOR) {
				try {
					const discriminatorResult = await this.discriminatorService.generateDiscriminator({
						username: user.username,
						user,
					});

					if (discriminatorResult.available && discriminatorResult.discriminator !== -1) {
						const updatedUser = await this.userRepository.patchUpsert(
							user.id,
							{
								discriminator: discriminatorResult.discriminator,
							},
							user.toRow(),
						);
						if (updatedUser) {
							Object.assign(user, updatedUser);
							this.userCacheService.setUserPartialResponseFromUserInBackground(user, requestCache);
						}
					}
				} catch (error) {
					Logger.error({userId: user.id.toString(), error}, 'Failed to reset discriminator after premium expired');
				}

				flagsToUpdate = (flagsToUpdate ?? user.flags) & ~UserFlags.PREMIUM_DISCRIMINATOR;
			}

			const guildIdsToProcess = userData.guildIds;
			try {
				const members = await Promise.all(
					guildIdsToProcess.map(async (guildId) => {
						try {
							const member = await this.guildRepository.getMember(guildId, user.id);
							return {guildId, member, error: null};
						} catch (error) {
							Logger.error(
								{userId: user.id.toString(), guildId: guildId.toString(), error},
								'Failed to fetch guild member for premium sanitization',
							);
							return {guildId, member: null, error};
						}
					}),
				);

				const membersToSanitize = members.filter(
					({member, error}) =>
						!error &&
						member &&
						!member.isPremiumSanitized &&
						(member.avatarHash || member.bannerHash || member.bio || member.accentColor !== null),
				);

				if (membersToSanitize.length > 0) {
					const updatePromises = membersToSanitize.map(({guildId, member}) =>
						this.guildRepository
							.upsertMember({
								...member!.toRow(),
								is_premium_sanitized: true,
							})
							.then((updatedMember) => ({guildId, updatedMember, error: null})),
					);

					const updatedResults = await Promise.all(updatePromises);

					const dispatchPromises = updatedResults.map(async ({guildId, updatedMember, error}) => {
						if (error) return;
						try {
							await this.gatewayService.dispatchGuild({
								guildId,
								event: 'GUILD_MEMBER_UPDATE',
								data: await mapGuildMemberToResponse(updatedMember!, this.userCacheService, requestCache),
							});
						} catch (error) {
							Logger.error(
								{userId: user.id.toString(), guildId: guildId.toString(), error},
								'Failed to dispatch guild member update after premium sanitization',
							);
						}
					});

					await Promise.all(dispatchPromises);
				}
			} catch (error) {
				Logger.error(
					{userId: user.id.toString(), guildIds: guildIdsToProcess.map(String), error},
					'Failed to sanitize guild member premium perks for multiple guilds',
				);
			}

			flagsToUpdate = (flagsToUpdate ?? user.flags) | UserFlags.PREMIUM_PERKS_SANITIZED;

			await this.gatewayService.dispatchPresence({
				userId: user.id,
				event: 'USER_UPDATE',
				data: {user: mapUserToPrivateResponse(user)},
			});
		}

		if (!user.isBot && (user.flags & UserFlags.BOT_SANITIZED) !== UserFlags.BOT_SANITIZED) {
			try {
				const sanitized = await this.sanitizeOwnedBotDiscriminators(user);
				if (sanitized) {
					flagsToUpdate = (flagsToUpdate ?? user.flags) | UserFlags.BOT_SANITIZED;
				}
			} catch (error) {
				Logger.warn(
					{userId: user.id.toString(), error},
					'Failed to run owned bot discriminator sanitization on RPC session start',
				);
			}
		}

		if (!(user.flags & UserFlags.HAS_RELATIONSHIPS_INDEXED)) {
			try {
				const relationships = userData.relationships;
				await this.userRepository.backfillRelationshipsIndex(user.id, relationships);
				flagsToUpdate = (flagsToUpdate ?? user.flags) | UserFlags.HAS_RELATIONSHIPS_INDEXED;
			} catch (error) {
				Logger.warn({userId: user.id, error}, 'Failed to backfill relationships index');
			}
		}

		if (!(user.flags & UserFlags.MESSAGES_BY_AUTHOR_BACKFILLED)) {
			try {
				await this.channelRepository.backfillMessagesByAuthorIndex(user.id);
				flagsToUpdate = (flagsToUpdate ?? user.flags) | UserFlags.MESSAGES_BY_AUTHOR_BACKFILLED;
			} catch (error) {
				Logger.warn({userId: user.id, error}, 'Failed to backfill messages by author index');
			}
		}

		if (flagsToUpdate !== null && flagsToUpdate !== user.flags) {
			await this.userRepository.patchUpsert(
				user.id,
				{
					flags: flagsToUpdate,
				},
				user.toRow(),
			);
		}

		await this.ensurePersonalNotesChannel(user);
		const cachedChannels = await this.ensurePrivateChannelsWithinLimit(user);
		userData.privateChannels = cachedChannels ?? (await this.userRepository.listPrivateChannels(user.id));

		const mapReadState = (readState: ReadState) => ({
			id: readState.channelId.toString(),
			mention_count: readState.mentionCount,
			last_message_id: readState.lastMessageId?.toString() ?? null,
			last_pin_timestamp: readState.lastPinTimestamp?.toISOString() ?? null,
		});

		let countryCode = 'US';
		if (ip) {
			const geoip = await lookupGeoip(ip);
			countryCode = geoip.countryCode ?? countryCode;
		} else {
			Logger.warn({context: 'rpc_geoip', reason: 'ip_missing'}, 'RPC session request missing IP for GeoIP');
		}

		const [privateChannels, relationships] = await Promise.all([
			this.mapRpcSessionPrivateChannels({
				channels: userData.privateChannels,
				userId: user.id,
				requestCache,
			}),
			this.mapRpcSessionRelationships({
				relationships: userData.relationships,
				userId: user.id,
				requestCache,
			}),
		]);

		const voiceAccessContext: VoiceAccessContext = {requestingUserId: user.id};
		const availableRegions = this.voiceAvailabilityService?.getAvailableRegions(voiceAccessContext) ?? [];
		const accessibleRegions = availableRegions.filter((region) => region.isAccessible);
		const sortedRegions = accessibleRegions.slice();

		const userLatitude = parseCoordinate(latitude);
		const userLongitude = parseCoordinate(longitude);
		const hasLocation = userLatitude !== null && userLongitude !== null;

		if (hasLocation) {
			sortedRegions.sort((a, b) => {
				const distanceA = calculateDistance(userLatitude, userLongitude, a.latitude, a.longitude);
				const distanceB = calculateDistance(userLatitude, userLongitude, b.latitude, b.longitude);

				if (distanceA !== distanceB) {
					return distanceA - distanceB;
				}

				return a.id.localeCompare(b.id);
			});
		} else {
			sortedRegions.sort((a, b) => a.id.localeCompare(b.id));
		}

		const rtcRegions = sortedRegions.map((region) => ({
			id: region.id,
			name: region.name,
			emoji: region.emoji,
		}));
		Logger.debug(
			{
				tokenType,
				tokenHashPrefix,
				userId: user.id.toString(),
				guildCount: userData.guildIds.length,
				privateChannelCount: privateChannels.length,
				relationshipCount: relationships.length,
				countryCode,
				rtcRegionCount: rtcRegions.length,
			},
			'RPC session handling completed',
		);

		return {
			auth_session_id_hash: authSession ? uint8ArrayToBase64(authSession.sessionIdHash, {urlSafe: true}) : undefined,
			user: mapUserToPrivateResponse(user),
			user_settings: userData.settings
				? mapUserSettingsToResponse({
						settings: userData.settings,
						memberGuildIds: userData.guildIds,
					})
				: null,
			user_guild_settings: userData.guildSettings.map(mapUserGuildSettingsToResponse),
			notes: Object.fromEntries(
				Array.from(userData.notes.entries()).map(([userId, note]) => [userId.toString(), note]),
			),
			read_states: userData.readStates.map(mapReadState),
			guild_ids: userData.guildIds.map(String),
			private_channels: privateChannels,
			relationships,
			favorite_memes: userData.favoriteMemes.map(mapFavoriteMemeToResponse),
			pinned_dms: userData.pinnedDMs?.map(String) ?? [],
			country_code: countryCode,
			rtc_regions: rtcRegions,
			version,
		};
	}

	private async handleGuildRequest({guildId, requestCache}: HandleGuildRequestParams): Promise<RpcResponseGuildData> {
		const guildData = await this.getGuildData({guildId});

		if (!guildData) {
			throw new UnknownGuildError();
		}

		const [channels, members] = await Promise.all([
			Promise.all(
				guildData.channels.map((channel) =>
					mapChannelToResponse({
						channel,
						currentUserId: null,
						userCacheService: this.userCacheService,
						requestCache,
					}),
				),
			),
			this.mapRpcGuildMembers({guildId, members: guildData.members, requestCache}),
		]);

		return {
			guild: mapGuildToGuildResponse(guildData.guild),
			roles: guildData.roles.map(mapGuildRoleToResponse),
			channels,
			emojis: guildData.emojis.map(mapGuildEmojiToResponse),
			stickers: guildData.stickers.map(mapGuildStickerToResponse),
			members,
		};
	}

	private async getGuildData({guildId}: GetGuildDataParams): Promise<GuildData | null> {
		const [guildResult, channels, emojis, stickers, members, roles] = await Promise.all([
			this.guildRepository.findUnique(guildId),
			this.channelRepository.listGuildChannels(guildId),
			this.guildRepository.listEmojis(guildId),
			this.guildRepository.listStickers(guildId),
			this.guildRepository.listMembers(guildId),
			this.guildRepository.listRoles(guildId),
		]);
		if (!guildResult) return null;

		let migratedStickers = stickers;
		const needsMigration = stickers.filter((s) => s.animated === null || s.animated === undefined);
		if (needsMigration.length > 0) {
			Logger.info({count: needsMigration.length, guildId}, 'Migrating sticker animated fields');
			const migrated = await Promise.all(needsMigration.map((s) => this.migrateStickerAnimated(s)));
			migratedStickers = stickers.map((s) => {
				const migratedSticker = migrated.find((m) => m.id === s.id);
				return migratedSticker ?? s;
			});
		}

		const repairedBannerGuild = await this.repairGuildBannerHeight(guildResult);
		const repairedSplashGuild = await this.repairGuildSplashDimensions(repairedBannerGuild);
		const repairedEmbedSplashGuild = await this.repairGuildEmbedSplashDimensions(repairedSplashGuild);
		const updatedGuild = await this.updateGuildMemberCount(repairedEmbedSplashGuild, members.length);

		this.repairOrphanedInvitesAndWebhooks({guild: updatedGuild, channels}).catch((error) => {
			Logger.warn({guildId: guildId.toString(), error}, 'Failed to repair orphaned invites/webhooks');
		});

		return {
			guild: updatedGuild,
			channels,
			emojis,
			stickers: migratedStickers,
			members,
			roles,
		};
	}

	private async getUserData({userId, includePrivateChannels = true}: GetUserDataParams): Promise<UserData | null> {
		const user = await this.userRepository.findUnique(userId);
		if (!user) return null;

		if (user.isBot) {
			const guildIds = await this.userRepository.getUserGuildIds(userId);

			return {
				user,
				settings: null,
				guildSettings: [],
				notes: new Map(),
				readStates: [],
				guildIds,
				privateChannels: [],
				relationships: [],
				favoriteMemes: [],
				pinnedDMs: [],
			};
		}

		const [settingsResult, notes, readStates, guildIds, relationships, favoriteMemes, pinnedDMs] = await Promise.all([
			this.userRepository.findSettings(userId),
			this.userRepository.getUserNotes(userId),
			this.readStateService.getReadStates(userId),
			this.userRepository.getUserGuildIds(userId),
			this.userRepository.listRelationships(userId),
			this.favoriteMemeRepository.findByUserId(userId),
			this.userRepository.getPinnedDms(userId),
		]);

		const privateChannels = includePrivateChannels ? await this.userRepository.listPrivateChannels(userId) : [];

		let settings = settingsResult;
		if (settings) {
			const needsIncomingCallRepair = settings.incomingCallFlags === 0;
			const needsGroupDmRepair = settings.groupDmAddPermissionFlags === 0;

			if (needsIncomingCallRepair || needsGroupDmRepair) {
				const isAdult = isUserAdult(user.dateOfBirth);

				const updatedRow = {
					...settings.toRow(),
					...(needsIncomingCallRepair && {
						incoming_call_flags: isAdult ? IncomingCallFlags.EVERYONE : IncomingCallFlags.FRIENDS_ONLY,
					}),
					...(needsGroupDmRepair && {group_dm_add_permission_flags: GroupDmAddPermissionFlags.FRIENDS_ONLY}),
				};
				await this.userRepository.upsertSettings(updatedRow);
				settings = new UserSettings(updatedRow);
			}
		}

		const guildSettings = await this.userRepository.findAllGuildSettings(userId);

		return {
			user,
			settings,
			guildSettings,
			notes,
			readStates,
			guildIds,
			privateChannels,
			relationships,
			favoriteMemes,
			pinnedDMs,
		};
	}

	private async ensurePrivateChannelsWithinLimit(user: User): Promise<Array<Channel> | null> {
		if (user.isBot) {
			return [];
		}

		const channels = await this.userRepository.listPrivateChannels(user.id);
		const totalPrivateChannels = channels.length;
		const limit = this.resolveLimitForUser(
			user ?? null,
			'max_private_channels_per_user',
			MAX_PRIVATE_CHANNELS_PER_USER,
		);
		if (totalPrivateChannels <= limit) {
			return channels;
		}

		const closableDms = channels
			.filter((channel) => channel.type === ChannelTypes.DM)
			.sort((a, b) => {
				const aValue = a.lastMessageId ?? 0n;
				const bValue = b.lastMessageId ?? 0n;
				if (aValue < bValue) return -1;
				if (aValue > bValue) return 1;
				return 0;
			});

		const toClose = totalPrivateChannels - limit;
		let closed = 0;
		for (const channel of closableDms) {
			if (closed >= toClose) {
				break;
			}
			await this.userRepository.closeDmForUser(user.id, channel.id);
			closed += 1;
		}

		if (closed < toClose) {
			Logger.warn(
				{
					user_id: user.id.toString(),
					total_private_channels: totalPrivateChannels,
					required_closures: toClose,
					actual_closures: closed,
				},
				'Unable to close enough DMs to satisfy private channel limit',
			);
		}

		return null;
	}

	private resolveLimitForUser(user: User | null, key: LimitKey, fallback: number): number {
		const ctx = createLimitMatchContext({user});
		return resolveLimitSafe(this.limitConfigService.getConfigSnapshot(), ctx, key, fallback);
	}

	private async getUserGuildSettings(params: {
		userIds: Array<UserID>;
		guildId: GuildID;
	}): Promise<{user_guild_settings: Array<UserGuildSettings | null>}> {
		const {userIds, guildId} = params;
		const actualGuildId = guildId === createGuildID(0n) ? null : guildId;
		const userGuildSettings = await Promise.all(
			userIds.map((userId) => this.userRepository.findGuildSettings(userId, actualGuildId)),
		);
		return {user_guild_settings: userGuildSettings};
	}

	private async getPushSubscriptions(params: {
		userIds: Array<UserID>;
	}): Promise<
		Record<string, Array<{subscription_id: string; endpoint: string; p256dh_key: string; auth_key: string}>>
	> {
		const {userIds} = params;
		const subscriptionsMap = await this.userRepository.getBulkPushSubscriptions(userIds);

		const result: Record<
			string,
			Array<{subscription_id: string; endpoint: string; p256dh_key: string; auth_key: string}>
		> = {};

		for (const [userId, subscriptions] of subscriptionsMap.entries()) {
			result[userId.toString()] = subscriptions.map((sub) => ({
				subscription_id: sub.subscriptionId,
				endpoint: sub.endpoint,
				p256dh_key: sub.p256dhKey,
				auth_key: sub.authKey,
			}));
		}

		return result;
	}

	private async getBadgeCounts(params: {userIds: Array<UserID>}): Promise<Record<string, number>> {
		const {userIds} = params;
		const uniqueUserIds = Array.from(new Set(userIds)) as Array<UserID>;

		const allReadStates = await Promise.all(uniqueUserIds.map((userId) => this.readStateService.getReadStates(userId)));

		const badgeCounts: Record<string, number> = {};
		uniqueUserIds.forEach((userId, index) => {
			const readStates = allReadStates[index];
			const totalMentions = readStates.reduce((sum, state) => sum + state.mentionCount, 0);
			badgeCounts[userId.toString()] = totalMentions;
		});

		return badgeCounts;
	}

	private async deletePushSubscriptions(params: {
		subscriptions: Array<{userId: UserID; subscriptionId: string}>;
	}): Promise<{success: boolean}> {
		const {subscriptions} = params;

		await Promise.all(
			subscriptions.map((sub) => this.userRepository.deletePushSubscription(sub.userId, sub.subscriptionId)),
		);

		return {success: true};
	}

	private stripAnimationPrefix(hash: string): string {
		return hash.startsWith('a_') ? hash.slice(2) : hash;
	}

	private async repairGuildBannerHeight(guild: Guild): Promise<Guild> {
		if (!guild.bannerHash || (guild.bannerHeight != null && guild.bannerWidth != null)) {
			return guild;
		}

		const s3Key = `banners/${guild.id}/${this.stripAnimationPrefix(guild.bannerHash)}`;

		try {
			const object = await this.storageService.readObject(Config.s3.buckets.cdn, s3Key);
			const metadata = await sharp(object).metadata();
			const bannerHeight = metadata.height ?? null;
			const bannerWidth = metadata.width ?? null;

			if (bannerHeight == null || bannerWidth == null) {
				return guild;
			}

			const repairedGuild = await this.guildRepository.upsert({
				...guild.toRow(),
				banner_height: bannerHeight,
				banner_width: bannerWidth,
			});
			return repairedGuild;
		} catch (error) {
			Logger.warn({guildId: guild.id, error}, 'Failed to repair guild banner height');
			return guild;
		}
	}

	private async repairGuildSplashDimensions(guild: Guild): Promise<Guild> {
		if (!guild.splashHash || (guild.splashWidth != null && guild.splashHeight != null)) {
			return guild;
		}

		const s3Key = `splashes/${guild.id}/${this.stripAnimationPrefix(guild.splashHash)}`;

		try {
			const object = await this.storageService.readObject(Config.s3.buckets.cdn, s3Key);
			const metadata = await sharp(object).metadata();
			const splashHeight = metadata.height ?? null;
			const splashWidth = metadata.width ?? null;

			if (splashHeight == null || splashWidth == null) {
				return guild;
			}

			const repairedGuild = await this.guildRepository.upsert({
				...guild.toRow(),
				splash_height: splashHeight,
				splash_width: splashWidth,
			});
			return repairedGuild;
		} catch (error) {
			Logger.warn({guildId: guild.id, error}, 'Failed to repair guild splash dimensions');
			return guild;
		}
	}

	private async repairGuildEmbedSplashDimensions(guild: Guild): Promise<Guild> {
		if (!guild.embedSplashHash || (guild.embedSplashWidth != null && guild.embedSplashHeight != null)) {
			return guild;
		}

		const s3Key = `embed-splashes/${guild.id}/${this.stripAnimationPrefix(guild.embedSplashHash)}`;

		try {
			const object = await this.storageService.readObject(Config.s3.buckets.cdn, s3Key);
			const metadata = await sharp(object).metadata();
			const embedSplashHeight = metadata.height ?? null;
			const embedSplashWidth = metadata.width ?? null;

			if (embedSplashHeight == null || embedSplashWidth == null) {
				return guild;
			}

			const repairedGuild = await this.guildRepository.upsert({
				...guild.toRow(),
				embed_splash_height: embedSplashHeight,
				embed_splash_width: embedSplashWidth,
			});
			return repairedGuild;
		} catch (error) {
			Logger.warn({guildId: guild.id, error}, 'Failed to repair guild embed splash dimensions');
			return guild;
		}
	}

	private async repairOrphanedInvitesAndWebhooks(params: {guild: Guild; channels: Array<Channel>}): Promise<void> {
		const {guild, channels} = params;
		const channelIds = new Set(channels.map((channel) => channel.id));
		const vanityInviteCode = guild.vanityUrlCode ? vanityCodeToInviteCode(guild.vanityUrlCode) : null;

		const [invites, webhooks] = await Promise.all([
			this.inviteRepository.listGuildInvites(guild.id),
			this.webhookRepository.listByGuild(guild.id),
		]);

		const orphanedInvites = invites.filter((invite) => {
			if (!invite.channelId) {
				return false;
			}
			if (vanityInviteCode && invite.code === vanityInviteCode) {
				return false;
			}
			return !channelIds.has(invite.channelId);
		});

		const orphanedWebhooks = webhooks.filter((webhook) => {
			if (!webhook.channelId) {
				return false;
			}
			return !channelIds.has(webhook.channelId);
		});

		if (orphanedInvites.length > 0) {
			Logger.info(
				{
					guildId: guild.id.toString(),
					count: orphanedInvites.length,
					codes: orphanedInvites.map((i) => i.code),
				},
				'Repairing orphaned invites',
			);
			await Promise.all(orphanedInvites.map((invite) => this.inviteRepository.delete(invite.code)));
		}

		if (orphanedWebhooks.length > 0) {
			Logger.info(
				{
					guildId: guild.id.toString(),
					count: orphanedWebhooks.length,
					webhookIds: orphanedWebhooks.map((w) => w.id.toString()),
				},
				'Repairing orphaned webhooks',
			);
			await Promise.all(orphanedWebhooks.map((webhook) => this.webhookRepository.delete(webhook.id)));
		}
	}

	private async getUserBlockedIds(params: {userIds: Array<UserID>}): Promise<Record<string, Array<string>>> {
		const {userIds} = params;
		const result: Record<string, Array<string>> = {};

		const relationshipsPromises = userIds.map(async (userId) => {
			const relationships = await this.userRepository.listRelationships(userId);
			const blockedIds = relationships.filter((rel) => rel.type === 2).map((rel) => rel.targetUserId.toString());
			return {userId, blockedIds};
		});

		const results = await Promise.all(relationshipsPromises);

		for (const {userId, blockedIds} of results) {
			result[userId.toString()] = blockedIds;
		}

		return result;
	}

	private async kickTemporaryMember(params: {userId: UserID; guildIds: Array<GuildID>}): Promise<boolean> {
		const {userId, guildIds} = params;

		try {
			await Promise.all(
				guildIds.map(async (guildId) => {
					try {
						const [member, guild] = await Promise.all([
							this.guildRepository.getMember(guildId, userId),
							this.guildRepository.findUnique(guildId),
						]);
						if (member?.isTemporary && guild) {
							await this.guildRepository.deleteMember(guildId, userId);
							await this.updateGuildMemberCount(guild, Math.max(0, guild.memberCount - 1));

							await this.gatewayService.dispatchGuild({
								guildId,
								event: 'GUILD_MEMBER_REMOVE',
								data: {user: {id: userId.toString()}},
							});

							await this.gatewayService.leaveGuild({userId, guildId});
						}
					} catch (error) {
						Logger.error(
							{userId: userId.toString(), guildId: guildId.toString(), error},
							'Failed to kick temporary member from guild',
						);
						throw error;
					}
				}),
			);
			return true;
		} catch (error) {
			Logger.error(
				{userId: userId.toString(), guildIds: guildIds.map(String), error},
				'Failed to kick temporary member from multiple guilds',
			);
			return false;
		}
	}

	private async handleCallEnded(params: {
		channelId: ChannelID;
		messageId: bigint;
		participants: Array<UserID>;
		endedTimestamp: Date;
		requestCache: RequestCache;
	}): Promise<void> {
		const {channelId, messageId, participants, endedTimestamp, requestCache} = params;

		const [message, channel] = await Promise.all([
			this.channelRepository.getMessage(channelId, createMessageID(messageId)),
			this.channelRepository.findUnique(channelId),
		]);

		if (!message || !channel) {
			return;
		}

		if (message.type !== MessageTypes.CALL) {
			return;
		}

		const messageRow = message.toRow();
		const updatedMessage = await this.channelRepository.upsertMessage({
			...messageRow,
			call: {
				participant_ids: new Set(participants),
				ended_timestamp: endedTimestamp,
			},
		});

		if (!updatedMessage) {
			return;
		}

		const messageResponse = await mapMessageToResponse({
			message: updatedMessage,
			userCacheService: this.userCacheService,
			requestCache,
			mediaService: this.mediaService,
			getReferencedMessage: (channelId, messageId) => this.channelRepository.getMessage(channelId, messageId),
		});

		for (const recipientId of channel.recipientIds) {
			await this.gatewayService.dispatchPresence({
				userId: recipientId,
				event: 'MESSAGE_UPDATE',
				data: messageResponse,
			});
		}
	}

	private async getDmChannel(params: {
		channelId: ChannelID;
		userId: UserID;
		requestCache: RequestCache;
	}): Promise<ChannelResponse | null> {
		const {channelId, userId, requestCache} = params;

		const channel = await this.channelRepository.findUnique(channelId);
		if (!channel) {
			return null;
		}

		if (!channel.recipientIds.has(userId)) {
			return null;
		}

		try {
			return await mapChannelToResponse({
				channel,
				currentUserId: userId,
				userCacheService: this.userCacheService,
				requestCache,
			});
		} catch (error) {
			if (this.isUnknownUserError(error)) {
				Logger.warn(
					{
						userId: userId.toString(),
						channelId: channelId.toString(),
					},
					'Skipping RPC get_dm_channel response with unknown user reference',
				);
				return null;
			}
			throw error;
		}
	}
}
