import { Elysia } from "elysia";
import { authPlugin } from "../../authPlugin";
import { loginEnabled } from "$lib/server/auth";
import { collections } from "$lib/server/database";
import { authCondition } from "$lib/server/auth";
import { config } from "$lib/server/config";
import yazl from "yazl";
import { downloadFile } from "$lib/server/files/downloadFile";
import mimeTypes from "mime-types";
import { logger } from "$lib/server/logger";

export interface FeatureFlags {
	enableAssistants: boolean;
	loginEnabled: boolean;
	isAdmin: boolean;
	transcriptionEnabled: boolean;
}

export const misc = new Elysia()
	.use(authPlugin)
	.get("/public-config", async () => config.getPublicConfig())
	.get("/feature-flags", async ({ locals }) => {
		return {
			enableAssistants: config.ENABLE_ASSISTANTS === "true",
			loginEnabled, // login feature is on when OID is configured
			isAdmin: locals.isAdmin,
			transcriptionEnabled: !!config.get("TRANSCRIPTION_MODEL"),
		} satisfies FeatureFlags;
	})
	.get("/export", async ({ locals }) => {
		if (!locals.user) {
			throw new Error("Not logged in");
		}

		if (!locals.isAdmin) {
			throw new Error("Not admin");
		}

		if (config.ENABLE_DATA_EXPORT !== "true") {
			throw new Error("Data export is not enabled");
		}

		const nExports = await collections.messageEvents.countDocuments({
			userId: locals.user._id,
			type: "export",
			expiresAt: { $gt: new Date() },
		});

		if (nExports >= 1) {
			throw new Error(
				"You have already exported your data recently. Please wait 1 hour before exporting again."
			);
		}

		const stats: {
			nConversations: number;
			nMessages: number;
			nFiles: number;
			nAssistants: number;
			nAvatars: number;
		} = {
			nConversations: 0,
			nMessages: 0,
			nFiles: 0,
			nAssistants: 0,
			nAvatars: 0,
		};

		const zipfile = new yazl.ZipFile();

		const promises = [
			collections.conversations
				.find({ ...authCondition(locals) })
				.toArray()
				.then(async (conversations) => {
					const formattedConversations = await Promise.all(
						conversations.map(async (conversation) => {
							stats.nConversations++;
							const hashes: string[] = [];
							conversation.messages.forEach(async (message) => {
								stats.nMessages++;
								if (message.files) {
									message.files.forEach((file) => {
										hashes.push(file.value);
									});
								}
							});
							const files = await Promise.all(
								hashes.map(async (hash) => {
									try {
										const fileData = await downloadFile(hash, conversation._id);
										return fileData;
									} catch {
										return null;
									}
								})
							);

							const filenames: string[] = [];
							files.forEach((file) => {
								if (!file) return;

								const extension = mimeTypes.extension(file.mime) || null;
								const convId = conversation._id.toString();
								const fileId = file.name.split("-")[1].slice(0, 8);
								const fileName = `file-${convId}-${fileId}` + (extension ? `.${extension}` : "");
								filenames.push(fileName);
								zipfile.addBuffer(Buffer.from(file.value, "base64"), fileName);
								stats.nFiles++;
							});

							return {
								...conversation,
								messages: conversation.messages.map((message) => {
									return {
										...message,
										files: filenames,
										updates: undefined,
									};
								}),
							};
						})
					);

					zipfile.addBuffer(
						Buffer.from(JSON.stringify(formattedConversations, null, 2)),
						"conversations.json"
					);
				}),
			collections.assistants
				.find({ createdById: locals.user._id })
				.toArray()
				.then(async (assistants) => {
					const formattedAssistants = await Promise.all(
						assistants.map(async (assistant) => {
							if (assistant.avatar) {
								const fileId = collections.bucket.find({ filename: assistant._id.toString() });

								const content = await fileId.next().then(async (file) => {
									if (!file?._id) return;

									const fileStream = collections.bucket.openDownloadStream(file?._id);

									const fileBuffer = await new Promise<Buffer>((resolve, reject) => {
										const chunks: Uint8Array[] = [];
										fileStream.on("data", (chunk) => chunks.push(chunk));
										fileStream.on("error", reject);
										fileStream.on("end", () => resolve(Buffer.concat(chunks)));
									});

									return fileBuffer;
								});

								if (!content) return;

								zipfile.addBuffer(content, `avatar-${assistant._id.toString()}.jpg`);
								stats.nAvatars++;
							}

							stats.nAssistants++;

							return {
								_id: assistant._id.toString(),
								name: assistant.name,
								createdById: assistant.createdById.toString(),
								createdByName: assistant.createdByName,
								avatar: `avatar-${assistant._id.toString()}.jpg`,
								modelId: assistant.modelId,
								preprompt: assistant.preprompt,
								description: assistant.description,
								dynamicPrompt: assistant.dynamicPrompt,
								exampleInputs: assistant.exampleInputs,
								generateSettings: assistant.generateSettings,
								createdAt: assistant.createdAt.toISOString(),
								updatedAt: assistant.updatedAt.toISOString(),
							};
						})
					);

					zipfile.addBuffer(
						Buffer.from(JSON.stringify(formattedAssistants, null, 2)),
						"assistants.json"
					);
				}),
		];

		Promise.all(promises).then(async () => {
			logger.info(
				{
					userId: locals.user?._id,
					...stats,
				},
				"Exported user data"
			);
			zipfile.end();
			if (locals.user?._id) {
				await collections.messageEvents.insertOne({
					userId: locals.user?._id,
					type: "export",
					createdAt: new Date(),
					expiresAt: new Date(Date.now() + 1000 * 60 * 60), // 1 hour
				});
			}
		});

		// @ts-expect-error - zipfile.outputStream is not typed correctly
		return new Response(zipfile.outputStream, {
			headers: {
				"Content-Type": "application/zip",
				"Content-Disposition": 'attachment; filename="export.zip"',
			},
		});
	});
