"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PROMPT_ME_DESIGNATOR_MACOS = exports.PROMPT_ME_DESIGNATOR = void 0;
const tslib_1 = require("tslib");
require("reflect-metadata");
const core_1 = require("@oclif/core");
const inquirer = require("inquirer");
const _ = require("lodash");
const autocomplete = require("inquirer-autocomplete-prompt");
const fuzzy = require("fuzzy");
const contracts_1 = require("@codiac.io/codiac-common/contracts");
const ops_1 = require("../ops");
const command_base_enterprise_1 = require("../command-base-enterprise");
const cloud_provider_contextualizer_1 = require("../uilogic/cloud-provider-contextualizer");
let assetVersions = [];
/** The value provided as input that indicates
 * a user's desire to be prompted with a list from which choose the value(s),
 * instead of entering them manually or taking a default. */
exports.PROMPT_ME_DESIGNATOR = "?";
exports.PROMPT_ME_DESIGNATOR_MACOS = "??";
class Deploy extends command_base_enterprise_1.CommandBaseEnterprise {
    // private get helmClient(): HelmController {
    //     if (this.__helmClient === undefined) {
    //         this.__helmClient = new HelmController(this._ui, this.logger);
    //     }
    //     return this.__helmClient;
    // };
    // private __helmClient: HelmController | undefined;
    bootstrap() {
        super.bootstrap();
        cloud_provider_contextualizer_1.CloudProviderContextualizer.bootstrap(this.ioc);
    }
    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* () {
                var _a;
                try {
                    this.bootstrap();
                    const { args, flags } = yield this.parse(Deploy);
                    this._relayContainer.silent = flags.silent;
                    (_a = this._relayContainer) === null || _a === void 0 ? void 0 : _a.ready$.subscribe(() => tslib_1.__awaiter(this, void 0, void 0, function* () {
                        var _b;
                        try {
                            if (flags.silent === true)
                                this.__ui = new ops_1.ThrowingUserInputProvider(this.logger);
                            this._userCntx = yield this.establishTenantContext(this._userCntx);
                            let rootMap = yield this.getRootMap();
                            const interactionMode = this.getInteractionMode(flags.silent, flags.echo, flags['to-script']);
                            let enterprise = (yield this.establishEnterpriseContext(rootMap, flags.enterpriseCode, interactionMode)).enterprise;
                            _.remove(rootMap.enterprises, e => e.name != enterprise.name); // trim down to match legacy conditions
                            let enterpriseCode = enterprise.name;
                            // Select the asset(s) to deploy
                            let assetVersions;
                            if (flags.asset == exports.PROMPT_ME_DESIGNATOR
                                || flags.asset == exports.PROMPT_ME_DESIGNATOR_MACOS
                                || flags.asset
                                || this._projCfg === undefined) {
                                if (flags.silent && flags.asset && flags.version)
                                    assetVersions = [{ a: flags.asset, v: flags.version, t: (_b = flags.tagAsset) !== null && _b !== void 0 ? _b : undefined }];
                                else {
                                    let asset = yield this.promptForAsset(enterpriseCode, (flags.asset == exports.PROMPT_ME_DESIGNATOR || flags.asset == exports.PROMPT_ME_DESIGNATOR_MACOS) ? undefined : flags.asset);
                                    let selected = yield this.promptForVersion(enterpriseCode, asset, flags.version);
                                    if (flags.tagAsset && flags.tagAsset.length > 0)
                                        selected.t = flags.tagAsset; // ?? this.promptForTags()
                                    // if (flags.labels && flags.labels.length > 0) selected.l = this.parseToDictionary(flags.labels); // ?? this.promptForLabels()
                                    assetVersions = [selected];
                                }
                            }
                            else {
                                // USE ASSET IN PROJECT SCOPE
                                // Update sync codiac with what we've got here in the project.
                                let requestedAssets = yield this.establishAssetContext(enterprise, flags.silent);
                                let versions = yield this.declareAssetVersions(requestedAssets, enterpriseCode, this._projCfg.codeVersion);
                                // We've updated Codiac.  Now trigger the deploy.
                                assetVersions = requestedAssets.map(a => {
                                    let version = versions.find(v => v.assetName == a);
                                    let av = { a: a, v: version ? version.assetVersion : undefined };
                                    if (flags.tagAsset && flags.tagAsset.length > 0)
                                        av.t = flags.tagAsset;
                                    // if (flags.labels && flags.labels.length > 0) av.l = this.parseToDictionary(flags.labels); // ?? this.promptForLabels()
                                    return av;
                                });
                            }
                            let cabCntx;
                            let cabCluster;
                            if (flags.tag != undefined) {
                                let selector = {
                                    enterprise: enterprise.name,
                                    tags: flags.tag
                                };
                                if (flags.tagMatch != undefined) {
                                    if (["any", "all", "none"].includes(flags.tagMatch) != true) {
                                        throw new Error("tagMatch must be one of: 'any', 'all', or 'none'.");
                                    }
                                    selector.matchTags = flags.tagMatch;
                                }
                                let cabRes = yield this.genRelayClient.Call.GetCabinets(selector);
                                if (cabRes.output && cabRes.output.length > 0) {
                                    if (cabRes.output.length > 1) {
                                        this.logger.detail("Found multiple cabinets matching deployment request; selecting the first of ".concat(cabRes.output.length.toString()));
                                    }
                                    let cab = cabRes.output[0];
                                    cabCntx = Object.assign(Object.assign({}, _.pick(cab, "enterprise", "environment")), { cabinet: cab.name });
                                    cabCluster = cab.cluster;
                                }
                                else {
                                    throw new Error("No matching cabinets were found.");
                                }
                            }
                            else {
                                let cabinet = yield this.establishCabinet(flags.cabinetName, flags.silent);
                                cabCntx = { enterprise: cabinet.enterpriseCode, environment: cabinet.environment, cabinet: cabinet.name };
                                cabCluster = cabinet.cluster;
                            }
                            // Get the clusterdef so we can get cloud pvd creds
                            let cluster = cabCluster !== null && cabCluster !== void 0 ? cabCluster : ((rootMap.clusters.length === 1) ? rootMap.clusters[0].name : undefined); // fall back to the one and only.
                            if (cluster === undefined) {
                                this.logger.warn(`Cannot confirm cloud provider credentials: cabinet has no cluster specified. Process will rely on cached credentials.`);
                                // throw new Error(`Cannot determine cloud provider: cabinet has no cluster specified.`);
                            }
                            else {
                                yield this.establishCloudProviderCreds(rootMap, cluster, flags.silent, flags.providerUserName, flags.providerPassword);
                            }
                            // COMMENTED OUT BC the relay service uses the k8s api client, which isnt bound to the singleton kubeconfig file.
                            // if (!cabinet.cluster) throw new Error(`The current cabinet record [${cabinet.name}] does not have a cluster reference.`);
                            // let kubeCntx = await this.establishK8sContext(cabinet.cluster);
                            yield this._ui.startSpinner(`Deploying to cabinet [${cabCntx.enterprise}/${cabCntx.environment}/${this._styler.hilite(cabCntx.cabinet)}]...`);
                            // let result: Result = await this.relayClient.deployAssets(cabCntx, assetVersions);
                            let result = yield this.genRelayClient.Call.postExecuteDeployment({ assets: assetVersions, target: cabCntx });
                            yield this._ui.stopSpinner(result.success ? "SUCCESS" : (result.error ? "ERROR" : "FAILED"));
                            if (result.success) {
                                this._ui.userMessage(`${this._styler.colorGood("SUCCESS")}: ${result.message}`);
                            }
                            else if (result.error) {
                                this._ui.userMessage(`${this._styler.colorCritical("ERROR")}: ${result.message}`);
                                throw result.error;
                            }
                            else {
                                this._ui.userMessage(`FAILED: ${result.message}`);
                                throw result.error;
                            }
                            resolve();
                        }
                        catch (err) {
                            this.handleError(err);
                            // reject(err);  // <-- dont rethrow - err handling in the observable isnt yet user-friendly.
                        }
                        finally {
                            yield this.closeConnections();
                            return;
                        }
                    }));
                }
                catch (err) {
                    this.handleError(err);
                    reject(err);
                }
                finally {
                    yield this.closeConnections();
                }
            }));
        });
    }
    /** Expands a string list of key=value into properties on an object.
     *
     * @param theseEqualThose Array of Key-value pairs in format: `this=that`, eg: `["favorite-color=blue", "age=36"]`
     */
    parseToDictionary(theseEqualThose) {
        if (theseEqualThose && theseEqualThose.length > 0) {
            let labels = {};
            theseEqualThose.forEach(x => {
                const [key, val] = x.split("=");
                labels[key] = val;
            });
            return labels;
        }
        else {
            return {};
        }
    }
    promptForAsset(enterprise, assetSearch) {
        return new Promise((resolve, reject) => tslib_1.__awaiter(this, void 0, void 0, function* () {
            try {
                // let ecrit = new EnterpriseCriteria();
                // ecrit.code = new StringFilter();
                // ecrit.code.$eq = enterprise;
                let ecrit = { code: enterprise };
                let enterprises = yield this._codiacApi.Enterprise.readMany(ecrit, { throwNotFound: false });
                if (!enterprises || enterprises.length == 0) {
                    throw new Error(`Could not find enterprise [${enterprise}].`);
                }
                else if (!enterprises[0].assets || enterprises[0].assets.length == 0) {
                    throw new Error(`The current enterprise [${enterprise}] has no assets defined.`);
                }
                // console.log(`${readResult.output[0].assets.length} assets found`)
                // Select from assets in the current enterprise
                let assets = enterprises[0].assets;
                if (assetSearch && assetSearch != exports.PROMPT_ME_DESIGNATOR) {
                    assets = assets.filter(a => a.name.startsWith(assetSearch));
                }
                // console.log(`${assets.length} assets found`)
                let selectedAss = yield this._ui.promptSelect('asset', 'Select an Asset:', assets.map(a => a.name).sort());
                let selected = assets.find(a => a.name == selectedAss);
                if (!selected)
                    throw new Error(`User failed to select a valid asset to deploy.`);
                resolve(selected);
            }
            catch (err) {
                reject(err);
            }
        }));
    }
    promptForVersion(enterpriseCode, asset, 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* () {
                var _a, _b;
                try {
                    let providerSubscriptionId = (_a = asset.image.registry.accountId) !== null && _a !== void 0 ? _a : asset.image.registry.subscriptionId;
                    yield this.cspContextualizer.promptForCloudProviderCreds(this.ioc, asset.image.registry.provider, undefined, undefined, undefined, providerSubscriptionId);
                    this._ui.startSpinner("Loading versions...");
                    // let sorted = await this.relayClient.fetchVersions(asset.image, asset.type, versionSearch);
                    let result = yield this.genRelayClient.Call.FetchAssetVersions({ image: asset.image, assetType: asset.type, versionSearch: versionSearch });
                    if (result.error)
                        throw result.error;
                    let sorted = (_b = result.output) !== null && _b !== void 0 ? _b : [];
                    let latest = sorted.length > 0 ? sorted[0] : undefined;
                    assetVersions = sorted.map(v => { return { value: v, name: v }; });
                    this._ui.stopSpinner(`found ${assetVersions.length}`);
                    // 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 asset version:', source: this.searchVersions, default: latest }]);
                    resolve({ a: asset.name, v: verSelect.version });
                }
                catch (err) {
                    reject(err);
                }
            }));
        });
    }
    // async promptForVersionConfig_NOTUSED(enterpriseCode: string, asset: Asset, versionSearch?: string): Promise<AssetVersion> {
    //   return new Promise<AssetVersion>(async (resolve,reject) => {
    //     try {
    //       let crit = <VersionConfigCriteria>{ enterprise: enterpriseCode, assetName: asset.name };
    //       let versionFetch = await this._codiacApi.VersionConfig.tryReadMany(crit);
    //       if (!versionFetch.output || versionFetch.output.length == 0) {
    //         throw new Error(`No versions found for the asset [${asset.name}].`);
    //       }
    //       let versions: { value: string, name: string }[] = [];
    //       // let latest = new Date("2000-01-01T00:00:00.999Z");
    //       let latest = _.maxBy(versionFetch.output, v => v.created);
    //       let sorted = _.orderBy(versionFetch.output, v => v.created, 'desc');
    //       if (versionSearch) {
    //         sorted = sorted.filter(v => v.assetVersion.indexOf(versionSearch) >= 0);
    //       }
    //       if (sorted.length == 0) {
    //         throw new Error(`No versions like '${versionSearch}' found for the asset [${asset.name}].`);
    //       }
    //       for (let v of sorted) {
    //         let marker = (latest && v.assetVersion == latest.assetVersion) ? " (latest)" : "";
    //         versions.push({
    //           value: v.assetVersion,
    //           name: `${v.assetVersion}  ${this._styler.dim(new Date(v.created).toLocaleDateString())} ${marker}`
    //         });
    //       }
    //       // 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);
    //       assetVersions = versions;
    //       let verSelect = await inquirer.prompt([{ type: 'autocomplete', name: 'version', message: 'Select the asset version:', source: this.searchVersions, default: latest }]);
    //       resolve({ a: asset.name, v: verSelect.version });
    //     } catch (err) {
    //       reject(err);
    //     }
    //   });
    // }
    // private async fetchVersions(image: Image, assetType: AssetTypesEnum | undefined, versionSearch?: string): Promise<string[]> {
    //     return new Promise<string[]>(async (resolve, reject) => {
    //         try {
    //             let versionsFetch: string[];
    //             if (assetType == AssetTypesEnum.helm) {
    //                 let chartName = image.name;
    //                 let repoName = image.registry.code;
    //                 let chartRegisryUrl = image.registry.url;
    //                 let chartVersions = await this.helmClient.getVersionsOf(chartName, repoName, chartRegisryUrl);
    //                 versionsFetch = chartVersions.output?.map(x => x.version) || [];
    //             } else {
    //                 let result = await this.relayClient.tryReadImageTags(image.registry.url, (image.scope) ? `${image.scope}/${image.name}` : image.name);  // is scope already prefixing the name?
    //                 versionsFetch = result.output;
    //             }
    //             if (!versionsFetch || versionsFetch.length == 0) {
    //                 throw new Error(`No versions found for the ${assetType} [${image.name}].`);
    //             }
    //             // let sorted = versionsFetch.filter(v => semver.clean(v)).sort(semver.rcompare);
    //             versionsFetch = versionsFetch.filter(av => av !== null && av !== undefined);  // Ignore untagged images
    //             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 ${assetType} [${image.name}].`);
    //             }
    //             // 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, assetVersions, options);
                        let filtered = results.map((fr) => fr.original);
                        resolve(filtered);
                    }, _.random(30, 500)); // why a random number?
                }
                catch (err) {
                    reject(err);
                }
            }));
        });
    }
    /** Writes Asset versions to the Codiac Api. */
    declareAssetVersions(assets, enterpriseCode, versionToUse) {
        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 versions = [];
                    let newVersions = [];
                    for (let asset of assets) {
                        let thisVersion = versionToUse;
                        let versionName = `${asset}-${thisVersion}`;
                        let vc = Object.assign({ name: versionName, enterprise: enterpriseCode, assetName: asset, assetVersion: thisVersion }, this.createAuditable());
                        // Making a call over the wire for every iteration in this loop JUST PLAIN SUCKS, but
                        // there's not currently a way to spec the following:
                        /*
                        filterDef = {
                          enterprise: cabCntx.enterprise,
                          labels: [
                            { cabinet: cabCntx.cabinet }
                          ],
                          $or: [
                            { assetName: a1.assetName, version: a1.version },
                            { assetName: a2.assetName, version: a2.version },
                            ...
                            { assetName: aN.assetName, version: aN.version }
                          ]
                        }
                        */
                        let crit = {
                            enterprise: vc.enterprise,
                            assetName: vc.assetName,
                            assetVersion: vc.assetVersion
                        };
                        let check = yield this._codiacApi.VersionConfig.tryReadMany(crit);
                        if (check.error) {
                            this.logger.debug(`Error checking existence of VersionConfig: [${vc.assetName}, v${vc.assetVersion}]: ${check.message || ""}; ${check.error.stack}`);
                        }
                        else if (check.count == 0) {
                            // Idempotence: create this version config only if doesnt already exist
                            newVersions.push(vc);
                        }
                        else {
                            versions.push(...check.output);
                            this.logger.debug(`Skipping version declaration for [${vc.assetName}:${vc.assetVersion}]: VersionConfig already exists [created: ${vc.created.toUTCString()}].`);
                        }
                    }
                    if (newVersions && newVersions.length > 0) {
                        let result = yield this._codiacApi.VersionConfig.tryCreateMany(newVersions);
                        if (result.success) {
                            versions.push(...result.successes);
                            this.logger.debug(`Asset versions successfully declared: [${newVersions.map(v => v.assetName + ':' + v.assetVersion).join(", ")}].`);
                        }
                        else {
                            // Throw, even on partial successes.
                            if (result.successes && result.successes.length > 0) {
                                this.logger.debug(`Some Asset version declarations succeeded: ${result.successes.map(v => v.assetName + ':' + v.assetVersion).join(", ")}.`);
                            }
                            if (result.failures && result.failures.length > 0) {
                                this.logger.debug(`Asset version declaration failures: ${result.failures.map(f => f.message)}`);
                            }
                            let failureCount = (result.failures) ? result.failures.length : 0;
                            let successCount = (result.successes) ? result.successes.length : 0;
                            if (result.error) {
                                throw new contracts_1.CodiacError(`Error declaring version configs for [${assets.join(",")}]: ${result.message || ""}; ${result.error.message}`, result.error);
                            }
                            else {
                                throw new contracts_1.CodiacError(`Partial failure declaring version configs  [${successCount} succeeded, ${failureCount} failed] for ${assets.join(",")}: ${result.message}`);
                            }
                        }
                    }
                    resolve(versions);
                }
                catch (err) {
                    reject(err);
                }
            }));
        });
    }
}
exports.default = Deploy;
Deploy.description = 'Deploys a given asset to a given cabinet.  Defaults to current version.';
Deploy.dependsOn = ["enterpriseCode", "cabinetName"];
Deploy.flags = {
    // ...commandBase.globalFlags,
    help: core_1.Flags.help({ char: 'h' }),
    enterpriseCode: core_1.Flags.string({ char: 'e', required: false, description: `Code (name) of the enterprise to deploy` }),
    cabinetName: core_1.Flags.string({ char: 'c', required: false, description: `Target cabinet name you want deploy` }),
    tag: core_1.Flags.string({ char: 't', exclusive: ["cabinetName"], required: false, multiple: true, description: `Cabinet tags with which to declare the target cabinet (instead of by name).` }),
    tagMatch: core_1.Flags.string({ char: 'm', required: false, dependsOn: ["tag"], options: ["any", "all", "none"], description: `(Optional: Defaults to any) Declares the logic that will be used to select cabinets from the given tags.` }),
    asset: core_1.Flags.string({ char: 'a', required: false, description: `Search string for the name of the asset to deploy (use ${exports.PROMPT_ME_DESIGNATOR} or ${exports.PROMPT_ME_DESIGNATOR_MACOS} to be prompted).` }),
    version: core_1.Flags.string({ char: 'u', required: false, description: `Search string for the version of the asset to deploy  (use ${exports.PROMPT_ME_DESIGNATOR} or ${exports.PROMPT_ME_DESIGNATOR_MACOS} to be prompted).` }),
    tagAsset: core_1.Flags.string({ char: 'w', multiple: true, required: false, description: `Tags to apply to the selected asset version.` }),
    // labels: Flags.string({ char: 'l', multiple: true, required: false, description: `Labels (aka: key-value pairs) to apply to the selected asset version.  Additive; any labels applied here will be added (but not duplicated) to any existing labels on the asset version.  Must be provided in format: key=value, eg: -l workitem=abc123 -l sprint=Sprint15` }),
    providerUserName: core_1.Flags.string({ char: 'n', required: false, description: `OPTIONAL: For silent/scripted calls, bring cloud creds to auth against` }),
    providerPassword: core_1.Flags.string({ char: 'p', required: false, description: `OPTIONAL: For silent/scripted calls, bring cloud creds to auth against` }),
    silent: core_1.Flags.boolean({ char: 'y', dependsOn: Deploy.dependsOn, required: false, description: `(Optional: defaults to false) Prevents confirmations of user-values that are remembered from prior runs. Requires: ${Deploy.dependsOn}` })
};
Deploy.args = {};
//# sourceMappingURL=deploy.js.map