import { sub } from "date-fns";
import { type NotNull, sql, type Transaction } from "kysely";
import { jsonArrayFrom, jsonBuildObject } from "kysely/helpers/sqlite";
import { db } from "~/db/sql";
import type { DB, Tables, UserMapModePreferences } from "~/db/tables";
import { actorId } from "~/features/auth/core/user.server";
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
import { shortNanoid } from "~/utils/id";
import invariant from "~/utils/invariant";
import {
	commonUserSelect,
	customAvatarUrl,
	matchProfileWeapons,
} from "~/utils/kysely.server";
import { errorIsSqliteForeignKeyConstraintFailure } from "~/utils/sql";
import { userIsBanned } from "../ban/core/banned.server";
import { FULL_GROUP_SIZE } from "./q-constants";
import { SendouQError } from "./q-utils.server";

export async function mapModePreferencesByGroupId(groupId: number) {
	const group = await db
		.selectFrom("Group")
		.leftJoin("AllTeam", "AllTeam.id", "Group.teamId")
		.select([
			"AllTeam.mapModePreferences as teamMapModePreferences",
			"AllTeam.name as teamName",
		])
		.where("Group.id", "=", groupId)
		.executeTakeFirst();

	if (group?.teamMapModePreferences) {
		const members = await db
			.selectFrom("GroupMember")
			.select("GroupMember.userId")
			.where("GroupMember.groupId", "=", groupId)
			.execute();

		return members.map((m) => ({
			userId: m.userId,
			preferences: group.teamMapModePreferences as UserMapModePreferences,
			teamName: group.teamName,
		}));
	}

	return db
		.selectFrom("GroupMember")
		.innerJoin("User", "User.id", "GroupMember.userId")
		.select(["User.id as userId", "User.mapModePreferences as preferences"])
		.where("GroupMember.groupId", "=", groupId)
		.where("User.mapModePreferences", "is not", null)
		.$narrowType<{ preferences: NotNull }>()
		.execute();
}

export async function findCurrentGroups() {
	type SendouQMemberObject = {
		id: Tables["User"]["id"];
		username: Tables["User"]["username"];
		discordId: Tables["User"]["discordId"];
		discordAvatar: Tables["User"]["discordAvatar"];
		customAvatarUrl: string | null;
		customUrl: Tables["User"]["customUrl"];
		pronouns: Tables["User"]["pronouns"] | null;
		mapModePreferences: Tables["User"]["mapModePreferences"];
		noScreen: Tables["User"]["noScreen"];
		languages: Tables["User"]["languages"];
		vc: Tables["User"]["vc"];
		role: Tables["GroupMember"]["role"];
		note: Tables["GroupMember"]["note"];
		weapons:
			| (Pick<Tables["UserWeaponPool"], "weaponSplId" | "isFavorite"> & {
					isTenStar: number;
			  })[]
			| null;
	};

	return (
		db
			.selectFrom("Group")
			.innerJoin("GroupMember", "GroupMember.groupId", "Group.id")
			.innerJoin("User", "User.id", "GroupMember.userId")
			.leftJoin("GroupMatch", (join) =>
				join.on((eb) =>
					eb.or([
						eb("GroupMatch.alphaGroupId", "=", eb.ref("Group.id")),
						eb("GroupMatch.bravoGroupId", "=", eb.ref("Group.id")),
					]),
				),
			)
			.select(({ fn, eb }) => [
				"Group.id",
				"Group.chatCode",
				"Group.inviteCode",
				"Group.latestActionAt",
				"Group.chatCode",
				"Group.inviteCode",
				"Group.status",
				"GroupMatch.id as matchId",
				fn
					.agg("json_group_array", [
						jsonBuildObject({
							id: eb.ref("User.id"),
							username: eb.ref("User.username"),
							discordId: eb.ref("User.discordId"),
							discordAvatar: eb.ref("User.discordAvatar"),
							customAvatarUrl: customAvatarUrl(eb),
							customUrl: eb.ref("User.customUrl"),
							mapModePreferences: eb.ref("User.mapModePreferences"),
							noScreen: eb.ref("User.noScreen"),
							role: eb.ref("GroupMember.role"),
							note: eb.ref("GroupMember.note"),
							weapons: matchProfileWeapons(eb),
							languages: eb.ref("User.languages"),
							vc: eb.ref("User.vc"),
						}),
					])
					.$castTo<SendouQMemberObject[]>()
					.as("members"),
			])
			// != INACTIVE (same set as ACTIVE or PREPARING) so the partial
			// group_status_active index applies
			.where("Group.status", "!=", "INACTIVE")
			.groupBy("Group.id")
			.execute()
	);
}

export async function findActiveGroupMembers() {
	return db
		.selectFrom("GroupMember")
		.innerJoin("Group", "Group.id", "GroupMember.groupId")
		.select("GroupMember.userId")
		.where("Group.status", "!=", "INACTIVE")
		.execute();
}

type CreateGroupArgs = {
	status: Exclude<Tables["Group"]["status"], "INACTIVE">;
	userId: number;
};
export async function createGroup(args: CreateGroupArgs) {
	return db.transaction().execute(async (trx) => {
		const createdGroup = await trx
			.insertInto("Group")
			.values({
				inviteCode: shortNanoid(),
				chatCode: shortNanoid(),
				status: args.status,
			})
			.returning("id")
			.executeTakeFirstOrThrow();

		await trx
			.insertInto("GroupMember")
			.values({
				groupId: createdGroup.id,
				userId: args.userId,
				role: "OWNER",
			})
			.execute();

		if (!(await isGroupCorrect(createdGroup.id, trx))) {
			throw new SendouQError("Group has a member in multiple groups");
		}

		const chatCodeToRevalidate = await recordImplicitRejoinNoVote(
			args.userId,
			trx,
		);

		return { id: createdGroup.id, chatCodeToRevalidate };
	});
}

type CreateGroupFromPreviousGroupArgs = {
	previousGroupId: number;
	members: {
		id: number;
		role: Tables["GroupMember"]["role"];
	}[];
	status?: Exclude<Tables["Group"]["status"], "INACTIVE">;
};
export async function createGroupFromPrevious(
	args: CreateGroupFromPreviousGroupArgs,
) {
	const status = args.status ?? "PREPARING";
	const membersWithEnsuredOwner = ensureOwnerRole(args.members);

	return db.transaction().execute(async (trx) => {
		const createdGroup = await trx
			.insertInto("Group")
			.columns(["teamId", "chatCode", "inviteCode", "status", "matchmade"])
			.expression((eb) =>
				eb
					.selectFrom("Group")
					.select((eb) => [
						"Group.teamId",
						"Group.chatCode",
						eb.val(shortNanoid()).as("inviteCode"),
						eb.val(status).as("status"),
						"Group.matchmade",
					])
					.where("Group.id", "=", args.previousGroupId),
			)
			.returning("id")
			.executeTakeFirstOrThrow();

		await trx
			.insertInto("GroupMember")
			.values(
				membersWithEnsuredOwner.map((member) => ({
					groupId: createdGroup.id,
					userId: member.id,
					role: member.role,
				})),
			)
			.execute();

		if (!(await isGroupCorrect(createdGroup.id, trx))) {
			throw new SendouQError(
				"Group has too many members or member in multiple groups",
			);
		}

		return createdGroup;
	});
}

function ensureOwnerRole(
	members: CreateGroupFromPreviousGroupArgs["members"],
): CreateGroupFromPreviousGroupArgs["members"] {
	if (members.some((m) => m.role === "OWNER")) return members;

	const promoteeIndex = members.findIndex((m) => m.role === "MANAGER");
	const targetIndex = promoteeIndex !== -1 ? promoteeIndex : 0;

	return members.map((m, i) =>
		i === targetIndex ? { ...m, role: "OWNER" as const } : m,
	);
}

function deleteLikesByGroupId(groupId: number, trx: Transaction<DB>) {
	return trx
		.deleteFrom("GroupLike")
		.where((eb) =>
			eb.or([
				eb("GroupLike.likerGroupId", "=", groupId),
				eb("GroupLike.targetGroupId", "=", groupId),
			]),
		)
		.execute();
}

export function morphGroups({
	survivingGroupId,
	otherGroupId,
}: {
	survivingGroupId: number;
	otherGroupId: number;
}) {
	return db.transaction().execute(async (trx) => {
		// reset chat code so previous messages are not visible, and mark as matchmade
		await trx
			.updateTable("Group")
			.set({ chatCode: shortNanoid(), matchmade: 1 })
			.where("Group.id", "=", survivingGroupId)
			.execute();

		const otherGroupMembers = await trx
			.selectFrom("GroupMember")
			.select(["GroupMember.userId", "GroupMember.role"])
			.where("GroupMember.groupId", "=", otherGroupId)
			.execute();

		for (const member of otherGroupMembers) {
			const oldRole = otherGroupMembers.find(
				(m) => m.userId === member.userId,
			)?.role;
			invariant(oldRole, "Member lacking a role");

			await trx
				.updateTable("GroupMember")
				.set({
					role:
						oldRole === "OWNER"
							? "MANAGER"
							: oldRole === "MANAGER"
								? "MANAGER"
								: "REGULAR",
					groupId: survivingGroupId,
				})
				.where("GroupMember.groupId", "=", otherGroupId)
				.where("GroupMember.userId", "=", member.userId)
				.execute();
		}

		await deleteLikesByGroupId(survivingGroupId, trx);
		await refreshGroup(survivingGroupId, trx);

		await trx
			.deleteFrom("Group")
			.where("Group.id", "=", otherGroupId)
			.execute();

		if (!(await isGroupCorrect(survivingGroupId, trx))) {
			throw new SendouQError(
				"Group has too many members or member in multiple groups",
			);
		}
	});
}

/** Check that the group has at most FULL_GROUP_SIZE members and each member is only in this group */
async function isGroupCorrect(
	groupId: number,
	trx: Transaction<DB>,
): Promise<boolean> {
	const members = await trx
		.selectFrom("GroupMember")
		.select("GroupMember.userId")
		.where("GroupMember.groupId", "=", groupId)
		.execute();

	if (members.length > FULL_GROUP_SIZE) {
		return false;
	}

	for (const member of members) {
		const otherGroup = await trx
			.selectFrom("GroupMember")
			.innerJoin("Group", "Group.id", "GroupMember.groupId")
			.select(["Group.id"])
			.where("GroupMember.userId", "=", member.userId)
			.where("Group.status", "!=", "INACTIVE")
			.where("GroupMember.groupId", "!=", groupId)
			.executeTakeFirst();

		if (otherGroup) {
			return false;
		}
	}

	return true;
}

export async function addMember(
	groupId: number,
	{
		userId,
		role = "REGULAR",
	}: {
		userId: number;
		role?: Tables["GroupMember"]["role"];
	},
) {
	const chatCodeToRevalidate = await db.transaction().execute(async (trx) => {
		await trx
			.insertInto("GroupMember")
			.values({
				groupId,
				userId,
				role,
			})
			.execute();

		await deleteLikesByGroupId(groupId, trx);

		if (!(await isGroupCorrect(groupId, trx))) {
			throw new SendouQError(
				"Group has too many members or member in multiple groups",
			);
		}

		return recordImplicitRejoinNoVote(userId, trx);
	});

	return { chatCodeToRevalidate };
}

export async function allLikesByGroupId(groupId: number) {
	const rows = await db
		.selectFrom("GroupLike")
		.select([
			"GroupLike.likerGroupId",
			"GroupLike.targetGroupId",
			"GroupLike.isRechallenge",
		])
		.where((eb) =>
			eb.or([
				eb("GroupLike.likerGroupId", "=", groupId),
				eb("GroupLike.targetGroupId", "=", groupId),
			]),
		)
		.execute();

	return {
		given: rows
			.filter((row) => row.likerGroupId === groupId)
			.map((row) => ({
				groupId: row.targetGroupId,
				isRechallenge: row.isRechallenge,
			})),
		received: rows
			.filter((row) => row.targetGroupId === groupId)
			.map((row) => ({
				groupId: row.likerGroupId,
				isRechallenge: row.isRechallenge,
			})),
	};
}

export function rechallenge({
	likerGroupId,
	targetGroupId,
}: {
	likerGroupId: number;
	targetGroupId: number;
}) {
	return db
		.updateTable("GroupLike")
		.set({ isRechallenge: 1 })
		.where("likerGroupId", "=", likerGroupId)
		.where("targetGroupId", "=", targetGroupId)
		.execute();
}

export async function friendsAndTeammates(userId: number) {
	const teams = await db
		.selectFrom("TeamMemberWithSecondary")
		.innerJoin("Team", "Team.id", "TeamMemberWithSecondary.teamId")
		.select(["Team.id", "Team.name", "TeamMemberWithSecondary.isMainTeam"])
		.where("userId", "=", userId)
		.execute();

	const rows = await db
		.selectFrom("TeamMemberWithSecondary")
		.innerJoin("User", "User.id", "TeamMemberWithSecondary.userId")
		.select((eb) => [
			...commonUserSelect(eb),
			"User.inGameName",
			"TeamMemberWithSecondary.teamId",
		])
		.where(
			"TeamMemberWithSecondary.teamId",
			"in",
			teams.map((t) => t.id),
		)
		.union((eb) =>
			eb
				.selectFrom("Friendship")
				.innerJoin("User", (join) =>
					join.on((eb) =>
						eb.or([
							eb.and([
								eb("Friendship.userOneId", "=", userId),
								eb("User.id", "=", eb.ref("Friendship.userTwoId")),
							]),
							eb.and([
								eb("Friendship.userTwoId", "=", userId),
								eb("User.id", "=", eb.ref("Friendship.userOneId")),
							]),
						]),
					),
				)
				.select((eb) => [
					...commonUserSelect(eb),
					"User.inGameName",
					sql<any>`null`.as("teamId"),
				]),
		)
		.execute();

	const rowsWithoutBanned = rows.filter((row) => !userIsBanned(row.id));

	const teamMemberIds = rowsWithoutBanned
		.filter((row) => row.teamId)
		.map((row) => row.id);

	// we want user to show twice if member of two different teams
	// but we don't want a user from the team to show in teamless section
	const deduplicatedRows = rowsWithoutBanned.filter(
		(row) => row.teamId || !teamMemberIds.includes(row.id),
	);

	// done here at not sql just because it was easier to do here ignoring case
	deduplicatedRows.sort((a, b) => a.username.localeCompare(b.username));

	return {
		teams: teams.sort((a, b) => b.isMainTeam - a.isMainTeam),
		friends: deduplicatedRows,
	};
}

export async function setOldGroupsAsInactive() {
	const oneHourAgo = sub(new Date(), { hours: 1 });

	return db.transaction().execute(async (trx) => {
		const groupsToSetInactive = await trx
			.selectFrom("Group")
			.leftJoin("GroupMatch", (join) =>
				join.on((eb) =>
					eb.or([
						eb("GroupMatch.alphaGroupId", "=", eb.ref("Group.id")),
						eb("GroupMatch.bravoGroupId", "=", eb.ref("Group.id")),
					]),
				),
			)
			.select(["Group.id"])
			.where("status", "!=", "INACTIVE")
			.where("GroupMatch.id", "is", null)
			.where("latestActionAt", "<", dateToDatabaseTimestamp(oneHourAgo))
			.execute();

		return trx
			.updateTable("Group")
			.set({ status: "INACTIVE" })
			.where(
				"Group.id",
				"in",
				groupsToSetInactive.map((g) => g.id),
			)
			.executeTakeFirst();
	});
}
export async function closeExpiredContinueVotes() {
	const cutoff = dateToDatabaseTimestamp(sub(new Date(), { hours: 1 }));

	return db.transaction().execute(async (trx) => {
		const eligibleGroups = await trx
			.selectFrom("Group")
			.innerJoin("GroupMatch", (join) =>
				join.on((eb) =>
					eb.or([
						eb("GroupMatch.alphaGroupId", "=", eb.ref("Group.id")),
						eb("GroupMatch.bravoGroupId", "=", eb.ref("Group.id")),
					]),
				),
			)
			.innerJoin("GroupMember", "GroupMember.groupId", "Group.id")
			.leftJoin("GroupMatchContinueVote", (join) =>
				join
					.onRef("GroupMatchContinueVote.groupId", "=", "Group.id")
					.onRef("GroupMatchContinueVote.userId", "=", "GroupMember.userId"),
			)
			.select(["Group.id as groupId", "GroupMatch.chatCode as matchChatCode"])
			.where("Group.matchmade", "=", 1)
			.where("GroupMatch.confirmedAt", "is not", null)
			.where("GroupMatch.confirmedAt", "<", cutoff)
			.where("GroupMatchContinueVote.id", "is", null)
			.groupBy("Group.id")
			.execute();

		const chatCodesToRevalidate: string[] = [];

		for (const { groupId, matchChatCode } of eligibleGroups) {
			const members = await trx
				.selectFrom("GroupMember")
				.select("GroupMember.userId")
				.where("GroupMember.groupId", "=", groupId)
				.execute();

			await trx
				.insertInto("GroupMatchContinueVote")
				.values(
					members.map((m) => ({
						groupId,
						userId: m.userId,
						isContinuing: 0 as const,
					})),
				)
				.onConflict((oc) =>
					oc.columns(["groupId", "userId"]).doUpdateSet({ isContinuing: 0 }),
				)
				.execute();

			if (matchChatCode) chatCodesToRevalidate.push(matchChatCode);
		}

		return {
			chatCodesToRevalidate,
			numAffectedGroups: eligibleGroups.length,
		};
	});
}

export async function mapModePreferencesBySeasonNth(seasonNth: number) {
	return db
		.selectFrom("User")
		.select("User.mapModePreferences")
		.where("User.mapModePreferences", "is not", null)
		.where(({ eb, exists }) =>
			exists(
				eb
					.selectFrom("Skill")
					.select("Skill.id")
					.whereRef("Skill.userId", "=", "User.id")
					.where("Skill.season", "=", seasonNth),
			),
		)
		.$narrowType<{ mapModePreferences: UserMapModePreferences }>()
		.execute();
}

export async function findRecentlyFinishedMatches() {
	const twoHoursAgo = sub(new Date(), { hours: 2 });

	const rows = await db
		.selectFrom("GroupMatch")
		.select((eb) => [
			jsonArrayFrom(
				eb
					.selectFrom("GroupMember")
					.select("GroupMember.userId")
					.whereRef("GroupMember.groupId", "=", "GroupMatch.alphaGroupId"),
			).as("groupAlphaMemberIds"),
			jsonArrayFrom(
				eb
					.selectFrom("GroupMember")
					.select("GroupMember.userId")
					.whereRef("GroupMember.groupId", "=", "GroupMatch.bravoGroupId"),
			).as("groupBravoMemberIds"),
		])
		.where("GroupMatch.confirmedAt", "is not", null)
		.where("GroupMatch.confirmedAt", ">", dateToDatabaseTimestamp(twoHoursAgo))
		.execute();

	return rows.map((row) => ({
		groupAlphaMemberIds: row.groupAlphaMemberIds.map((m) => m.userId),
		groupBravoMemberIds: row.groupBravoMemberIds.map((m) => m.userId),
	}));
}

export function addLike({
	likerGroupId,
	targetGroupId,
}: {
	likerGroupId: number;
	targetGroupId: number;
}) {
	return db.transaction().execute(async (trx) => {
		try {
			await trx
				.insertInto("GroupLike")
				.values({ likerGroupId, targetGroupId })
				.onConflict((oc) =>
					oc.columns(["likerGroupId", "targetGroupId"]).doNothing(),
				)
				.execute();
		} catch (error) {
			if (errorIsSqliteForeignKeyConstraintFailure(error)) {
				throw new SendouQError(error.message);
			}
			throw error;
		}

		await refreshGroup(likerGroupId, trx);
	});
}

export function deleteLike({
	likerGroupId,
	targetGroupId,
}: {
	likerGroupId: number;
	targetGroupId: number;
}) {
	return db.transaction().execute(async (trx) => {
		await trx
			.deleteFrom("GroupLike")
			.where("likerGroupId", "=", likerGroupId)
			.where("targetGroupId", "=", targetGroupId)
			.execute();

		await refreshGroup(likerGroupId, trx);
	});
}

export function leaveGroup(userId: number) {
	return db.transaction().execute(async (trx) => {
		const userGroup = await trx
			.selectFrom("GroupMember")
			.innerJoin("Group", "Group.id", "GroupMember.groupId")
			.select(["Group.id", "GroupMember.role"])
			.where("userId", "=", userId)
			.where("Group.status", "!=", "INACTIVE")
			.executeTakeFirstOrThrow();

		await trx
			.deleteFrom("GroupMember")
			.where("userId", "=", userId)
			.where("GroupMember.groupId", "=", userGroup.id)
			.execute();

		const remainingMembers = await trx
			.selectFrom("GroupMember")
			.select(["userId", "role"])
			.where("groupId", "=", userGroup.id)
			.execute();

		if (remainingMembers.length === 0) {
			await trx.deleteFrom("Group").where("id", "=", userGroup.id).execute();
			return;
		}

		if (userGroup.role === "OWNER") {
			const newOwner =
				remainingMembers.find((m) => m.role === "MANAGER") ??
				remainingMembers[0];

			await trx
				.updateTable("GroupMember")
				.set({ role: "OWNER" })
				.where("userId", "=", newOwner.userId)
				.where("groupId", "=", userGroup.id)
				.execute();
		}

		const match = await trx
			.selectFrom("GroupMatch")
			.select(["GroupMatch.id"])
			.where((eb) =>
				eb.or([
					eb("alphaGroupId", "=", userGroup.id),
					eb("bravoGroupId", "=", userGroup.id),
				]),
			)
			.executeTakeFirst();

		if (match) {
			throw new SendouQError("Can't leave group when already in a match");
		}
	});
}

export function refreshGroup(groupId: number, trx?: Transaction<DB>) {
	return (trx ?? db)
		.updateTable("Group")
		.set({ latestActionAt: databaseTimestampNow() })
		.where("Group.id", "=", groupId)
		.execute();
}

export function updateOwnMemberNote({
	groupId,
	value,
}: {
	groupId: number;
	value: string | null;
}) {
	return db.transaction().execute(async (trx) => {
		await trx
			.updateTable("GroupMember")
			.set({ note: value })
			.where("groupId", "=", groupId)
			.where("userId", "=", actorId())
			.execute();

		await refreshGroup(groupId, trx);
	});
}

export function updateMemberRole({
	userId,
	groupId,
	role,
}: {
	userId: number;
	groupId: number;
	role: Tables["GroupMember"]["role"];
}) {
	if (role === "OWNER") {
		throw new Error("Can't set role to OWNER with this function");
	}

	return db.transaction().execute(async (trx) => {
		await trx
			.updateTable("GroupMember")
			.set({ role })
			.where("userId", "=", userId)
			.where("groupId", "=", groupId)
			.execute();

		await refreshGroup(groupId, trx);
	});
}

export function setPreparingGroupAsActive(groupId: number) {
	return db
		.updateTable("Group")
		.set({ status: "ACTIVE", latestActionAt: databaseTimestampNow() })
		.where("id", "=", groupId)
		.where("status", "=", "PREPARING")
		.execute();
}

export function setAsInactive(groupId: number, trx?: Transaction<DB>) {
	return (trx ?? db)
		.updateTable("Group")
		.set({ status: "INACTIVE" })
		.where("id", "=", groupId)
		.execute();
}
async function recordImplicitRejoinNoVote(
	userId: number,
	trx: Transaction<DB>,
): Promise<string | null> {
	const candidate = await trx
		.selectFrom("GroupMember")
		.innerJoin("Group", "Group.id", "GroupMember.groupId")
		.innerJoin("GroupMatch", (join) =>
			join.on((eb) =>
				eb.or([
					eb("GroupMatch.alphaGroupId", "=", eb.ref("Group.id")),
					eb("GroupMatch.bravoGroupId", "=", eb.ref("Group.id")),
				]),
			),
		)
		.leftJoin("GroupMatchContinueVote", (join) =>
			join
				.onRef("GroupMatchContinueVote.groupId", "=", "Group.id")
				.on("GroupMatchContinueVote.userId", "=", userId),
		)
		.select(["Group.id as groupId", "GroupMatch.chatCode as matchChatCode"])
		.where("GroupMember.userId", "=", userId)
		.where("Group.matchmade", "=", 1)
		.where("GroupMatchContinueVote.id", "is", null)
		.executeTakeFirst();

	if (!candidate) return null;

	await trx
		.deleteFrom("GroupMatchContinueVote")
		.where("GroupMatchContinueVote.groupId", "=", candidate.groupId)
		.where("GroupMatchContinueVote.isContinuing", "=", 1)
		.execute();

	await trx
		.insertInto("GroupMatchContinueVote")
		.values({
			groupId: candidate.groupId,
			userId,
			isContinuing: 0,
		})
		.onConflict((oc) =>
			oc.columns(["groupId", "userId"]).doUpdateSet({ isContinuing: 0 }),
		)
		.execute();

	return candidate.matchChatCode;
}
