"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HostNameValidator = exports.AssetConfigurator = exports.ports = void 0;
const tslib_1 = require("tslib");
const inversify_1 = require("inversify");
const codiac_common_1 = require("@codiac.io/codiac-common");
const codiac_relay_1 = require("../../apis/codiac-relay");
const contracts_1 = require("../../apis/codiac-api/contracts");
const cli_common_1 = require("../../cli-common");
const ops_1 = require("../../ops");
const _ = require("lodash");
const type_safety_utils_1 = require("../type-safety-utils");
// Has to be module-scoped bc the autocomplete redefines "this", so we can't use it inside searchPorts().
exports.ports = [];
let AssetConfigurator = class AssetConfigurator {
    constructor(_ui, _styler, genRelayClient, logger) {
        this._ui = _ui;
        this._styler = _styler;
        this.genRelayClient = genRelayClient;
        this.logger = logger;
        this.isLoaded = false;
        /** The largest valid port number. */
        this.MAX_PORT_NUMBER = 65535;
        /** The value to select from a port number prompt
         *  indicating the intention to remove the port number
         *  assignment altogether. */
        this.REMOVE_PORT_NUMBER = -1;
    }
    load(assets) {
        var _a, _b;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let portResult = yield this.genRelayClient.Call.getKnownPortNumbers({});
            if (portResult.error) {
                this.logger.warn(`Error retrieving known port number list: ${(_a = portResult.error.message) !== null && _a !== void 0 ? _a : portResult.message}`);
            }
            exports.ports = this.getPortOptionsList((_b = portResult.output) !== null && _b !== void 0 ? _b : [], assets);
            exports.ports.unshift({ name: this.REMOVE_PORT_NUMBER + " (none / remove)", value: this.REMOVE_PORT_NUMBER.toString() });
            this.isLoaded = true;
        });
    }
    /** Prompts for port, ingress, ingressInternal, HostName
     *
     * Throws any encountered exceptions. */
    editAsset(partial) {
        var _a, _b, _c, _d;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            // PORTS
            if (partial.type == contracts_1.AssetTypesEnum.helm) {
                partial.port = 0; // Q: Do we need this placeholder?  Better off undefined?
            }
            else {
                partial.port = yield this.promptForAllPortAssignments(partial.port);
            }
            // INGRESS
            if (partial.type == contracts_1.AssetTypesEnum.helm) {
                partial.hasIngress = false;
                delete partial.routedWithoutName;
                delete partial.code;
            }
            else if (partial.kit || partial.hasIngress == undefined /* || confirmed != undefined */) {
                partial.hasIngress = yield this._ui.promptConfirm('hasIngress', 'Accessible from outside the cabinet (aka: has ingress route): ', (_a = partial.hasIngress) !== null && _a !== void 0 ? _a : false);
                if (!partial.hasIngress) {
                    delete partial.routedWithoutName;
                    delete partial.code;
                }
            }
            // INGRESS INTERNAL
            if (partial.type == contracts_1.AssetTypesEnum.helm || !type_safety_utils_1.TypeSafetyUtils.toBoolean(partial.hasIngress)) {
                delete partial.ingressInternal;
            }
            else {
                let keep = yield this._ui.promptConfirm("isPrivateKeep", "Usage: ".concat(this._styler.hilite(type_safety_utils_1.TypeSafetyUtils.toBoolean(partial.ingressInternal) ? "private" : "external")), true);
                if (!keep) {
                    let choices = [{ name: "External (Public DNS)", value: "false" }, { name: "Private (not available on the internet)", value: "true" }];
                    let selected = yield this._ui.promptSelect("isPrivate", "Usage:", choices, false, true);
                    partial.ingressInternal = type_safety_utils_1.TypeSafetyUtils.toBoolean(selected);
                }
            }
            // HOST NAME
            if (partial.kit || partial.hasIngress == true) {
                let entry = "";
                while (!entry || entry.trim() == "") {
                    entry = (yield this._ui.prompt('code', 'Host name on the inbound domain url: ', false, (_b = partial.code) !== null && _b !== void 0 ? _b : (_c = partial.name) === null || _c === void 0 ? void 0 : _c.toLowerCase(), new HostNameValidator()));
                }
                partial.code = entry.toLowerCase().trim();
                partial.routedWithoutName = yield this._ui.promptConfirm('routedWithoutName', 'Will this asset be the base ingress against your DNS. eg: true services yourwebsite.com (as root/naked): ', (_d = partial.routedWithoutName) !== null && _d !== void 0 ? _d : false);
                // TODO:  Three can only be one asset with the option selected.  Unset the others if they set this.
            }
            if (!partial.code)
                partial.code = partial.name;
            return partial; // cast bc TS wouldnt infer this for us
        });
    }
    promptForAllPortAssignments(existingPorts) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            try {
                let ultimate;
                const existings = [];
                if (existingPorts) {
                    if (_.isArray(existingPorts)) {
                        existings.push(...existingPorts);
                    }
                    else {
                        existings.push(existingPorts);
                    }
                }
                // CONFIRM/EDIT EACH EXISTING
                let index = 0;
                for (let existing of existings) {
                    index++;
                    let portDef = yield this.promptForPortAssignment(existing, index);
                    // Append to the ultimate
                    if (portDef == undefined) {
                        this._ui.userMessage("Port removed.");
                        index--; // reuse this index for the next port assignment
                    }
                    else {
                        if (ultimate == undefined) {
                            ultimate = portDef;
                        }
                        else if (_.isArray(ultimate)) {
                            ultimate.push(portDef);
                        }
                        else {
                            ultimate = [ultimate, portDef];
                        }
                    }
                }
                // ADD NEW ONES
                // Automatically start adding when none are defined yet.  Otherwise, ask to add more.
                while ((ultimate == undefined || ultimate.length == 0) || (yield this._ui.promptConfirm("onemore", "Assign another port?", false))) {
                    index++;
                    const thisPort = yield this.promptForPortAssignment(undefined, index);
                    if (thisPort == undefined) {
                        index--; // re-use on the next port number
                    }
                    else {
                        this._ui.userMessage("Port ".concat(index + ": ", (typeof thisPort == 'number') ? thisPort.toString() : this.buildPortDesc(thisPort)));
                        // Append to the ultimate
                        if (ultimate == undefined) {
                            ultimate = thisPort;
                        }
                        else if (_.isArray(ultimate)) {
                            ultimate.push(thisPort);
                        }
                        else {
                            ultimate = [ultimate, thisPort];
                        }
                    }
                }
                return ultimate;
            }
            catch (err) {
                throw err;
            }
        });
    }
    /** Returns undefined if user opts to remove the given port assignment. */
    promptForPortAssignment(portDef, index) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            try {
                let thisPort;
                // 1.  Confirm existing portDef
                let keepMapping = false;
                if (portDef != undefined) {
                    thisPort = (typeof portDef == 'number') ? { external: portDef } : _.cloneDeep(portDef);
                    let desc = this.buildPortDesc(thisPort);
                    keepMapping = yield this._ui.promptConfirm("keepMapping", "Port ".concat(index + ": ", desc), true);
                }
                // 2.  Declare/Edit
                if (thisPort == undefined || !keepMapping) {
                    // SERVICE PORT (EXTERNAL)
                    let keepExt = false;
                    let promptExt = "Port ".concat(index + ": ", "Service Port (external): ");
                    if ((thisPort === null || thisPort === void 0 ? void 0 : thisPort.external) !== undefined) {
                        keepExt = yield this._ui.promptConfirm("keepExt", promptExt.concat(this._styler.hilite("" + thisPort.external)), true);
                    }
                    if (thisPort == undefined)
                        thisPort = { external: 0 };
                    if (keepExt !== true) {
                        thisPort.external = yield this.selectPortNumber(thisPort.external, promptExt);
                    }
                    // CONTAINER PORT (INTERNAL)
                    // TODO: allow for none and swiching to none
                    if (thisPort.external == this.REMOVE_PORT_NUMBER) {
                        return undefined; // User has opted for removal of this whole port assignment 
                    }
                    else {
                        let keepInt = false;
                        let promptInt = "Port ".concat(index + ": ", "Container Port (internal): ");
                        if (thisPort.internal) {
                            keepInt = yield this._ui.promptConfirm("keepInt", promptInt.concat(thisPort.internal.toString()));
                        }
                        else {
                            keepInt = !(yield this._ui.promptConfirm("keepInt", "Port ".concat(index + ": ", "Forwarding?" /* "Forward to a different port on the container?" */), false));
                        }
                        if (keepInt !== true) {
                            thisPort.internal = yield this.selectPortNumber(thisPort.internal, promptInt);
                            if (thisPort.internal == this.REMOVE_PORT_NUMBER) { // User selected "NONE"
                                delete thisPort.internal;
                            }
                        }
                    }
                }
                return ((thisPort.internal != undefined) && (thisPort.internal != thisPort.external)) ? thisPort : thisPort.external;
            }
            catch (err) {
                throw err;
            }
        });
    }
    selectPortNumber(defaultPort, prompt) {
        var _a;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let portSelector = new cli_common_1.PromptSelectFuzzy(this.logger, this._ui, this._styler);
            let fetcher = (search) => { return new Promise((resolve) => resolve(exports.ports)); };
            let fuzzyOnRemote = false;
            let validator = (candidate) => { return candidate == this.REMOVE_PORT_NUMBER.toString() || this.portIsValid(+(candidate) /*, assets, ports */); };
            let selectedPort = yield portSelector.go('port', prompt !== null && prompt !== void 0 ? prompt : 'Default port number to assign this asset when deploying to a cabinet:', false, (_a = defaultPort === null || defaultPort === void 0 ? void 0 : defaultPort.toString()) !== null && _a !== void 0 ? _a : "", fuzzyOnRemote, fetcher, validator);
            // if (thisPort == undefined) thisPort = { external: 0 };
            let selected = +(selectedPort !== null && selectedPort !== void 0 ? selectedPort : 0);
            return selected;
        });
    }
    /** Assembles the options list for port selection.
   * Decorates any options for ports that are already taken by an existing asset.
  */
    getPortOptionsList(portOptions, assets) {
        let output = [];
        for (let port of portOptions) {
            if (typeof port.port == "number") {
                let option = { value: `${port.port}`, name: `${port.port} ${port.reservedFor} [${port.group}]` };
                let asset = assets.find(a => a.port == port.port);
                if (asset) {
                    option.name = option.name.concat(" ", this._styler.colorWarning(`TAKEN!! [by ${asset.name}]`));
                }
                output.push(option);
            }
            else {
                for (let portNum = port.port.min; portNum <= port.port.max; portNum++) {
                    // ports.push({ value: `${portNum}`, name: `${portNum} ${port.reservedFor} [${port.group}]` }) 
                    let option = { value: `${portNum}`, name: `${portNum} ${port.reservedFor} [${port.group}]` };
                    let asset = assets.find(a => a.port == port.port);
                    if (asset) {
                        option.name = option.name.concat(" ", this._styler.colorWarning(`TAKEN!! [by ${asset.name}]`));
                    }
                    output.push(option);
                }
            }
        }
        // Add any asset ports that arent in the standard list.
        for (let asset of assets) {
            if (!output.find(p => p.value == `${asset.port}`)) {
                let option = { value: `${asset.port}`, name: `${asset.port} ` + this._styler.colorWarning(`TAKEN!! [by ${asset.name}]`) };
                output.push(option);
            }
        }
        // Add any remaining numbers
        let flush = [];
        let port = 0;
        for (var bespoke of output) {
            port++;
            let bport = Number.parseInt(bespoke.value);
            while (port < bport) {
                flush.push({ value: "" + port, name: port + " unreserved" });
                port++;
            }
            flush.push(bespoke);
            if (port > this.MAX_PORT_NUMBER)
                break;
        }
        return flush; // output;
    }
    /** @param otherAssets  CAN NOT include the current asset. */
    portIsValid(candidatePort /*, otherAssets: Asset[], allPorts: PromptChoice[] */, allowZero = false) {
        let isValid = false;
        let reason;
        let entry;
        // if (allPorts.find(p => p.value == `${candidatePort}`)) {
        //   // Means it was in the list of standard/registered ports
        //   entry = +(candidatePort);
        //   let owner = otherAssets.find(a => a.port == entry);
        //   isValid = (owner == undefined);
        //   if (owner) {
        //     reason = `Port number [${entry}] is ` + this._styler.colorWarning(`TAKEN!! [by ${owner.name}]`);
        //   }
        // } else {
        // Means it was a raw entry, un-matched in the autoselect
        entry = candidatePort;
        if (isNaN(+entry)) {
            isValid = false;
            reason = `[${entry}]: Not a port number.`; //; use TAB to autocomplete your selection.`;
        }
        else if ((allowZero && entry < 0) || (!allowZero && entry <= 0) || entry > 65535) {
            isValid = false;
            reason = `[${entry}]: Port number must be between 1 and 65535`;
            // } else {
            //   let owner = otherAssets.find(a => a.port == entry);
            //   isValid = (owner == undefined);
            //   if (owner) {
            //     reason = `Port number [${entry}] is ` + this._styler.colorWarning(`TAKEN!! [by ${owner.name}]`);
            //   }
        }
        else {
            isValid = true;
        }
        // }
        return (reason) ? { isValid: isValid, reason: reason } : isValid;
    }
    buildPortDesc(existingPort) {
        let desc;
        if (existingPort.internal == undefined) {
            desc = this._styler.hilite(existingPort.external.toString());
        }
        else {
            desc = "Fwd ".concat(this._styler.hilite(existingPort.external.toString()), "(ext)", " --> ", this._styler.hilite(existingPort.internal.toString()), "(int)");
        }
        return desc;
    }
    /** Parses strings in formats: a) `<number>:<number>` and `<number>:` and `<number>`,
     * eg: `8181` or `8181:80` or `8181:`
     * Throws invalids. */
    toPortMapping(portSpec) {
        var _a, _b, _c;
        let parts = portSpec.split(":");
        if (parts.length == 1) {
            let output = Number.parseInt(parts[0]);
            const valid = this.portIsValid(output);
            if (typeof valid === 'object')
                throw new Error("Invalid port number: ".concat((_a = valid.reason) !== null && _a !== void 0 ? _a : "Must be an integer between 0 and ".concat(this.MAX_PORT_NUMBER.toString())));
            return output;
        }
        else {
            let output = { external: Number.parseInt(parts[0]) };
            output.internal = Number.parseInt(parts[1]);
            const extValid = this.portIsValid(output.external);
            const intValid = this.portIsValid(output.internal);
            if (typeof extValid === 'object')
                throw new Error("Invalid external port number: ".concat((_b = extValid.reason) !== null && _b !== void 0 ? _b : "Must be an integer between 0 and ", this.MAX_PORT_NUMBER.toString()));
            if (typeof intValid === 'object')
                throw new Error("Invalid internal port number: ".concat((_c = intValid.reason) !== null && _c !== void 0 ? _c : "Must be an integer between 0 and ", this.MAX_PORT_NUMBER.toString()));
            return output;
        }
    }
    toPortString(portDef) {
        let output;
        if (typeof portDef == 'number') {
            output = portDef.toString();
        }
        else {
            let parts = [portDef.external];
            if (portDef.internal)
                parts.push(portDef.internal);
            output = parts.join(":");
        }
        return output;
    }
};
AssetConfigurator = tslib_1.__decorate([
    (0, inversify_1.injectable)(),
    tslib_1.__param(0, (0, inversify_1.inject)(ops_1.INTERFACES_CLI.IUserInputProvider)),
    tslib_1.__param(1, (0, inversify_1.inject)(ops_1.INTERFACES_CLI.IOutputStyler)),
    tslib_1.__param(2, (0, inversify_1.inject)(codiac_relay_1.RelayClient)),
    tslib_1.__param(3, (0, inversify_1.inject)(codiac_common_1.INTERFACES.IBetterLogger)),
    tslib_1.__metadata("design:paramtypes", [Object, Object, codiac_relay_1.RelayClient, Object])
], AssetConfigurator);
exports.AssetConfigurator = AssetConfigurator;
class HostNameValidator {
    constructor() {
    }
    isValid(candidate) {
        let isValid = false;
        let reason;
        let must = "must be 3- to 50-character string of alphanumerics and hyphens, starting with a letter.";
        if (candidate == undefined || _.isEmpty(candidate)) {
            // throw new Error("Missing ".concat(propertyName));
            reason = must;
        }
        else {
            let rxName = /^[a-zA-Z][a-zA-Z0-9-]{2,50}$/;
            if (!rxName.test(candidate)) {
                // throw new Error("Invalid ".concat(propertyName, ": ", must));
                reason = must;
            }
            else {
                isValid = true;
            }
        }
        return { isValid: isValid, reason: reason };
    }
    ;
}
exports.HostNameValidator = HostNameValidator;
//# sourceMappingURL=asset-configurator.js.map