/*
 * 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, StickerID, UserID} from '@fluxer/api/src/BrandedTypes';
import {createEmojiID, createGuildID, createStickerID} from '@fluxer/api/src/BrandedTypes';
import {
	mapGuildEmojisWithUsersToResponse,
	mapGuildEmojiToResponse,
	mapGuildStickersWithUsersToResponse,
	mapGuildStickerToResponse,
} from '@fluxer/api/src/guild/GuildModel';
import type {IGuildRepositoryAggregate} from '@fluxer/api/src/guild/repositories/IGuildRepositoryAggregate';
import type {ExpressionAssetPurger} from '@fluxer/api/src/guild/services/content/ExpressionAssetPurger';
import type {AvatarService} from '@fluxer/api/src/infrastructure/AvatarService';
import {getMetricsService} from '@fluxer/api/src/infrastructure/MetricsService';
import type {SnowflakeService} from '@fluxer/api/src/infrastructure/SnowflakeService';
import type {UserCacheService} from '@fluxer/api/src/infrastructure/UserCacheService';
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 {RequestCache} from '@fluxer/api/src/middleware/RequestCacheMiddleware';
import {ExpressionPack} from '@fluxer/api/src/models/ExpressionPack';
import type {User} from '@fluxer/api/src/models/User';
import type {
	PackExpressionAccessResolution,
	PackExpressionAccessResolver,
	PackExpressionAccessResolverParams,
} from '@fluxer/api/src/pack/PackExpressionAccessResolver';
import {mapPackToSummary} from '@fluxer/api/src/pack/PackModel';
import type {PackRepository, PackType} from '@fluxer/api/src/pack/PackRepository';
import type {IUserRepository} from '@fluxer/api/src/user/IUserRepository';
import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
import {
	MAX_CREATED_PACKS_NON_PREMIUM,
	MAX_INSTALLED_PACKS_NON_PREMIUM,
	MAX_PACK_EXPRESSIONS,
} from '@fluxer/constants/src/LimitConstants';
import {ManagedTraits} from '@fluxer/constants/src/ManagedTraits';
import {FeatureAccessError} from '@fluxer/errors/src/domains/core/FeatureAccessError';
import {FeatureTemporarilyDisabledError} from '@fluxer/errors/src/domains/core/FeatureTemporarilyDisabledError';
import {UnknownGuildEmojiError} from '@fluxer/errors/src/domains/guild/UnknownGuildEmojiError';
import {UnknownGuildStickerError} from '@fluxer/errors/src/domains/guild/UnknownGuildStickerError';
import {InvalidPackTypeError} from '@fluxer/errors/src/domains/pack/InvalidPackTypeError';
import {MaxPackExpressionsError} from '@fluxer/errors/src/domains/pack/MaxPackExpressionsError';
import {MaxPackLimitError} from '@fluxer/errors/src/domains/pack/MaxPackLimitError';
import {PackAccessDeniedError} from '@fluxer/errors/src/domains/pack/PackAccessDeniedError';
import {UnknownPackError} from '@fluxer/errors/src/domains/pack/UnknownPackError';
import type {
	GuildEmojiResponse,
	GuildEmojiWithUserResponse,
	GuildStickerResponse,
	GuildStickerWithUserResponse,
} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas';
import type {PackDashboardResponse, PackDashboardSectionResponse} from '@fluxer/schema/src/domains/pack/PackSchemas';

export class PackService {
	constructor(
		private readonly packRepository: PackRepository,
		private readonly guildRepository: IGuildRepositoryAggregate,
		private readonly avatarService: AvatarService,
		private readonly snowflakeService: SnowflakeService,
		private readonly assetPurger: ExpressionAssetPurger,
		private readonly userRepository: IUserRepository,
		private readonly userCacheService: UserCacheService,
		private readonly limitConfigService: LimitConfigService,
	) {}

	private async requireExpressionPackAccess(userId: UserID): Promise<void> {
		const user = await this.userRepository.findUnique(userId);
		if (!user || !user.traits.has(ManagedTraits.EXPRESSION_PACKS)) {
			throw new FeatureTemporarilyDisabledError();
		}
	}

	private async hasFeatureAccess(userId: UserID, limitKey: LimitKey): Promise<boolean> {
		const user = await this.userRepository.findUnique(userId);
		if (!user) {
			return false;
		}

		const ctx = createLimitMatchContext({user});
		const value = resolveLimitSafe(this.limitConfigService.getConfigSnapshot(), ctx, limitKey, 0);
		return value > 0;
	}

	private async ensurePackOwner(userId: UserID, packId: GuildID): Promise<ExpressionPack> {
		const pack = await this.packRepository.getPack(packId);
		if (!pack) {
			throw new UnknownPackError();
		}
		if (pack.creatorId !== userId) {
			throw new PackAccessDeniedError();
		}
		return pack;
	}

	private async collectInstalledPacks(userId: UserID): Promise<Array<{pack: ExpressionPack; installedAt: Date}>> {
		const installations = await this.packRepository.listInstallations(userId);
		const results: Array<{pack: ExpressionPack; installedAt: Date}> = [];
		for (const row of installations) {
			const pack = await this.packRepository.getPack(row.pack_id);
			if (!pack) {
				await this.packRepository.removeInstallation(userId, row.pack_id);
				continue;
			}
			results.push({pack, installedAt: row.installed_at});
		}
		return results;
	}

	private async requireFeature(userId: UserID, limitKey: LimitKey): Promise<void> {
		if (!(await this.hasFeatureAccess(userId, limitKey))) {
			throw new FeatureAccessError();
		}
	}

	private async getInstalledPackIdsByType(userId: UserID, packType: PackType): Promise<Set<GuildID>> {
		const installations = await this.packRepository.listInstallations(userId);
		return new Set(installations.filter((row) => row.pack_type === packType).map((row) => row.pack_id));
	}

	private buildPackExpressionAccessResolution(
		userId: UserID,
		packType: PackType,
		pack: ExpressionPack | null,
	): PackExpressionAccessResolution {
		if (!pack) {
			return 'not-pack';
		}
		if (pack.type !== packType) {
			return 'not-pack';
		}
		return pack.creatorId === userId ? 'accessible' : 'not-accessible';
	}

	async createPackExpressionAccessResolver(
		params: PackExpressionAccessResolverParams,
	): Promise<PackExpressionAccessResolver> {
		const {userId, type} = params;

		if (!userId) {
			return {
				resolve: async () => 'not-pack',
			};
		}

		const installedPackIds = await this.getInstalledPackIdsByType(userId, type);
		const resolutionCache = new Map<GuildID, PackExpressionAccessResolution>();

		return {
			resolve: async (packId: GuildID) => {
				if (installedPackIds.has(packId)) {
					resolutionCache.set(packId, 'accessible');
					return 'accessible';
				}

				const cached = resolutionCache.get(packId);
				if (cached) {
					return cached;
				}

				const pack = await this.packRepository.getPack(packId);
				const resolution = this.buildPackExpressionAccessResolution(userId, type, pack);
				resolutionCache.set(packId, resolution);
				return resolution;
			},
		};
	}

	async listUserPacks(userId: UserID): Promise<PackDashboardResponse> {
		await this.requireExpressionPackAccess(userId);
		const user = await this.userRepository.findUnique(userId);
		const ctx = createLimitMatchContext({user});
		resolveLimitSafe(this.limitConfigService.getConfigSnapshot(), ctx, 'feature_global_expressions', 0);
		const createdEmoji = await this.packRepository.listPacksByCreator(userId, 'emoji');
		const createdSticker = await this.packRepository.listPacksByCreator(userId, 'sticker');
		const installations = await this.collectInstalledPacks(userId);

		const installedEmoji = installations
			.filter((entry) => entry.pack.type === 'emoji')
			.map((entry) => mapPackToSummary(entry.pack, entry.installedAt));
		const installedSticker = installations
			.filter((entry) => entry.pack.type === 'sticker')
			.map((entry) => mapPackToSummary(entry.pack, entry.installedAt));

		const fallbackCreatedLimit = MAX_CREATED_PACKS_NON_PREMIUM;
		const fallbackInstalledLimit = MAX_INSTALLED_PACKS_NON_PREMIUM;
		const createdLimit = this.resolveLimitForUser(user ?? null, 'max_created_packs', fallbackCreatedLimit);
		const installedLimit = this.resolveLimitForUser(user ?? null, 'max_installed_packs', fallbackInstalledLimit);

		const emojiSection: PackDashboardSectionResponse = {
			installed_limit: installedLimit,
			created_limit: createdLimit,
			installed: installedEmoji,
			created: createdEmoji.map((pack) => mapPackToSummary(pack)),
		};

		const stickerSection: PackDashboardSectionResponse = {
			installed_limit: installedLimit,
			created_limit: createdLimit,
			installed: installedSticker,
			created: createdSticker.map((pack) => mapPackToSummary(pack)),
		};

		return {
			emoji: emojiSection,
			sticker: stickerSection,
		};
	}

	async createPack(params: {
		user: User;
		type: PackType;
		name: string;
		description?: string | null;
	}): Promise<ExpressionPack> {
		await this.requireExpressionPackAccess(params.user.id);
		await this.requireFeature(params.user.id, 'feature_global_expressions');
		const createdCount = await this.packRepository.countPacksByCreator(params.user.id, params.type);
		const fallbackLimit = MAX_CREATED_PACKS_NON_PREMIUM;
		const limit = this.resolveLimitForUser(params.user, 'max_created_packs', fallbackLimit);
		if (createdCount >= limit) {
			throw new MaxPackLimitError(params.type, limit, 'create');
		}

		const now = new Date();
		const packId = createGuildID(await this.snowflakeService.generate());
		return await this.packRepository.upsertPack({
			pack_id: packId,
			pack_type: params.type,
			creator_id: params.user.id,
			name: params.name,
			description: params.description ?? null,
			created_at: now,
			updated_at: now,
			version: 1,
		});
	}

	async updatePack(params: {
		userId: UserID;
		packId: GuildID;
		name?: string;
		description?: string | null;
	}): Promise<ExpressionPack> {
		await this.requireExpressionPackAccess(params.userId);
		const pack = await this.ensurePackOwner(params.userId, params.packId);
		const now = new Date();
		const updatedPack = new ExpressionPack({
			...pack.toRow(),
			name: params.name ?? pack.name,
			description: params.description === undefined ? pack.description : params.description,
			updated_at: now,
		});
		return await this.packRepository.upsertPack(updatedPack.toRow());
	}

	async deletePack(userId: UserID, packId: GuildID): Promise<void> {
		await this.requireExpressionPackAccess(userId);
		await this.ensurePackOwner(userId, packId);
		await this.packRepository.deletePack(packId);
	}

	async installPack(userId: UserID, packId: GuildID): Promise<void> {
		await this.requireExpressionPackAccess(userId);
		const pack = await this.packRepository.getPack(packId);
		if (!pack) {
			throw new UnknownPackError();
		}

		const alreadyInstalled = await this.packRepository.hasInstallation(userId, packId);
		if (alreadyInstalled) {
			return;
		}

		await this.requireFeature(userId, 'feature_global_expressions');
		const user = await this.userRepository.findUnique(userId);
		const fallbackLimit = MAX_INSTALLED_PACKS_NON_PREMIUM;
		const limit = this.resolveLimitForUser(user ?? null, 'max_installed_packs', fallbackLimit);
		const installations = await this.collectInstalledPacks(userId);
		const typeCount = installations.filter((entry) => entry.pack.type === pack.type).length;
		if (typeCount >= limit) {
			throw new MaxPackLimitError(pack.type, limit, 'install');
		}

		await this.packRepository.addInstallation({
			user_id: userId,
			pack_id: packId,
			pack_type: pack.type,
			installed_at: new Date(),
		});
	}

	async uninstallPack(userId: UserID, packId: GuildID): Promise<void> {
		await this.requireExpressionPackAccess(userId);
		await this.packRepository.removeInstallation(userId, packId);
	}

	async getInstalledPackIds(userId: UserID): Promise<Set<GuildID>> {
		await this.requireExpressionPackAccess(userId);
		const installations = await this.collectInstalledPacks(userId);
		return new Set(installations.map((entry) => entry.pack.id));
	}

	async getPackEmojis(params: {
		userId: UserID;
		packId: GuildID;
		requestCache: RequestCache;
	}): Promise<Array<GuildEmojiWithUserResponse>> {
		await this.requireExpressionPackAccess(params.userId);
		const pack = await this.ensurePackOwner(params.userId, params.packId);
		if (pack.type !== 'emoji') {
			throw new InvalidPackTypeError('emoji');
		}

		const emojis = await this.guildRepository.listEmojis(pack.id);
		return await mapGuildEmojisWithUsersToResponse(emojis, this.userCacheService, params.requestCache);
	}

	async getPackStickers(params: {
		userId: UserID;
		packId: GuildID;
		requestCache: RequestCache;
	}): Promise<Array<GuildStickerWithUserResponse>> {
		await this.requireExpressionPackAccess(params.userId);
		const pack = await this.ensurePackOwner(params.userId, params.packId);
		if (pack.type !== 'sticker') {
			throw new InvalidPackTypeError('sticker');
		}

		const stickers = await this.guildRepository.listStickers(pack.id);
		return await mapGuildStickersWithUsersToResponse(stickers, this.userCacheService, params.requestCache);
	}

	async createPackEmoji(params: {
		user: User;
		packId: GuildID;
		name: string;
		image: string;
	}): Promise<GuildEmojiResponse> {
		await this.requireExpressionPackAccess(params.user.id);
		await this.requireFeature(params.user.id, 'feature_global_expressions');
		const pack = await this.ensurePackOwner(params.user.id, params.packId);
		if (pack.type !== 'emoji') {
			throw new InvalidPackTypeError('emoji');
		}

		const emojiCount = await this.guildRepository.countEmojis(pack.id);
		const expressionLimit = this.resolveLimitForUser(params.user, 'max_pack_expressions', MAX_PACK_EXPRESSIONS);
		if (emojiCount >= expressionLimit) {
			throw new MaxPackExpressionsError(expressionLimit);
		}

		const {animated, imageBuffer, contentType} = await this.avatarService.processEmoji({
			errorPath: 'image',
			base64Image: params.image,
		});

		const emojiId = createEmojiID(await this.snowflakeService.generate());
		await this.avatarService.uploadEmoji({
			prefix: 'emojis',
			emojiId,
			imageBuffer,
			contentType,
		});

		const emoji = await this.guildRepository.upsertEmoji({
			guild_id: pack.id,
			emoji_id: emojiId,
			name: params.name,
			creator_id: params.user.id,
			animated,
			version: 1,
		});

		return mapGuildEmojiToResponse(emoji);
	}

	async bulkCreatePackEmojis(params: {
		user: User;
		packId: GuildID;
		emojis: Array<{name: string; image: string}>;
	}): Promise<{success: Array<GuildEmojiResponse>; failed: Array<{name: string; error: string}>}> {
		await this.requireExpressionPackAccess(params.user.id);
		await this.requireFeature(params.user.id, 'feature_global_expressions');
		const pack = await this.ensurePackOwner(params.user.id, params.packId);
		if (pack.type !== 'emoji') {
			throw new InvalidPackTypeError('emoji');
		}

		let emojiCount = await this.guildRepository.countEmojis(pack.id);
		const expressionLimit = this.resolveLimitForUser(params.user, 'max_pack_expressions', MAX_PACK_EXPRESSIONS);
		const success: Array<GuildEmojiResponse> = [];
		const failed: Array<{name: string; error: string}> = [];

		for (const emojiData of params.emojis) {
			if (emojiCount >= expressionLimit) {
				failed.push({name: emojiData.name, error: 'Pack expression limit reached'});
				continue;
			}

			try {
				const {animated, imageBuffer, contentType} = await this.avatarService.processEmoji({
					errorPath: `emojis[${success.length + failed.length}].image`,
					base64Image: emojiData.image,
				});

				const emojiId = createEmojiID(await this.snowflakeService.generate());
				await this.avatarService.uploadEmoji({
					prefix: 'emojis',
					emojiId,
					imageBuffer,
					contentType,
				});

				const emoji = await this.guildRepository.upsertEmoji({
					guild_id: pack.id,
					emoji_id: emojiId,
					name: emojiData.name,
					creator_id: params.user.id,
					animated,
					version: 1,
				});

				success.push(mapGuildEmojiToResponse(emoji));
				emojiCount += 1;
			} catch (error) {
				const message = error instanceof Error ? error.message : 'Unknown error';
				failed.push({name: emojiData.name, error: message});
			}
		}

		return {success, failed};
	}

	async updatePackEmoji(params: {
		userId: UserID;
		packId: GuildID;
		emojiId: EmojiID;
		name: string;
	}): Promise<GuildEmojiResponse> {
		await this.requireExpressionPackAccess(params.userId);
		const pack = await this.ensurePackOwner(params.userId, params.packId);
		if (pack.type !== 'emoji') {
			throw new InvalidPackTypeError('emoji');
		}

		const emoji = await this.guildRepository.getEmoji(params.emojiId, pack.id);
		if (!emoji) {
			throw new UnknownGuildEmojiError();
		}

		const updatedEmoji = await this.guildRepository.upsertEmoji({
			...emoji.toRow(),
			name: params.name,
		});

		return mapGuildEmojiToResponse(updatedEmoji);
	}

	async deletePackEmoji(params: {userId: UserID; packId: GuildID; emojiId: EmojiID; purge?: boolean}): Promise<void> {
		await this.requireExpressionPackAccess(params.userId);
		const pack = await this.ensurePackOwner(params.userId, params.packId);
		if (pack.type !== 'emoji') {
			throw new InvalidPackTypeError('emoji');
		}

		const emoji = await this.guildRepository.getEmoji(params.emojiId, pack.id);
		if (!emoji) {
			throw new UnknownGuildEmojiError();
		}

		await this.guildRepository.deleteEmoji(pack.id, params.emojiId);

		if (params.purge) {
			await this.assetPurger.purgeEmoji(emoji.id.toString());
		}
	}

	async createPackSticker(params: {
		user: User;
		packId: GuildID;
		name: string;
		description?: string | null;
		tags: Array<string>;
		image: string;
	}): Promise<GuildStickerResponse> {
		await this.requireExpressionPackAccess(params.user.id);
		await this.requireFeature(params.user.id, 'feature_global_expressions');
		const pack = await this.ensurePackOwner(params.user.id, params.packId);
		if (pack.type !== 'sticker') {
			throw new InvalidPackTypeError('sticker');
		}

		const stickerCount = await this.guildRepository.countStickers(pack.id);
		const expressionLimit = this.resolveLimitForUser(params.user, 'max_pack_expressions', MAX_PACK_EXPRESSIONS);
		if (stickerCount >= expressionLimit) {
			throw new MaxPackExpressionsError(expressionLimit);
		}

		const {animated, imageBuffer} = await this.avatarService.processSticker({
			errorPath: 'image',
			base64Image: params.image,
		});

		const stickerId = createStickerID(await this.snowflakeService.generate());
		await this.avatarService.uploadSticker({prefix: 'stickers', stickerId, imageBuffer});

		const sticker = await this.guildRepository.upsertSticker({
			guild_id: pack.id,
			sticker_id: stickerId,
			name: params.name,
			description: params.description ?? null,
			tags: params.tags,
			animated,
			creator_id: params.user.id,
			version: 1,
		});

		const response = mapGuildStickerToResponse(sticker);
		getMetricsService().counter({name: 'fluxer.stickers.created', value: 1});
		return response;
	}

	async bulkCreatePackStickers(params: {
		user: User;
		packId: GuildID;
		stickers: Array<{name: string; description?: string | null; tags: Array<string>; image: string}>;
	}): Promise<{success: Array<GuildStickerResponse>; failed: Array<{name: string; error: string}>}> {
		await this.requireExpressionPackAccess(params.user.id);
		await this.requireFeature(params.user.id, 'feature_global_expressions');
		const pack = await this.ensurePackOwner(params.user.id, params.packId);
		if (pack.type !== 'sticker') {
			throw new InvalidPackTypeError('sticker');
		}

		let stickerCount = await this.guildRepository.countStickers(pack.id);
		const expressionLimit = this.resolveLimitForUser(params.user, 'max_pack_expressions', MAX_PACK_EXPRESSIONS);

		const success: Array<GuildStickerResponse> = [];
		const failed: Array<{name: string; error: string}> = [];

		for (const stickerData of params.stickers) {
			if (stickerCount >= expressionLimit) {
				failed.push({name: stickerData.name, error: 'Pack expression limit reached'});
				continue;
			}

			try {
				const {animated, imageBuffer} = await this.avatarService.processSticker({
					errorPath: `stickers[${success.length + failed.length}].image`,
					base64Image: stickerData.image,
				});

				const stickerId = createStickerID(await this.snowflakeService.generate());
				await this.avatarService.uploadSticker({prefix: 'stickers', stickerId, imageBuffer});

				const sticker = await this.guildRepository.upsertSticker({
					guild_id: pack.id,
					sticker_id: stickerId,
					name: stickerData.name,
					description: stickerData.description ?? null,
					tags: stickerData.tags,
					animated,
					creator_id: params.user.id,
					version: 1,
				});

				success.push(mapGuildStickerToResponse(sticker));
				stickerCount += 1;
			} catch (error) {
				const message = error instanceof Error ? error.message : 'Unknown error';
				failed.push({name: stickerData.name, error: message});
			}
		}

		for (const _ of success) {
			getMetricsService().counter({name: 'fluxer.stickers.created', value: 1});
		}

		return {success, failed};
	}

	async updatePackSticker(params: {
		userId: UserID;
		packId: GuildID;
		stickerId: StickerID;
		name: string;
		description?: string | null;
		tags: Array<string>;
	}): Promise<GuildStickerResponse> {
		await this.requireExpressionPackAccess(params.userId);
		const pack = await this.ensurePackOwner(params.userId, params.packId);
		if (pack.type !== 'sticker') {
			throw new InvalidPackTypeError('sticker');
		}

		const sticker = await this.guildRepository.getSticker(params.stickerId, pack.id);
		if (!sticker) {
			throw new UnknownGuildStickerError();
		}

		const updatedSticker = await this.guildRepository.upsertSticker({
			...sticker.toRow(),
			name: params.name,
			description: params.description ?? null,
			tags: params.tags,
		});

		return mapGuildStickerToResponse(updatedSticker);
	}

	async deletePackSticker(params: {
		userId: UserID;
		packId: GuildID;
		stickerId: StickerID;
		purge?: boolean;
	}): Promise<void> {
		await this.requireExpressionPackAccess(params.userId);
		const pack = await this.ensurePackOwner(params.userId, params.packId);
		if (pack.type !== 'sticker') {
			throw new InvalidPackTypeError('sticker');
		}

		const sticker = await this.guildRepository.getSticker(params.stickerId, pack.id);
		if (!sticker) {
			throw new UnknownGuildStickerError();
		}

		await this.guildRepository.deleteSticker(pack.id, params.stickerId);

		if (params.purge) {
			await this.assetPurger.purgeSticker(sticker.id.toString());
		}

		getMetricsService().counter({name: 'fluxer.stickers.deleted', value: 1});
	}

	private resolveLimitForUser(user: User | null, key: LimitKey, fallback: number): number {
		const ctx = createLimitMatchContext({user});
		return resolveLimitSafe(this.limitConfigService.getConfigSnapshot(), ctx, key, fallback);
	}
}
