"use strict";
var ConfigUiUtils_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.findScopeTarget = exports.ConfigUiUtils = exports.RegexBySettingType = exports.InteractionModes = void 0;
const tslib_1 = require("tslib");
const path = require("path");
const inversify_1 = require("inversify");
const _ = require("lodash");
const YAML = require("yaml");
const minimatch_1 = require("minimatch");
const codiac_common_1 = require("@codiac.io/codiac-common");
const dom = require("@codiac.io/codiac-domain");
const contracts_1 = require("../apis/codiac-api/contracts");
const CodiacConstants = require("../codiac-constants");
const rootmap_ui_utils_1 = require("./rootmap-ui-utils");
const INTERFACES_CLI_1 = require("../ops/INTERFACES_CLI");
const type_safety_utils_1 = require("./type-safety-utils");
const cloud_resource_configurator_factory_1 = require("./cloud-resource-configurator-factory");
var InteractionModes;
(function (InteractionModes) {
    InteractionModes["echo"] = "echo";
    InteractionModes["toscript"] = "to-script";
    InteractionModes["silent"] = "silent";
})(InteractionModes = exports.InteractionModes || (exports.InteractionModes = {}));
/** Regular expressions for validating config settings.  */
exports.RegexBySettingType = {
    /** @see [K8s docs / Labels: Syntax and Character Set](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set) */
    label: {
        // TODO: whyInvalid: 
        //    "Valid label keys have two segments: an optional prefix and name, separated by a slash (/). 
        //    The name segment is required and must be 63 characters or less, beginning and ending with an alphanumeric character ([a-z0-9A-Z]) 
        //    with dashes (-), underscores (_), dots (.), and alphanumerics between. 
        //    The prefix is optional. If specified, the prefix must be an RFC 1123 DNS subdomain: 
        //    a series of DNS labels separated by dots (.), not longer than 253 characters in total, followed by a slash (/)."
        rxForKey: /[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/,
        // rxForKey: /^([a-z0-9A-Z-\.\\]{0,253}\/)?[a-z0-9A-Z\-_\.\\]{1,63}$/g,  // our own old regex
        rxForValue: /(^[a-z0-9A-Z]([a-z0-9A-Z\-_\.]{0,61}[a-z0-9A-Z])?$)?/g
    },
    /** @see [K8s Docs / Annotations: Syntax and Character Set](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set) */
    annotation: {
        rxForKey: /^([a-z0-9A-Z-\.\\]{0,253}\/)?[a-z0-9A-Z\-_\.\\]{1,63}$/g
        // K8s doesnt validate indiv values - the sum total of annotations just neeeds to fall within 256k
    },
    env: {
        // this from k8s:  "a valid config key must consist of alphanumeric characters, '-', '_' or '.' 
        // (e.g. 'key.name',  or 'KEY_NAME',  or 'key-name', regex used for validation is '[-._a-zA-Z0-9]+')"
        rxForKey: /^[-._a-zA-Z0-9]+$/
    },
    filestore: undefined,
    footprint: undefined,
    helm: undefined,
    probing: undefined,
    permit: undefined,
    k8spatch: undefined,
    file: {
        // this from k8s:  "a valid config key must consist of alphanumeric characters, '-', '_' or '.' 
        // (e.g. 'key.name',  or 'KEY_NAME',  or 'key-name', regex used for validation is '[-._a-zA-Z0-9]+')"
        rxForKey: /^[-._a-zA-Z0-9]+$/
    }
};
let ConfigUiUtils = ConfigUiUtils_1 = class ConfigUiUtils extends rootmap_ui_utils_1.RootMapUiUtils {
    /** Converts the source properties of a ScopedSetting into a parseable string so that it can be rendered as a string value.
     *
     * eg: converts this:
     * ```
     * {
     *  "type": "awsKeyVault",
     *  "name": "our-aws-east-dev-secrets",
     *  "key": "mongo-db-user-dev"
     * }
     * ```
     * into this: `#REF|awsKeyVault|our-aws-east-dev-secrets|mongo-db-user-dev`
     *
      */
    serializeSourcedValue(source) {
        return `${ConfigUiUtils_1.SOURCED_VALUE_PREFIX}|${source.type}|${source.name}|${source.key}`;
    }
    /** Determines if the given setting value is a serialized representation of a sourced setting. */
    isSourced(value) {
        if ((value != undefined) && (typeof value === 'string') && value.trim().startsWith(ConfigUiUtils_1.SOURCED_VALUE_PREFIX.concat("|"))) {
            if (value.trim().split("|").length === 4) {
                return true;
            }
            else {
                return true; // <--- It probably IS a sourced setting, just incorrectly formatted.  We are returning true, so that parsing will fail with more descriptive errors. 
            }
        }
        else {
            return false;
        }
    }
    /** Mutates the setting object to convert a value that is a serialized source reference into the explicit source property  */
    deserializeSourcedValue(setting) {
        var _a, _b, _c, _d;
        try {
            if (this.isSourced(setting.value)) {
                let parts = (typeof setting.value == 'string') ? (_b = (_a = setting.value) === null || _a === void 0 ? void 0 : _a.trim().toString().split("|")) !== null && _b !== void 0 ? _b : [] : [];
                if (parts.length < 4)
                    throw new Error("Invalid source value syntax; Must be '#REF|<type>|<name>|<key>'.");
                parts.shift();
                let secretStoreType = type_safety_utils_1.TypeSafetyUtils.toEnum(contracts_1.KvpStoreTypesEnum, parts.shift());
                if (secretStoreType == undefined)
                    throw new Error("Unrecognized source type.");
                let key = parts.pop();
                let storeName = parts.join("|"); // merge everything that's left (in aws, this is a bunch of params)
                // MUTATE THE GIVEN SETTING OBJECT
                setting.source = {
                    type: secretStoreType,
                    name: storeName,
                    key: key
                };
                delete setting.value;
            }
        }
        catch (err) {
            throw new Error(`Error parsing sourced value: ${(_c = err.name) !== null && _c !== void 0 ? _c : ""} ${(_d = err.message) !== null && _d !== void 0 ? _d : ""}`);
        }
    }
    buildConfigTree(settings) {
        let tree = [];
        for (let setting of settings) {
            let branch = { text: `${this.styler.dim(setting.setting)}:  ${setting.value}`, value: "" };
            // TODO: unflatten dotpaths and recurse...
            tree.push(branch);
        }
        return tree;
    }
    constructor(ui, styler, _codiacApi, logger) {
        super(ui, styler, logger);
        this._codiacApi = _codiacApi;
        this.logger = logger;
    }
    promptForTypeAndFile(enterprise, asset, assetType, configType, targetFile, existingConfigDefs, allowAddNew = false, takeDefaults = false, setEntireFile = false) {
        var _a, _b, _c;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let typeFileChanged = false; // true when original input is invalid
            let partial = {
                configType: configType,
                targetFile: targetFile
            };
            let configDefs;
            if (existingConfigDefs) {
                // Use the given list
                configDefs = existingConfigDefs;
            }
            else {
                // Query the api for the list of existing configDefs for this asset
                let crit = {
                    asset: asset,
                    enterprise: enterprise
                };
                let cfgResult = yield this._codiacApi.Call.getConfigDefs(crit, { projection: { pathsExcluded: [], pathsIncluded: ["type", "targetFile", "settings"] } });
                if (cfgResult.error)
                    throw cfgResult.error;
                configDefs = (_a = cfgResult.output) !== null && _a !== void 0 ? _a : [];
            }
            // Commented out bc: We dont need to do this bc it's being done a few lines down from here
            // if (assetType == AssetTypesEnum.helm) {
            //   // Limit even the existing configs to helm config. 
            //   // ie: if you changed an existing service container asset to a helm asset, 
            //   //     then any prior non-helm configs are now irrelevant (and probably should have been purged anyway).
            //   configDefs = configDefs.filter(c => c.type = ConfigDefTypesEnum.helm);
            // }
            // Validate Type/File
            if (assetType == contracts_1.AssetTypesEnum.helm) {
                if (partial.targetFile)
                    delete partial.targetFile;
                partial.configType = dom.SpecialConfigTypesEnum.helm; // WTF, the JS syntax was supposed to allow this assignment without requiring a cast like this...
                // Not setting typeFileChanged bc we dont want any confirmations on config type (there's no discussion; youre getting helm config, and that's that)
            }
            if (partial.targetFile != undefined) {
                const found = configDefs.find(x => x.targetFile == partial.targetFile);
                if (found) {
                    partial.targetFile = found.targetFile;
                    partial.existingSettings = found.settings;
                }
                else {
                    // Mispellings of config files are too common to let through.  Force a prompt when creating a new file ConfigDef
                    // TODO:  We shouldnt throw away their value tho...  We should use it as a default after theyve confirmed they want to add a new file def.  
                    partial.targetFile = undefined;
                    typeFileChanged = true;
                }
            }
            else if (partial.configType != undefined) {
                const found = configDefs.find(x => { var _a; return (x.type != undefined) && (x.type.toString() == ((_a = partial.configType) === null || _a === void 0 ? void 0 : _a.toString())); });
                if (found) {
                    partial.configType = type_safety_utils_1.TypeSafetyUtils.toEnum(dom.SpecialConfigTypesEnum, found.type);
                    partial.existingSettings = found.settings;
                }
                else {
                    // Validate the configType provided
                    if (type_safety_utils_1.TypeSafetyUtils.toEnum(dom.SpecialConfigTypesEnum, partial.configType) == undefined) {
                        // The given type was invalid
                        partial.configType = undefined;
                        typeFileChanged = true;
                    }
                }
            }
            if (partial.targetFile == undefined && partial.configType == undefined) {
                let choices = configDefs.filter(cfg => (setEntireFile) ? cfg.type == contracts_1.ConfigDefTypesEnum.file : true).map(x => {
                    var _a, _b;
                    return {
                        name: (x.type == contracts_1.ConfigDefTypesEnum.file && x.targetFile != undefined) ? x.targetFile : x.type,
                        value: `${(_a = x.type) !== null && _a !== void 0 ? _a : ""}|${(_b = x.targetFile) !== null && _b !== void 0 ? _b : ""}`
                    };
                });
                let selected;
                if (allowAddNew === true) {
                    // ----------------------------------------------------------------------
                    // WRITE MODE (so user can declare a new file or choose an un-used type)
                    // ----------------------------------------------------------------------
                    choices.push({ name: CodiacConstants.CliConstants.CHOICE_VALUE_FOR_NEW, value: CodiacConstants.CliConstants.CHOICE_VALUE_FOR_NEW });
                    if (choices.length > 0) {
                        selected = (yield this._ui.promptSelect("existingCfgTarget", "Config: ", choices));
                    }
                    if ((selected === undefined) || (selected === CodiacConstants.CliConstants.CHOICE_VALUE_FOR_NEW)) {
                        // 1. Select a config type first
                        if (setEntireFile) {
                            selected = contracts_1.ConfigDefTypesEnum.file;
                        }
                        else {
                            let newCfgTypeChoices = [contracts_1.ConfigDefTypesEnum.file, ...Object.keys(dom.SpecialConfigTypesEnum)];
                            selected = (yield this._ui.promptSelect("cfgType", "Target: ", newCfgTypeChoices));
                        }
                        // 2. Target file declaration
                        if (selected == contracts_1.ConfigDefTypesEnum.file) {
                            selected = (yield this._ui.prompt("filefreehand", "Path from image root: ('?' to browse): "));
                            if (selected === "?") {
                                let localPath = (yield this._ui.promptSelectFile("filebrowse", "Select file: ", { asRelative: true }));
                                // settingsInLocalFile = await this.extractSettingsFromFile(localPath);
                                let filename = path.basename(localPath);
                                let pathInImage = path.dirname(localPath);
                                this._ui.userMessage(`Target file: ${this.styler.hilite(filename)}`);
                                pathInImage = (yield this._ui.prompt("pathinimage", "Path from image root: ", true, pathInImage));
                                selected = path.posix.join(pathInImage, filename);
                            }
                            else {
                                // the user free-handed the targetFile field
                                // this._ui.userMessage(`Target file: ${this._styler.hilite(selected)}`);
                            }
                            selected = "|".concat(selected); // to conform with the format we're using below
                        }
                        else {
                            selected = selected.concat("|"); // to conform with the format we're using below
                        }
                    }
                }
                else {
                    // ----------------------------------------------------------------------
                    // Readonly mode - Limit choices to existing files/types
                    // ----------------------------------------------------------------------
                    if (choices.length == 0) {
                        throw new Error("This asset has no configs defined.");
                    }
                    else if (choices.length === 1) {
                        selected = choices[0].value; // Auto-select the one-and-only
                        if (typeFileChanged) {
                            // if we changed their value, then we need their confirmation
                            let keep = yield this._ui.promptConfirm("type-file-changed", `Only 1 config found; Continue?  ${this.styler.hilite(choices[0].name)} `);
                            if (!keep)
                                throw new Error("You declined the only available config definition.");
                        }
                        else {
                            if (takeDefaults != true) {
                                this._ui.userMessage(`Only 1 config found: ${this.styler.hilite(choices[0].name)}`);
                            }
                        }
                    }
                    else {
                        selected = (yield this._ui.promptSelect("cfgType", "Config: ", choices));
                    }
                }
                partial.configType = type_safety_utils_1.TypeSafetyUtils.toEnum(dom.SpecialConfigTypesEnum, selected.split("|")[0]);
                if (_.isEmpty(partial.configType))
                    partial.configType = undefined;
                partial.targetFile = selected.split("|")[1];
                if (_.isEmpty(partial.targetFile))
                    partial.targetFile = undefined;
                if (partial.configType)
                    partial.existingSettings = (_b = configDefs.find(x => { var _a; return x.type.toString() == ((_a = partial.configType) === null || _a === void 0 ? void 0 : _a.toString()); })) === null || _b === void 0 ? void 0 : _b.settings;
                else if (partial.targetFile)
                    partial.existingSettings = (_c = configDefs.find(x => x.targetFile == partial.targetFile)) === null || _c === void 0 ? void 0 : _c.settings;
            }
            return [partial.configType, partial.targetFile, partial.existingSettings, typeFileChanged];
        });
    }
    /** Prompts for a single setting name.
   *
   * TODO: Add ability to select branches of settings (globbed dot-paths)
   * so user can delete entire sections at a time (eg: database-settings.*).
   *
   *
   * Use this inquirer plugin for navigating the object structure:
   *    https://github.com/insightfuls/inquirer-tree-prompt
   *
   * See if this file tree selector could be fashioned to do the same:
   *    https://github.com/anc95/inquirer-file-tree-selection
   *
   * @param existing The list of existing settings to choose from.
   * @param mode Set this to `write` only if you want the user to be able to add new settings.  Indicates whether "Add New" (mode: `write`) or "All" (mode: `read`) options will be provided.
   * @param given A prompt gets invoked in `read` mode only if none match any existing settings, and in `write` mode only if none are given at all.
   */
    promptForOneSetting(configType, existing, mode = "read", predefs, given) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (mode == "read") {
                let [selectedMany, instances, changedGivens] = yield this.promptForManySettings(existing, given, false); // TODO: prevent multi
                if (selectedMany.length == 0)
                    throw new Error("User canceled setting selection");
                return [selectedMany[0], instances, changedGivens, undefined];
            }
            else {
                // mode == "write"
                let selected;
                let settingSchema;
                if ((given != undefined) && !_.isEmpty(given)) {
                    selected = given; // NOTE:  blindly accepting the given without a prompt.  Because in write mode, misspellings arent our problem.
                }
                else {
                    if (existing.length > 0) {
                        // let choices = existing.map(s => { return { name: s.setting, value: s.setting }; });
                        let choices = _.uniq(existing.map(s => s.setting));
                        choices.sort();
                        choices = _.uniq(choices);
                        choices.push(CodiacConstants.CliConstants.CHOICE_VALUE_FOR_NEW);
                        selected = (yield this._ui.promptSelect("settingSelect", "Setting: ", choices));
                    }
                    if (selected == undefined || selected === CodiacConstants.CliConstants.CHOICE_VALUE_FOR_NEW) {
                        let newSetting;
                        if (predefs && !_.isEmpty(predefs)) {
                            // TODO: Make this a fuzzy-select if there's > 10 choices
                            newSetting = (yield this._ui.promptSelect("settingPredef", "Setting: ", Object.keys(predefs).sort(), false, false));
                            settingSchema = predefs[newSetting];
                        }
                        else {
                            // do {
                            let validator = { isValid: (val) => ConfigUiUtils_1.settingNameIsValid(val, configType) };
                            newSetting = (yield this._ui.prompt("settingFreehand", "Setting (use '.' for hierarchy): ", true, newSetting, validator));
                            // isValid = ConfigUiUtils.settingNameIsValid(newSetting, configType).isValid;
                            // if (!isValid) this._ui.userMessage(`The setting name you entered is invalid.`);
                            // } while (!isValid)
                        }
                        newSetting = newSetting.trim();
                        // Handle custom settings (like for a new dictionary entry)
                        let parts = newSetting.split(".");
                        let customIdx = parts.indexOf("*"); // Might need to escape this *
                        while (customIdx >= 0) {
                            let prompt;
                            if (customIdx > 0) {
                                prompt = parts.slice(0, customIdx).join(".").concat("."); // dot-path leading up to this placeholder 
                            }
                            else {
                                prompt = "Name for *";
                            }
                            let custom = (yield this._ui.prompt("customProp", prompt, true));
                            parts[customIdx] = custom;
                            newSetting = parts.join(".");
                            customIdx = parts.indexOf("*"); // next placeholder
                        }
                        selected = newSetting;
                    }
                }
                let instances = existing.filter(s => s.setting == selected);
                let settingChanged = ConfigUiUtils_1.areDifferent(selected, given);
                return [selected, instances, settingChanged, settingSchema];
            }
        });
    }
    /** Performs regex validation on the given config setting name/key. */
    static settingNameIsValid(settingName, type) {
        if (type == undefined)
            return { isValid: true }; // TODO: Validate config file keys
        if (settingName == undefined)
            return { isValid: false, reason: "Setting name cannot be empty" };
        let rules = exports.RegexBySettingType[type];
        let output = { isValid: true };
        if (rules === null || rules === void 0 ? void 0 : rules.rxForKey) {
            output.isValid = rules.rxForKey.test(settingName);
            // if (output.isValid != true && rules.must) output.whyInvalid = rules.must;
        }
        return output;
    }
    /** Performs regex validation on the given value for the config setting. */
    static settingValueIsValid(settingValue, type) {
        if (type == undefined)
            return true; // config file 
        let rules = exports.RegexBySettingType[type];
        let isValid = (rules === null || rules === void 0 ? void 0 : rules.rxForValue) ? rules.rxForValue.test(settingValue) : true;
        return isValid;
    }
    /** Prompts for one or many setting names.  Includes an `ALL` but NOT an `ADDNEW` option.
     *
     * TODO: Add ability to select branches of settings (globbed dot-paths)
     * so user can delete entire sections at a time (eg: database-settings.*).
     *
     *
     * Use this inquirer plugin for navigating the object structure:
     *    https://github.com/insightfuls/inquirer-tree-prompt
     *
     * See if this file tree selector could be fashioned to do the same:
     *    https://github.com/anc95/inquirer-file-tree-selection
     *
     * @param existing
     * @param given A prompt gets invoked only if no givens match any existing settings.
     */
    promptForManySettings(existing, given, multi = true) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            try {
                let selected = ((given != undefined) && (typeof given == 'string')) ? [given] : given;
                let settingChanged = false;
                let instances;
                if (existing.length == 0) {
                    throw new Error("No existing settings were found");
                }
                // TODO: Check for glob magic  - globs should pass through
                instances = (selected != undefined) ? ConfigUiUtils_1.matchMultipleWithGlobbing(existing, selected) : [];
                if (instances.length > 0) {
                    // (at least one or more of) the given setting(s) are valid.  
                }
                else {
                    if (given != undefined && !_.isEmpty(given)) {
                        this._ui.userMessage(`None of your requested settings were found.`);
                    }
                    // TODO: render a tree of settings choices 
                    // so they can select either branches (glob) or leaf nodes (distinct)
                    // let choices = existing.map(s => { return { name: s.setting, value: s.setting }; });
                    let choices = existing.map(s => { return { name: s.setting, value: s.setting }; });
                    choices = _.orderBy(choices, x => x.name);
                    choices = _.uniqBy(choices, x => x.name);
                    choices.unshift({ name: CodiacConstants.CliConstants.CHOICE_VALUE_FOR_ALL, value: CodiacConstants.CliConstants.GLOB_FOR_ALL });
                    selected = [(yield this._ui.promptSelect("settingSelect", "Setting: ", choices))]; // stuffing it into an Array here!!!
                    instances = ConfigUiUtils_1.matchMultipleWithGlobbing(existing, selected);
                }
                settingChanged = ConfigUiUtils_1.areDifferent(selected, given);
                // ----------------------------------------------------------------------------------------------
                // // Try to implement un-selectable options...
                // let settings = ["bill", "sam", "tom", "harry"];
                // let choices: any[] = settings.map(s => { return { name: s, value: s }; });
                // // choices.push({ name: "some branch", value: new inquirer.Separator("--I am a branch") });
                // choices.push(new inquirer.Separator("--I am a branch"));
                // choices.push({ name: "sally", value: "sally" });
                // choices.push({ name: "Sue", value: "Sue" });
                // choices.push({ name: "Suzy", value: "Suzy" });
                // let nada = await this._ui.promptSelect("test", "try this", choices);
                // ----------------------------------------------------------------------------------------------
                return [selected, instances, settingChanged];
            }
            catch (err) {
                throw err;
            }
        });
    }
    /** Searches out availables that match ANY of the given specs.
     * @param availables: setting objects to search for.
     * @param matchSpecs: each can be either a distinct setting name (eg `shoe-size`) or a glob (eg `api-settings.*`).
     */
    static matchMultipleWithGlobbing(availables, matchSpecs) {
        let patterns = matchSpecs.filter(x => (new minimatch_1.Minimatch(x, {})).hasMagic());
        let names = _.difference(matchSpecs, patterns);
        let globs = patterns.map(g => new minimatch_1.Minimatch(g));
        let instances = [];
        if (names) {
            instances = instances.concat(availables.filter(s => names.includes(s.setting)));
        }
        if (globs) {
            for (let glob of globs) {
                instances = instances.concat(availables.filter(s => glob.match(s.setting)));
            }
        }
        return instances;
    }
    /** Determines if the two collections have ANY different concrete values,
     * regardless of order or string/array/undefined/empty)
     *
     * ```
     * DIFFERENT IF:
     * any of A not in B
     * AND
     * any of B not in A
     * ```
     * */
    static areDifferent(thingA, thingB) {
        if (thingA == undefined || _.isEmpty(thingA)) {
            return !Boolean(thingB == undefined || _.isEmpty(thingB));
        }
        else if (thingB == undefined || _.isEmpty(thingB)) {
            return true; // because A is not empty
        }
        else {
            let areDiff = (Boolean(_.difference((typeof thingA == 'string') ? [thingA] : thingA, thingB).length > 0)
                || Boolean(_.difference((typeof thingB == 'string') ? [thingB] : thingB, thingA).length > 0));
            return areDiff;
        }
    }
    /** RETURNS Definitive thisMachine flag.
     * Performs a findScopeTarget()
     * and THROWS on invalid. */
    validateScope(rootMap, enterprise, scope, target) {
        let thisMachine;
        // Validate scope/target
        if (scope == undefined || _.isEmpty(scope)) {
            throw new Error(`Invalid input: scope required.  Please set --enterprise-scope, --enviro, or --cabinet.`);
        }
        else if (!(Object.keys(contracts_1.ScopeLevel)).includes(scope)) {
            throw new Error(`Invalid input: invalid scope level. Please set --enterprise-scope, --enviro, or --cabinet.`);
        }
        if (scope == contracts_1.ScopeLevel.enterprise) {
            // This code should never get reached.  Its just here for completeness.
            if ((target != undefined || _.isEmpty(target)) && (target != enterprise)) {
                throw new Error(`Invalid input: scope target for enterprise scope should match the enterprise.`);
            }
            thisMachine = false;
        }
        else if (target == undefined || _.isEmpty(target)) {
            throw new Error(`Invalid input: missing scope target. Please set --enterprise-scope, --enviro, or --cabinet.`);
        }
        else {
            const targetObj = findScopeTarget(rootMap, scope, target);
            if (targetObj == undefined) {
                throw new Error(`Invalid input: the given ${scope} was not found.`);
            }
            else {
                thisMachine = type_safety_utils_1.TypeSafetyUtils.toBoolean(targetObj === null || targetObj === void 0 ? void 0 : targetObj.thisMachine);
            }
        }
        return thisMachine;
    }
    /**
     *
     * @param rootMap
     * @param enterprise
     * @param scope
     * @param target
     * @param existings
     * @param allowEverywhere
     * @returns Array consisting of: [<scope>, <target>, <targetChanged>, <thisMachine>]
     */
    validateAndPromptForScope(rootMap, enterprise, scope, target, existings, allowEverywhere = true) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let targetChanged = false;
            let partial = {
                scope: scope,
                target: target
            };
            if (partial.scope != undefined || partial.target != undefined) {
                // Validate
                partial.scope = (partial.scope == undefined) ? undefined : type_safety_utils_1.TypeSafetyUtils.toEnum(contracts_1.ScopeLevel, partial.scope);
                if (partial.scope == undefined) {
                    partial.target = undefined; // NOTE:  Instead of dropping the target, you could deduce the scope by searching around the map for a single target with this name...
                    partial.thisMachine = undefined;
                }
                if (partial.scope != undefined && partial.target != undefined) {
                    let found = findScopeTarget(rootMap, partial.scope, partial.target);
                    partial.target = found === null || found === void 0 ? void 0 : found.name;
                    partial.thisMachine = type_safety_utils_1.TypeSafetyUtils.toBoolean(found === null || found === void 0 ? void 0 : found.thisMachine);
                    targetChanged = Boolean(partial.target == undefined);
                }
            }
            if (partial.scope == undefined || (partial.target == undefined && partial.scope != contracts_1.ScopeLevel.enterprise)) {
                let scope = yield this.promptForScope(enterprise, rootMap, existings, allowEverywhere, undefined);
                if (scope) {
                    partial.scope = scope.scope;
                    partial.target = scope.target;
                    partial.thisMachine = scope.thisMachine;
                }
            }
            if (partial.thisMachine == undefined) {
                partial.thisMachine = false;
            }
            return [partial.scope, partial.target, targetChanged, partial.thisMachine];
        });
    }
    /** NOTE: /target output can be undefined ONLY if allowEverywhere is set to true.  If false, you can safely apply an assertion operator (!) to scope and target. */
    promptForScope(enterprise, rootMap, existings, allowEverywhere = true, prompt) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            try {
                let output;
                // render cab tree with all lines selectable
                let embellisher = this.createSettingsEmbellisher(existings);
                let cabTree = this.buildCabinetTree(rootMap, embellisher);
                let choices = this.buildSelectChoicesFrom(cabTree);
                if (allowEverywhere)
                    choices.unshift({ name: "** Everywhere **", value: CodiacConstants.CliConstants.GLOB_FOR_ALL });
                let delimited = (yield this._ui.promptSelect("scope", prompt !== null && prompt !== void 0 ? prompt : "Scope: ", choices, undefined, false));
                if (delimited == CodiacConstants.CliConstants.GLOB_FOR_ALL) {
                    output = undefined;
                }
                else {
                    let parts = delimited.split("|");
                    output = { scope: parts[0], target: parts[1], thisMachine: type_safety_utils_1.TypeSafetyUtils.toBoolean(parts[2]) };
                }
                return output;
            }
            catch (err) {
                throw (err);
            }
        });
    }
    /** Appends the setting (or a count of settings) applied at each scope level */
    createSettingsEmbellisher(existings) {
        // If there's more than a single setting name in the list, then we're gonna show counts>0 at each scope, otherwise we'll show ellipsised values.
        let showCounts = false;
        if (existings != undefined && _.uniq(existings.map(x => x.setting).sort()).length > 1) {
            showCounts = true;
        }
        let embellisher = {
            eachEnterprise: (entBranch, ent, rootMap) => {
                var _a;
                let entSettings = existings === null || existings === void 0 ? void 0 : existings.filter(s => s.scope === contracts_1.ScopeLevel.enterprise && s.target === ent.name);
                if (entSettings !== undefined && entSettings.length > 0) {
                    let entValue = entSettings[0].source ? this.serializeSourcedValue(entSettings[0].source) : ((_a = entSettings[0].value) !== null && _a !== void 0 ? _a : "[set to undefined]");
                    let valueSuffix = showCounts ? `${entSettings.length} found` : (this.ellipsis(35, entValue));
                    valueSuffix = `(${valueSuffix})`;
                    entBranch.text = entBranch.text.concat(" ", this.styler.dim(valueSuffix));
                }
            },
            eachEnviro: (envBranch, env, ent, rootMap) => {
                var _a;
                let envSettings = existings === null || existings === void 0 ? void 0 : existings.filter(s => s.scope === contracts_1.ScopeLevel.environment && s.target === env.name);
                if (envSettings !== undefined && envSettings.length > 0) {
                    let envValue = envSettings[0].source ? this.serializeSourcedValue(envSettings[0].source) : ((_a = envSettings[0].value) !== null && _a !== void 0 ? _a : "[set to undefined]");
                    let valueSuffix = showCounts ? `${envSettings.length} found` : (this.ellipsis(35, envValue));
                    valueSuffix = `(${valueSuffix})`;
                    envBranch.text = envBranch.text.concat(" ", this.styler.dim(valueSuffix));
                }
            },
            eachCabinet: (cabBranch, cab, env, ent, rootMap) => {
                var _a;
                let cabSettings = existings === null || existings === void 0 ? void 0 : existings.filter(s => s.scope === contracts_1.ScopeLevel.cabinet && s.target === cab.name);
                if (cabSettings !== undefined && cabSettings.length > 0) {
                    let cabValue = cabSettings[0].source ? this.serializeSourcedValue(cabSettings[0].source) : ((_a = cabSettings[0].value) !== null && _a !== void 0 ? _a : "[set to undefined]");
                    let valueSuffix = showCounts ? `${cabSettings.length} found` : this.ellipsis(35, cabValue);
                    valueSuffix = `(${valueSuffix})`;
                    cabBranch.text = cabBranch.text.concat(" ", this.styler.dim(valueSuffix));
                }
            }
        };
        return embellisher;
    }
    /** Determines the format of the string that was piped in,
   * and parses it into an object in memory.
   *
   * Supports formats:  json, yaml
   */
    parseInputString(input) {
        var _a, _b;
        let output;
        try {
            // Determine format
            let format; // | "csv" | "tsv";
            // if (input.startsWith("setting,value\n")) {
            //   format = "csv";
            // } else if (input.startsWith("setting\tvalue\n")) {
            //   format = "tsv";
            /* } else */ if (input.startsWith("{") || input.startsWith("[")) {
                format = "json";
                // } else if (input.startsWith("- setting\n")) {
                //   format = "yaml";
            }
            else {
                // throw new Error("Invalid stdin data; data format not recognized.");
                format = "yaml";
            }
            // Parse
            switch (format) {
                case "json":
                    let jsonDoc = JSON.parse(input);
                    output = codiac_common_1.ObjectFlattener.flatten(jsonDoc).map(x => { return { setting: x.key, value: x.value }; });
                    break;
                case "yaml":
                    let yamlDoc = YAML.parse(input);
                    output = codiac_common_1.ObjectFlattener.flatten(yamlDoc).map(x => { return { setting: x.key, value: x.value }; });
                    break;
                // case "csv":
                //   output = this.parseTable(input, ",");
                //   break;
                // case "tsv":
                //   output = this.parseTable(input, "\t");
                //   break;
            }
            return output;
        }
        catch (err) {
            throw new Error(`Error parsing stdin: ${(_a = err.name) !== null && _a !== void 0 ? _a : ""} ${(_b = err.message) !== null && _b !== void 0 ? _b : ""}`);
        }
    }
};
/** The prefix on a setting value indicating that the setting value is actually sourced from a key-value store (such as an Azure key vault or AWS secret store). */
ConfigUiUtils.SOURCED_VALUE_PREFIX = "#REF";
ConfigUiUtils = ConfigUiUtils_1 = tslib_1.__decorate([
    (0, inversify_1.injectable)(),
    tslib_1.__param(0, (0, inversify_1.inject)(INTERFACES_CLI_1.INTERFACES_CLI.IUserInputProvider)),
    tslib_1.__param(1, (0, inversify_1.inject)(INTERFACES_CLI_1.INTERFACES_CLI.IOutputStyler)),
    tslib_1.__param(2, (0, inversify_1.inject)(cloud_resource_configurator_factory_1.INTERFACES_CLI_ENTERPRISE.ICodiacClient)),
    tslib_1.__param(3, (0, inversify_1.inject)(codiac_common_1.INTERFACES.IBetterLogger)),
    tslib_1.__metadata("design:paramtypes", [Object, Object, Object, Object])
], ConfigUiUtils);
exports.ConfigUiUtils = ConfigUiUtils;
/** Finds the object in the rootmap (ent, env, or cab) specified by the given scope.
*
* Returns undefined if the scope/target combination does not exist.
*/
function findScopeTarget(rootMap, scope, target) {
    switch (scope) {
        case contracts_1.ScopeLevel.enterprise:
            return rootMap.enterprises.find(e => e.name == target);
        case contracts_1.ScopeLevel.environment:
            let env;
            for (let ent of rootMap.enterprises) {
                env = ent.environments.find(v => v.name == target);
                if (env)
                    break;
            }
            return env;
        case contracts_1.ScopeLevel.cabinet:
            let cab;
            for (let ent of rootMap.enterprises) {
                for (let env of ent.environments) {
                    cab = env.cabinets.find(v => v.name == target);
                    if (cab)
                        break;
                }
                if (cab)
                    break;
            }
            return cab;
        default:
            return undefined;
    }
}
exports.findScopeTarget = findScopeTarget;
//# sourceMappingURL=config-ui-utils.js.map