"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const core_1 = require("@oclif/core");
const contracts_1 = require("../../relay/contracts");
const command_base_exec_1 = require("../../command-base-exec");
const codiac_constants_1 = require("../../codiac-constants");
const config_ui_utils_1 = require("../../uilogic/config-ui-utils");
const cli_common_1 = require("../../cli-common");
const _ = require("lodash");
const type_safety_utils_1 = require("../../uilogic/type-safety-utils");
class AssetDeploy extends command_base_exec_1.CommandBaseExec {
    constructor() {
        super(...arguments);
        this.versionsByAsset = {};
    }
    captureArgs() {
        var _a, _b;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { args, flags } = yield this.parse(AssetDeploy);
            let partial = {};
            if (flags.enterpriseCode)
                partial.enterprise = flags.enterpriseCode;
            if (flags.cabinetName)
                partial.cabinet = flags.cabinetName;
            if (flags.tag)
                partial.tags = flags.tag;
            if (flags.tagMatch)
                partial.matchTags = type_safety_utils_1.TypeSafetyUtils.toEnum(contracts_1.AnyAllNoneEnum, flags.tagMatch); // already validated by oclif using its flag options
            if (flags.asset)
                partial.asset = flags.asset;
            if (flags.version)
                partial.version = flags.version;
            if (flags.providerUserName)
                partial.providerUserName = flags.providerUserName;
            if (flags.providerPassword)
                partial.providerPassword = flags.providerPassword;
            if (flags['aws-session-token'])
                partial.proprietaryAuthInput = { sessionToken: flags['aws-session-token'] };
            // ---------------------------------------------------------------
            // TODO: move everything from here down into the base class
            // ---------------------------------------------------------------
            const interactionMode = (this.pipedInput) ? config_ui_utils_1.InteractionModes.silent : (_b = (_a = undefined !== null && undefined !== void 0 ? undefined : (flags.echo ? config_ui_utils_1.InteractionModes.echo : undefined)) !== null && _a !== void 0 ? _a : (flags["to-script"] ? config_ui_utils_1.InteractionModes.toscript : undefined)) !== null && _b !== void 0 ? _b : (flags.silent ? config_ui_utils_1.InteractionModes.silent : undefined);
            const takeDefaults = flags['take-defaults'];
            return [partial, interactionMode, takeDefaults];
        });
    }
    buildCandidate_silently(partial, rootMap) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            // Validate asset
            let ent = rootMap.enterprises.find(e => e.name == partial.enterprise);
            if (ent == undefined)
                throw new Error("Selected enterprise does not exist.");
            if (partial.asset == undefined) {
                throw new Error("Asset to deploy must be provided");
            }
            if (!ent.assets.find(a => a.name == partial.asset)) {
                throw new Error("Given asset was not found in enterprise: ".concat(ent.name));
            }
            if (partial.version == undefined) {
                throw new Error("Asset version to deploy must be provided.");
            }
            else {
                // We could validate here, but because it takes such a long time to do so, 
                // we can avoid the query on the registry 
                // by letting a bad version number fail later in the deployment 
            }
            if ((partial.cabinet == undefined) && (partial.tags == undefined || partial.tags.length == 0)) {
                throw new Error("Target cabinet must be specified by name or tag.");
            }
            // Validate cabinet and environment
            if (partial.cabinet) {
                let found;
                // Validate environment
                let env;
                if (partial.environment) {
                    env = ent.environments.find(e => e.name == partial.environment);
                    if (env == undefined) {
                        throw new Error("Target environment was not found in enterprise ".concat(ent.name, "."));
                    }
                }
                // validate cabinet name
                let envs = env ? [env] : ent.environments;
                for (let env of envs) {
                    found = env.cabinets.find(c => c.name == partial.cabinet);
                    if (found) {
                        partial.environment = env.name;
                        break;
                    }
                }
                if (found == undefined) {
                    throw new Error("Target cabinet was not found in enterprise ".concat(ent.name, "."));
                }
            }
            // Validate cabinet tags
            if (partial.tags) {
                // This same logic gets fired during the deployment itself, 
                // but just for good practice validation, 
                // check it anyway so we can throw early if need be.
                let cabRes = yield this.genRelayClient.Call.GetCabinets(partial);
                if (cabRes.output == undefined || cabRes.output.length == 0) {
                    throw new Error("No matching cabinets were found for the given tags.");
                }
            }
            // Assemble the ultimate candidate
            let output = {
                enterprise: partial.enterprise,
                asset: partial.asset,
                version: partial.version
            };
            if (partial.cabinet)
                output.cabinet = partial.cabinet;
            if (partial.environment)
                output.environment = partial.environment;
            if (partial.tags)
                output.tags = partial.tags;
            if (partial.matchTags)
                output.matchTags = partial.matchTags;
            return output;
        });
    }
    buildCommandString(candidate, cmd) {
        cmd.push("-e", candidate.enterprise);
        // This should only ever be a single asset in this command 
        // (Later we can still add support for multiple assets by allowing multi on these args)
        cmd.push("-a", candidate.asset);
        if (candidate.version)
            cmd.push("--version", candidate.version);
        // Destination (by tag or by name)
        if (candidate.tags && (candidate.tags.length > 0)) {
            candidate.tags.forEach(t => cmd.push("-t", t));
            if (candidate.matchTags)
                cmd.push("-m", candidate.matchTags);
        }
        else if (candidate.cabinet) {
            cmd.push("-c", candidate.cabinet);
        }
        cmd.push("--silent");
        return cmd;
    }
    executeTask(candidate) {
        var _a;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let dest = candidate.cabinet ? candidate.enterprise.concat("/", (_a = candidate.environment) !== null && _a !== void 0 ? _a : "", "/", this._styler.hilite(candidate.cabinet)) : "with tags";
            yield this._ui.startSpinner("Deploying to cabinet ".concat("[", dest, "]..."));
            let result = yield this.genRelayClient.Call.postExecuteDeployment({ assets: [{ a: candidate.asset, v: candidate.version }], target: candidate });
            yield this._ui.stopSpinner(result.success ? "SUCCESS" : (result.error ? "ERROR" : "FAILED"));
            if (result.success) {
                this._ui.userMessage(`${this._styler.colorGood("SUCCESS")}: ${result.message}`);
                this._ui.userMessage("For status and logs use: ".concat(this._styler.bold("cod asset view -a ".concat(candidate.asset))));
            }
            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;
            }
            return;
        });
    }
    buildCandidate_interactively(partial, rootMap, takeDefaults) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let ent = rootMap.enterprises.find(e => e.name == partial.enterprise); // <-- assertion ok - we've already ensured a valid ent
            // ASSET 
            let asset = yield this.setAsset(partial.asset, ent, takeDefaults);
            partial.asset = asset.name;
            // CABINET
            let [cab, versioninCab] = yield this.setCabinet(rootMap, ent, takeDefaults, partial.cabinet, partial.environment, partial.tags, partial.matchTags, partial.asset);
            partial.cabinet = cab.name;
            partial.environment = cab.environment;
            // VERSION  (Might need Image registry creds for this)
            partial.version = yield this.setVersion(ent, asset, partial.version, versioninCab, takeDefaults);
            // CLUSTER & CSP CREDS
            // let cluster = this.deriveClusterFrom(partial.enterprise, partial.environment, partial.cabinet, rootMap);
            let cluster = rootMap.clusters.find(c => c.name == cab.cluster);
            if (cluster == undefined) {
                throw new Error(`Did not recognize cluster [${cab.cluster}]`);
            }
            else {
                yield this.cspContextualizer.promptForCloudProviderCreds(this.ioc, cluster.provider, takeDefaults, partial.providerUserName, partial.providerPassword, cluster.providerSubscriptionId, partial.proprietaryAuthInput);
            }
            // // FINAL CONFIRMATION
            // let confirmed = await this._ui.promptConfirm("confirmDeploy", "Deploy?", true);
            // if (!confirmed) {
            //   throw new Error("Operation cancelled; no work was done.");
            // } else {
            // 
            //   Save to workspace and user context  
            //   TODO: Consider a process context (to isolate concurrent cli windows)  
            //         (Perhaps this goes in the execute method?  perhaps only after success?)
            // 
            // }
            let output = {
                enterprise: partial.enterprise,
                asset: partial.asset,
                version: partial.version
            };
            if (partial.environment)
                output.environment = partial.environment;
            if (partial.cabinet)
                output.cabinet = partial.cabinet;
            if (partial.tags)
                output.tags = partial.tags;
            if (partial.matchTags)
                output.matchTags = partial.matchTags;
            return output;
        });
    }
    /** Returns true only if the asset is found in the given enterprise. */
    isValidAsset(assetName, ent) {
        // let invalids = _.difference(assetNames, ent.assets.map(a => a.name));
        let found = ent.assets.find(a => a.name == assetName);
        return Boolean(found != undefined);
    }
    getValidDefaultAsset(ent) {
        let found;
        // value from previous execution
        // Workspace default value
        // Tenant User default value
        // System default value
        return found;
    }
    setAsset(userInput, ent, takeDefaults) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let output;
            if (userInput != codiac_constants_1.CliConstants.PROMPT_ME_DESIGNATOR) {
                // I.  Validate 
                let forceConfirm;
                let defaultValue;
                if (userInput != undefined && !this.isValidAsset(userInput, ent)) {
                    userInput = undefined;
                    forceConfirm = true;
                    defaultValue = this.getValidDefaultAsset(ent);
                }
                // II. Take the input or the default
                if (userInput) {
                    output = userInput;
                }
                else if ((defaultValue != undefined) && (takeDefaults == true) && (forceConfirm != true)) {
                    output = defaultValue;
                }
                else if ((defaultValue != undefined) && (yield this._ui.promptConfirm("confirmAsset", "Asset: ".concat(this._styler.hilite(defaultValue)), true))) {
                    output = defaultValue;
                }
            }
            // III. Open selection
            if (output == undefined) {
                // output = (await this._ui.promptSelect('asset', 'Select an Asset:', ent.assets.map(a => a.name).sort()))!;
                let assetType;
                let assetChanged;
                [output, assetChanged, assetType] = yield this.rootMapUtils.promptForAsset(ent.assets, undefined);
            }
            return ent.assets.find(a => a.name == output); // assertion ok bc we've already validated
        });
    }
    setVersion(ent, asset, userInput, defaultValue, takeDefaults) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let output;
            let availables = yield this.loadAssetVersions(asset);
            if (userInput != codiac_constants_1.CliConstants.PROMPT_ME_DESIGNATOR) {
                // I.  Validate 
                let forceConfirm;
                // let defaultValue: string | undefined;
                if (userInput != undefined && !this.isValidAssetVersion(asset.name, userInput, ent, availables)) {
                    userInput = undefined;
                    forceConfirm = true;
                    defaultValue = defaultValue !== null && defaultValue !== void 0 ? defaultValue : this.getValidDefaultAssetVersion(asset.name, ent);
                }
                // II. Take the input or the default
                if (userInput) {
                    output = userInput;
                    this._ui.userMessage("Continuing with ".concat(asset.name, ":", output));
                }
                else if ((defaultValue != undefined) && (takeDefaults == true) && (forceConfirm != true)) {
                    output = defaultValue;
                    this._ui.userMessage("Continuing with ".concat(asset.name, ":", output));
                }
                else if ((defaultValue != undefined) && (yield this._ui.promptConfirm("confirmAssetVersion", "Version: ".concat(this._styler.hilite(defaultValue)), true))) {
                    output = defaultValue;
                }
            }
            // III. Open selection
            if (output == undefined) {
                const assetObj = asset; // ent.assets.find(a => a.name == asset)!;  // <-- assertion ok bc we've already validated a selected asset
                // NOTE:  This method will look up versions from the remote image repository, so it does have a visible performance hit.
                // TODO:  Consider caching the version list, also consider either doing this lookup sooner so we can validate in step I, 
                //        or firing off an individual remote request to validate a single value 
                //        (if getting one is faster, which it could be depending on the number of versions theyve got in that repo)
                // ALSO:  Would be EXTRA nice if we could get some more meta about the version (say date and author?)
                // ALSO: would ALSO be EXTRA nice if we could merge our own version data in so we could get tags and current deployments in there
                const selectedV = yield this.promptForVersion(ent.name, assetObj, availables, undefined);
                output = selectedV.v;
            }
            return output;
        });
    }
    /** Case-INsensitive.  Returns true only if the version is found in the image registry. */
    isValidAssetVersion(assetName, version, ent, existingVersions) {
        return existingVersions.map(v => v.trim().toLowerCase())
            .includes(version.trim().toLowerCase());
        // return true;
    }
    getValidDefaultAssetVersion(assetName, ent) {
        let found;
        // value from previous execution
        // Workspace default value
        // Tenant User default value
        // System default value
        return found;
    }
    loadAssetVersions(asset, versionSearch) {
        var _a;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let sorted;
            if (this.versionsByAsset[asset.name]) {
                sorted = this.versionsByAsset[asset.name];
            }
            else {
                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) {
                    this._ui.stopSpinner("failed");
                    throw result.error;
                }
                if (result.output)
                    this.versionsByAsset[asset.name] = result.output;
                sorted = (_a = this.versionsByAsset[asset.name]) !== null && _a !== void 0 ? _a : [];
                this._ui.stopSpinner(`found ${sorted.length}`);
            }
            return sorted;
        });
    }
    promptForVersion(enterpriseCode, asset, existingVersions, 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;
                try {
                    let providerSubscriptionId = (_a = asset.image.registry.accountId) !== null && _a !== void 0 ? _a : asset.image.registry.subscriptionId; // (Aws) TODO: make this property generic as providerSubscriptionId
                    yield this.cspContextualizer.promptForCloudProviderCreds(this.ioc, asset.image.registry.provider, undefined, undefined, undefined, providerSubscriptionId);
                    let sorted = existingVersions !== null && existingVersions !== void 0 ? existingVersions : yield this.loadAssetVersions(asset, versionSearch);
                    // 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 = await inquirer.prompt([{ type: 'autocomplete', name: 'version', message: 'Select the asset version:', source: this.searchVersions, default: latest }]);
                    let fuzzySelector = new cli_common_1.PromptSelectFuzzy(this.logger, this._ui, this._styler);
                    let selected = yield fuzzySelector.go("assetVersion", "Select the asset version:", false, versionSearch, false, () => tslib_1.__awaiter(this, void 0, void 0, function* () { return sorted.map(v => ({ name: v, value: v })); }));
                    resolve({ a: asset.name, v: selected });
                }
                catch (err) {
                    reject(err);
                }
            }));
        });
    }
    /** Returns array of `[ cabinet: CabinetClusterRoot, versionInCabinet: string|undefined ]` */
    setCabinet(rootMap, ent, takeDefaults, cabFromUser, envFromuser, tagsFromUser, tagMatch, asset) {
        var _a, _b, _c;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let cab;
            let versionInCab;
            if (tagsFromUser != undefined) {
                // Using tags
                let selector = {
                    enterprise: ent.name,
                    tags: tagsFromUser
                };
                if (tagMatch != undefined) {
                    if (["any", "all", "none"].includes(tagMatch) != true) {
                        throw new Error("tagMatch must be one of: 'any', 'all', or 'none'.");
                    }
                    selector.matchTags = 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()));
                    }
                    cab = cabRes.output[0];
                    this._ui.userMessage("Cabinet: ".concat(cab.environment, "/", this._styler.hilite(cab.name)));
                }
                else {
                    throw new Error("No matching cabinets were found.");
                }
            }
            else {
                //build embelisher (only if there are deployed versions of the asset)
                let embellisher;
                let result = yield this._codiacApi.Call.assetWhere({ enterprise: ent.name, asset: asset, rootMap: rootMap });
                if (result.error != undefined) {
                    // throw result.error;
                    result.error.message = "Error loading list of currently deployed asset versions: ".concat((_a = result.error.message) !== null && _a !== void 0 ? _a : "(no description available; check logs for details).");
                    this.logger.error(result.error.message, result.error);
                }
                else if (result.output) {
                    embellisher = this.createAssetVersionEmbellisher(result.output);
                }
                // Using cab name
                [cab] = yield this.rootMapUtils.promptForCabinet(ent.name, rootMap, cabFromUser, false, embellisher);
                versionInCab = (_c = (_b = result.output) === null || _b === void 0 ? void 0 : _b.find(x => x.cabinet == cab.name)) === null || _c === void 0 ? void 0 : _c.assetVersion;
            }
            if (cab.infrx == undefined || _.isEmpty(cab.infrx)) {
                throw new Error(`Cannot connect to cabinet ${cab.name}: it has no infrastructure record.`);
            }
            if (cab.cluster == undefined || _.isEmpty(cab.cluster)) {
                throw new Error(`Cannot connect to cabinet ${cab.name}: it has no cluster assigned.`);
            }
            return [cab, versionInCab];
        });
    }
    createAssetVersionEmbellisher(deployments) {
        let embellisher = {
            eachEnterprise: (entBranch, ent, rootMap) => {
                // Do nothing
            },
            eachEnviro: (envBranch, env, ent, rootMap) => {
                // Do nothing
            },
            eachCabinet: (cabBranch, cab, env, ent, rootMap) => {
                let dep = deployments.find(d => d.cabinet == cab.name);
                if (dep === null || dep === void 0 ? void 0 : dep.assetVersion) {
                    let version = this._styler.dim("(".concat(dep.assetVersion, ")"));
                    cabBranch.text = cabBranch.text.concat("  ", version);
                }
            },
        };
        return embellisher;
    }
    deriveClusterFrom(enterprise, environment, cabinet, rootMap) {
        try {
            let ent = rootMap.enterprises.find(e => e.name === enterprise);
            if (ent === undefined)
                throw new Error(`Cannot find cluster: did not recognize enterprise [${enterprise}]`);
            let enviro = ent.environments.find(x => x.cabinets.find(c => c.name === cabinet));
            if (enviro === undefined)
                throw new Error(`Cannot find cluster: did not recognize cabinet [${cabinet}]`);
            let cabObj = enviro.cabinets.find(c => c.name === cabinet);
            if (cabObj == undefined)
                throw new Error(`Cannot find cluster: did not recognize cabinet [${cabinet}]`);
            if (cabObj.cluster == undefined)
                throw new Error(`Cannot find cluster: cabinet [${cabinet}] does not have a cluster specified.`);
            let cluster = rootMap.clusters.find(c => c.name == cabObj.cluster);
            if (cluster == undefined)
                throw new Error(`Did not recognize cluster [${cabObj.cluster}]`);
            return cluster;
        }
        catch (err) {
            throw err;
        }
    }
}
exports.default = AssetDeploy;
// static aliases = ["deploy"];  // UNcomment this when this command is production-ready
AssetDeploy.hidden = true; // COMMENT this out when this command is production-ready
AssetDeploy.description = 'Deploys a given asset to a given cabinet.';
AssetDeploy.dependsOn = ["enterpriseCode", "cabinetName"];
AssetDeploy.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: [...Object.keys(contracts_1.AnyAllNoneEnum)], 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 ${codiac_constants_1.CliConstants.PROMPT_ME_DESIGNATOR} or ${codiac_constants_1.CliConstants.PROMPT_ME_DESIGNATOR_MACOS} to be prompted).` }),
    version: core_1.Flags.string({ char: 'u', required: false, dependsOn: ["asset"], description: `Search string for the version of the asset to deploy  (use ${codiac_constants_1.CliConstants.PROMPT_ME_DESIGNATOR} or ${codiac_constants_1.CliConstants.PROMPT_ME_DESIGNATOR_MACOS} to be prompted).` }),
    // applyTag: Flags.string({ multiple: true, required: false, dependsOn: ["asset"], description: `Tags to apply to the selected asset version.` }),
    // applyLabel: Flags.string({ multiple: true, required: false, dependsOn: ["asset"], 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` }),
    "aws-session-token": core_1.Flags.string({ required: false, dependsOn: ["providerUserName", "providerPassword"], description: "(for AWS only) The session token parameter for use when authenticating with a short-term AWS access key credentials." }),
    // TODO: Move these into the base class
    silent: core_1.Flags.boolean({ exclusive: ["echo", "to-script"], description: 'Non-Interactive mode; executes without any user interaction and fails on any missing or invalid arguments.' }),
    echo: core_1.Flags.boolean({ exclusive: ["to-script", "silent"], description: 'Echo interaction mode; renders the equivalent non-interactive cli command for future use before final execution.' }),
    "to-script": core_1.Flags.boolean({ exclusive: ["echo", "silent"], description: 'Toscript interaction mode; renders the equivalent non-interactive cli command WITHOUT any execution.' }),
    "take-defaults": core_1.Flags.boolean({ exclusive: ["silent"], description: "Setting this to true prevents prompting for confirmation on parameters that were passed in or were automatically set to default values (Irrelevant in --silent mode).  This behavior does NOT apply to scenarios that the command needs to override invalid user input; such cases will always prompt for confirmation.  eg: when no argument was provided for file or type, and only one exists, the system will render the autoselected file unless this flag is set." })
    // TODO: Add support for memorized commands... (se we can use:  `cod deploy --as api-dev`)
    // , ...commandBase.globalFlags // <-- also needs:  this.exec.runWithDefaults(params, async (args, flags) => ...
};
AssetDeploy.args = {};
//# sourceMappingURL=deploy.js.map