import { exec } from 'child_process';
import * as semver from 'semver';

const changeTypes = ['major', 'minor', 'patch'] as const;

const validateChangeType = (maybeChangeType: string = 'minor') => {
	maybeChangeType = maybeChangeType.toLowerCase();
	switch (maybeChangeType) {
		case 'patch':
		case 'minor':
		case 'major':
			return maybeChangeType;
		default:
			throw new Error(`Invalid change type: '${maybeChangeType}'`);
	}
};

const compareSemverChangeType = (oldVersion: string, newVersion: string) => {
	const oldSemver = semver.parse(oldVersion)!;
	const newSemver = semver.parse(newVersion)!;

	for (const changeType of changeTypes) {
		if (oldSemver[changeType] !== newSemver[changeType]) {
			return changeType;
		}
	}
};

const run = async (cmd: string) => {
	console.info(`Running '${cmd}'`);
	return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
		const p = exec(cmd, { encoding: 'utf8' }, (err, stdout, stderr) => {
			if (err) {
				reject(err);
				return;
			}
			resolve({ stdout, stderr });
		});
		p.stdout?.pipe(process.stdout);
		p.stderr?.pipe(process.stderr);
	});
};

const getVersion = async (module: string): Promise<string> => {
	const { stdout } = await run(`npm ls --json --depth 0 ${module}`);
	return JSON.parse(stdout).dependencies[module].version;
};

interface Upstream {
	repo: string;
	url: string;
	module?: string;
}

const getUpstreams = async () => {
	const fs = await import('fs');
	const repoYaml = fs.readFileSync(__dirname + '/../repo.yml', 'utf8');

	const yaml = await import('js-yaml');
	const { upstream } = yaml.load(repoYaml) as {
		upstream: Upstream[];
	};

	return upstream;
};

const getUsage = (upstreams: Upstream[], upstreamName: string) => `
Usage: npm run update ${upstreamName} $version [$changeType=minor]

Upstream names: ${upstreams.map(({ repo }) => repo).join(', ')}
`;

async function $main() {
	const upstreams = await getUpstreams();

	if (process.argv.length < 3) {
		throw new Error(getUsage(upstreams, '$upstreamName'));
	}

	const upstreamName = process.argv[2];

	const upstream = upstreams.find((v) => v.repo === upstreamName);

	if (!upstream) {
		throw new Error(
			`Invalid upstream name '${upstreamName}', valid options: ${upstreams
				.map(({ repo }) => repo)
				.join(', ')}`,
		);
	}

	if (process.argv.length < 4) {
		throw new Error(getUsage(upstreams, upstreamName));
	}

	const packageName = upstream.module || upstream.repo;

	const oldVersion = await getVersion(packageName);
	await run(`npm install ${packageName}@${process.argv[3]}`);
	const newVersion = await getVersion(packageName);
	if (newVersion === oldVersion) {
		throw new Error(`Already on version '${newVersion}'`);
	}

	console.log(`Updated ${upstreamName} from ${oldVersion} to ${newVersion}`);
	const semverChangeType = compareSemverChangeType(oldVersion, newVersion);

	const changeType = process.argv[4]
		? // if the caller specified a change type, use that one
		  validateChangeType(process.argv[4])
		: // use the same change type as in the dependency, but avoid major bumps
		semverChangeType && semverChangeType !== 'major'
		? semverChangeType
		: 'minor';
	console.log(`Using Change-type: ${changeType}`);

	let { stdout: currentBranch } = await run('git rev-parse --abbrev-ref HEAD');
	currentBranch = currentBranch.trim();
	console.log(`Currenty on branch: '${currentBranch}'`);
	if (currentBranch === 'master') {
		await run(`git checkout -b "update-${upstreamName}-${newVersion}"`);
	}

	await run(`git add package.json npm-shrinkwrap.json`);
	await run(
		`git commit --message "Update ${upstreamName} to ${newVersion}" --message "Update ${upstreamName} from ${oldVersion} to ${newVersion}" --message "Change-type: ${changeType}"`,
	);
}

async function main() {
	try {
		await $main();
	} catch (e) {
		console.error(e);
		process.exitCode = 1;
	}
}

// eslint-disable-next-line @typescript-eslint/no-floating-promises
main();