"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const core_1 = require("@oclif/core");
const semver = require("semver");
const child_process_1 = require("child_process");
const nodeUtil = require("util");
const _ = require("lodash");
const fuzzy = require("fuzzy");
const autocomplete = require("inquirer-autocomplete-prompt");
const inquirer = require("inquirer");
const codiac_ops_common_1 = require("@codiac.io/codiac-ops-common");
const command_base_tenantless_1 = require("../../command-base-tenantless");
const ops_1 = require("../../ops");
let execAsync = nodeUtil.promisify(child_process_1.exec);
// export type PromptChoice = { value: string, name: string };
let availableVersions = [];
class DepUpgrade extends command_base_tenantless_1.CommandBaseTenantless {
    run() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                try {
                    this.bootstrap();
                    const { args, flags } = yield this.parse(DepUpgrade);
                    let projectRoot = ops_1.CliPathWalker.getProjectRoot(undefined, false);
                    // let projectRoot = this._ops.getConfigRoot(undefined, false);
                    let dep = args.dependency;
                    let version = flags.version;
                    // Support version suffixes (eg: my-fav-pkg@3.14.5)
                    if (dep && dep.indexOf("@") > 0) {
                        if (flags.version && flags.version != dep.split("@")[1]) {
                            throw new Error(`Conflicting versions specified: the version suffix on (${args.dependency}) versus the -v|--version parameter (${flags.version}). No work was done.`);
                        }
                        else {
                            version = dep.split("@")[1];
                        }
                        dep = dep.split("@")[0];
                    }
                    // let depNames: string[] = await this._ops.getNpmDependencies(projectRoot);
                    // if (!dep || depNames.indexOf(dep) < 0) {
                    //   let result = await inquirer.prompt({ type: 'list', name: 'name', message: 'dependency to edit:', choices: depNames });
                    this._ui.startSpinner("checking for available upgrades...");
                    let upgrades = yield this.getNpmUpgrades(projectRoot);
                    this._ui.stopSpinner(`found ${Object.keys(upgrades).length}`);
                    if (!dep || !upgrades[dep]) {
                        // this.logger.trace("upgrades", upgrades);
                        let widthPkg = 15;
                        let widthCur = "Current".length;
                        let widthWan = "Wanted".length;
                        let widthLat = "Latest".length;
                        for (let pkg in upgrades) {
                            widthPkg = (pkg.length > widthPkg) ? pkg.length : widthPkg;
                            widthCur = (upgrades[pkg].current && upgrades[pkg].current.length > widthCur) ? upgrades[pkg].current.length : widthCur;
                            widthWan = (upgrades[pkg].wanted && upgrades[pkg].wanted.length > widthWan) ? upgrades[pkg].wanted.length : widthWan;
                            widthLat = (upgrades[pkg].latest && upgrades[pkg].latest.length > widthLat) ? upgrades[pkg].latest.length : widthLat;
                        }
                        let header = `  ${"Name".padEnd(widthPkg)}  ${"Current".padEnd(widthCur)}  ${"Wanted".padEnd(widthWan)}  ${"Latest".padEnd(widthLat)}`;
                        let choices = [];
                        for (let pkg in upgrades) {
                            let up = upgrades[pkg];
                            let cols = [];
                            cols.push(pkg.padEnd(widthPkg));
                            cols.push(this._styler.dim((up.current || "missing").padEnd(widthCur)));
                            cols.push(((up.wanted == up.current) ? this._styler.dim(this._styler.colorGood("-----".padEnd(widthWan))) : this._styler.colorGood(up.wanted.padEnd(widthWan))));
                            cols.push(this._styler.hilite(((up.latest == up.wanted) ? "<----" : up.latest).padEnd(widthLat)));
                            let line = cols.join("  ");
                            // let line = `${pkg.padEnd(widthPkg)}  ${this._styler.dim(up.current.padEnd(widthCur))}  ${this._styler.colorGood(up.wanted.padEnd(widthWan))}  ${inOrange(((up.latest == up.wanted) ? "<----" : up.latest).padEnd(widthLat))}`
                            choices.push({ value: pkg, name: line });
                        }
                        // let result = await inquirer.prompt({ type: 'list', name: 'name', message: `Dependency to upgrade:\n${header}\n`, choices: choices });
                        if (choices.length > 0) {
                            dep = yield this._ui.promptSelect("dep", `Dependency to upgrade:\n${header}\n`, choices);
                        }
                        else {
                            this.logger.notice(`All up to date!  No upgrades to make.`);
                            this._ui.userMessage(`All up to date!  No upgrades to make.`);
                        }
                        // this.logger.trace(`selected: result.name`);
                        // if (!dep) {
                        //   this._ui.userMessage("Operation cancelled: No work was done.");
                        //   this.exit(0);
                        // } else {
                        if (dep && upgrades[dep]) {
                            // dep = result.name;
                            let up = upgrades[dep];
                            version = version || (yield this._ui.prompt("version", `version (enter for latest) `, false, up.latest));
                            // TODO: use this instead - its a picklist and will force a valid version selection.
                            // version = version ?? await this.promptForVersion(dep, up.latest);
                        }
                    }
                    if (dep && version) {
                        yield this._ops.dependencyUpgrade(dep, version);
                        if (version) {
                            this._ui.userMessage(`COMPLETED!  Successfully upgraded ${dep} to version ${version}`);
                        }
                        else {
                            let pj = yield this._ops.readNpmPackageJson();
                            if (pj.dependencies && pj.dependencies[dep]) {
                                let latest = pj.dependencies[dep];
                                this._ui.userMessage(`COMPLETED!  Successfully upgraded ${this._styler.hilite(dep)} to latest version ${this._styler.hilite(latest)}`);
                            }
                            else {
                                // this should never happen
                                throw new Error(`Upgrade operation was completed but there was a problem finding the dependency in the package definition file.  Please check the dependencies in your package.json.`);
                            }
                        }
                    }
                    else {
                        this.logger.notice(`No work was done.`);
                        this._ui.userMessage(`No work was done.`);
                    }
                    resolve();
                }
                catch (err) {
                    this.handleError(err);
                    reject(err);
                }
            }));
        });
    }
    promptForVersion(packageName, defaultVersion, versionSearch) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                try {
                    let sorted = yield this.fetchVersions(packageName, versionSearch);
                    availableVersions = sorted.map(v => { return { value: v, name: v }; });
                    let defaultIndex = 8; // (defaultVersion) ? sorted.indexOf(defaultVersion) : 0;
                    // let defaultSelection = availableVersions.find(s => s.value == defaultVersion)?.value ?? availableVersions[0];
                    let defaultSelection = availableVersions[defaultIndex];
                    this.logger.trace(`Default selection for ${packageName} --> ${defaultSelection.value}`);
                    // Without autocomplete...
                    // let verSelect = await inquirer.prompt({ type: 'list', name: 'version', message: 'Select the asset version:', choices: versions, default: latest });
                    // With fuzzy autocomplete...
                    inquirer.registerPrompt('autocomplete', autocomplete);
                    let verSelect = yield inquirer.prompt([{ type: 'autocomplete', name: 'version', message: 'Select the version:', source: this.searchVersions, default: defaultSelection }]);
                    resolve(verSelect.version);
                }
                catch (err) {
                    reject(err);
                }
            }));
        });
    }
    fetchVersions(packageName, versionSearch) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                try {
                    let exec = new codiac_ops_common_1.CliExec(this.logger);
                    let versionsFetch = yield exec.execAsyncJson(`npm view ${packageName} versions --json`, { cwd: process.cwd() });
                    if (!versionsFetch || versionsFetch.length == 0) {
                        throw new Error(`No versions found for the package [${packageName}].`);
                    }
                    // let sorted = versionsFetch.filter(v => semver.clean(v)).sort(semver.rcompare);
                    let semvers = _.remove(versionsFetch, v => semver.clean(v)).sort(semver.rcompare); // removes semvers so they can be sorted uniquely
                    let sorted = semvers.concat(versionsFetch.sort().reverse());
                    // let sorted = versionsFetch.reverse();  // the reg client shd do this for us...
                    if (versionSearch) {
                        sorted = sorted.filter(v => v.indexOf(versionSearch) >= 0); // this will give too many results - it shd be an equals not a contains
                    }
                    if (sorted.length == 0) {
                        throw new Error(`No versions like '${versionSearch}' found for the package [${packageName}].`);
                    }
                    // TODO:  It would be nice to mark versions with their dates or their canonical tags (latest, stable, etc)
                    //  eg:  name = `${v.assetVersion}  ${this._styler.dim(new Date(v.created).toLocaleDateString())} (latest)`;
                    resolve(sorted);
                }
                catch (err) {
                    reject(err);
                }
            }));
        });
    }
    searchVersions(answers, input) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                try {
                    // Whatever voodoo they're doing inside the autocomplete plugin, 
                    // they're redefining "this", so we cant use it here.  
                    // Great, we'll just HACK UP a fucking module scoped variable to hold onto the goddamn list then.
                    // let vers = this.getVersions();  
                    input = input || "";
                    setTimeout(() => {
                        let options = { extract: (ver) => { return ver.value; } };
                        let results = fuzzy.filter(input, availableVersions, options);
                        let filtered = results.map((fr) => fr.original);
                        resolve(filtered);
                    }, _.random(30, 500)); // why a random number?
                }
                catch (err) {
                    reject(err);
                }
            }));
        });
    }
    /** Retrieves the list of dependencies that are out of date with the registry.
     * Provides for each package: the version currently installed,
     * the latest version that complies with the symantic range,
     * and the latest version overall.
     */
    getNpmUpgrades(projectRoot) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                try {
                    let cmd = "npm outdated --json";
                    let exec = new codiac_ops_common_1.CliExec(this.logger);
                    // FUCKING NPM EVERY TIME.  What a bunch of hacks.
                    // They return a Code = 1  on a successful run of this command, 
                    // which is an error scenario, and node throws.
                    // So we have to manually catch it and check it and continue...  
                    // NOPE.  Cant do this...   let outputJson = await exec.execAsyncJson(cmd, { cwd: projectRoot });
                    let result;
                    try {
                        result = yield execAsync(cmd, { cwd: projectRoot });
                    }
                    catch (err) {
                        if (err.code == 1 && (err.stderr == undefined || err.stderr == "")) {
                            // This is the success scenario.
                            result = { stdout: err.stdout }; // Look what Im writing here!  Goddamn this pisses me off.
                        }
                        else {
                            throw err;
                        }
                    }
                    let outputJson = result.stdout;
                    // this.logger.trace("parsing json...", outputJson);
                    let output = JSON.parse(outputJson);
                    // this.logger.trace("output", output);
                    resolve(output);
                }
                catch (err) {
                    reject(err);
                }
            }));
        });
    }
}
exports.default = DepUpgrade;
DepUpgrade.description = 'describe the command here';
DepUpgrade.flags = {
    help: core_1.Flags.help({ char: 'h' }),
    version: core_1.Flags.string({ char: 'v', description: 'version of the package to install' })
};
DepUpgrade.args = {
    dependency: core_1.Args.string({ description: 'The dependency to upgrade' })
};
//# sourceMappingURL=upgrade.js.map