
'use strict';

const path = require('path');
const util = require('util');
const semver = require('semver');
const readline = require('readline');
const winston = require('winston');

const db = require('./database');
const file = require('./file');
const { paths } = require('./constants');

/*
 * Need to write an upgrade script for NodeBB? Cool.
 *
 * 1. Copy TEMPLATE to a unique file name of your choice. Try to be succinct.
 * 2. Open up that file and change the user-friendly name (can be longer/more descriptive than the file name)
 *    and timestamp (don't forget the timestamp!)
 * 3. Add your script under the "method" property
 */

const Upgrade = module.exports;

Upgrade.getAll = async function () {
	let files = await file.walk(path.join(__dirname, './upgrades'));

	// Sort the upgrade scripts based on version
	files = files.filter(file => path.basename(file) !== 'TEMPLATE').sort((a, b) => {
		const versionA = path.dirname(a).split(path.sep).pop();
		const versionB = path.dirname(b).split(path.sep).pop();
		const semverCompare = semver.compare(versionA, versionB);
		if (semverCompare) {
			return semverCompare;
		}
		const timestampA = require(a).timestamp;
		const timestampB = require(b).timestamp;
		return timestampA - timestampB;
	});

	await Upgrade.appendPluginScripts(files);

	// check duplicates and error
	const seen = {};
	const dupes = [];
	files.forEach((file) => {
		if (seen[file]) {
			dupes.push(file);
		} else {
			seen[file] = true;
		}
	});
	if (dupes.length) {
		winston.error(`Found duplicate upgrade scripts\n${dupes}`);
		throw new Error('[[error:duplicate-upgrade-scripts]]');
	}

	return files;
};

Upgrade.appendPluginScripts = async function (files) {
	// Find all active plugins
	const plugins = await db.getSortedSetRange('plugins:active', 0, -1);
	plugins.forEach((plugin) => {
		const configPath = path.join(paths.nodeModules, plugin, 'plugin.json');
		try {
			const pluginConfig = require(configPath);
			if (pluginConfig.hasOwnProperty('upgrades') && Array.isArray(pluginConfig.upgrades)) {
				pluginConfig.upgrades.forEach((script) => {
					files.push(path.join(path.dirname(configPath), script));
				});
			}
		} catch (e) {
			if (e.code !== 'MODULE_NOT_FOUND') {
				winston.error(e.stack);
			}
		}
	});
	return files;
};

Upgrade.check = async function () {
	// Throw 'schema-out-of-date' if not all upgrade scripts have run
	const files = await Upgrade.getAll();
	const executed = await db.getSortedSetRange('schemaLog', 0, -1);
	const remainder = files.filter(name => !executed.includes(path.basename(name, '.js')));
	if (remainder.length > 0) {
		throw new Error('schema-out-of-date');
	}
};

Upgrade.run = async function () {
	console.log('\nParsing upgrade scripts... ');

	const [completed, available] = await Promise.all([
		db.getSortedSetRange('schemaLog', 0, -1),
		Upgrade.getAll(),
	]);

	let skipped = 0;
	const queue = available.filter((cur) => {
		const upgradeRan = completed.includes(path.basename(cur, '.js'));
		if (upgradeRan) {
			skipped += 1;
		}
		return !upgradeRan;
	});

	await Upgrade.process(queue, skipped);
};

Upgrade.runParticular = async function (names) {
	console.log('\nParsing upgrade scripts... ');
	const files = await file.walk(path.join(__dirname, './upgrades'));
	await Upgrade.appendPluginScripts(files);
	const upgrades = files.filter(file => names.includes(path.basename(file, '.js')));
	await Upgrade.process(upgrades, 0);
};

Upgrade.process = async function (files, skipCount) {
	console.log('OK'.green + ' | '.reset + String(files.length).cyan + ' script(s) found'.cyan + (skipCount > 0 ? ', '.cyan + String(skipCount).cyan + ' skipped'.cyan : ''));
	const [schemaDate, schemaLogCount] = await Promise.all([
		db.get('schemaDate'),
		db.sortedSetCard('schemaLog'),
	]);

	for (const file of files) {
		/* eslint-disable no-await-in-loop */
		const scriptExport = require(file);
		const date = new Date(scriptExport.timestamp);
		const version = path.dirname(file).split('/').pop();
		const progress = {
			current: 0,
			counter: 0,
			total: 0,
			incr: Upgrade.incrementProgress,
			script: scriptExport,
			date: date,
		};

		process.stdout.write(`${'  → '.white + String(`[${[date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()].join('/')}] `).gray + String(scriptExport.name).reset}...`);

		// For backwards compatibility, cross-reference with schemaDate (if found). If a script's date is older, skip it
		if ((!schemaDate && !schemaLogCount) || (scriptExport.timestamp <= schemaDate && semver.lt(version, '1.5.0'))) {
			process.stdout.write(' skipped\n'.grey);

			await db.sortedSetAdd('schemaLog', Date.now(), path.basename(file, '.js'));
			// eslint-disable-next-line no-continue
			continue;
		}

		// Promisify method if necessary
		if (scriptExport.method.constructor && scriptExport.method.constructor.name !== 'AsyncFunction') {
			scriptExport.method = util.promisify(scriptExport.method);
		}

		// Do the upgrade...
		const upgradeStart = Date.now();
		try {
			await scriptExport.method.bind({
				progress: progress,
			})();
		} catch (err) {
			console.error('Error occurred');
			throw err;
		}
		const upgradeDuration = ((Date.now() - upgradeStart) / 1000).toFixed(2);
		process.stdout.write(` OK (${upgradeDuration} seconds)\n`.green);

		// Record success in schemaLog
		await db.sortedSetAdd('schemaLog', Date.now(), path.basename(file, '.js'));
	}

	console.log('Schema update complete!\n'.green);
};

Upgrade.incrementProgress = function (value) {
	// Newline on first invocation
	if (this.current === 0) {
		process.stdout.write('\n');
	}

	this.current += value || 1;
	this.counter += value || 1;
	const step = (this.total ? Math.floor(this.total / 100) : 100);

	if (this.counter > step || this.current >= this.total) {
		this.counter -= step;
		let percentage = 0;
		let filled = 0;
		let unfilled = 15;
		if (this.total) {
			percentage = `${Math.floor((this.current / this.total) * 100)}%`;
			filled = Math.floor((this.current / this.total) * 15);
			unfilled = Math.max(0, 15 - filled);
		}

		readline.cursorTo(process.stdout, 0);
		process.stdout.write(`    [${filled ? new Array(filled).join('#') : ''}${new Array(unfilled).join(' ')}] (${this.current}/${this.total || '??'}) ${percentage} `);
	}
};

require('./promisify')(Upgrade);
