const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('lib/locale.js');
const { OneDriveApiNodeUtils } = require('./onedrive-api-node-utils.js');
const Setting = require('lib/models/Setting.js');
const BaseItem = require('lib/models/BaseItem.js');
const { Synchronizer } = require('lib/synchronizer.js');
const { reg } = require('lib/registry.js');
const { cliUtils } = require('./cli-utils.js');
const md5 = require('md5');
const locker = require('proper-lockfile');
const fs = require('fs-extra');
const osTmpdir = require('os-tmpdir');
const SyncTargetRegistry = require('lib/SyncTargetRegistry');

class Command extends BaseCommand {

	constructor() {
		super();
		this.syncTargetId_ = null;
		this.releaseLockFn_ = null;
		this.oneDriveApiUtils_ = null;
	}

	usage() {
		return 'sync';
	}

	description() {
		return _('Synchronises with remote storage.');
	}

	options() {
		return [
			['--target <target>', _('Sync to provided target (defaults to sync.target config value)')],
		];
	}

	static lockFile(filePath) {
		return new Promise((resolve, reject) => {
			locker.lock(filePath, { stale: 1000 * 60 * 5 }, (error, release) => {
				if (error) {
					reject(error);
					return;
				}

				resolve(release);
			});
		});
	}

	static isLocked(filePath) {
		return new Promise((resolve, reject) => {
			locker.check(filePath, (error, isLocked) => {
				if (error) {
					reject(error);
					return;
				}

				resolve(isLocked);
			});
		});
	}

	async doAuth() {
		const syncTarget = reg.syncTarget(this.syncTargetId_);
		const syncTargetMd = SyncTargetRegistry.idToMetadata(this.syncTargetId_);

		if (this.syncTargetId_ === 3 || this.syncTargetId_ === 4) { // OneDrive
			this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(syncTarget.api());
			const auth = await this.oneDriveApiUtils_.oauthDance({
				log: (...s) => { return this.stdout(...s); }
			});
			this.oneDriveApiUtils_ = null;
			
			Setting.setValue('sync.' + this.syncTargetId_ + '.auth', auth ? JSON.stringify(auth) : null);
			if (!auth) {
				this.stdout(_('Authentication was not completed (did not receive an authentication token).'));
				return false;
			}

			return true;
		}

		this.stdout(_('Not authentified with %s. Please provide any missing credentials.', syncTarget.label()));
		return false;
	}

	cancelAuth() {
		if (this.oneDriveApiUtils_) {
			this.oneDriveApiUtils_.cancelOAuthDance();
			return;
		}
	}

	doingAuth() {
		return !!this.oneDriveApiUtils_;
	}

	async action(args) {
		this.releaseLockFn_ = null;

		// Lock is unique per profile/database
		const lockFilePath = osTmpdir() + '/synclock_' + md5(escape(Setting.value('profileDir'))); // https://github.com/pvorb/node-md5/issues/41
		if (!await fs.pathExists(lockFilePath)) await fs.writeFile(lockFilePath, 'synclock');

		try {
			if (await Command.isLocked(lockFilePath)) throw new Error(_('Synchronisation is already in progress.'));

			this.releaseLockFn_ = await Command.lockFile(lockFilePath);
		} catch (error) {
			if (error.code == 'ELOCKED') {
				const msg = _('Lock file is already being hold. If you know that no synchronisation is taking place, you may delete the lock file at "%s" and resume the operation.', error.file);
				this.stdout(msg);
				return;
			}
			throw error;
		}

		const cleanUp = () => {
			cliUtils.redrawDone();
			if (this.releaseLockFn_) {
				this.releaseLockFn_();
				this.releaseLockFn_ = null;
			}
		};

		try {
			this.syncTargetId_ = Setting.value('sync.target');
			if (args.options.target) this.syncTargetId_ = args.options.target;

			const syncTarget = reg.syncTarget(this.syncTargetId_);

			if (!syncTarget.isAuthenticated()) {
				app().gui().showConsole();
				app().gui().maximizeConsole();

				const authDone = await this.doAuth();
				if (!authDone) return cleanUp();
			}
			
			const sync = await syncTarget.synchronizer();

			let options = {
				onProgress: (report) => {
					let lines = Synchronizer.reportToLines(report);
					if (lines.length) cliUtils.redraw(lines.join(' '));
				},
				onMessage: (msg) => {
					cliUtils.redrawDone();
					this.stdout(msg);
				},
			};

			this.stdout(_('Synchronisation target: %s (%s)', Setting.enumOptionLabel('sync.target', this.syncTargetId_), this.syncTargetId_));

			if (!sync) throw new Error(_('Cannot initialize synchroniser.'));

			this.stdout(_('Starting synchronisation...'));

			const contextKey = 'sync.' + this.syncTargetId_ + '.context';
			let context = Setting.value(contextKey);

			context = context ? JSON.parse(context) : {};
			options.context = context;

			try {
				let newContext = await sync.start(options);
				Setting.setValue(contextKey, JSON.stringify(newContext));
			} catch (error) {
				if (error.code == 'alreadyStarted') {
					this.stdout(error.message);
				} else {
					throw error;
				}
			}

			await app().refreshCurrentFolder();
		} catch (error) {
			cleanUp();
			throw error;
		}

		cleanUp();
	}

	async cancel() {
		if (this.doingAuth()) {
			this.cancelAuth();
			return;
		}

		const syncTargetId = this.syncTargetId_ ? this.syncTargetId_ : Setting.value('sync.target');

		cliUtils.redrawDone();

		this.stdout(_('Cancelling... Please wait.'));

		const syncTarget = reg.syncTarget(syncTargetId);

		if (syncTarget.isAuthenticated()) {
			const sync = await syncTarget.synchronizer();
			if (sync) await sync.cancel();
		} else {
			if (this.releaseLockFn_) this.releaseLockFn_();
			this.releaseLockFn_ = null;
		}

		this.syncTargetId_ = null;
	}

	cancellable() {
		return true;
	}

}

module.exports = Command;