import path from "node:path";
import * as p from "@clack/prompts";
import { execa } from "execa";
import fse from "fs-extra";
import c from "picocolors";
import {
	exitPrompt,
	getModulePaths,
	isPathname,
	normalizePath,
	wait,
} from "../utils/index.js";

/** @param {Context} ctx */
export async function createProject(ctx) {
	let { args, dryRun, initGitRepo, installDeps, template } = ctx;

	const s = p.spinner();
	let cwd = process.cwd();

	// 1. Set up project directory
	const project = await getProjectDetails(args[0], { cwd });
	if (dryRun) {
		await wait(1);
	} else {
		await fse.ensureDir(project.pathname);
	}

	// 2. Create the damned thing
	cwd = project.pathname;
	const relativePath = path.relative(process.cwd(), project.pathname);
	s.start(
		`${c.yellow(`Creating a new Astro-GhostCMS project in ${relativePath}`)}`,
	);
	if (dryRun) {
		await wait(2000);
	} else {
		await createApp(project.name, project.pathname, template, {
			onError(error) {
				s.stop(`${c.red("Failed to create new project")}`);
				p.cancel();
				console.error(error);
				process.exit(1);
			},
		});
	}
	s.stop(
		`${c.green("New Astro-GhostCMS project")} '${project.name}' ${c.green(
			"created",
		)} 🚀`,
	);
	const fCheck = await p.group(
		{
			installDeps: () =>
				p.confirm({
					message: `${c.cyan("Install dependencies? (Recommended)")}`,
					initialValue: true,
				}),
			//GitRepo: () => p.confirm({
			//	message: `${c.cyan('Initialize a Git repository?')} ${c.italic(c.gray("( Tip: This Option gets Stuck Press Enter Twice if you get no reponse! )"))}`,
			//	initialValue: false,
			//}),
			readyCheck: () =>
				p.confirm({
					message: `${c.bgYellow(
						c.black(
							c.bold(
								" CONFIRM: Press Enter Twice to continue or `Ctrl+C` to Cancel. ",
							),
						),
					)}`,
					initialValue: true,
				}),
		},
		{
			onCancel: () => {
				exitPrompt();
			},
		},
	);

	if (fCheck.readyCheck) {
		// 3. Initialize git repo
		if (initGitRepo) {
			if (dryRun) {
				await wait(1);
			} else {
				await exec("git", ["init"], { cwd });
			}
			p.log.success(c.green("Initialized Git repository"));
		} else {
			p.log.info(`${c.gray("Skipped Git initialization")}`);
		}

		const nextSteps = `If you didnt opt to install Dependencies dont forget to run: \n ${c.yellow(
			"npm install",
		)} / ${c.yellow("pnpm install")} / ${c.yellow(
			"yarn install",
		)} inside your project directory! \n \n ${c.bgYellow(
			c.black(
				c.bold(
					" Dont forget to modify your .env file for YOUR ghost install! ",
				),
			),
		)} `;

		// 4. Install dependencies
		installDeps = installDeps ?? fCheck.installDeps;
		const pm = ctx.pkgManager ?? "pnpm";
		if (installDeps) {
			s.start(`${c.cyan(`Installing dependencies with ${pm}`)} `);
			if (dryRun) {
				await wait(1);
			} else {
				await installDependencies(pm, { cwd });
			}
			s.stop(`${c.green(`Dependencies installed with ${pm}`)}`);
			success();
		} else {
			p.log.info(`${c.gray("Skipped dependency installation")}`);
			success();
		}

		async function success() {
			p.note(nextSteps);
			p.outro(c.green("Deployment Complete!"));
		}
	} else {
		exitPrompt();
	}
}

/**
 *
 * @param {string} projectName
 * @param {string} projectPathname
 * @param {string} template
 * @param {{ onError: (err: unknown) => any }} opts
 */
async function createApp(projectName, projectPathname, template, { onError }) {
	const { pathname } = getModulePaths(import.meta.url);
	const templatesDir = path.resolve(pathname, "..", "..", "templates");
	const sharedTemplateDir = path.join(templatesDir, "_shared");
	const runnerTemplateDir = path.join(templatesDir, template);

	await fse.ensureDir(projectPathname);

	// TODO: Detect if project directory is empty, otherwise we
	// can't create a new project here.
	await fse.copy(runnerTemplateDir, projectPathname);

	// Copy misc files from shared
	const filesToCopy = [
		{
			src: path.join(sharedTemplateDir, ".env"),
			dest: path.join(projectPathname, ".env"),
		},
	];
	await Promise.all(
		filesToCopy.map(async ({ src, dest }) => await fse.copy(src, dest)),
	);

	/** @type {Array<{ pathname: string; getUpdates: (contents: string) => string }>} */
	const filesToUpdate = [
		{
			pathname: path.join(projectPathname, "package.json"),
			getUpdates: updateProjectName,
		},
	];
	await Promise.all(
		filesToUpdate.map(async ({ pathname, getUpdates }) => {
			const contents = await fse.readFile(pathname, "utf-8");
			const updatedContents = getUpdates(contents);
			await fse.writeFile(pathname, updatedContents, "utf-8");
		}),
	);

	/** @param {string} contents */
	function updateProjectName(contents) {
		return contents.replace(/{{PROJECT_NAME}}/g, projectName);
	}
}

/**
 * @param {string|undefined} projectNameInput
 * @param {{ cwd: string }} opts
 */
async function getProjectDetails(projectNameInput, opts) {
	let projectName = projectNameInput;
	if (!projectName) {
		const defaultProjectName = "my-astro-ghost";
		let answer = await p.text({
			message: `${c.cyan("Where would you like to create your project?")}`,
			placeholder: `.${path.sep}${defaultProjectName}`,
		});
		if (p.isCancel(answer)) exitPrompt();

		answer = answer?.trim();
		projectName = answer || defaultProjectName;
	}

	/** @type {string} */
	let pathname;

	if (isPathname(projectName)) {
		const dir = path.resolve(
			opts.cwd,
			path.dirname(normalizePath(projectName)),
		);
		projectName = toValidProjectName(path.basename(projectName));
		pathname = path.join(dir, projectName);
	} else {
		projectName = toValidProjectName(projectName);
		pathname = path.resolve(opts.cwd, projectName);
	}

	return {
		name: projectName,
		pathname,
	};
}

/**
 * @param {string} command
 * @param {readonly string[]} args
 * @param {{ cwd: string }} options
 * @returns {Promise<{ code: number | null; signal: NodeJS.Signals | null }>}
 */
async function exec(command, args, options) {
	const installExec = execa(command, ["init"], { ...options, stdio: "ignore" });
	return new Promise((resolve, reject) => {
		installExec.on("error", (error) => reject(error));
		installExec.on("close", (code, signal) => resolve({ code, signal }));
	});
}

/**
 * @param {"npm" | "yarn" | "pnpm"} packageManager
 * @param {{ cwd: string }} opts
 * @returns
 */
async function installDependencies(packageManager, { cwd }) {
	const installExec = execa(packageManager, ["install"], { cwd });
	return new Promise((resolve, reject) => {
		installExec.on("error", (error) => reject(error));
		installExec.on("close", () => resolve());
	});
}

/**
 * @param {string} projectName
 */
function toValidProjectName(projectName) {
	if (isValidProjectName(projectName)) {
		return projectName;
	}
	return projectName
		.trim()
		.toLowerCase()
		.replace(/\s+/g, "-")
		.replace(/^[._]/, "")
		.replace(/[^a-z\d\-~]+/g, "-")
		.replace(/^-+/, "")
		.replace(/-+$/, "");
}

/**
 * @param {string} projectName
 */
function isValidProjectName(projectName) {
	return /^(?:@[a-z\d\-*~][a-z\d\-*._~]*\/)?[a-z\d\-~][a-z\d\-._~]*$/.test(
		projectName,
	);
}

/**
 * @typedef {import("../types.js").Template} Template
 * @typedef {import("../types.js").PackageManager} PackageManager
 * @typedef {import("../types.js").Context} Context
 * @typedef {import("../types.js").Serializable} Serializable
 */
