import type { BuildConfig, BunPlugin, PluginBuilder } from "bun";
import type { FileImporter } from "sass";
import { type Config, type Entrypoint, EntrypointType, LogType } from "./types";

import { basename, join, normalize } from "node:path";
import { fileURLToPath } from "node:url";

import { rm } from "node:fs/promises";
import slug from "slug";

// This is used to prevent creating folders with external packages names
slug.extend({ "/": "-" });

const nodeModuleImporter: FileImporter<"async"> = {
	findFileUrl(url) {
		if (url.startsWith("@")) {
			return new URL(import.meta.resolve(url));
		}

		return null;
	},
};

const sassPlugin: BunPlugin = {
	name: "Sass Loader",
	async setup(build: PluginBuilder) {
		const sass = await import("sass");

		build.onLoad({ filter: /\.scss$/ }, async ({ path }) => {
			const result = await sass.compileAsync(path, {
				importers: [nodeModuleImporter],
			});

			return {
				loader: "css",
				contents: result.css,
			};
		});
	},
};

function log(logtype: LogType, msg: string) {
	const reset = "\x1b[0m";
	let color = Bun.color("white", "ansi");

	switch (logtype) {
		case LogType.Success:
			color = Bun.color("green", "ansi");
			break;
		case LogType.Error:
			color = Bun.color("#f24444", "ansi");
			break;

		default:
			break;
	}

	if (!color) {
		color = reset;
	}

	console.log(color + msg + reset);
}

// Packages needs to exist in the package.json file for this to work
function resolvePackage(pkg: string): Entrypoint {
	const path = normalize(fileURLToPath(import.meta.resolve(pkg)));
	const file = Bun.file(import.meta.resolve(pkg));
	const mimetype = file.type.split(";").at(0);

	let type: EntrypointType;

	switch (mimetype) {
		case "text/x-scss":
			type = EntrypointType.Sass;
			break;
		case "text/javascript":
			type = EntrypointType.Js;
			break;
		default:
			throw new Error(`No loader found for type ${mimetype} at path ${path}`);
	}

	return {
		path,
		type,
	};
}

export default {
	log,

	build: async (config: Config, entrypoints: Entrypoint[]) => {
		// Resolve external packages

		const external_cache: string[] = [];
		const resolved_entrypoints = entrypoints.map((item) => {
			if (item.type !== EntrypointType.Package) {
				return item;
			}

			external_cache.push(item.path);
			return resolvePackage(item.path);
		});

		const baseConfig = {
			minify: config.production,
			outdir: `${config.outdir}/js`,
			packages: "external",
			root: config.root,
			splitting: config.production,
		};

		// Apply build config per type
		const loaders = {
			assetLoader: {
				entrypoints: resolved_entrypoints
					.filter((entry) => entry.type === EntrypointType.Asset)
					.map((entry) => entry.path),
				outdir: `${config.outdir}/asset`,
			},
			stylesLoader: {
				...baseConfig,
				entrypoints: resolved_entrypoints
					.filter((entry) =>
						[EntrypointType.Css, EntrypointType.Sass].includes(entry.type),
					)
					.map((entry) => entry.path),
				outdir: `${config.outdir}/css`,
				plugins: [sassPlugin],
			},
			jsLoader: {
				...baseConfig,
				entrypoints: resolved_entrypoints
					.filter((entry) => entry.type === EntrypointType.Js)
					.map((entry) => entry.path),
				target: "browser",
			},
		};

		// Transform into a list to later use Promise.all
		const assets = Object.values(loaders).filter(
			(item) => item.entrypoints.length !== 0,
		);

		log(LogType.Info, "Building assets...");

		const out = await Promise.all(
			assets.map(async (item) => {
				const result = await Bun.build(item as BuildConfig);

				if (!result.success) {
					throw new AggregateError(result.logs, "Build failed");
				}

				// Normalize external packages folder structure
				for (const out of result.outputs) {
					if (!out.path.includes("node_modules")) {
						continue;
					}

					let package_name = external_cache.find((pkg) =>
						out.path.includes(pkg),
					);

					if (!package_name) {
						throw new Error(
							`Could not normilize path for external package: ${out.path}`,
						);
					}

					package_name = slug(package_name);

					if (!package_name) {
						throw new Error(
							`Could not normilize path for external package: ${out.path}`,
						);
					}

					const new_path = join(
						config.outdir,
						"pkgs",
						package_name,
						basename(out.path),
					);

					await Bun.write(new_path, out);
				}

				return result;
			}),
		);

		if (out.some((item) => !item.success)) {
			throw new Error(`Some entrypoint failed to build: ${out}`);
		}

		await rm(join(config.outdir, "node_modules"), {
			recursive: true,
			force: true,
		});

		log(LogType.Success, "Complete!");
	},
};
