"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const content_checker_1 = require("@ewizardjs/content-checker");
const system_settings_1 = require("@ewizardjs/system-settings");
const fs_1 = require("fs");
const os_1 = require("os");
const fs_extra_1 = require("fs-extra");
const https = require("https");
const terminalLink = require("terminal-link");
const listr2_1 = require("listr2");
const upath_1 = require("upath");
const path_1 = require("path");
const archiveContent_1 = require("../../bin/utils/archiveContent");
const browser_callback_server_1 = require("../../modules/browser-callback-server");
const Config_1 = require("../../modules/Config");
const I18n_1 = require("../../modules/I18n");
const Token_handler_1 = require("../../modules/Token-handler");
const WizError_1 = require("../../modules/WizError");
const Key_cloak_1 = require("../../services/Key-cloak");
const cli_customize_1 = require("../../utils/cli-customize");
const Unzipper_1 = require("../../utils/Unzipper");
const contentClient_1 = require("./contentClient");
const urlJoin = require("url-join");
const getFilenameFromUrl_1 = require("../../utils/getFilenameFromUrl");
const CheckSumsGenerator_1 = require("../../modules/CheckSumsGenerator");
const Checksums_1 = require("../../modules/CheckSumsGenerator/Checksums");
const MergeTool_interface_1 = require("../../interfaces/MergeTool.interface");
const MergeTool_1 = require("./MergeTool");
const semver_1 = require("semver");
const { readdir, mkdtemp } = fs_1.promises;
const APP_NAME = 'ewizard-cli';
const CHECKSUMS_PATH = '.ewizard/checksum.json';
function getProgressTracker(task) {
    let progress = 0;
    return (next) => {
        const normalizedProgress = parseInt(next.toFixed(), 10);
        if (normalizedProgress === progress)
            return;
        progress = normalizedProgress;
        task.output = `Progress: ${cli_customize_1.renderBar(normalizedProgress, '#4285f4')}  ${normalizedProgress}%`;
    };
}
class VersionControlSystem {
    constructor() {
        var _a;
        this.vcs = 'vcs';
        this.vcs_id = 'vcs_id';
        const browserCallbackServer = new browser_callback_server_1.BrowserCallbackServer();
        const tokenHandler = createTokenHandler({ access_token: null, refresh_token: null });
        const keyCloakClient = new Key_cloak_1.KeyCloakClient(tokenHandler, browserCallbackServer);
        function createTokenHandler(options) {
            return Token_handler_1.TokenHandlerFactory.createTokenHandler(options);
        }
        const config = Config_1.Config.getSync();
        const apiUrl = (_a = config === null || config === void 0 ? void 0 : config.ewizard) === null || _a === void 0 ? void 0 : _a.apiUrl;
        const apiPath = config === null || config === void 0 ? void 0 : config.apiPath;
        const api = urlJoin(apiUrl, apiPath);
        this.contentApi = new contentClient_1.default(api, tokenHandler, keyCloakClient);
    }
    clone(cwd, { id }) {
        return __awaiter(this, void 0, void 0, function* () {
            const tasks = new listr2_1.Listr([
                {
                    task: (context) => __awaiter(this, void 0, void 0, function* () {
                        var _a;
                        const idValid = this.validateContentId(id);
                        if (!idValid) {
                            throw new WizError_1.WizError('INCORRECT_PROJECT_ID');
                        }
                        context.content = yield this.getContent(id);
                        const ZIP_NAME = (_a = getFilenameFromUrl_1.default(context.content.links.download)) !== null && _a !== void 0 ? _a : `${id}.zip`;
                        context.downloadedArchivePath = upath_1.join(cwd, ZIP_NAME);
                        context.destinationPath = upath_1.join(cwd, path_1.basename(ZIP_NAME, path_1.extname(ZIP_NAME)));
                        const isValidDest = yield this.validateDestinationPath(context.destinationPath);
                        if (!isValidDest) {
                            throw new WizError_1.WizError('FOLDER_IS_NOT_EMPTY');
                        }
                    }),
                },
                {
                    title: 'Downloading project 📥',
                    task: (context, task) => __awaiter(this, void 0, void 0, function* () {
                        const { content, downloadedArchivePath } = context;
                        yield this.downloadFromLink(content.links.download, downloadedArchivePath, getProgressTracker(task));
                    }),
                },
                {
                    title: 'Unzipping project 🗂️',
                    task: (context, task) => __awaiter(this, void 0, void 0, function* () {
                        const { downloadedArchivePath, destinationPath } = context;
                        try {
                            yield fs_extra_1.ensureDir(destinationPath);
                            yield new Unzipper_1.default(downloadedArchivePath).unzip(destinationPath, {}, getProgressTracker(task));
                        }
                        finally {
                            fs_extra_1.remove(downloadedArchivePath);
                        }
                    }),
                },
                {
                    task: ({ destinationPath, content }) => this.finalizeProject(destinationPath, content),
                },
            ]);
            return tasks.run();
        });
    }
    pull(cwd, { accept }) {
        return __awaiter(this, void 0, void 0, function* () {
            const tmp = yield mkdtemp(upath_1.join(os_1.tmpdir(), `${APP_NAME}-`));
            const preparationTasks = new listr2_1.Listr([
                {
                    task: (context) => __awaiter(this, void 0, void 0, function* () {
                        const vscInfo = yield this.getVcsInfo(cwd);
                        context.contentType = yield system_settings_1.getByPath(cwd, 'content.type');
                        if (!(vscInfo === null || vscInfo === void 0 ? void 0 : vscInfo.id) || !this.validateContentId(vscInfo.id)) {
                            throw new WizError_1.WizError('NO_UPSTREAM_PROJECT', context.contentType);
                        }
                        context.vscInfo = vscInfo;
                    }),
                },
                {
                    skip: ({ vscInfo }) => vscInfo.version,
                    task: (context, task) => __awaiter(this, void 0, void 0, function* () {
                        context.fallbackMode = true;
                        const warningMessage = I18n_1.default.error('DANGER_UPSTREAM_OVERWRITE');
                        const styledWarning = warningMessage.replace('OVERWRITE YOUR CHANGES', cli_customize_1.colorize('#DE3157')('OVERWRITE YOUR CHANGES'));
                        context.confirmed = yield task.prompt({
                            type: 'Toggle',
                            message: styledWarning,
                        });
                        if (!context.confirmed) {
                            task.title = 'Action discarded';
                        }
                    }),
                },
            ]);
            const preparationResult = yield preparationTasks.run();
            const mainTasks = new listr2_1.Listr([
                {
                    options: {
                        persistentOutput: true,
                    },
                    task: (context, task) => __awaiter(this, void 0, void 0, function* () {
                        const { vscInfo } = context;
                        const content = yield this.getContent(vscInfo.id);
                        context.nothingToUpdate = semver_1.eq(vscInfo.version, content.version);
                        if (context.nothingToUpdate) {
                            task.output = I18n_1.default.info('UP_TO_DATE');
                        }
                    }),
                },
                {
                    title: 'Downloading project 📥',
                    skip: ({ nothingToUpdate }) => nothingToUpdate,
                    task: (context, task) => __awaiter(this, void 0, void 0, function* () {
                        const { vscInfo, contentType } = context;
                        try {
                            const { archives, content } = yield this.downloadContent(vscInfo, tmp, getProgressTracker(task));
                            context.downloadedArchives = archives;
                            context.content = content;
                        }
                        catch (e) {
                            if (e.code === 'BAD_REQUEST') {
                                throw new WizError_1.WizError('NO_UPSTREAM_PROJECT', contentType);
                            }
                            throw e;
                        }
                    }),
                },
                {
                    title: 'Preparing for merge...',
                    skip: ({ nothingToUpdate }) => nothingToUpdate,
                    task: (context, task) => __awaiter(this, void 0, void 0, function* () {
                        const { downloadedArchives } = context;
                        const mergeFolder = upath_1.join(tmp, 'merge-project');
                        context.mergeTool = new MergeTool_1.default(mergeFolder);
                        yield context.mergeTool.init();
                        yield context.mergeTool.prepareToMerge({
                            local: cwd,
                            base: downloadedArchives.base,
                            remote: downloadedArchives.latest,
                        }, getProgressTracker(task));
                    }),
                },
                {
                    options: { persistentOutput: true },
                    title: 'Merging...',
                    skip: ({ nothingToUpdate }) => nothingToUpdate,
                    task: (context, task) => __awaiter(this, void 0, void 0, function* () {
                        const { mergeTool, contentType } = context;
                        const mergeResult = yield mergeTool.merge();
                        let resolveConflicts = accept;
                        if (!resolveConflicts && mergeResult.status.conflicted.length) {
                            resolveConflicts = yield task.prompt([{
                                    type: 'Select',
                                    name: 'resolve conflicts',
                                    message: I18n_1.default.info('CONFLICTS_PROMPT', contentType),
                                    choices: [
                                        { name: MergeTool_interface_1.ResolveConflicts.ApplyLocal, message: I18n_1.default.info('DISCARD_UPSTREAM') },
                                        { name: MergeTool_interface_1.ResolveConflicts.ApplyRemote, message: I18n_1.default.info('OVERWRITE_LOCAL') },
                                        { name: MergeTool_interface_1.ResolveConflicts.Manually, message: I18n_1.default.info('RESOLVE_MANUALLY') },
                                    ],
                                }]);
                        }
                        const status = mergeTool.resolveConflictsForDeletedFiles(resolveConflicts, mergeResult);
                        yield mergeTool.applyMergeResult(cwd, status, resolveConflicts);
                        task.output = mergeTool.generateMergeStatusMessage(status, resolveConflicts);
                    }),
                },
            ]);
            const fallbackTasks = new listr2_1.Listr([
                {
                    title: 'Downloading project 📥',
                    task: (context, task) => __awaiter(this, void 0, void 0, function* () {
                        const { vscInfo, contentType } = context;
                        try {
                            const contentInfo = yield this.getContent(vscInfo.id);
                            const archivePath = upath_1.join(tmp, `${contentInfo.id}.zip`);
                            yield this.downloadFromLink(contentInfo.links.download, archivePath, getProgressTracker(task));
                            context.downloadedArchivePath = archivePath;
                            context.content = contentInfo;
                        }
                        catch (e) {
                            if (e.code === 'BAD_REQUEST') {
                                throw new WizError_1.WizError('NO_UPSTREAM_PROJECT', contentType);
                            }
                            throw e;
                        }
                    }),
                },
                {
                    title: 'Unzipping project 🗂️',
                    task: ({ downloadedArchivePath }, task) => __awaiter(this, void 0, void 0, function* () {
                        yield new Unzipper_1.default(downloadedArchivePath).unzip(cwd, { clean: true }, getProgressTracker(task));
                    }),
                },
                {
                    task: () => this.removeOldAttribute(cwd),
                }
            ]);
            try {
                let taskToExecute;
                const { fallbackMode, confirmed } = preparationResult;
                if (!fallbackMode) {
                    taskToExecute = mainTasks;
                }
                else if (fallbackMode && confirmed) {
                    taskToExecute = fallbackTasks;
                }
                const result = yield (taskToExecute === null || taskToExecute === void 0 ? void 0 : taskToExecute.run(preparationResult));
                if (result && !result.nothingToUpdate)
                    this.finalizeProject(cwd, result.content);
            }
            finally {
                yield fs_extra_1.remove(tmp);
            }
        });
    }
    push(cwd, { force }) {
        return __awaiter(this, void 0, void 0, function* () {
            const skipMainTask = ({ nothingToUpdate, hasNewerVersionOnRemote }) => !force && hasNewerVersionOnRemote || nothingToUpdate;
            const tasks = new listr2_1.Listr([
                {
                    options: { persistentOutput: true },
                    task: (context, task) => __awaiter(this, void 0, void 0, function* () {
                        const metadata = yield Promise.all([
                            this.getVcsInfo(cwd),
                            this.getContentType(cwd),
                            (() => __awaiter(this, void 0, void 0, function* () {
                                const settingsPath = yield system_settings_1.getByPath(cwd, 'path.settings');
                                const settingJSON = yield fs_extra_1.readJson(upath_1.join(cwd, settingsPath));
                                return settingJSON.name;
                            }))(),
                        ]);
                        const [vcsInfo, contentType, projectName] = metadata;
                        context.contentType = contentType;
                        context.projectName = projectName;
                        let content;
                        try {
                            content = yield this.contentApi.get({ id: vcsInfo.id });
                        }
                        catch (e) {
                            if (['NOT_FOUND', 'BAD_REQUEST'].includes(e.code) || !(vcsInfo === null || vcsInfo === void 0 ? void 0 : vcsInfo.id)) {
                                context.vscInfo = { id: null };
                                return;
                            }
                        }
                        context.hasNewerVersionOnRemote = (content === null || content === void 0 ? void 0 : content.version) && vcsInfo.version && semver_1.gt(content.version, vcsInfo.version);
                        context.nothingToUpdate = (vcsInfo === null || vcsInfo === void 0 ? void 0 : vcsInfo.id) && !force && !(yield this.hasChanges(cwd));
                        if (context.nothingToUpdate) {
                            task.output = I18n_1.default.info('NOTHING_TO_UPDATE');
                        }
                        else if (!force && context.hasNewerVersionOnRemote) {
                            throw new WizError_1.WizError('NEWER_VERSION_ON_REMOTE', context.contentType);
                        }
                        context.vscInfo = vcsInfo;
                    }),
                },
                {
                    options: { persistentOutput: true },
                    skip: ({ vscInfo }) => vscInfo.id,
                    task: ({ contentType, projectName }, task) => __awaiter(this, void 0, void 0, function* () {
                        const upstreamTip = I18n_1.default.info('NO_UPSTREAM_PROJECT', contentType, cli_customize_1.colorize('#39BF7F')(projectName), contentType);
                        task.output = upstreamTip.replace('INFO:', cli_customize_1.colorize('#39BF7F')('INFO:'));
                    }),
                },
                {
                    title: 'Archive project..🗃️',
                    skip: skipMainTask,
                    task: (context, task) => __awaiter(this, void 0, void 0, function* () {
                        const animationInterval = cli_customize_1.archivingAnimation(task);
                        const archiveName = `${context.projectName}.zip`;
                        const archivePath = upath_1.join(cwd, archiveName);
                        yield this.archive(cwd);
                        clearInterval(animationInterval);
                        context.archivePath = archivePath;
                        task.title = 'Archive project 🗃️';
                    }),
                },
                {
                    title: 'Upload project..🚀',
                    skip: skipMainTask,
                    task: (context, task) => __awaiter(this, void 0, void 0, function* () {
                        const { vscInfo, archivePath } = context;
                        const animationInterval = cli_customize_1.rocketAnimation(task);
                        const socketConnection = yield this.contentApi.establishConnection();
                        const progressTracker = getProgressTracker(task);
                        const trackValidationProgress = this.contentApi.startTrackSocketsProgress(socketConnection, progressTracker);
                        yield this.contentApi.upload(archivePath, vscInfo.id, progressTracker).catch(({ code, editingBy }) => {
                            clearInterval(animationInterval);
                            task.title = 'Upload project 🚀';
                            socketConnection.close();
                            socketConnection.disconnect();
                            if (editingBy) {
                                throw new WizError_1.WizError('CONTENT_IS_EDITING', cli_customize_1.colorize('#39BF7F')(editingBy), context.contentType);
                            }
                            if (code === 403) {
                                throw new WizError_1.WizError('NO_UPLOAD_PERMISSIONS');
                            }
                        });
                        const { hid, version } = yield trackValidationProgress;
                        context.content = yield this.getContent(hid, version);
                        clearInterval(animationInterval);
                        task.title = 'Upload project 🚀';
                    }),
                },
                {
                    options: { persistentOutput: true },
                    skip: skipMainTask,
                    task: ({ content, contentType }, task) => {
                        const detailsLink = terminalLink(`eWizard ${contentType}`, content.links.details).replace(/[^\x20-\x7E]/g, '');
                        task.output = I18n_1.default.info('PUSH_RESULT', detailsLink);
                    },
                },
                {
                    skip: skipMainTask,
                    task: ({ content }) => this.finalizeProject(cwd, content),
                }
            ], { exitOnError: true, rendererOptions: { suffixSkips: false, showErrorMessage: false } });
            return tasks.run();
        });
    }
    finalizeProject(cwd, { id, version }) {
        return __awaiter(this, void 0, void 0, function* () {
            yield this.setContentInformation(cwd, { id, version });
            yield this.saveChecksum(cwd);
        });
    }
    getContentType(workingDir) {
        return system_settings_1.getByPath(workingDir, 'content.type');
    }
    validateContentId(id) {
        const REGEX = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;
        return typeof id === 'string' && REGEX.test(id);
    }
    validateDestinationPath(path) {
        return __awaiter(this, void 0, void 0, function* () {
            const existsPath = yield fs_extra_1.pathExists(path);
            if (!existsPath)
                return true;
            const isEmpty = !(yield readdir(path)).length;
            return isEmpty;
        });
    }
    setContentInformation(root, content) {
        return __awaiter(this, void 0, void 0, function* () {
            yield system_settings_1.setByPath(root, this.vcs, content);
        });
    }
    getContent(id, version) {
        var _a;
        return __awaiter(this, void 0, void 0, function* () {
            const apiMethod = version ? 'getByVersion' : 'get';
            const content = yield this.contentApi[apiMethod]({ id, version }).catch((e) => {
                if (e.code === 'NOT_FOUND') {
                    throw new WizError_1.WizError('PROJECT_IS_NOT_AVAILABLE', id);
                }
                if (e.code === 'FORBIDDEN') {
                    throw new WizError_1.WizError('NO_DOWNLOAD_PERMISSIONS');
                }
                throw e;
            });
            if (!((_a = content === null || content === void 0 ? void 0 : content.links) === null || _a === void 0 ? void 0 : _a.download)) {
                throw new WizError_1.WizError('NO_DOWNLOAD_PERMISSIONS');
            }
            return content;
        });
    }
    downloadContent({ id, version }, dest, handleProgress = () => { }) {
        return __awaiter(this, void 0, void 0, function* () {
            const [latest, base] = yield Promise.all([
                this.getContent(id),
                this.getContent(id, version),
            ]);
            const latestZip = `${latest.id}-${latest.version}.zip`;
            const latestArchivePath = upath_1.join(dest, latestZip);
            const baseZip = `${base.id}-${base.version}.zip`;
            const baseArchivePath = upath_1.join(dest, baseZip);
            let latestProgress = 0;
            let baseProgress = 0;
            const emitProgress = () => handleProgress((latestProgress + baseProgress) / 2);
            yield Promise.all([
                this.downloadFromLink(latest.links.download, latestArchivePath, (progress) => {
                    latestProgress = progress;
                    emitProgress();
                }),
                this.downloadFromLink(base.links.download, baseArchivePath, (progress) => {
                    baseProgress = progress;
                    emitProgress();
                }),
            ]);
            return {
                content: latest,
                archives: {
                    latest: latestArchivePath,
                    base: baseArchivePath,
                },
            };
        });
    }
    downloadFromLink(url, dest, progressHandler = () => { }) {
        const file = fs_1.createWriteStream(dest);
        return new Promise((res, rej) => {
            https.get(url, (response) => {
                response.pipe(file);
                file.on('finish', () => {
                    file.close();
                    return res(true);
                });
                const len = parseInt(response.headers['content-length'], 10);
                let cur = 0;
                response.on('data', (chunk) => {
                    cur += chunk.length;
                    const progressPercentage = parseInt((100.0 * cur / len).toFixed(), 10);
                    progressHandler(progressPercentage);
                });
            })
                .on('error', (err) => {
                fs_extra_1.remove(dest, () => { });
                return rej(err);
            });
        });
    }
    archive(presentationPath) {
        return __awaiter(this, void 0, void 0, function* () {
            yield content_checker_1.validateEwizardContent(presentationPath);
            yield content_checker_1.validateInstalledDependencies(presentationPath);
            return archiveContent_1.archiveContent(presentationPath, {});
        });
    }
    getVcsInfo(cwd) {
        return __awaiter(this, void 0, void 0, function* () {
            const vcs = yield system_settings_1.getByPath(cwd, 'vcs');
            if (vcs)
                return vcs;
            const settingsPath = yield system_settings_1.getByPath(cwd, 'path.settings');
            const settingsJsonPath = upath_1.resolve(cwd, settingsPath);
            const settingsExists = yield fs_extra_1.pathExists(settingsJsonPath);
            if (settingsExists) {
                const contentSettings = require(settingsJsonPath);
                return { id: contentSettings[this.vcs_id] };
            }
        });
    }
    saveChecksum(dest) {
        return __awaiter(this, void 0, void 0, function* () {
            const checkSums = yield CheckSumsGenerator_1.default.calculate(dest);
            yield fs_extra_1.outputJSON(upath_1.join(dest, CHECKSUMS_PATH), checkSums.getChecksums());
        });
    }
    hasChanges(dest) {
        return __awaiter(this, void 0, void 0, function* () {
            const [oldChecksumJSON, newChecksum] = yield Promise.all([
                fs_extra_1.readJson(upath_1.join(dest, CHECKSUMS_PATH)),
                CheckSumsGenerator_1.default.calculate(dest),
            ]);
            return !Checksums_1.default.equals(new Checksums_1.default(oldChecksumJSON), newChecksum);
        });
    }
    removeOldAttribute(cwd) {
        return __awaiter(this, void 0, void 0, function* () {
            const settingsPath = yield system_settings_1.getByPath(cwd, 'path.settings');
            const settingJSON = yield fs_extra_1.readJson(upath_1.join(cwd, settingsPath));
            delete settingJSON[this.vcs_id];
            yield fs_extra_1.writeJson(settingsPath, settingJSON, { spaces: 2 });
        });
    }
}
exports.default = VersionControlSystem;
//# sourceMappingURL=versionControlSystem.js.map