import { fromAsync, isOk, isErr } from '@sapphire/result';
import { blueBright, red } from 'colorette';
import { execa } from 'execa';
import { cp, readFile, rm, writeFile } from 'node:fs/promises';
import prompts from 'prompts';
import { PromptNew, PromptNewObjectKey } from '#prompts/PromptNew';
import { Spinner } from '#functions/Spinner';
import { fileExists } from '#functions/FileExists';
import { repoUrl } from '#constants';
import { CommandExists } from '#functions/CommandExists';

async function editPackageJson(
	location: string,
	name: string,
	description: string,
	version: string,
	heroku: boolean
) {
	const pjLocation = `${location}/package.json`;
	const output = JSON.parse(await readFile(pjLocation, 'utf8'));
	if (!output) throw new Error("Can't read file.");

	output.name = name.toLowerCase();
	output.description = description;
	output.version = version;

	if (heroku)
		output.scripts.heroku = `git add . && git commit -m \"Update bot\" && git push heroku main`;

	const result = await fromAsync(() =>
		writeFile(pjLocation, JSON.stringify(output, null, 2))
	);

	return isOk(result);
}

async function cloneRepo(location: string, verbose: boolean) {
	const result = await fromAsync(async () =>
		execa('git', ['clone', repoUrl, `${location}/ghr`], {
			stdio: verbose ? 'inherit' : undefined,
		})
	);

	if (isErr(result)) {
		throw result.error;
	}

	if (result.value.exitCode !== 0) {
		throw new Error(
			'An unknown error occurred while cloning the repository. Try running BotKit CLI with "--verbose" flag.'
		);
	}
}

async function initializeGitRepo(location: string) {
	await execa('git', ['init'], { cwd: `./${location}/` });

	return true;
}

async function herokuLogin() {
	const result = await fromAsync(() =>
		execa('heroku', ['login'], {
			stdin: 'inherit',
		})
	);

	if (isErr(result)) {
		throw result.error;
	}

	if (result.value.exitCode !== 0) {
		throw result.error;
	}

	return true;
}

async function initializeHeroku(location: string, verbose: boolean) {
	const result = await fromAsync(() =>
		execa('heroku', ['create'], {
			stdio: verbose ? 'inherit' : undefined,
			cwd: `./${location}/`,
		})
	);

	if (isErr(result)) {
		throw result.error;
	}

	if (result.value.exitCode !== 0) {
		throw new Error(
			'An unknown error occurred while creating Heroku app. Try running BotKit CLI with "--verbose" flag.'
		);
	}

	return true;
}

async function pushToHeroku(location: string, verbose: boolean) {
	await execa('git', ['add', '.'], { cwd: `./${location}/` });

	await execa('git', ['commit', '-m', '"Initial commit"'], {
		cwd: `./${location}/`,
	});

	const result = await fromAsync(async () =>
		execa('git', ['push', 'heroku', 'main'], {
			stdio: verbose ? 'inherit' : undefined,
			cwd: `./${location}/`,
		})
	);

	if (isErr(result)) {
		throw result.error;
	}

	if (result.value.exitCode !== 0) {
		throw new Error(
			'An unknown error occurred while pushing to Heroku. Try running BotKit CLI with "--verbose" flag.'
		);
	}

	return true;
}

async function configureHeroku(location: string, verbose: boolean) {
	const result = await fromAsync(async () =>
		execa('heroku', ['ps:scale', 'web=0', 'worker=1'], {
			stdio: verbose ? 'inherit' : undefined,
			cwd: `./${location}/`,
		})
	);

	if (isErr(result)) {
		throw result.error;
	}

	if (result.value.exitCode !== 0) {
		throw new Error(
			'An unknown error occurred while configuring Heroku. Try running BotKit CLI with "--verbose" flag.'
		);
	}

	return true;
}

async function installDeps(location: string, verbose: boolean) {
	const result = await fromAsync(() =>
		execa('npm', ['install'], {
			stdio: verbose ? 'inherit' : undefined,
			cwd: `./${location}/`,
		})
	);

	if (isErr(result)) {
		throw result.error;
	}

	if (result.value.exitCode !== 0) {
		throw new Error(
			'An unknown error occurred while installing dependencies. Try running BotKit CLI with "--verbose" flag.'
		);
	}

	const oppositeLockfile = `./${location}/package-lock.json`;

	if (await fileExists(oppositeLockfile)) {
		await rm(oppositeLockfile);
	}

	return true;
}

async function runJob(
	job: () => Promise<any>,
	name: string,
	showError: boolean = true
) {
	const spinner = new Spinner(name).start();

	const result = await fromAsync(async () => job());

	if (isErr(result)) {
		spinner.error();
		if (showError) {
			console.error(red((result.error as Error).message));
		}
		process.exit(1);
	}

	spinner.success();
	return true;
}

export default async (name: string, flags: Record<string, boolean>) => {
	const response = await prompts<PromptNewObjectKey>(
		PromptNew(name, await CommandExists('heroku'))
	);

	if (!response.botName || !response.botStructure || !response.commands) {
		process.exit(1);
	}

	let botTemplate = '';

	if (response.botStructure === 'basic') {
		if (response.commands === 'slashCmd') {
			botTemplate = 'with-basic-slash-commands';
		} else if (response.commands === 'prefixCmd') {
			botTemplate = 'with-basic-prefix-commands';
		} else if (response.commands === 'bothCmd') {
			botTemplate = 'with-basic-both-commands';
		}
	} else if (response.botStructure === 'advanced') {
		if (response.commands === 'slashCmd') {
			botTemplate = 'with-advanced-slash-commands';
		} else if (response.commands === 'prefixCmd') {
			botTemplate = 'with-advanced-prefix-commands';
		} else if (response.commands === 'bothCmd') {
			botTemplate = 'with-advanced-both-commands';
		}
	}

	if (response.heroku) response.git = true;

	const botName = response.botName === '.' ? '' : response.botName;

	const stpJob = async () => {
		await cp(
			`./${response.botName}/ghr/templates/${response.botStructure}/${botTemplate}/.`,
			`./${response.botName}/`,
			{ recursive: true }
		);

		if (response.git && !response.heroku) {
			await cp(
				`./${response.botName}/ghr/.gitignore`,
				`./${response.botName}/.gitignore`
			);
		}

		if (response.heroku) {
			for (const p of ['.gitignore', 'Procfile', 'HEROKU.md']) {
				await cp(
					`./${response.botName}/ghr/resources/with-heroku/${p}`,
					`./${response.botName}/${p}`
				);
			}
		}

		await rm(`./${response.botName}/ghr`, { recursive: true, force: true });

		await editPackageJson(
			response.botName,
			botName,
			response.botDescription,
			response.botVersion,
			response.heroku
		);
	};

	const jobs: [() => any, string, boolean?][] = [];

	if (response.heroku) {
		jobs.push([
			() => herokuLogin(),
			'Logging in to Heroku, press any key to open the browser, or q to quit',
			false,
		]);
	}

	jobs.push([
		() => cloneRepo(response.botName, flags.verbose),
		'Cloning repository',
	]);
	jobs.push([stpJob, 'Setting up project']);

	if (response.git) {
		jobs.push([() => initializeGitRepo(response.botName), 'Initializing git']);
	}

	if (response.heroku) {
		jobs.push([
			() => initializeHeroku(response.botName, flags.verbose),
			'Initializing Heroku',
		]);

		jobs.push([
			() => pushToHeroku(response.botName, flags.verbose),
			'Pushing to Heroku',
		]);

		jobs.push([
			() => configureHeroku(response.botName, flags.verbose),
			'Configuring Heroku',
		]);
	}

	jobs.push([
		() => installDeps(response.botName, flags.verbose),
		'Installing dependencies',
	]);

	for (const [job, name, showError] of jobs) {
		await runJob(job, name, showError).catch(() => process.exit(1));
	}

	if (response.heroku) {
		const { stdout } = await execa('heroku', ['info'], {
			cwd: `./${response.botName}/`,
		});
		console.log(`Information about your bot in Heroku:\n${stdout}`);
	}

	console.log(blueBright('Done!'));
};
