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

import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError';
import {MissingPermissionsError} from '@fluxer/errors/src/domains/core/MissingPermissionsError';
import {BannedFromGuildError} from '@fluxer/errors/src/domains/guild/BannedFromGuildError';
import {IpBannedFromGuildError} from '@fluxer/errors/src/domains/guild/IpBannedFromGuildError';
import {UnknownGuildMemberError} from '@fluxer/errors/src/domains/guild/UnknownGuildMemberError';
import {UnknownUserError} from '@fluxer/errors/src/domains/user/UnknownUserError';
import {isSameIpDecisionMatch} from '@fluxer/ip_utils/src/IpAddress';
import type {GuildBanResponse} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
import type {IpInfoService} from '@pkgs/geoip/src/IpInfoService';
import type {IWorkerService} from '@pkgs/worker/src/contracts/IWorkerService';
import type {GuildID, UserID} from '../../BrandedTypes';
import type {IGatewayService} from '../../infrastructure/IGatewayService';
import type {UserCacheService} from '../../infrastructure/UserCacheService';
import {Logger} from '../../Logger';
import type {RequestCache} from '../../middleware/RequestCacheMiddleware';
import type {GuildBan} from '../../models/GuildBan';
import {hasHighCgnatBlastRadiusRisk, isSingleIpBanCandidate} from '../../risk/IpBanCgnatGuard';
import {isIpBanExempt} from '../../risk/IpBanExemptions';
import type {IUserRepository} from '../../user/IUserRepository';
import type {WorkerTaskName} from '../../worker/WorkerLaneConfig';
import type {GuildAuditLogService} from '../GuildAuditLogService';
import type {GuildAuditLogChange} from '../GuildAuditLogTypes';
import {mapGuildBansToResponse} from '../GuildModel';
import type {IGuildRepositoryAggregate} from '../repositories/IGuildRepositoryAggregate';
import {createGuildMfaEnforcer} from './GuildMfaEnforcement';
import {GuildMemberSearchIndexService} from './member/GuildMemberSearchIndexService';

const SECONDS_PER_DAY = 86_400;

export class GuildModerationService {
	private readonly searchIndexService: GuildMemberSearchIndexService;

	constructor(
		private readonly guildRepository: IGuildRepositoryAggregate,
		private readonly userRepository: IUserRepository,
		private readonly gatewayService: IGatewayService,
		private readonly userCacheService: UserCacheService,
		private readonly workerService: IWorkerService<WorkerTaskName>,
		private readonly guildAuditLogService: GuildAuditLogService,
		private readonly ipInfoService: IpInfoService,
	) {
		this.searchIndexService = new GuildMemberSearchIndexService();
	}

	private async checkModerationPermission(params: {
		guildId: GuildID;
		userId: UserID;
		permission: bigint;
	}): Promise<void> {
		const {guildId, userId, permission} = params;
		const hasPermission = await this.gatewayService.checkPermission({guildId, userId, permission});
		if (!hasPermission) throw new MissingPermissionsError();
		const guildData = await this.gatewayService.getGuildData({guildId, userId});
		const enforceGuildMfa = await createGuildMfaEnforcer({userRepository: this.userRepository, guildData, userId});
		enforceGuildMfa(permission);
	}

	async banMember(
		params: {
			userId: UserID;
			targetId: UserID;
			guildId: GuildID;
			deleteMessageDays?: number;
			deleteMessageSeconds?: number;
			reason?: string | null;
			banDurationSeconds?: number;
			skipGuildAuditLog?: boolean;
		},
		auditLogReason?: string | null,
	): Promise<void> {
		const {
			userId,
			guildId,
			targetId,
			deleteMessageDays,
			deleteMessageSeconds,
			reason,
			banDurationSeconds,
			skipGuildAuditLog,
		} = params;
		await this.checkModerationPermission({guildId, userId, permission: Permissions.BAN_MEMBERS});
		if (userId === targetId) throw new UnknownGuildMemberError();
		const targetUser = await this.userRepository.findUnique(targetId);
		if (!targetUser) {
			throw new UnknownUserError();
		}
		const targetMember = await this.guildRepository.getMember(guildId, targetId);
		if (targetMember) {
			const canManage = await this.gatewayService.checkTargetMember({guildId, userId, targetUserId: targetId});
			if (!canManage) throw new MissingPermissionsError();
		}
		const effectiveDeleteMessageSeconds =
			deleteMessageSeconds ?? (deleteMessageDays !== undefined ? deleteMessageDays * SECONDS_PER_DAY : undefined);
		if (effectiveDeleteMessageSeconds && effectiveDeleteMessageSeconds > 0) {
			await this.workerService.addJob('deleteUserMessagesInGuildByTime', {
				guildId: guildId.toString(),
				userId: targetId.toString(),
				seconds: effectiveDeleteMessageSeconds,
			});
		}
		const targetIp = isIpBanExempt(targetUser.lastActiveIp) ? null : targetUser.lastActiveIp || null;
		const targetEmail = targetUser.email?.toLowerCase() || null;
		let expiresAt: Date | null = null;
		if (banDurationSeconds && banDurationSeconds > 0) {
			expiresAt = new Date(Date.now() + banDurationSeconds * 1000);
		}
		const ban = await this.guildRepository.upsertBan({
			guild_id: guildId,
			user_id: targetId,
			moderator_id: userId,
			banned_at: new Date(),
			expires_at: expiresAt,
			reason: reason || null,
			ip: targetIp,
			email: targetEmail,
		});
		if (!skipGuildAuditLog) {
			const metadata: Record<string, string> | undefined =
				deleteMessageDays !== undefined ? {delete_member_days: deleteMessageDays.toString()} : undefined;
			await this.recordAuditLog({
				guildId,
				userId,
				action: AuditLogActionType.MEMBER_BAN_ADD,
				targetId: targetId,
				auditLogReason: auditLogReason ?? null,
				metadata,
				changes: this.guildAuditLogService.computeChanges(null, this.serializeBanForAudit(ban)),
			});
		}
		await this.gatewayService.dispatchGuild({
			guildId,
			event: 'GUILD_BAN_ADD',
			data: {
				guild_id: guildId.toString(),
				user: {id: targetId.toString()},
			},
		});
		if (targetMember) {
			await this.guildRepository.deleteMember(guildId, targetId);
			const guild = await this.guildRepository.findUnique(guildId);
			if (guild) {
				await this.guildRepository.upsertPartial(
					guildId,
					{member_count: Math.max(0, guild.memberCount - 1)},
					guild.toRow(),
				);
			}
			await this.dispatchGuildMemberRemove({guildId, userId: targetId});
			await this.gatewayService.leaveGuild({userId: targetId, guildId});
			const guildForSearch = await this.guildRepository.findUnique(guildId);
			if (guildForSearch) {
				const includeDefault = guildForSearch.membersIndexedAt != null;
				if (includeDefault) {
					void this.searchIndexService.deleteMember(guildId, targetId, {includeDefault});
				}
			}
		}
	}

	async listBans(params: {
		userId: UserID;
		guildId: GuildID;
		requestCache: RequestCache;
	}): Promise<Array<GuildBanResponse>> {
		const {userId, guildId, requestCache} = params;
		await this.checkModerationPermission({guildId, userId, permission: Permissions.BAN_MEMBERS});
		const bans = await this.guildRepository.listBans(guildId);
		return await mapGuildBansToResponse(bans, this.userCacheService, requestCache);
	}

	async unbanMember(
		params: {
			userId: UserID;
			targetId: UserID;
			guildId: GuildID;
		},
		auditLogReason?: string | null,
	): Promise<void> {
		const {userId, guildId, targetId} = params;
		await this.checkModerationPermission({guildId, userId, permission: Permissions.BAN_MEMBERS});
		const ban = await this.guildRepository.getBan(guildId, targetId);
		if (!ban) {
			throw InputValidationError.fromCode('user_id', ValidationErrorCodes.USER_IS_NOT_BANNED);
		}
		await this.guildRepository.deleteBan(guildId, targetId);
		await this.recordAuditLog({
			guildId,
			userId,
			action: AuditLogActionType.MEMBER_BAN_REMOVE,
			targetId: targetId,
			auditLogReason: auditLogReason ?? null,
			changes: this.guildAuditLogService.computeChanges(this.serializeBanForAudit(ban), null),
		});
		await this.gatewayService.dispatchGuild({
			guildId,
			event: 'GUILD_BAN_REMOVE',
			data: {
				guild_id: guildId.toString(),
				user: {id: targetId.toString()},
			},
		});
	}

	async checkUserBanStatus(params: {userId: UserID; guildId: GuildID}): Promise<void> {
		const {userId, guildId} = params;
		const [bans, user] = await Promise.all([
			this.guildRepository.listBans(guildId),
			this.userRepository.findUnique(userId),
		]);
		const userIp = user?.lastActiveIp;
		const userEmail = user?.email?.toLowerCase();
		for (const ban of bans) {
			if (ban.userId === userId) throw new BannedFromGuildError();
			if (isSameIpDecisionMatch(userIp, ban.ipAddress) && (await this.shouldEnforceIpBan(userIp, ban.ipAddress))) {
				throw new IpBannedFromGuildError();
			}
		}
		if (userEmail) {
			const emailBan = await this.guildRepository.getBanByEmail(guildId, userEmail);
			if (emailBan) throw new BannedFromGuildError();
		}
	}

	private async shouldEnforceIpBan(
		userIp: string | null | undefined,
		bannedIp: string | null | undefined,
	): Promise<boolean> {
		if (isIpBanExempt(userIp)) {
			return false;
		}
		if (!userIp || !bannedIp || !isSingleIpBanCandidate(bannedIp)) {
			return true;
		}
		try {
			const highRisk = await hasHighCgnatBlastRadiusRisk(userIp, this.ipInfoService, {
				source: 'guild.ip_ban',
				reason: 'join_cgnat_guard',
			});
			if (highRisk) {
				Logger.warn(
					{userIp, bannedIp},
					'Skipping guild IP ban match because IPInfo indicates high CGNAT blast-radius risk',
				);
			}
			return !highRisk;
		} catch (error) {
			Logger.warn({error, userIp, bannedIp}, 'IPInfo CGNAT guard failed while checking guild IP ban');
			return true;
		}
	}

	private serializeBanForAudit(ban: GuildBan): Record<string, unknown> {
		return {
			user_id: ban.userId.toString(),
			moderator_id: ban.moderatorId.toString(),
			banned_at: ban.bannedAt.toISOString(),
			expires_at: ban.expiresAt ? ban.expiresAt.toISOString() : null,
			reason: ban.reason ?? null,
		};
	}

	private async dispatchGuildMemberRemove({guildId, userId}: {guildId: GuildID; userId: UserID}): Promise<void> {
		await this.gatewayService.dispatchGuild({
			guildId,
			event: 'GUILD_MEMBER_REMOVE',
			data: {user: {id: userId.toString()}},
		});
	}

	private async recordAuditLog(params: {
		guildId: GuildID;
		userId: UserID;
		action: AuditLogActionType;
		targetId?: UserID | string | null;
		auditLogReason?: string | null;
		metadata?: Map<string, string> | Record<string, string>;
		changes?: GuildAuditLogChange | null;
		createdAt?: Date;
	}): Promise<void> {
		const targetId =
			params.targetId === undefined || params.targetId === null
				? null
				: typeof params.targetId === 'string'
					? params.targetId
					: params.targetId.toString();
		try {
			const builder = this.guildAuditLogService
				.createBuilder(params.guildId, params.userId)
				.withAction(params.action, targetId)
				.withReason(params.auditLogReason ?? null);
			if (params.metadata) {
				builder.withMetadata(params.metadata);
			}
			if (params.changes) {
				builder.withChanges(params.changes);
			}
			if (params.createdAt) {
				builder.withCreatedAt(params.createdAt);
			}
			await builder.commit();
		} catch (error) {
			Logger.error(
				{
					error,
					guildId: params.guildId.toString(),
					userId: params.userId.toString(),
					action: params.action,
					targetId,
				},
				'Failed to record guild audit log',
			);
		}
	}
}
