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

import type {WorkerTaskHandler} from '@pkgs/worker/src/contracts/WorkerTask';
import {z} from 'zod';
import type {UserID} from '../../BrandedTypes';
import {createGuildID, createUserID} from '../../BrandedTypes';
import {Logger} from '../../Logger';
import type {User} from '../../models/User';
import {getGuildMemberSearchService} from '../../SearchFactory';
import type {IGuildMemberSearchService} from '../../search/IGuildMemberSearchService';
import {getWorkerDependencies} from '../WorkerContext';

const PayloadSchema = z.object({
	guildId: z.string(),
	lastUserId: z.string().nullable().optional(),
});
const PAGE_SIZE = 1000;
const INDEX_CONCURRENCY = 4;
const INDEX_CHUNK_SIZE = 500;

function getGuildMemberIndexServices(): Array<IGuildMemberSearchService> {
	const services = [getGuildMemberSearchService()].filter(
		(service): service is IGuildMemberSearchService => service != null,
	);
	return Array.from(new Set(services));
}

const indexGuildMembers: WorkerTaskHandler = async (payload, _helpers) => {
	const validated = PayloadSchema.parse(payload);
	const searchServices = getGuildMemberIndexServices();
	if (searchServices.length === 0) {
		return;
	}
	const guildId = createGuildID(BigInt(validated.guildId));
	const {guildRepository, userRepository} = getWorkerDependencies();
	try {
		let cursor: UserID | undefined = validated.lastUserId ? createUserID(BigInt(validated.lastUserId)) : undefined;
		let totalIndexed = 0;
		let hasMore = true;
		while (hasMore) {
			const members = await guildRepository.listMembersPaginated(guildId, PAGE_SIZE, cursor);
			if (members.length === 0) {
				break;
			}
			const uniqueUserIds = Array.from(new Set(members.map((m) => m.userId)));
			const users = await userRepository.listUsers(uniqueUserIds);
			const userMap = new Map<UserID, User>(users.map((u) => [u.id, u]));
			const membersWithUsers = members
				.map((member) => {
					const user = userMap.get(member.userId);
					return user ? {member, user} : null;
				})
				.filter((item): item is NonNullable<typeof item> => item != null);
			if (membersWithUsers.length > 0) {
				if (membersWithUsers.length <= INDEX_CHUNK_SIZE) {
					await Promise.all(searchServices.map((searchService) => searchService.indexMembers(membersWithUsers)));
				} else {
					const chunks: Array<
						Array<{
							member: (typeof membersWithUsers)[0]['member'];
							user: User;
						}>
					> = [];
					for (let i = 0; i < membersWithUsers.length; i += INDEX_CHUNK_SIZE) {
						chunks.push(membersWithUsers.slice(i, i + INDEX_CHUNK_SIZE));
					}
					for (let i = 0; i < chunks.length; i += INDEX_CONCURRENCY) {
						const batch = chunks.slice(i, i + INDEX_CONCURRENCY);
						await Promise.all(
							batch.flatMap((chunk) => searchServices.map((searchService) => searchService.indexMembers(chunk))),
						);
					}
				}
				totalIndexed += membersWithUsers.length;
			}
			Logger.debug(
				{
					guildId: guildId.toString(),
					batchSize: membersWithUsers.length,
					totalIndexed,
					hasMore: members.length === PAGE_SIZE,
				},
				'Indexed guild member batch',
			);
			hasMore = members.length === PAGE_SIZE;
			if (hasMore) {
				cursor = members[members.length - 1]!.userId;
			}
		}
		Logger.debug({guildId: guildId.toString(), totalIndexed}, 'Guild member indexing complete');
		const guild = await guildRepository.findUnique(guildId);
		if (guild) {
			await guildRepository.upsertPartial(guildId, {members_indexed_at: new Date()}, guild.toRow());
		}
	} catch (error) {
		Logger.error({error, guildId: guildId.toString()}, 'Failed to index guild members');
		throw error;
	}
};

export default indexGuildMembers;
