import fs from "node:fs";
import fsp from "node:fs/promises";
import path from "node:path";
import archiver from "archiver";
import { format } from "date-fns";
import { createLogger } from "../logger/logger.controller.js";
import { emitBuildLog } from "./build.utils.js";
import { updateAppStats } from "./updateAppStats.utils.js";

const log = createLogger({ module: "utils", subModule: "zip" });

const exists = async (target: string) => {
	try {
		await fsp.access(target);
		return true;
	} catch {
		return false;
	}
};

const getNextBuildNumber = async (versionPath: string) => {
	const raw = await fsp.readFile(versionPath, "utf8");
	const config = JSON.parse(raw);
	const current = Number.parseInt(config.build.trim(), 10);

	let nextBuild: string;
	if (Number.isNaN(current) || current < 1) {
		nextBuild = "1";
	} else {
		nextBuild = String(current + 1); // Incrementing the build number
	}

	const updatedConfig = {
		...config,
		build: nextBuild,
		lastBuildDate: format(new Date(Date.now()), "M/d/yyyy HH:mm"),
	};

	await fsp.writeFile(
		versionPath,
		JSON.stringify(updatedConfig, null, 4) + "\n",
		"utf8",
	);

	return { version: config.version, build: config.build };
};

const cleanupOldBuilds = async (buildFolder: string, maxBuilds: number) => {
	const entries = await fsp.readdir(buildFolder, { withFileTypes: true });

	const zipFiles: { fullPath: string; name: string; mtimeMs: number }[] = [];

	for (const entry of entries) {
		if (!entry.isFile()) continue;
		//if (!/^LSTV3-\d+\.zip$/i.test(entry.name)) continue;
		if (!entry.name.includes("LSTV3")) continue;

		const fullPath = path.join(buildFolder, entry.name);
		const stat = await fsp.stat(fullPath);

		zipFiles.push({
			fullPath,
			name: entry.name,
			mtimeMs: stat.mtimeMs,
		});
	}

	zipFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);

	const toRemove = zipFiles.slice(maxBuilds);

	for (const file of toRemove) {
		await fsp.rm(file.fullPath, { force: true });
		emitBuildLog(`Removed old build: ${file.name}`);
	}
};

export const zipBuild = async () => {
	const appDir = process.env.DEV_DIR ?? "";
	const maxBuilds = Number(process.env.MAX_BUILDS ?? 5);

	if (!appDir) {
		log.error({ notify: true }, "Forgot to add in the dev dir into the env");
		return;
	}

	const includesFile = path.join(appDir, ".includes");
	const version = path.join(appDir, "package.json");
	const buildFolder = path.join(appDir, "builds");
	const tempFolder = path.join(appDir, "temp", "zip-temp");
	if (!(await exists(includesFile))) {
		log.error({ notify: true }, "Missing .includes file common");
		return;
	}

	await fsp.mkdir(buildFolder, { recursive: true });

	const lstVersion = await getNextBuildNumber(version);
	const zipFileName = `LSTV3-${lstVersion.version}.${lstVersion.build}.zip`;
	const zipFile = path.join(buildFolder, zipFileName);
	// make the folders in case they are not created already
	emitBuildLog(
		`Using version, build number: ${lstVersion.version}.${lstVersion.build}`,
	);

	if (await exists(tempFolder)) {
		await fsp.rm(tempFolder, { recursive: true, force: true });
	}

	await fsp.mkdir(tempFolder, { recursive: true });

	const includes = (await fsp.readFile(includesFile, "utf8"))
		.split(/\r?\n/)
		.map((line) => line.trim())
		.filter(Boolean);

	emitBuildLog(`Preparing zip from ${includes.length} include entries`);

	for (const relPath of includes) {
		const source = path.join(appDir, relPath);
		const dest = path.join(tempFolder, relPath);

		if (!(await exists(source))) {
			emitBuildLog(`Skipping missing path: ${relPath}`, "error");
			continue;
		}

		const stat = await fsp.stat(source);
		await fsp.mkdir(path.dirname(dest), { recursive: true });

		if (stat.isDirectory()) {
			emitBuildLog(`Copying folder: ${relPath}`);
			await fsp.cp(source, dest, { recursive: true });
		} else {
			emitBuildLog(`Copying file: ${relPath}`);
			await fsp.copyFile(source, dest);
		}
	}

	// if something crazy happens and we get the same build lets just reuse it
	// if (await exists(zipFile)) {
	// 	await fsp.rm(zipFile, { force: true });
	// }

	emitBuildLog(`Creating zip: ${zipFile}`);

	await new Promise<void>((resolve, reject) => {
		const output = fs.createWriteStream(zipFile);
		const archive = archiver("zip", { zlib: { level: 9 } });

		output.on("close", () => resolve());
		output.on("error", reject);
		archive.on("error", reject);

		archive.pipe(output);

		// zip contents of temp folder, not temp folder itself
		archive.directory(tempFolder, false);
		archive.finalize();
	});

	await fsp.rm(tempFolder, { recursive: true, force: true });

	emitBuildLog(`Zip completed successfully: ${zipFile}`);

	await cleanupOldBuilds(buildFolder, maxBuilds);

	await updateAppStats({
		lastUpdated: new Date(),
		building: false,
		currentBuild: lstVersion.build,
	});

	return {
		success: true,
		build: lstVersion.build,
		zipFile,
		zipFileName,
	};
};
