#!/usr/bin/env node
/*
 * JQuery Migrate Plugin Release Management
 */

"use strict";

// Debugging variables
var	dryrun = false,
	skipRemote = false;

import fs from "fs";
import child from "child_process";
import path from "path";
import chalk from "chalk";
import enquirer from "enquirer";

var releaseVersion,
	nextVersion,
	isBeta,
	pkg,

	prompt = enquirer.prompt,

	repoURL = "git@github.com:jquery/jquery-migrate.git",
	branch = "main",

	// Windows needs the .cmd version but will find the non-.cmd
	// On Windows, also ensure the HOME environment variable is set
	gruntCmd = process.platform === "win32" ? "grunt.cmd" : "grunt",
	npmCmd = process.platform === "win32" ? "npm.cmd" : "npm",

	readmeFile = "README.md",
	packageFile = "package.json",
	versionFile = path.join( "src", "version.js" ),

	releaseDir = "CDN",
	distDir = "dist";

steps(
	initialize,
	checkGitStatus,
	gruntBuild,
	updateVersions,
	tagReleaseVersion,
	gruntBuild,
	makeReleaseCopies,
	publishToNPM,
	setNextVersion,
	pushToRemote,
	remindAboutCDN,
	remindAboutSites,
	exit
);

function initialize( next ) {

	// -d dryrun mode, no commands are executed at all
	if ( process.argv[ 2 ] === "-d" ) {
		process.argv.shift();
		dryrun = true;
		console.warn( "=== DRY RUN MODE ===" );
	}

	// -r skip remote mode, no remote commands are executed
	// (git push, npm publish, cdn copy)
	// Reset with `git reset --hard HEAD~2 && git tag -d (version) && grunt`
	if ( process.argv[ 2 ] === "-r" ) {
		process.argv.shift();
		skipRemote = true;
		console.warn( "=== SKIPREMOTE MODE ===" );
	}

	// First arg should be the version number being released; this is a proper subset
	// of a full semver, see https://github.com/mojombo/semver/issues/32
	// Examples: 1.0.1, 1.0.1-pre, 1.0.1-rc1, 1.0.1-rc1.1
	var newver, oldver,
		rsemver = /^(\d+)\.(\d+)\.(\d+)(?:-([\dA-Za-z\-]+(?:\.[\dA-Za-z\-]+)*))?$/,
		version = rsemver.exec( process.argv[ 2 ] || "" ) || [],
		major = version[ 1 ],
		minor = version[ 2 ],
		patch = version[ 3 ],
		xbeta = version[ 4 ];

	releaseVersion = process.argv[ 2 ];
	isBeta = !!xbeta;

	if ( !releaseVersion ) {
		log( "Usage: release [ -d -r ] releaseVersion" );
		log( "       -d  Dry-run; no commands are executed at all" );
		log( "       -r  Skip-remote; nothing is pushed externally" );
		die( "Invalid args" );
	}
	if ( !version.length ) {
		die( "'" + releaseVersion + "' is not a valid semver!" );
	}
	if ( xbeta === "pre" ) {
		die( "Cannot release a 'pre' version!" );
	}
	if ( !( fs.existsSync || path.existsSync )( packageFile ) ) {
		die( "No " + packageFile + " in this directory" );
	}
	pkg = JSON.parse( fs.readFileSync( packageFile ) );

	status( "Current version is " + pkg.version + "; generating release " + releaseVersion );
	version = rsemver.exec( pkg.version );
	oldver = ( +version[ 1 ] ) * 10000 + ( +version[ 2 ] * 100 ) + ( +version[ 3 ] );
	newver = ( +major ) * 10000 + ( +minor * 100 ) + ( +patch );
	if ( newver < oldver ) {
		die( "Next version is older than current version!" );
	}

	nextVersion = major + "." + minor + "." + ( isBeta ? patch : +patch + 1 ) + "-pre";
	next();
}

//TODO: Check that remote doesn't have newer commits:
// git fetch repoURL
// git remote show repoURL
// (look for " BRANCH     pushes to BRANCH     (up to date)")

function checkGitStatus( next ) {
	child.execFile( "git", [ "status" ], function( error, stdout ) {
		if ( error ) {
			throw error;
		}
		var onBranch = ( ( stdout || "" ).match( /On branch (\S+)/ ) || [] )[ 1 ];
		if ( onBranch !== branch ) {
			die( "Branches don't match: Wanted " + branch + ", got " + onBranch );
		}
		if ( /Changes to be committed/i.test( stdout ) ) {
			die( "Please commit changed files before attemping to push a release." );
		}
		if ( /Changes not staged for commit/i.test( stdout ) ) {
			die( "Please stash files before attempting to push a release." );
		}
		next();
	} );
}

function tagReleaseVersion( next ) {
	git( [ "commit", "-a", "--no-verify", "-m", "Tagging the " + releaseVersion + " release." ],
		function() {
			git( [ "tag", releaseVersion ], next );
		}
	);
}

function updateVersions( next ) {
	updateSourceVersion( releaseVersion );
	updateReadmeVersion( releaseVersion );
	updatePackageVersion( releaseVersion );
	next();
}

function gruntBuild( next ) {
	exec( gruntCmd, [], function( error, stdout, stderr ) {
		if ( error ) {
			die( error + stderr );
		}
		log( stdout || "(no output)" );
		next();
	} );
}

function makeReleaseCopies( next ) {
	if ( !fs.existsSync( releaseDir ) ) {
		fs.mkdirSync( releaseDir );
	}
	var passThrough = function( t ) {
		return t;
	};
	var releaseFiles = {
		"jquery-migrate-VER.js": passThrough,
		"jquery-migrate-VER.min.js": fixMinRef,
		"jquery-migrate-VER.min.map": fixMapRef
	};
	Object.keys( releaseFiles ).forEach( function( key ) {
		var distFile = key.replace( /-VER/g, "" ),
			distPath = path.join( distDir, distFile ),
			releaseFile = key.replace( /VER/g, releaseVersion ),
			releasePath = path.join( releaseDir, releaseFile );

		// Remove Windows CRLF if it's there, on *nix this is a no-op
		log( "Processing " + distPath + " => " + releasePath );
		var distText = fs.readFileSync( distPath, "utf8" ).replace( /\r\n/g, "\n" );
		var releaseText = releaseFiles[ key ]( distText, releaseFile );
		if ( !dryrun ) {
			fs.writeFileSync( releasePath, releaseText );
		}
	} );
	next();
}

async function publishToNPM( next ) {
	const { input: otp } = await prompt( {
		type: "input",
		name: "input",
		message: "Enter one-time password if you have 2FA enabled and press Enter.\n" +
			"Otherwise, just press Enter."
	} );

	// Don't update "latest" if this is a beta
	if ( isBeta ) {
		exec( npmCmd, [
			"publish",
			"--tag",
			"beta",
			...( otp ? [ "--otp", otp ] : [] )
		], next, skipRemote );
	} else {
		exec( npmCmd, [
			"publish",
			...( otp ? [ "--otp", otp ] : [] )
		], next, skipRemote );
	}
}

function setNextVersion( next ) {
	updateSourceVersion( nextVersion );
	updatePackageVersion( nextVersion, "main" );
	git( [ "commit", "-a", "--no-verify", "-m", "Updating the source version to " + nextVersion ],
		next );
}

function pushToRemote( next ) {
	git( [ "push", "--tags", repoURL, branch ], next, skipRemote );
}

function remindAboutCDN( next ) {
	console.log( chalk.red( "TODO: Update CDN with jquery-migrate." +
		releaseVersion + " files (min and regular)" ) );
	console.log( chalk.red( "  clone codeorigin.jquery.com, git add files, commit, push" ) );
	next();
}

function remindAboutSites( next ) {
	console.log( chalk.red( "TODO: Update jquery.com download page to " + releaseVersion ) );
	next();
}

//==============================

function steps() {
	var cur = 0,
		steps = arguments;
	( function next() {
		process.nextTick( function() {
			steps[ cur++ ]( next );
		} );
	} )();
}

function updatePackageVersion( ver, blobVer ) {
	status( "Updating " + packageFile + " version to " + ver );
	blobVer = blobVer || ver;
	pkg.version = ver;
	pkg.author.url = setBlobVersion( pkg.author.url, blobVer );
	writeJsonSync( packageFile, pkg );
}

function updateSourceVersion( ver ) {
	var stmt = "\njQuery.migrateVersion = \"" + ver + "\";\n";

	status( "Updating " + stmt.replace( /\n/g, "" ) );
	if ( !dryrun ) {
		fs.writeFileSync( versionFile, stmt );
	}
}

function updateReadmeVersion() {
	var readme = fs.readFileSync( readmeFile, "utf8" );

	// Change version references from the old version to the new one.
	// The regex can update beta versions in case it was changed manually.
	if ( isBeta ) {
		status( "Skipping " + readmeFile + " update (beta release)" );
	} else {
		status( "Updating " + readmeFile );
		readme = readme.replace(
			/jquery-migrate-\d+\.\d+\.\d+(?:-\w+)?/g,
			"jquery-migrate-" + releaseVersion
		);
		if ( !dryrun ) {
			fs.writeFileSync( readmeFile, readme );
		}
	}
}

function setBlobVersion( s, v ) {
	return s.replace( /\/blob\/(?:(\d+\.\d+[^\/]+)|main)/, "/blob/" + v );
}

function writeJsonSync( fname, json ) {
	if ( dryrun ) {
		console.log( JSON.stringify( json, null, "  " ) );
	} else {
		fs.writeFileSync( fname, JSON.stringify( json, null, "\t" ) + "\n" );
	}
}

function fixMinRef( oldText ) {
	var mapRef = new RegExp( "^//# sourceMappingURL=jquery-migrate.min.map\\n?", "m" );

	// Remove the ref for now rather than try to fix it
	var newText = oldText.replace( mapRef, "" );
	if ( oldText === newText ) {
		throw Error( "fixMinRef: Unable to find the sourceMappingURL" );
	}
	return newText;
}

function fixMapRef( oldText, newFile ) {
	var mapJSON = JSON.parse( oldText );
	var sources = mapJSON.sources;
	if ( sources.join() !== "../src/migratemute.js,jquery-migrate.js" ) {
		throw Error( "fixMapRef: Unexpected sources entry: " + sources );
	}

	// This file isn't published, not sure the best way to deal with that
	sources[ 0 ] = "migratemute.js";
	sources[ 1 ] = newFile.replace( /\.map$/, ".js" );
	return JSON.stringify( mapJSON );
}

function git( args, fn, skip ) {
	exec( "git", args, fn, skip );
}

function exec( cmd, args, fn, skip ) {
	if ( dryrun || skip ) {
		log( chalk.black.bgBlue( "# " + cmd + " " + args.join( " " ) ) );
		fn();
	} else {
		log( chalk.green( cmd + " " + args.join( " " ) ) );
		child.execFile( cmd, args, { env: process.env },
			function( err, stdout, stderr ) {
				if ( err ) {
					die( stderr || stdout || err );
				}
				fn();
			}
		);
	}
}

function status( msg ) {
	console.log( chalk.black.bgGreen( msg ) );
}

function log( msg ) {
	console.log( msg );
}

function die( msg ) {
	console.error( chalk.red( "ERROR: " + msg ) );
	process.exit( 1 );
}

function exit() {
	process.exit( 0 );
}
