/*
 * Copyright (C) 2026 Fluxer Contributors
 *
 * This file is part of Fluxer.
 *
 * Fluxer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Fluxer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
 */

import type {EmojiID, GuildID, UserID, WebhookID} from '@fluxer/api/src/BrandedTypes';
import {createEmojiID} from '@fluxer/api/src/BrandedTypes';
import type {IGuildRepositoryAggregate} from '@fluxer/api/src/guild/repositories/IGuildRepositoryAggregate';
import type {LimitConfigService} from '@fluxer/api/src/limits/LimitConfigService';
import {resolveLimitSafe} from '@fluxer/api/src/limits/LimitConfigUtils';
import {createLimitMatchContext} from '@fluxer/api/src/limits/LimitMatchContextBuilder';
import type {GuildEmoji} from '@fluxer/api/src/models/GuildEmoji';
import type {
	PackExpressionAccessResolution,
	PackExpressionAccessResolver,
} from '@fluxer/api/src/pack/PackExpressionAccessResolver';
import type {IUserAccountRepository} from '@fluxer/api/src/user/repositories/IUserAccountRepository';
import {Permissions} from '@fluxer/constants/src/ChannelConstants';

type EmojiGuildRepository = Pick<IGuildRepositoryAggregate, 'getEmoji' | 'getEmojiById'>;
type EmojiUserRepository = Pick<IUserAccountRepository, 'findUnique'>;

const CUSTOM_EMOJI_MARKDOWN_REGEX = /<(a)?:([^:]+):(\d+)>/g;
const CUSTOM_EMOJI_MARKDOWN_REGEX_GLOBAL = new RegExp(CUSTOM_EMOJI_MARKDOWN_REGEX.source, 'g');

interface SanitizeCustomEmojisParams {
	content: string;
	userId: UserID | null;
	webhookId: WebhookID | null;
	guildId: GuildID | null;
	userRepository: EmojiUserRepository;
	guildRepository: EmojiGuildRepository;
	limitConfigService: LimitConfigService;
	hasPermission?: (permission: bigint) => Promise<boolean>;
	packResolver?: PackExpressionAccessResolver;
}

interface EmojiMatch {
	fullMatch: string;
	name: string;
	emojiId: EmojiID;
	start: number;
	end: number;
}

interface CodeBlock {
	start: number;
	end: number;
}

export async function sanitizeCustomEmojis(params: SanitizeCustomEmojisParams): Promise<string> {
	const {
		content,
		userId,
		webhookId,
		guildId,
		userRepository,
		guildRepository,
		limitConfigService,
		hasPermission,
		packResolver,
	} = params;

	const escapedContexts = parseEscapedContexts(content);
	const isInEscapedContext = (index: number): boolean =>
		escapedContexts.some((ctx) => index >= ctx.start && index < ctx.end);

	const emojiMatches = collectEmojiMatches(content, isInEscapedContext);
	if (emojiMatches.length === 0) {
		return content;
	}

	const hasGlobalExpressions = userId
		? await checkUserGlobalExpressions(userId, userRepository, limitConfigService)
		: 0;
	const isWebhook = webhookId != null;

	const emojiLookups = await batchFetchEmojis(emojiMatches, guildId, guildRepository);

	let canUseExternalEmojis: boolean | null = null;
	if (hasGlobalExpressions > 0 && hasPermission && guildId) {
		const hasExternalEmojis = emojiLookups.some((lookup) => lookup.guildEmoji === null && lookup.globalEmoji !== null);
		if (hasExternalEmojis) {
			canUseExternalEmojis = await hasPermission(Permissions.USE_EXTERNAL_EMOJIS);
		}
	}

	const replacements = await determineReplacements({
		emojiMatches,
		emojiLookups,
		guildId,
		isWebhook,
		hasGlobalExpressions,
		canUseExternalEmojis,
		packResolver,
	});

	return applyReplacements(content, replacements);
}

function parseEscapedContexts(content: string): Array<CodeBlock> {
	const contexts: Array<CodeBlock> = [];

	const blockCodeRegex = /```[\s\S]*?```/g;
	let match: RegExpExecArray | null;
	while ((match = blockCodeRegex.exec(content)) !== null) {
		contexts.push({start: match.index, end: match.index + match[0].length});
	}

	const inlineCodeRegex = /`[^`]+`/g;
	while ((match = inlineCodeRegex.exec(content)) !== null) {
		const isInsideBlock = contexts.some((ctx) => match!.index >= ctx.start && match!.index < ctx.end);
		if (!isInsideBlock) {
			contexts.push({start: match.index, end: match.index + match[0].length});
		}
	}

	return contexts;
}

function collectEmojiMatches(content: string, isInEscapedContext: (index: number) => boolean): Array<EmojiMatch> {
	const matches: Array<EmojiMatch> = [];
	CUSTOM_EMOJI_MARKDOWN_REGEX_GLOBAL.lastIndex = 0;

	let match: RegExpExecArray | null;
	while ((match = CUSTOM_EMOJI_MARKDOWN_REGEX_GLOBAL.exec(content)) !== null) {
		if (isInEscapedContext(match.index)) continue;

		const [fullMatch, , name, emojiId] = match;
		matches.push({
			fullMatch,
			name,
			emojiId: createEmojiID(BigInt(emojiId)),
			start: match.index,
			end: match.index + fullMatch.length,
		});
	}

	return matches;
}

async function checkUserGlobalExpressions(
	userId: UserID,
	userRepository: EmojiUserRepository,
	limitConfigService: LimitConfigService,
): Promise<number> {
	const user = await userRepository.findUnique(userId);
	const ctx = createLimitMatchContext({user});
	return resolveLimitSafe(limitConfigService.getConfigSnapshot(), ctx, 'feature_global_expressions', 0);
}

interface EmojiLookupResult {
	emojiId: EmojiID;
	guildEmoji: GuildEmoji | null;
	globalEmoji: GuildEmoji | null;
}

async function batchFetchEmojis(
	matches: Array<EmojiMatch>,
	guildId: GuildID | null,
	guildRepository: EmojiGuildRepository,
): Promise<Array<EmojiLookupResult>> {
	const uniqueEmojiIds = [...new Set(matches.map((m) => m.emojiId))];

	const lookupResults = await Promise.all(
		uniqueEmojiIds.map(async (emojiId) => {
			const [guildEmoji, globalEmoji] = await Promise.all([
				guildId ? guildRepository.getEmoji(emojiId, guildId) : Promise.resolve(null),
				guildRepository.getEmojiById(emojiId),
			]);
			return {emojiId, guildEmoji, globalEmoji};
		}),
	);

	const lookupMap = new Map<EmojiID, EmojiLookupResult>();
	for (const result of lookupResults) {
		lookupMap.set(result.emojiId, result);
	}

	return matches.map((match) => lookupMap.get(match.emojiId)!);
}

interface Replacement {
	start: number;
	end: number;
	replacement: string;
}

async function determineReplacements(params: {
	emojiMatches: Array<EmojiMatch>;
	emojiLookups: Array<EmojiLookupResult>;
	guildId: GuildID | null;
	isWebhook: boolean;
	hasGlobalExpressions: number;
	canUseExternalEmojis: boolean | null;
	packResolver?: PackExpressionAccessResolver;
}): Promise<Array<Replacement>> {
	const {emojiMatches, emojiLookups, guildId, isWebhook, hasGlobalExpressions, canUseExternalEmojis} = params;
	const replacements: Array<Replacement> = [];

	for (let i = 0; i < emojiMatches.length; i++) {
		const match = emojiMatches[i];
		const lookup = emojiLookups[i];
		const shouldReplace = await shouldReplaceEmoji({
			lookup,
			guildId,
			isWebhook,
			hasGlobalExpressions,
			canUseExternalEmojis,
			packResolver: params.packResolver,
		});

		if (shouldReplace) {
			replacements.push({
				start: match.start,
				end: match.end,
				replacement: `:${match.name}:`,
			});
		}
	}

	return replacements;
}

async function shouldReplaceEmoji(params: {
	lookup: EmojiLookupResult;
	guildId: GuildID | null;
	isWebhook: boolean;
	hasGlobalExpressions: number;
	canUseExternalEmojis: boolean | null;
	packResolver?: PackExpressionAccessResolver;
}): Promise<boolean> {
	const {lookup, guildId, isWebhook, hasGlobalExpressions, canUseExternalEmojis, packResolver} = params;

	if (!guildId) {
		if (!lookup.globalEmoji) return true;
		if (!isWebhook && hasGlobalExpressions === 0) return true;
		return false;
	}

	if (lookup.guildEmoji) {
		return false;
	}

	if (!lookup.globalEmoji) return true;

	const packAccess = await resolvePackAccessStatus(lookup.globalEmoji.guildId, packResolver);
	if (packAccess === 'not-accessible') {
		return true;
	}

	if (!isWebhook && hasGlobalExpressions === 0) return true;

	if (hasGlobalExpressions > 0 && canUseExternalEmojis === false) return true;

	return false;
}

async function resolvePackAccessStatus(
	packId: GuildID,
	packResolver?: PackExpressionAccessResolver,
): Promise<PackExpressionAccessResolution> {
	if (!packResolver) return 'not-pack';
	return await packResolver.resolve(packId);
}

function applyReplacements(content: string, replacements: Array<Replacement>): string {
	if (replacements.length === 0) return content;

	const sorted = [...replacements].sort((a, b) => b.start - a.start);

	let result = content;
	for (const {start, end, replacement} of sorted) {
		result = result.substring(0, start) + replacement + result.substring(end);
	}

	return result;
}
