"use strict";
/*
 * Copyright 2018, collaboration Factory AG. All rights reserved.
 */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    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) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const path = require("path");
const process = require("process");
const fs = require("fs");
const fs_1 = require("fs");
const crypto = require("crypto");
const spawn = require("cross-spawn");
const chokidar = require("chokidar");
const executor_1 = require("../executor");
const utils_1 = require("../utils");
const PackageVersion_1 = require("./PackageVersion");
const rimraf = require("rimraf");
const child_process_1 = require("child_process");
class NPMResolver {
    constructor(mainRepo, watch) {
        this.watch = watch;
        this.mainRepo = mainRepo;
        this.hashFilePath = this.getHashFilePath();
        this.watchers = [];
    }
    /**
     * Installs plugin dependencies and create hash
     * @param pluginName Plugin name
     * @param assetsPath Assets folder path
     */
    static installPluginDependenciesAndCreateHash(pluginName, assetsPath) {
        if (fs.existsSync(NPMResolver.getPluginHashFilePath(assetsPath))) {
            const packageLockUpdated = NPMResolver.packageLockWasUpdated(NPMResolver.getPluginHashFilePath(assetsPath), NPMResolver.getPluginPackageLockPath(assetsPath), pluginName);
            if (packageLockUpdated) {
                console.log(utils_1.cgreen `⇢`, `[${pluginName}] (NPM) package-lock.json was updated...`);
            }
            const hasNodeModules = NPMResolver.getPluginNodeModulesPath(assetsPath);
            if (!hasNodeModules || packageLockUpdated) {
                return NPMResolver.installPluginDependencies(pluginName, assetsPath);
            }
            return false;
        }
        else {
            return NPMResolver.installPluginDependencies(pluginName, assetsPath);
        }
    }
    /**
     * Gets plugin node_modules path
     * @param assetsPath Assets path
     * @private
     */
    static getPluginNodeModulesPath(assetsPath) {
        return path.resolve(assetsPath, NPMResolver.NODE_MODULES);
    }
    /**
     * Installs plugin dependencies
     * @param pluginName Plugin name
     * @param assetsPath Assets folder path
     * @private
     */
    static installPluginDependencies(pluginName, assetsPath) {
        const oldCwd = process.cwd();
        process.chdir(assetsPath);
        console.log(`⟲ [${pluginName}] (NPM) installing dependencies...`);
        const res = spawn.sync('npm', ['install', '--no-save']);
        if (res.status !== 0) {
            utils_1.debug(`[${pluginName}] (NPM) installing dependencies failed with error ${res.stderr}`);
            throw Error(`[${pluginName}] (NPM) installing dependencies failed!`);
        }
        console.log(utils_1.cgreen `✓`, `[${pluginName}] (NPM) dependencies successfully installed`);
        NPMResolver.createPluginHashFile(assetsPath);
        NPMResolver.removePluginSymlinks(pluginName, assetsPath);
        process.chdir(oldCwd);
        return true;
    }
    /**
     * Removes symlinks inside plugin node_modules folder
     * @param pluginName Plugin name
     * @param assetsPath Assets folder path
     * @private
     */
    static removePluginSymlinks(pluginName, assetsPath) {
        const isWindows = process.platform === 'win32';
        if (isWindows) {
            console.log(`⟲ [${pluginName}] (NPM) removing symlinks...`);
            const nodeModulesPath = path.resolve(assetsPath, 'node_modules');
            fs.readdirSync(nodeModulesPath).forEach(dir => {
                const dirPath = path.resolve(nodeModulesPath, dir);
                if (fs.lstatSync(dirPath).isSymbolicLink()) {
                    rimraf.sync(dirPath);
                }
            });
            console.log(utils_1.cgreen `✓`, `[${pluginName}] (NPM) symlinks removed`);
        }
    }
    /**
     * Gets package-lock.json hash
     * @param packageLockPath package-lock.json path
     * @private
     */
    static getHash4PackageLock(packageLockPath) {
        const hash = crypto.createHash('sha256');
        const data = fs.readFileSync(packageLockPath);
        hash.update(data);
        return hash.digest('hex');
    }
    /**
     * Gets plugin hash file path
     * @param assetsPath
     * @private
     */
    static getPluginHashFilePath(assetsPath) {
        return path.join(assetsPath, NPMResolver.NODE_MODULES, NPMResolver.PACKAGE_LOCK_HASH);
    }
    /**
     * Gets plugin package-lock.json path
     * @param assetsPath Assets path
     * @private
     */
    static getPluginPackageLockPath(assetsPath) {
        return path.resolve(assetsPath, NPMResolver.PACKAGE_LOCK_JSON);
    }
    /**
     * Creates plugin hash file
     * @param assetsPath Assets path
     * @private
     */
    static createPluginHashFile(assetsPath) {
        fs.writeFileSync(NPMResolver.getPluginHashFilePath(assetsPath), NPMResolver.getHash4PackageLock(NPMResolver.getPluginPackageLockPath(assetsPath)), { encoding: 'utf8' });
    }
    /**
     * Checks if package-lock.json was updated
     * @param hashFilePath Hash file path
     * @param packageLockPath package-lock.json path
     * @param pluginName Provided plugin name
     * @private
     */
    static packageLockWasUpdated(hashFilePath, packageLockPath, pluginName) {
        const oldHash = fs.readFileSync(hashFilePath, { encoding: 'utf8' });
        if (oldHash === NPMResolver.getHash4PackageLock(packageLockPath)) {
            const pluginLog = pluginName ? `[${pluginName}] ` : '';
            console.log(utils_1.cgreen `✓`, `${pluginLog}(NPM) node_modules are up to date`);
            return false;
        }
        return true;
    }
    static shouldResolveNpmModules() {
        return PackageVersion_1.PackageVersion.get().major !== 1;
    }
    resolve() {
        return __awaiter(this, void 0, void 0, function* () {
            this.initRegistry();
            this.checkAndInstall();
            if (this.watch) {
                this.registerWatchers();
            }
            return Promise.resolve();
        });
    }
    stop() {
        this.watchers.forEach(watcher => {
            watcher.close();
        });
    }
    registerWatchers() {
        // packageLockWatcher
        const packageJsonWatcher = chokidar.watch([this.getPackageLockPath()]);
        this.watchers.push(packageJsonWatcher);
        packageJsonWatcher
            .on('change', () => {
            this.checkAndInstall();
        })
            .on('error', (e) => {
            console.error(utils_1.cerr `(NPM) error while watching package-lock.json: ${e}`);
            packageJsonWatcher.close();
        });
        // nodeModulesWatcher
        const glob = executor_1.Scheduler.convertToUnixPath(`${this.getNodeModulesPath()}`);
        const nodeModulesWatcher = chokidar.watch(glob);
        this.watchers.push(nodeModulesWatcher);
        let ready = false;
        let debounce;
        const handleEvent = () => {
            if (!ready) {
                return;
            }
            debounce && clearTimeout(debounce);
            debounce = setTimeout(() => {
                if (!fs.existsSync(this.getNodeModulesPath())) {
                    console.log(utils_1.cerr `(NPM) node_modules folder has been removed - restart cplace-asc`);
                    process.exit();
                }
                else {
                    this.checkAndInstall();
                }
            }, 500);
        };
        nodeModulesWatcher
            .on('ready', () => ready = true)
            .on('unlink', handleEvent)
            .on('unlinkDir', handleEvent)
            .on('error', (e) => {
            console.error(utils_1.cerr `(NPM) node_modules watcher failed: ${e}`);
            nodeModulesWatcher.close();
        });
    }
    checkAndInstall() {
        if (!NPMResolver.shouldResolveNpmModules()) {
            // clean up the checked-in node_modules if required
            if (fs_1.existsSync(path.join(NPMResolver.NODE_MODULES, 'webdriverio'))) {
                console.log(utils_1.cgreen `⇢`, "Deleting the node_modules folder...");
                rimraf.sync(path.join(NPMResolver.NODE_MODULES));
                // Fun on Windows! rmdirSync can return before the folder is actually deleted completely.
                utils_1.debug("Deleted, try to recreate...");
                let delCount = 0;
                while (true) {
                    if (delCount > 20) {
                        throw "Waited too long for the node_modules folder to be deleted. Giving up.";
                    }
                    try {
                        fs.mkdirSync(NPMResolver.NODE_MODULES);
                    }
                    catch (e) {
                        utils_1.debug(e);
                        utils_1.debug("Wait a little...");
                        utils_1.sleepBusy(1000);
                        delCount++;
                        continue;
                    }
                    utils_1.debug("Delete again...");
                    rimraf.sync(path.join(NPMResolver.NODE_MODULES));
                    break;
                }
            }
            if (!fs_1.existsSync(NPMResolver.NODE_MODULES)) {
                console.log(utils_1.cgreen `⇢`, "Checkout the node_modules from Git");
                spawn.sync('git', ['checkout', '--', NPMResolver.NODE_MODULES], {
                    stdio: [process.stdin, process.stdout, process.stderr],
                    cwd: this.mainRepo
                });
            }
            console.log(utils_1.cgreen `⇢`, `(NPM) package.json:v1.0.0 -> node_modules checked in`);
            return;
        }
        else {
            console.log(utils_1.cgreen `⇢`, `(NPM) package.json:>v2.0.0 -> checking for npm install`);
        }
        if (this.hasNoNodeModules()) {
            console.log(utils_1.cgreen `⇢`, `(NPM) node_modules don't exist...`);
            this.doNpmInstallAndCreateHash();
        }
        else {
            if (fs.existsSync(this.hashFilePath)) {
                if (NPMResolver.packageLockWasUpdated(this.hashFilePath, this.getPackageLockPath())) {
                    console.log(utils_1.cgreen `⇢`, `(NPM) package-lock.json was updated...`);
                    this.doNpmInstallAndCreateHash();
                }
            }
            else {
                this.doNpmInstallAndCreateHash();
            }
        }
    }
    doNpmInstallAndCreateHash() {
        console.log(`⟲ (NPM) executing npm install`);
        const result = spawn.sync('npm', ['install'], {
            stdio: [process.stdin, process.stdout, process.stderr],
            cwd: this.mainRepo
        });
        if (result.status !== 0) {
            console.log(utils_1.cred `✗`, `(NPM) npm install ran into: ${result.error} and failed`);
            throw Error(`✗ (NPM) npm install failed...`);
        }
        console.log(utils_1.cgreen `⇢`, `(NPM) npm install successful`);
        this.createHashFile();
    }
    createHashFile() {
        fs.writeFileSync(this.hashFilePath, NPMResolver.getHash4PackageLock(this.getPackageLockPath()), { encoding: 'utf8' });
    }
    hasNoNodeModules() {
        const nodeModulesPath = this.getNodeModulesPath();
        if (fs.existsSync(nodeModulesPath)) {
            const directories = fs.readdirSync(nodeModulesPath)
                .filter((name) => fs.statSync(path.join(nodeModulesPath, name)).isDirectory());
            if (directories.length === 0) {
                return true;
            }
        }
        else {
            fs.mkdirSync(this.getNodeModulesPath());
            return true;
        }
        return false;
    }
    getHashFilePath() {
        return path.resolve(this.mainRepo, NPMResolver.NODE_MODULES, NPMResolver.PACKAGE_LOCK_HASH);
    }
    getNodeModulesPath() {
        return path.resolve(this.mainRepo, NPMResolver.NODE_MODULES);
    }
    getPackageLockPath() {
        return path.resolve(this.mainRepo, NPMResolver.PACKAGE_LOCK_JSON);
    }
    initRegistry() {
        console.info("⟲ Initialising cplace jfrog registry for NPM");
        const registry = '//cplace.jfrog.io/artifactory/api/npm/cplace-npm-local/';
        const npmConfig = child_process_1.execSync('npm config ls -l').toString();
        const npmrcPath = (npmConfig.match(/userconfig *= *".*"/gi) || [])[0];
        const cleanNpmrcPath = npmrcPath.replace(/userconfig *= */, '').replace(/"/gi, '').replace(/\\\\/g, '\\');
        if (!cleanNpmrcPath) {
            console.error(utils_1.cred `✗`, 'No user npmrc found');
            return;
        }
        if (process.env.ENV_CPLACE_ARTIFACTORY_ACTOR && process.env.ENV_CPLACE_ARTIFACTORY_TOKEN) {
            console.info("⟲ Configuring npm jfrog registry via environment variables");
            if (!this.isJFrogConfigured(registry, cleanNpmrcPath)) {
                this.writeNPMRC(registry, cleanNpmrcPath, Buffer.from(`${process.env.ENV_CPLACE_ARTIFACTORY_ACTOR}:${process.env.ENV_CPLACE_ARTIFACTORY_TOKEN}`).toString('base64'), process.env.ENV_CPLACE_ARTIFACTORY_ACTOR);
            }
            else {
                console.info(utils_1.cgreen `✓`, 'cplace npmrc configuration for jfrog already found');
            }
        }
        else {
            const gradleProps = (child_process_1.execSync(path.join(this.mainRepo, 'gradlew properties')) || '').toString();
            if (!gradleProps) {
                console.error(utils_1.cred `✗`, 'Gradle Properties not found');
            }
            else {
                const token = (gradleProps.match(/repo\.cplace\.apiToken: *([a-z0-9]+)/gi) || [])[0];
                const user = (gradleProps.match(/repo\.cplace\.apiTokenUser: *([a-z0-9@\-_\.]+)/gi) || [])[0];
                if (token && user) {
                    const cleanToken = token.replace(/repo\.cplace\.apiToken: */, '');
                    const cleanUser = user.replace(/repo\.cplace\.apiTokenUser: */, '');
                    if (!this.isJFrogConfigured(registry, cleanNpmrcPath)) {
                        this.writeNPMRC(registry, cleanNpmrcPath, Buffer.from(`${cleanUser}:${cleanToken}`).toString('base64'), cleanUser);
                    }
                    else {
                        console.info(utils_1.cgreen `✓`, 'cplace npmrc configuration for jfrog already found');
                    }
                }
                else {
                    console.error(utils_1.cred `✗`, 'jfrog credentials for Gradle not found or configured correctly:', 'https://docs.cplace.io/dev-docs/cplace-architecture/platform-component/build-system/java-artifact-based-builds/#creating-an-api-token-on-cplacejfrogio');
                }
            }
        }
    }
    isJFrogConfigured(registry, npmrcPath) {
        utils_1.debug(`Checking for npmrc at ${npmrcPath}`);
        if (!fs.existsSync(npmrcPath)) {
            return false;
        }
        const currentNpmrcConfig = fs.readFileSync(npmrcPath, { encoding: 'utf-8' }).toString();
        return currentNpmrcConfig.includes(registry);
    }
    writeNPMRC(registry, npmrcPath, auth, user) {
        console.info("⟲ Writing npm config to:", npmrcPath);
        let npmrc = `\n@cplace-next:registry=https:${registry}\n`;
        npmrc = npmrc + `${registry}:_auth=${auth}\n`;
        npmrc = npmrc + `${registry}:always-auth=true\n`;
        npmrc = npmrc + `${registry}:email=${user}\n`;
        console.log(utils_1.cgreen `✓`, 'Configured cplace jfrog to .npmrc: ', npmrcPath);
        fs.appendFileSync(npmrcPath, npmrc, { encoding: 'utf-8' });
    }
    ;
}
NPMResolver.PACKAGE_LOCK_HASH = 'package-lock.hash';
NPMResolver.PACKAGE_LOCK_JSON = 'package-lock.json';
NPMResolver.NODE_MODULES = 'node_modules';
exports.NPMResolver = NPMResolver;
//# sourceMappingURL=NPMResolver.js.map