// SPDX-License-Identifier: AGPL-3.0-or-later

import {AUTOMATIC_VOICE_REGION_ID, ChannelTypes, MessageTypes} from '@fluxer/constants/src/ChannelConstants';
import {IncomingCallFlags, RelationshipTypes} from '@fluxer/constants/src/UserConstants';
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
import {CallAlreadyExistsError} from '@fluxer/errors/src/domains/channel/CallAlreadyExistsError';
import {InvalidChannelTypeForCallError} from '@fluxer/errors/src/domains/channel/InvalidChannelTypeForCallError';
import {NoActiveCallError} from '@fluxer/errors/src/domains/channel/NoActiveCallError';
import {UnknownChannelError} from '@fluxer/errors/src/domains/channel/UnknownChannelError';
import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError';
import * as BucketUtils from '@fluxer/snowflake/src/SnowflakeBuckets';
import {type ChannelID, createMessageID, type MessageID, type UserID} from '../../BrandedTypes';
import type {IGuildRepositoryAggregate} from '../../guild/repositories/IGuildRepositoryAggregate';
import type {CallData, IGatewayService} from '../../infrastructure/IGatewayService';
import type {ISnowflakeService} from '../../infrastructure/ISnowflakeService';
import type {IVoiceRoomStore} from '../../infrastructure/IVoiceRoomStore';
import type {UserCacheService} from '../../infrastructure/UserCacheService';
import type {RequestCache} from '../../middleware/RequestCacheMiddleware';
import type {ReadStateService} from '../../read_state/ReadStateService';
import type {IUserRepository} from '../../user/IUserRepository';
import type {VoiceAccessContext, VoiceAvailabilityService} from '../../voice/VoiceAvailabilityService';
import {mapChannelToResponse} from '../ChannelMappers';
import type {IChannelRepository} from '../IChannelRepository';
import {DMPermissionValidator} from './DMPermissionValidator';
import {buildBroadcastMessageData} from './message/MessageGatewayDispatch';
import {incrementDmMentionCounts} from './message/ReadStateHelpers';

export class CallService {
	private dmPermissionValidator: DMPermissionValidator;

	constructor(
		private channelRepository: IChannelRepository,
		private userRepository: IUserRepository,
		private guildRepository: IGuildRepositoryAggregate,
		private gatewayService: IGatewayService,
		private userCacheService: UserCacheService,
		private snowflakeService: ISnowflakeService,
		private readStateService: ReadStateService,
		private voiceAvailabilityService: VoiceAvailabilityService | null,
		private voiceRoomStore: IVoiceRoomStore,
	) {
		this.dmPermissionValidator = new DMPermissionValidator({
			userRepository: this.userRepository,
			guildRepository: this.guildRepository,
		});
	}

	async checkCallEligibility({userId, channelId}: {userId: UserID; channelId: ChannelID}): Promise<{
		ringable: boolean;
		silent?: boolean;
	}> {
		const channel = await this.channelRepository.findUnique(channelId);
		if (!channel) throw new UnknownChannelError();
		if (channel.type !== ChannelTypes.DM && channel.type !== ChannelTypes.GROUP_DM) {
			throw new InvalidChannelTypeForCallError();
		}
		this.ensureRequesterIsRecipient({
			channelType: channel.type,
			channelRecipientIds: channel.recipientIds,
			userId,
		});
		const caller = await this.userRepository.findUnique(userId);
		const isUnclaimedCaller = caller?.isUnclaimedAccount() ?? false;
		if (isUnclaimedCaller && channel.type === ChannelTypes.DM) {
			return {ringable: false};
		}
		const call = await this.gatewayService.getCall(channelId);
		const alreadyInCall = call ? call.voice_states.some((vs) => vs.user_id === userId.toString()) : false;
		if (alreadyInCall) {
			return {ringable: false};
		}
		if (channel.type === ChannelTypes.DM) {
			const recipientId = Array.from(channel.recipientIds).find((id) => id !== userId);
			if (!recipientId) {
				return {ringable: true};
			}
			return this.evaluateRecipientRingPolicy({callerId: userId, recipientId});
		}
		return {ringable: true};
	}

	private async evaluateRecipientRingPolicy({callerId, recipientId}: {callerId: UserID; recipientId: UserID}): Promise<{
		ringable: boolean;
		silent: boolean;
	}> {
		const recipientSettings = await this.userRepository.findSettings(recipientId);
		if (!recipientSettings) {
			return {ringable: true, silent: false};
		}
		const incomingCallFlags = recipientSettings.incomingCallFlags;
		if ((incomingCallFlags & IncomingCallFlags.NOBODY) === IncomingCallFlags.NOBODY) {
			return {ringable: false, silent: false};
		}
		const friendship = await this.userRepository.getRelationship(callerId, recipientId, RelationshipTypes.FRIEND);
		const areFriends = friendship !== null;
		if ((incomingCallFlags & IncomingCallFlags.FRIENDS_ONLY) === IncomingCallFlags.FRIENDS_ONLY) {
			return {ringable: areFriends, silent: false};
		}
		if (areFriends) {
			return {ringable: true, silent: false};
		}
		const shouldBeSilent =
			(incomingCallFlags & IncomingCallFlags.SILENT_EVERYONE) === IncomingCallFlags.SILENT_EVERYONE;
		if ((incomingCallFlags & IncomingCallFlags.FRIENDS_OF_FRIENDS) === IncomingCallFlags.FRIENDS_OF_FRIENDS) {
			const callerRelationships = await this.userRepository.listRelationships(callerId);
			const recipientRelationships = await this.userRepository.listRelationships(recipientId);
			const callerFriendIds = new Set(
				callerRelationships.filter((r) => r.type === RelationshipTypes.FRIEND).map((r) => r.targetUserId.toString()),
			);
			const hasMutualFriends = recipientRelationships
				.filter((r) => r.type === RelationshipTypes.FRIEND)
				.some((r) => callerFriendIds.has(r.targetUserId.toString()));
			if (hasMutualFriends) {
				return {ringable: true, silent: shouldBeSilent};
			}
		}
		if ((incomingCallFlags & IncomingCallFlags.GUILD_MEMBERS) === IncomingCallFlags.GUILD_MEMBERS) {
			const callerGuildIds = new Set(
				(await this.userRepository.getUserGuildIds(callerId)).map((guildId) => guildId.toString()),
			);
			const recipientGuildIds = await this.userRepository.getUserGuildIds(recipientId);
			const hasMutualGuilds = recipientGuildIds.some((guildId) => callerGuildIds.has(guildId.toString()));
			if (hasMutualGuilds) {
				return {ringable: true, silent: shouldBeSilent};
			}
		}
		if ((incomingCallFlags & IncomingCallFlags.EVERYONE) === IncomingCallFlags.EVERYONE) {
			return {ringable: true, silent: shouldBeSilent};
		}
		return {ringable: false, silent: false};
	}

	async createOrGetCall({
		userId,
		channelId,
		region,
		ringing,
		latitude: _latitude,
		longitude: _longitude,
	}: {
		userId: UserID;
		channelId: ChannelID;
		region?: string;
		ringing: Array<UserID>;
		requestCache: RequestCache;
		latitude?: string;
		longitude?: string;
	}): Promise<CallData> {
		const channel = await this.channelRepository.findUnique(channelId);
		if (!channel) throw new UnknownChannelError();
		if (channel.type !== ChannelTypes.DM && channel.type !== ChannelTypes.GROUP_DM) {
			throw new InvalidChannelTypeForCallError();
		}
		this.ensureRequesterIsRecipient({
			channelType: channel.type,
			channelRecipientIds: channel.recipientIds,
			userId,
		});
		this.validateExplicitRingRecipients({
			channelRecipientIds: channel.recipientIds,
			userId,
			recipients: ringing,
		});
		const recipientIds = Array.from(channel.recipientIds);
		const channelRecipients = recipientIds.length > 0 ? await this.userRepository.listUsers(recipientIds) : [];
		if (channel.type === ChannelTypes.DM) {
			const dmRecipientIds = recipientIds.filter((id) => id !== userId);
			if (dmRecipientIds.length === 1) {
				await this.dmPermissionValidator.validate({senderId: userId, recipientId: dmRecipientIds[0]});
			}
		}
		const existingCall = await this.gatewayService.getCall(channelId);
		if (existingCall) {
			throw new CallAlreadyExistsError();
		}
		if (region && region !== AUTOMATIC_VOICE_REGION_ID) {
			this.ensureCallRegionAccessible(userId, region);
		}
		const selectedRegion = region && region !== AUTOMATIC_VOICE_REGION_ID ? region : AUTOMATIC_VOICE_REGION_ID;
		const allRecipients = Array.from(new Set([userId, ...Array.from(channel.recipientIds)]));
		const messageId = createMessageID(await this.snowflakeService.generateForChannel(channelId));
		await this.channelRepository.upsertMessage({
			channel_id: channelId,
			bucket: BucketUtils.makeBucket(messageId),
			message_id: messageId,
			author_id: userId,
			type: MessageTypes.CALL,
			webhook_id: null,
			webhook_name: null,
			webhook_avatar_hash: null,
			content: null,
			edited_timestamp: null,
			pinned_timestamp: null,
			flags: 0,
			mention_everyone: false,
			mention_users: new Set(),
			mention_roles: null,
			mention_channels: null,
			attachments: null,
			embeds: null,
			sticker_items: null,
			message_reference: null,
			message_snapshots: null,
			call: {
				participant_ids: new Set(allRecipients),
				ended_timestamp: null,
			},
			nsfw_emojis: null,
			has_reaction: false,
			version: 1,
		});
		const call = await this.gatewayService.createCall(
			channelId,
			messageId.toString(),
			selectedRegion,
			ringing.map((id) => id.toString()),
			allRecipients.map((id) => id.toString()),
		);
		const author = await this.userRepository.findUnique(userId);
		await incrementDmMentionCounts({
			readStateService: this.readStateService,
			userRepository: this.userRepository,
			user: author,
			recipients: channelRecipients,
			channelId,
			messageId,
		});
		if (author && !author.isBot) {
			await this.readStateService.ackMessage({
				userId,
				channelId,
				messageId,
				mentionCount: 0,
				silent: true,
				emitGateway: false,
			});
		}
		const message = await this.channelRepository.getMessage(channelId, messageId);
		if (message) {
			const messageResponse = await buildBroadcastMessageData({
				channel,
				message,
			});
			for (const recipientId of allRecipients) {
				await this.gatewayService.dispatchPresence({
					userId: recipientId,
					event: 'MESSAGE_CREATE',
					data: messageResponse,
				});
			}
		}
		return call;
	}

	async updateCall({
		userId,
		channelId,
		region,
	}: {
		userId: UserID;
		channelId: ChannelID;
		region?: string | null;
		latitude?: string;
		longitude?: string;
	}): Promise<void> {
		const channel = await this.channelRepository.findUnique(channelId);
		if (!channel) throw new UnknownChannelError();
		if (channel.type !== ChannelTypes.DM && channel.type !== ChannelTypes.GROUP_DM) {
			throw new InvalidChannelTypeForCallError();
		}
		this.ensureRequesterIsRecipient({
			channelType: channel.type,
			channelRecipientIds: channel.recipientIds,
			userId,
		});
		const call = await this.gatewayService.getCall(channelId);
		if (!call) {
			throw new NoActiveCallError();
		}
		if (region !== undefined) {
			if (region !== null && region !== AUTOMATIC_VOICE_REGION_ID) {
				this.ensureCallRegionAccessible(userId, region);
			}
			const selectedRegion =
				region === null || region === AUTOMATIC_VOICE_REGION_ID ? AUTOMATIC_VOICE_REGION_ID : region;
			await this.voiceRoomStore.deleteRoomServer(undefined, channelId);
			await this.gatewayService.updateCallRegion(channelId, selectedRegion);
		}
	}

	async ringCallRecipients({
		userId,
		channelId,
		recipients,
		requestCache,
		latitude,
		longitude,
	}: {
		userId: UserID;
		channelId: ChannelID;
		recipients?: Array<UserID>;
		requestCache: RequestCache;
		latitude?: string;
		longitude?: string;
	}): Promise<void> {
		const channel = await this.channelRepository.findUnique(channelId);
		if (!channel) throw new UnknownChannelError();
		if (channel.type !== ChannelTypes.DM && channel.type !== ChannelTypes.GROUP_DM) {
			throw new InvalidChannelTypeForCallError();
		}
		this.ensureRequesterIsRecipient({
			channelType: channel.type,
			channelRecipientIds: channel.recipientIds,
			userId,
		});
		if (recipients !== undefined) {
			this.validateExplicitRingRecipients({
				channelRecipientIds: channel.recipientIds,
				userId,
				recipients,
			});
		}
		if (channel.type === ChannelTypes.DM) {
			const dmRecipientIds = Array.from(channel.recipientIds).filter((id) => id !== userId);
			if (dmRecipientIds.length === 1) {
				await this.dmPermissionValidator.validate({senderId: userId, recipientId: dmRecipientIds[0]});
			}
		}
		const callerRequestedNoRing = recipients !== undefined && recipients.length === 0;
		const recipientsToNotify = (
			callerRequestedNoRing ? Array.from(channel.recipientIds) : recipients || Array.from(channel.recipientIds)
		).filter((id) => id !== userId);
		const recipientsToRing: Array<UserID> = [];
		if (!callerRequestedNoRing) {
			const candidateRecipientIds =
				channel.type === ChannelTypes.DM
					? Array.from(channel.recipientIds).filter((id) => id !== userId)
					: recipientsToNotify;
			for (const recipientId of candidateRecipientIds) {
				const {ringable, silent} = await this.evaluateRecipientRingPolicy({
					callerId: userId,
					recipientId,
				});
				if (ringable && !silent) {
					recipientsToRing.push(recipientId);
				}
			}
		}
		if (channel.type === ChannelTypes.DM || channel.type === ChannelTypes.GROUP_DM) {
			const callerHasChannelOpen = await this.userRepository.isDmChannelOpen(userId, channelId);
			if (!callerHasChannelOpen) {
				await this.userRepository.openPrivateChannelForUser(userId, channel);
				const channelResponse = await mapChannelToResponse({
					channel,
					currentUserId: userId,
					userCacheService: this.userCacheService,
					requestCache,
				});
				await this.gatewayService.dispatchPresence({
					userId,
					event: 'CHANNEL_CREATE',
					data: channelResponse,
				});
			}
			for (const recipientId of recipientsToNotify) {
				const isOpen = await this.userRepository.isDmChannelOpen(recipientId, channelId);
				if (!isOpen) {
					await this.userRepository.openPrivateChannelForUser(recipientId, channel);
					const channelResponse = await mapChannelToResponse({
						channel,
						currentUserId: recipientId,
						userCacheService: this.userCacheService,
						requestCache,
					});
					await this.gatewayService.dispatchPresence({
						userId: recipientId,
						event: 'CHANNEL_CREATE',
						data: channelResponse,
					});
				}
			}
		}
		const existingCall = await this.gatewayService.getCall(channelId);
		if (!existingCall) {
			await this.createOrGetCall({
				userId,
				channelId,
				ringing: recipientsToRing,
				requestCache,
				latitude,
				longitude,
			});
		} else {
			await this.gatewayService.ringCallRecipients(
				channelId,
				recipientsToRing.map((id) => id.toString()),
			);
		}
	}

	async stopRingingCallRecipients({
		userId,
		channelId,
		recipients,
	}: {
		userId: UserID;
		channelId: ChannelID;
		recipients?: Array<UserID>;
	}): Promise<void> {
		const channel = await this.channelRepository.findUnique(channelId);
		if (!channel) throw new UnknownChannelError();
		if (channel.type !== ChannelTypes.DM && channel.type !== ChannelTypes.GROUP_DM) {
			throw new InvalidChannelTypeForCallError();
		}
		this.ensureRequesterIsRecipient({
			channelType: channel.type,
			channelRecipientIds: channel.recipientIds,
			userId,
		});
		const call = await this.gatewayService.getCall(channelId);
		if (!call) {
			throw new NoActiveCallError();
		}
		const toStop = recipients || [userId];
		await this.gatewayService.stopRingingCallRecipients(
			channelId,
			toStop.map((id) => id.toString()),
		);
	}

	private ensureCallRegionAccessible(userId: UserID, region: string): void {
		if (this.voiceAvailabilityService === null) {
			return;
		}
		const context: VoiceAccessContext = {
			requestingUserId: userId,
		};
		const availableRegions = this.voiceAvailabilityService.getAvailableRegions(context);
		const isAllowed = availableRegions.some(
			(availableRegion) => availableRegion.id === region && availableRegion.isAccessible,
		);
		if (!isAllowed) {
			throw InputValidationError.fromCode('region', ValidationErrorCodes.INVALID_OR_RESTRICTED_RTC_REGION, {
				region,
			});
		}
	}

	private ensureRequesterIsRecipient({
		channelType,
		channelRecipientIds,
		userId,
	}: {
		channelType: number;
		channelRecipientIds: Set<UserID>;
		userId: UserID;
	}): void {
		if (channelType !== ChannelTypes.DM && channelType !== ChannelTypes.GROUP_DM) {
			return;
		}
		if (!channelRecipientIds.has(userId)) {
			throw new UnknownChannelError();
		}
	}

	private validateExplicitRingRecipients({
		channelRecipientIds,
		userId,
		recipients,
	}: {
		channelRecipientIds: Set<UserID>;
		userId: UserID;
		recipients: Array<UserID>;
	}): void {
		const allowedRecipientIds = new Set(Array.from(channelRecipientIds).filter((id) => id !== userId));
		for (const recipientId of recipients) {
			if (!allowedRecipientIds.has(recipientId)) {
				throw InputValidationError.fromCode('recipients', ValidationErrorCodes.USER_NOT_IN_CHANNEL);
			}
		}
	}

	async updateCallMessageEnded({
		channelId,
		messageId,
		participants,
		endedTimestamp,
	}: {
		channelId: ChannelID;
		messageId: MessageID;
		participants: Array<UserID>;
		endedTimestamp: Date;
	}): Promise<void> {
		const message = await this.channelRepository.getMessage(channelId, messageId);
		if (!message) {
			return;
		}
		if (message.type !== MessageTypes.CALL) {
			return;
		}
		const messageRow = message.toRow();
		await this.channelRepository.upsertMessage({
			...messageRow,
			call: {
				participant_ids: new Set(participants),
				ended_timestamp: endedTimestamp,
			},
		});
	}
}
