"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const locale_1 = require("@joplin/lib/locale");
const Setting_1 = require("@joplin/lib/models/Setting");
const SyncTargetRegistry_1 = require("@joplin/lib/SyncTargetRegistry");
const MigrationHandler_1 = require("@joplin/lib/services/synchronizer/MigrationHandler");
const ResourceFetcher_1 = require("@joplin/lib/services/ResourceFetcher");
const Synchronizer_1 = require("@joplin/lib/Synchronizer");
const utils_1 = require("@joplin/lib/services/e2ee/utils");
const LockHandler_1 = require("@joplin/lib/services/synchronizer/LockHandler");
const BaseCommand = require('./base-command').default;
const app_1 = require("./app");
const { OneDriveApiNodeUtils } = require('@joplin/lib/onedrive-api-node-utils.js');
const registry_1 = require("@joplin/lib/registry");
const { cliUtils } = require('./cli-utils.js');
const md5 = require('md5');
const locker = require("proper-lockfile");
const fs_extra_1 = require("fs-extra");
const joplinCloudUtils_1 = require("@joplin/lib/services/joplinCloudUtils");
const Logger_1 = require("@joplin/utils/Logger");
const uuid_1 = require("@joplin/lib/uuid");
const ShareService_1 = require("@joplin/lib/services/share/ShareService");
const logger = Logger_1.default.create('command-sync');
class Command extends BaseCommand {
    constructor() {
        super(...arguments);
        this.syncTargetId_ = null;
        // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
        this.releaseLockFn_ = null;
        // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
        this.oneDriveApiUtils_ = null;
    }
    usage() {
        return 'sync';
    }
    description() {
        return (0, locale_1._)('Synchronises with remote storage.');
    }
    options() {
        return [
            ['--target <target>', (0, locale_1._)('Sync to provided target (defaults to sync.target config value)')],
            ['--upgrade', (0, locale_1._)('Upgrade the sync target to the latest version.')],
            ['--use-lock <value>', 'Disable local locks that prevent multiple clients from synchronizing at the same time (Default = 1)'],
        ];
    }
    static async lockFile(filePath) {
        return locker.lock(filePath, { stale: 1000 * 60 * 5 });
    }
    static async isLocked(filePath) {
        return locker.check(filePath);
    }
    async doAuth() {
        const syncTarget = registry_1.reg.syncTarget(this.syncTargetId_);
        const syncTargetMd = SyncTargetRegistry_1.default.idToMetadata(this.syncTargetId_);
        if (this.syncTargetId_ === 3 || this.syncTargetId_ === 4) {
            // OneDrive
            this.oneDriveApiUtils_ = new OneDriveApiNodeUtils(syncTarget.api());
            const auth = await this.oneDriveApiUtils_.oauthDance({
                // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
                log: (...s) => {
                    return this.stdout(...s);
                },
            });
            this.oneDriveApiUtils_ = null;
            Setting_1.default.setValue(`sync.${this.syncTargetId_}.auth`, auth ? JSON.stringify(auth) : null);
            if (!auth) {
                this.stdout((0, locale_1._)('Authentication was not completed (did not receive an authentication token).'));
                return false;
            }
            return true;
        }
        else if (syncTargetMd.name === 'dropbox') {
            // Dropbox
            const api = await syncTarget.api();
            const loginUrl = api.loginUrl();
            this.stdout((0, locale_1._)('To allow Joplin to synchronise with Dropbox, please follow the steps below:'));
            this.stdout((0, locale_1._)('Step 1: Open this URL in your browser to authorise the application:'));
            this.stdout(loginUrl);
            const authCode = await this.prompt((0, locale_1._)('Step 2: Enter the code provided by Dropbox:'), { type: 'string' });
            if (!authCode) {
                this.stdout((0, locale_1._)('Authentication was not completed (did not receive an authentication token).'));
                return false;
            }
            const response = await api.execAuthToken(authCode);
            Setting_1.default.setValue(`sync.${this.syncTargetId_}.auth`, response.access_token);
            api.setAuthToken(response.access_token);
            return true;
        }
        else if (syncTargetMd.name === 'joplinCloud') {
            const applicationAuthId = (0, uuid_1.uuidgen)();
            const checkForCredentials = async () => {
                try {
                    const applicationAuthUrl = `${Setting_1.default.value('sync.10.path')}/api/application_auth/${applicationAuthId}`;
                    const response = await (0, joplinCloudUtils_1.checkIfLoginWasSuccessful)(applicationAuthUrl);
                    if (response && response.success) {
                        return response;
                    }
                    return null;
                }
                catch (error) {
                    logger.error(error);
                    throw error;
                }
            };
            this.stdout((0, locale_1._)('To allow Joplin to synchronise with Joplin Cloud, please login using this URL:'));
            const confirmUrl = `${Setting_1.default.value('sync.10.website')}/applications/${applicationAuthId}/confirm`;
            const urlWithClient = await (0, joplinCloudUtils_1.generateApplicationConfirmUrl)(confirmUrl);
            this.stdout(urlWithClient);
            const authorized = await this.prompt((0, locale_1._)('Have you authorised the application login in the above URL?'), { booleanAnswerDefault: 'y' });
            if (!authorized)
                return false;
            const result = await checkForCredentials();
            if (!result)
                return false;
            return true;
        }
        this.stdout((0, locale_1._)('Not authenticated with %s. Please provide any missing credentials.', syncTargetMd.label));
        return false;
    }
    cancelAuth() {
        if (this.oneDriveApiUtils_) {
            this.oneDriveApiUtils_.cancelOAuthDance();
            return;
        }
    }
    doingAuth() {
        return !!this.oneDriveApiUtils_;
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
    async action(args) {
        this.releaseLockFn_ = null;
        // Lock is unique per profile/database
        const lockFilePath = `${require('os').tmpdir()}/synclock_${md5(escape(Setting_1.default.value('profileDir')))}`; // https://github.com/pvorb/node-md5/issues/41
        if (!(await (0, fs_extra_1.pathExists)(lockFilePath)))
            await (0, fs_extra_1.writeFile)(lockFilePath, 'synclock');
        const useLock = args.options.useLock !== 0;
        if (useLock) {
            try {
                if (await Command.isLocked(lockFilePath))
                    throw new Error((0, locale_1._)('Synchronisation is already in progress.'));
                this.releaseLockFn_ = await Command.lockFile(lockFilePath);
            }
            catch (error) {
                if (error.code === 'ELOCKED') {
                    const msg = (0, locale_1._)('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_1.default.value('sync.target');
            if (args.options.target)
                this.syncTargetId_ = args.options.target;
            const syncTarget = registry_1.reg.syncTarget(this.syncTargetId_);
            if (!(await syncTarget.isAuthenticated())) {
                (0, app_1.default)().gui().showConsole();
                (0, app_1.default)().gui().maximizeConsole();
                const authDone = await this.doAuth();
                if (!authDone)
                    return cleanUp();
            }
            const sync = await syncTarget.synchronizer();
            // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
            const options = {
                // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
                onProgress: (report) => {
                    const lines = Synchronizer_1.default.reportToLines(report);
                    if (lines.length)
                        cliUtils.redraw(lines.join(' '));
                },
                onMessage: (msg) => {
                    cliUtils.redrawDone();
                    this.stdout(msg);
                },
            };
            this.stdout((0, locale_1._)('Synchronisation target: %s (%s)', Setting_1.default.enumOptionLabel('sync.target', this.syncTargetId_), this.syncTargetId_));
            if (!sync)
                throw new Error((0, locale_1._)('Cannot initialise synchroniser.'));
            if (args.options.upgrade) {
                let migrationError = null;
                try {
                    const migrationHandler = new MigrationHandler_1.default(sync.api(), registry_1.reg.db(), sync.lockHandler(), (0, LockHandler_1.appTypeToLockType)(Setting_1.default.value('appType')), Setting_1.default.value('clientId'));
                    migrationHandler.setLogger(cliUtils.stdoutLogger(this.stdout.bind(this)));
                    await migrationHandler.upgrade();
                }
                catch (error) {
                    migrationError = error;
                }
                if (!migrationError) {
                    Setting_1.default.setValue('sync.upgradeState', Setting_1.default.SYNC_UPGRADE_STATE_IDLE);
                    await Setting_1.default.saveAll();
                }
                if (migrationError)
                    throw migrationError;
                return cleanUp();
            }
            // Refresh share invitations -- if running without a GUI, some of the
            // maintenance tasks may otherwise be skipped.
            await ShareService_1.default.instance().maintenance();
            this.stdout((0, locale_1._)('Starting synchronisation...'));
            const contextKey = `sync.${this.syncTargetId_}.context`;
            let context = Setting_1.default.value(contextKey);
            context = context ? JSON.parse(context) : {};
            options.context = context;
            try {
                const newContext = await sync.start(options);
                Setting_1.default.setValue(contextKey, JSON.stringify(newContext));
            }
            catch (error) {
                if (error.code === 'alreadyStarted') {
                    this.stdout(error.message);
                }
                else {
                    throw error;
                }
            }
            // When using the tool in command line mode, the ResourceFetcher service is
            // not going to be running in the background, so the resources need to be
            // explicitly downloaded below.
            if (!(0, app_1.default)().hasGui()) {
                this.stdout((0, locale_1._)('Downloading resources...'));
                await ResourceFetcher_1.default.instance().fetchAll();
                await ResourceFetcher_1.default.instance().waitForAllFinished();
            }
            const noPasswordMkIds = await (0, utils_1.masterKeysWithoutPassword)();
            if (noPasswordMkIds.length)
                this.stdout(`/!\\ ${(0, locale_1._)('Your password is needed to decrypt some of your data. Type `:e2ee decrypt` to set it.')}`);
            await (0, app_1.default)().refreshCurrentFolder();
        }
        catch (error) {
            cleanUp();
            throw error;
        }
        if (Setting_1.default.value('sync.upgradeState') > Setting_1.default.SYNC_UPGRADE_STATE_IDLE) {
            this.stdout(`/!\\ ${(0, locale_1._)('Sync target must be upgraded! Run `%s` to proceed.', 'sync --upgrade')}`);
            (0, app_1.default)().gui().showConsole();
            (0, app_1.default)().gui().maximizeConsole();
        }
        cleanUp();
    }
    async cancel() {
        if (this.doingAuth()) {
            this.cancelAuth();
            return;
        }
        const syncTargetId = this.syncTargetId_ ? this.syncTargetId_ : Setting_1.default.value('sync.target');
        cliUtils.redrawDone();
        this.stdout((0, locale_1._)('Cancelling... Please wait.'));
        const syncTarget = registry_1.reg.syncTarget(syncTargetId);
        if (await 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;
//# sourceMappingURL=command-sync.js.map