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

import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
import type {JoinSourceType} from '@fluxer/constants/src/GuildConstants';
import {UnknownGuildMemberError} from '@fluxer/errors/src/domains/guild/UnknownGuildMemberError';
import type {GuildMemberResponse} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
import type {GuildMemberUpdateRequest} from '@fluxer/schema/src/domains/guild/GuildRequestSchemas';
import type {IpInfoService} from '@pkgs/geoip/src/IpInfoService';
import type {IRateLimitService} from '@pkgs/rate_limit/src/IRateLimitService';
import type {GuildID, InviteCode, RoleID, UserID} from '../../BrandedTypes';
import type {ChannelService} from '../../channel/services/ChannelService';
import type {EntityAssetService} from '../../infrastructure/EntityAssetService';
import type {IGatewayService} from '../../infrastructure/IGatewayService';
import type {UserCacheService} from '../../infrastructure/UserCacheService';
import type {LimitConfigService} from '../../limits/LimitConfigService';
import type {RequestCache} from '../../middleware/RequestCacheMiddleware';
import type {Guild} from '../../models/Guild';
import type {GuildMember} from '../../models/GuildMember';
import type {IUserRepository} from '../../user/IUserRepository';
import type {GuildAuditLogService} from '../GuildAuditLogService';
import type {IGuildRepositoryAggregate} from '../repositories/IGuildRepositoryAggregate';
import {GuildMemberAuditService} from './member/GuildMemberAuditService';
import {GuildMemberAuthService} from './member/GuildMemberAuthService';
import {GuildMemberEventService} from './member/GuildMemberEventService';
import {GuildMemberOperationsService} from './member/GuildMemberOperationsService';
import {GuildMemberRoleService} from './member/GuildMemberRoleService';
import {GuildMemberSearchIndexService} from './member/GuildMemberSearchIndexService';
import {GuildMemberValidationService} from './member/GuildMemberValidationService';

export class GuildMemberService {
	private readonly authService: GuildMemberAuthService;
	private readonly validationService: GuildMemberValidationService;
	private readonly auditService: GuildMemberAuditService;
	private readonly eventService: GuildMemberEventService;
	private readonly operationsService: GuildMemberOperationsService;
	private readonly roleService: GuildMemberRoleService;
	private readonly searchIndexService: GuildMemberSearchIndexService;
	private readonly userRepository: IUserRepository;

	constructor(
		private readonly guildRepository: IGuildRepositoryAggregate,
		channelService: ChannelService,
		userCacheService: UserCacheService,
		gatewayService: IGatewayService,
		entityAssetService: EntityAssetService,
		userRepository: IUserRepository,
		rateLimitService: IRateLimitService,
		private readonly guildAuditLogService: GuildAuditLogService,
		limitConfigService: LimitConfigService,
		ipInfoService: IpInfoService,
	) {
		this.userRepository = userRepository;
		this.authService = new GuildMemberAuthService(gatewayService, userRepository);
		this.validationService = new GuildMemberValidationService(guildRepository, userRepository, ipInfoService);
		this.auditService = new GuildMemberAuditService(guildAuditLogService);
		this.eventService = new GuildMemberEventService(gatewayService, userCacheService);
		this.searchIndexService = new GuildMemberSearchIndexService();
		this.operationsService = new GuildMemberOperationsService(
			guildRepository,
			channelService,
			userCacheService,
			gatewayService,
			entityAssetService,
			userRepository,
			rateLimitService,
			this.authService,
			this.validationService,
			this.guildAuditLogService,
			limitConfigService,
			this.searchIndexService,
		);
		this.roleService = new GuildMemberRoleService(
			guildRepository,
			gatewayService,
			this.authService,
			this.validationService,
		);
	}

	private getMemberSearchIndexOptions(guild: Guild | null | undefined) {
		if (!guild) {
			return null;
		}
		const includeDefault = guild.membersIndexedAt != null;
		return includeDefault ? {includeDefault} : null;
	}

	async getMembers(params: {
		userId: UserID;
		guildId: GuildID;
		limit?: number;
		after?: UserID;
		requestCache: RequestCache;
	}): Promise<Array<GuildMemberResponse>> {
		return this.operationsService.getMembers(params);
	}

	async getMember(params: {
		userId: UserID;
		targetId: UserID;
		guildId: GuildID;
		requestCache: RequestCache;
	}): Promise<GuildMemberResponse> {
		return this.operationsService.getMember(params);
	}

	async updateMember(
		params: {
			userId: UserID;
			targetId: UserID;
			guildId: GuildID;
			data: GuildMemberUpdateRequest | Omit<GuildMemberUpdateRequest, 'roles'>;
			requestCache: RequestCache;
		},
		auditLogReason?: string | null,
	): Promise<GuildMemberResponse> {
		const {userId, targetId, guildId, data, requestCache} = params;
		const targetMember = await this.guildRepository.getMember(guildId, targetId);
		if (!targetMember) throw new UnknownGuildMemberError();
		const previousSnapshot = this.auditService.serializeMemberForAudit(targetMember);
		const result = await this.operationsService.updateMember({
			userId,
			targetId,
			guildId,
			data,
			requestCache,
			auditLogReason,
		});
		const updatedMember = await this.guildRepository.getMember(guildId, targetId);
		if (!updatedMember) throw new UnknownGuildMemberError();
		await this.eventService.dispatchGuildMemberUpdate({guildId, member: updatedMember, requestCache});
		const targetUser = await this.userRepository.findUnique(targetId);
		if (targetUser) {
			const guild = await this.guildRepository.findUnique(guildId);
			const searchIndexOptions = this.getMemberSearchIndexOptions(guild);
			if (searchIndexOptions) {
				void this.searchIndexService.updateMember(updatedMember, targetUser, searchIndexOptions);
			}
		}
		const timeoutMetadata = (() => {
			if (data.communication_disabled_until === undefined) {
				return undefined;
			}
			const metadata: Record<string, string> = {};
			if (data.communication_disabled_until !== null) {
				metadata['communication_disabled_until'] = data.communication_disabled_until;
			}
			const trimmedReason = data.timeout_reason?.trim();
			if (trimmedReason) {
				metadata['timeout_reason'] = trimmedReason;
			}
			return Object.keys(metadata).length > 0 ? metadata : undefined;
		})();
		await this.auditService.recordAuditLog({
			guildId,
			userId,
			action: AuditLogActionType.MEMBER_UPDATE,
			targetId: targetId,
			auditLogReason: auditLogReason ?? null,
			metadata: timeoutMetadata,
			changes: this.guildAuditLogService.computeChanges(
				previousSnapshot,
				this.auditService.serializeMemberForAudit(updatedMember),
			),
		});
		return result;
	}

	async addMemberRole(
		params: {
			userId: UserID;
			targetId: UserID;
			guildId: GuildID;
			roleId: RoleID;
			requestCache: RequestCache;
		},
		auditLogReason?: string | null,
	): Promise<void> {
		const {userId, targetId, guildId, roleId, requestCache} = params;
		const targetMember = await this.guildRepository.getMember(guildId, targetId);
		if (!targetMember) throw new UnknownGuildMemberError();
		const previousSnapshot = this.auditService.serializeMemberForAudit(targetMember);
		await this.roleService.addMemberRole(params);
		const updatedMember = await this.guildRepository.getMember(guildId, targetId);
		if (updatedMember) {
			await this.eventService.dispatchGuildMemberUpdate({guildId, member: updatedMember, requestCache});
			const roleTargetUser = await this.userRepository.findUnique(targetId);
			if (roleTargetUser) {
				const guild = await this.guildRepository.findUnique(guildId);
				const searchIndexOptions = this.getMemberSearchIndexOptions(guild);
				if (searchIndexOptions) {
					void this.searchIndexService.updateMember(updatedMember, roleTargetUser, searchIndexOptions);
				}
			}
			await this.auditService.recordAuditLog({
				guildId,
				userId,
				action: AuditLogActionType.MEMBER_ROLE_UPDATE,
				targetId: targetId,
				auditLogReason: auditLogReason ?? null,
				metadata: {role_id: roleId.toString(), action: 'add'},
				changes: this.guildAuditLogService.computeChanges(
					previousSnapshot,
					this.auditService.serializeMemberForAudit(updatedMember),
				),
			});
		}
	}

	async systemAddMemberRole(params: {
		targetId: UserID;
		guildId: GuildID;
		roleId: RoleID;
		initiatorId: UserID;
		requestCache: RequestCache;
	}): Promise<void> {
		const {targetId, guildId, roleId, initiatorId, requestCache} = params;
		const targetMember = await this.guildRepository.getMember(guildId, targetId);
		if (!targetMember) throw new UnknownGuildMemberError();
		const previousSnapshot = this.auditService.serializeMemberForAudit(targetMember);
		await this.roleService.systemAddMemberRole({targetId, guildId, roleId});
		const updatedMember = await this.guildRepository.getMember(guildId, targetId);
		if (updatedMember) {
			await this.eventService.dispatchGuildMemberUpdate({guildId, member: updatedMember, requestCache});
			const roleTargetUser = await this.userRepository.findUnique(targetId);
			if (roleTargetUser) {
				const guild = await this.guildRepository.findUnique(guildId);
				const searchIndexOptions = this.getMemberSearchIndexOptions(guild);
				if (searchIndexOptions) {
					void this.searchIndexService.updateMember(updatedMember, roleTargetUser, searchIndexOptions);
				}
			}
			await this.auditService.recordAuditLog({
				guildId,
				userId: initiatorId,
				action: AuditLogActionType.MEMBER_ROLE_UPDATE,
				targetId: targetId,
				auditLogReason: null,
				metadata: {role_id: roleId.toString(), action: 'add'},
				changes: this.guildAuditLogService.computeChanges(
					previousSnapshot,
					this.auditService.serializeMemberForAudit(updatedMember),
				),
			});
		}
	}

	async removeMemberRole(
		params: {
			userId: UserID;
			targetId: UserID;
			guildId: GuildID;
			roleId: RoleID;
			requestCache: RequestCache;
		},
		auditLogReason?: string | null,
	): Promise<void> {
		const {userId, targetId, guildId, roleId, requestCache} = params;
		const targetMember = await this.guildRepository.getMember(guildId, targetId);
		if (!targetMember) throw new UnknownGuildMemberError();
		const previousSnapshot = this.auditService.serializeMemberForAudit(targetMember);
		await this.roleService.removeMemberRole(params);
		const updatedMember = await this.guildRepository.getMember(guildId, targetId);
		if (updatedMember) {
			await this.eventService.dispatchGuildMemberUpdate({guildId, member: updatedMember, requestCache});
			const roleTargetUser = await this.userRepository.findUnique(targetId);
			if (roleTargetUser) {
				const guild = await this.guildRepository.findUnique(guildId);
				const searchIndexOptions = this.getMemberSearchIndexOptions(guild);
				if (searchIndexOptions) {
					void this.searchIndexService.updateMember(updatedMember, roleTargetUser, searchIndexOptions);
				}
			}
			await this.auditService.recordAuditLog({
				guildId,
				userId,
				action: AuditLogActionType.MEMBER_ROLE_UPDATE,
				targetId: targetId,
				auditLogReason: auditLogReason ?? null,
				metadata: {role_id: roleId.toString(), action: 'remove'},
				changes: this.guildAuditLogService.computeChanges(
					previousSnapshot,
					this.auditService.serializeMemberForAudit(updatedMember),
				),
			});
		}
	}

	async removeMember(
		params: {
			userId: UserID;
			targetId: UserID;
			guildId: GuildID;
		},
		auditLogReason?: string | null,
	): Promise<void> {
		const {userId, targetId, guildId} = params;
		const targetMember = await this.guildRepository.getMember(guildId, targetId);
		if (!targetMember) throw new UnknownGuildMemberError();
		await this.operationsService.removeMember(params);
		const guild = await this.guildRepository.findUnique(guildId);
		const searchIndexOptions = this.getMemberSearchIndexOptions(guild);
		if (searchIndexOptions) {
			void this.searchIndexService.deleteMember(guildId, targetId, searchIndexOptions);
		}
		await this.eventService.dispatchGuildMemberRemove({guildId, userId: targetId});
		await this.auditService.recordAuditLog({
			guildId,
			userId,
			action: AuditLogActionType.MEMBER_KICK,
			targetId: targetId,
			auditLogReason: auditLogReason ?? null,
		});
	}

	async addUserToGuild(params: {
		userId: UserID;
		guildId: GuildID;
		sendJoinMessage?: boolean;
		skipGuildLimitCheck?: boolean;
		skipBanCheck?: boolean;
		skipRiskGate?: boolean;
		isTemporary?: boolean;
		joinSourceType?: JoinSourceType;
		sourceInviteCode?: InviteCode;
		inviterId?: UserID;
		requestCache: RequestCache;
		initiatorId?: UserID;
	}): Promise<GuildMember> {
		return this.operationsService.addUserToGuild(params, this.eventService);
	}

	async leaveGuild(
		params: {
			userId: UserID;
			guildId: GuildID;
		},
		_auditLogReason?: string | null,
	): Promise<void> {
		const {userId, guildId} = params;
		const member = await this.guildRepository.getMember(guildId, userId);
		if (!member) throw new UnknownGuildMemberError();
		await this.operationsService.leaveGuild(params);
		const guild = await this.guildRepository.findUnique(guildId);
		const searchIndexOptions = this.getMemberSearchIndexOptions(guild);
		if (searchIndexOptions) {
			void this.searchIndexService.deleteMember(guildId, userId, searchIndexOptions);
		}
		await this.eventService.dispatchGuildMemberRemove({guildId, userId});
	}
}
