"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const core_1 = require("@oclif/core");
const command_base_exec_1 = require("../../../command-base-exec");
const contracts_1 = require("../../../apis/codiac-api/contracts");
const cluster_def_1 = require("@codiac.io/codiac-domain/cluster-def");
const prettify_1 = require("../../../prettify");
const cloud_provider_contextualizer_1 = require("../../../uilogic/cloud-provider-contextualizer");
const security_account_1 = require("@codiac.io/codiac-domain/security-account");
const codiac_ops_common_1 = require("@codiac.io/codiac-ops-common");
const contracts_2 = require("../../../apis/codiac-relay/contracts");
const contracts_3 = require("@codiac.io/codiac-common/contracts");
const config_ui_utils_1 = require("../../../uilogic/config-ui-utils");
const _ = require("lodash");
const child_process_1 = require("child_process");
const IBetterLogger_1 = require("@codiac.io/codiac-common/IBetterLogger");
const terminal_utils_1 = require("../../../ops/terminal-utils");
const local_command_runner_1 = require("../../../ops/terminal-command-runner/local-command-runner");
class NocClusterCreate extends command_base_exec_1.CommandBaseExec {
    get uiCloudPvd() {
        if (this._uiCloudPvd_backer === undefined) {
            this._uiCloudPvd_backer = this.ioc.get(cloud_provider_contextualizer_1.CloudProviderContextualizer);
        }
        return this._uiCloudPvd_backer;
    }
    get termUtils() {
        if (this._termUtils_backer === undefined) {
            this._termUtils_backer = this.ioc.get(terminal_utils_1.TerminalUtils); // new TerminalUtils(this.logger);
        }
        return this._termUtils_backer;
    }
    get localCommandRunner() {
        if (this._localCommandRunner_backer === undefined) {
            this._localCommandRunner_backer = this.ioc.get(local_command_runner_1.LocalCommandRunner); // new LocalCommandRunner(this.msgClient, this.cliEnviro, this.termUtils, this.logger); 
        }
        return this._localCommandRunner_backer;
    }
    bootstrap() {
        super.bootstrap();
        cloud_provider_contextualizer_1.CloudProviderContextualizer.bootstrap(this.ioc);
        this.ioc.bind(terminal_utils_1.TerminalUtils).toDynamicValue(ctx => new terminal_utils_1.TerminalUtils(this.logger));
        this.ioc.bind(local_command_runner_1.LocalCommandRunner).toDynamicValue(ctx => new local_command_runner_1.LocalCommandRunner(this.msgClient, this.cliEnviro, this.termUtils, this.logger));
    }
    stringToEnum(enumObj, str) {
        if (str === undefined)
            return undefined;
        return Object.values(enumObj).includes(str) ? str : undefined;
    }
    captureArgs() {
        var _a, _b, _c;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { args, flags } = yield this.parse(NocClusterCreate);
            let partial = {
                name: args.name,
                locationName: flags.location,
                privateOwner: flags.owner,
                provider: this.stringToEnum(contracts_2.CloudProviderEnum, flags.provider),
                providerSubscription: flags.providerSubscription,
                providerGroup: flags.resourceGroup,
                subnet: flags.subnet,
                nodeSpec: flags.nodeSpec,
                startingNodeCount: flags.nodeQty,
                k8sVersion: flags.k8sVersion,
                nodesMin: flags.nodesMin,
                nodesMax: flags.nodesMax
            };
            if (args.name) {
                partial.identitySpec = {
                    type: cluster_def_1.ClusterDefConstants.DEFAULT_IDENTITY_TYPE,
                    name: cluster_def_1.ClusterDefConstants.buildDefaultIdentityName(args.name)
                };
            }
            if (flags.identityUserId && flags.identityUserName) {
                let systemName = flags.identityUserName;
                partial.identityInstance = new security_account_1.SecurityAccount(flags.identityUserId, flags.identityUserName, systemName, flags.identityUserPassword);
            }
            const interactionMode = (_b = (_a = (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 = (_c = flags['take-defaults']) !== null && _c !== void 0 ? _c : false;
            return [partial, interactionMode, takeDefaults];
        });
    }
    validatePartial(partial, rootMap) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (!partial.name)
                throw new Error("Cluster name must be set");
            if (!partial.provider)
                throw new Error("Provider must be set");
            if (!partial.providerSubscription)
                throw new Error("Provider subscription must be set");
            if (!partial.locationName)
                throw new Error("Location must be set");
            if (!partial.nodeSpec)
                throw new Error("Node spec must be set");
            if (!partial.startingNodeCount)
                throw new Error("Node quantity must be set");
            if (!partial.providerGroup)
                throw new Error("Resource group must be set");
            if (!partial.k8sVersion)
                throw new Error("Kubernetes version must be set");
            return partial;
        });
    }
    buildCandidate_silently(partial, rootMap) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            //Set some under the hood values
            partial.privateOwner = this.ioc.get(codiac_ops_common_1.UserNameTenantCode).tenantCode;
            //Should these be configurable via CLI?
            partial.identitySpec = { type: contracts_1.SecurityAccountTypeEnum.servicePrincipal, name: "" };
            partial.ingressDef = undefined;
            partial.nodeCount = undefined;
            partial = yield this.validatePartial(partial, rootMap);
            return partial;
        });
    }
    buildCandidate_interactively(partial, rootMap, takeDefaults) {
        var _a;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let candidate;
            let confirm;
            // await this.runSomeWslCommands();
            let defaults;
            candidate = partial;
            do {
                this._ui.userMessage(this._styler.bold("New Cluster: (creates after you confirm the overall input)"));
                let allPvds = this.cspUiFactory.getAllClusterProviders(this.ioc);
                candidate.provider = yield this.uiCloudPvd.promptForCloudProvider(allPvds, candidate.provider);
                let cspUser = yield this.uiCloudPvd.promptForCloudProviderCreds(this.ioc, candidate.provider, undefined, undefined, undefined, candidate.providerSubscription);
                if (candidate.providerSubscription == undefined && cspUser.subscriptionId) {
                    candidate.providerSubscription = cspUser.subscriptionId;
                }
                let regCfg = this.cspUiFactory.create(candidate.provider, this.ioc);
                let def = yield regCfg.defineCluster(candidate);
                // defaults = def.defaults;
                candidate = def.spec;
                if (typeof candidate.startingNodeCount == "string") {
                    candidate.startingNodeCount = Number.parseInt(candidate.startingNodeCount);
                }
                candidate = yield this.validatePartial(candidate, rootMap);
                console.clear();
                this._ui.userMessage((0, prettify_1.prettify)(candidate, [(_a = candidate.identityInstance) === null || _a === void 0 ? void 0 : _a.password]));
                this._ui.userMessage("You are about to create this cluster");
                confirm = yield this._ui.promptConfirm("confirmCreate", `Continue? hit N to edit your input`, false);
            } while (confirm !== true);
            return candidate;
        });
    }
    buildCommandString(candidate, cmd) {
        var _a, _b, _c, _d;
        cmd.push(candidate.name);
        cmd.push("--provider", candidate.provider);
        cmd.push("--providerSubscription", candidate.providerSubscription);
        cmd.push("--resourceGroup", candidate.providerGroup);
        cmd.push("--location", candidate.locationName);
        cmd.push("--nodeSpec", candidate.nodeSpec);
        cmd.push("--nodeQty", candidate.startingNodeCount.toString());
        cmd.push("--nodesMin", (_b = (_a = candidate.nodesMin) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "0");
        cmd.push("--nodesMax", (_d = (_c = candidate.nodesMax) === null || _c === void 0 ? void 0 : _c.toString()) !== null && _d !== void 0 ? _d : "0");
        // if (candidate.privateOwner) cmd.push("--owner", candidate.privateOwner);
        if (candidate.subnet)
            cmd.push("--subnet", candidate.subnet);
        if (candidate.identityInstance) {
            cmd.push("--identityUserName", candidate.identityInstance.name);
            cmd.push("--identityUserId", candidate.identityInstance.id);
            if (candidate.identityInstance.password) {
                cmd.push("--identityUserPassword", candidate.identityInstance.password);
            }
        }
        if (candidate.k8sVersion)
            cmd.push("--k8sVersion", candidate.k8sVersion);
        cmd.push("--silent");
        return cmd;
    }
    executeTask(candidate) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            try {
                if (candidate.provider == contracts_2.CloudProviderEnum.localMachine) {
                    let socketId = yield this.msgClient.connect();
                    this.localCommandRunner.listen(); // to fire off any local machine terminal commands requested by the relay during the cluster create
                }
                // this._ui.startSpinner(`Creating new cluster [${candidate.name}]...`);
                let result = yield this.watcher.triggerAndWatch((socketId) => this.genRelayClient.Call.clusterCreateAsync({ spec: candidate, socketId: socketId }));
                // this._ui.stopSpinner(`${result.success ? "Success!" : "Failed."}`);
                if (result.error) {
                    this.logger.error(`Error creating cluster: ${result.message || ""}; ${result.error.message}`, result.error);
                    this._ui.userMessage(`Error creating cluster: ${result.message || ""}; ${result.error.message}`);
                }
                else if (!result.success) {
                    this.logger.notice(`Cluster creation failed without error. ${result.message}`);
                    this._ui.userMessage(`Cluster creation failed without error. ${result.message}`);
                }
                else {
                    let msg = `${this._styler.colorGood("SUCCESS!")} Created cluster [${this._styler.hilite(candidate.name)}] \nin location [${candidate.locationName}], resource group: [${candidate.providerGroup}], subscription [${candidate.providerSubscription}].`;
                    this.logger.notice(msg, result);
                    this._ui.userMessage(msg);
                }
            }
            catch (err) {
                throw err;
            }
            finally {
                if (this.msgClient.isConnected()) {
                    yield this.msgClient.disconnect();
                }
            }
        });
    }
    setUpLocalCommandRunner() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            try {
                let subject = "mylocalmachine.executeinterminal";
                this.localTerminalCommandRunnerHandlerId = this.msgClient.subscribe(subject, (err, msg, meta, raw) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                    var _a, _b;
                    // 1) Execute the command in a terminal
                    this.logger.debug("Terminal command received", msg);
                    let response = new contracts_3.SingleResult(false, "command executor is not yet completely implemented on the cli.", undefined, { code: 1, stdout: "Nothing to see here; move along." });
                    try {
                        // TODO: Actually execute the prescribed command
                        let ultReq = _.cloneDeep(msg);
                        ultReq.env = Object.assign(Object.assign({}, process.env), msg.env);
                        response.output = yield this.termUtils.execute(ultReq);
                    }
                    catch (err) {
                        response.output.code = 1; // generic error code (if we're here, then we're not able to parse it from the terminal output)
                        response.output.error = err;
                        response.message = (_a = err.message) !== null && _a !== void 0 ? _a : response.message;
                    }
                    finally {
                        response.success = Boolean(response.output.code == 0);
                        if (response.output) {
                            if (response.output.error && !_.isEmpty(response.output.error)) {
                                response.error = response.output.error;
                                if (response.error.message && !_.isEmpty(response.error.message))
                                    response.message = response.output.error.message;
                            }
                        }
                    }
                    // 2) Reply with results
                    let responseMeta = {
                        action: meta.action,
                        correlationId: meta.correlationId,
                        subject: (_b = meta.replyOn) !== null && _b !== void 0 ? _b : subject
                    };
                    yield this.msgClient.publish(response, responseMeta);
                    return;
                }));
            }
            catch (err) {
                throw err;
            }
        });
    }
    runSomeWslCommands() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let localUser = "mfreydl";
            // in order to avoid sudo everywhere, we can add the user to the "microk8s" group...
            // sudo usermod -a -G microk8s mfreydl
            // sudo chown -R mfreydl ~/.kube
            // const term = spawn("wsl.exe", ["-e", "bash"]);
            // const term = spawn("wsl.exe -d ubuntu-20.04", ["-e", "bash"]);
            const termCommand = "wsl";
            const termArgs = ["-d", "ubuntu-20.04", "bash", "-l", "-c"];
            // const cmd = ["wsl.exe", "-d", "ubuntu-20.04"].join(" ");
            const cmd = ["snap list && microk8s status --wait-ready && microk8s kubectl config view && microk8s kubectl get nodes"];
            // const commandSet = [cmd, cmd2].join(" && ");
            // const outcode = await this.spawnAndLog(cmd, process.cwd(), process.env, LogLevel.trace, true);
            const outcode = yield this.spawnTerminalCommands(termCommand, termArgs, cmd, process.cwd(), process.env, IBetterLogger_1.LogLevel.trace, true);
            // // Handle output from the terminal
            // term.stdout.on('data', (data) => {
            //     this._ui.userMessage(`stdout: ${data}`);
            // });
            // term.stderr.on('data', (data) => {
            //     this._ui.userMessage(`stderr: ${data}`);
            // });
            // term.on('close', (code) => {
            //     this._ui.userMessage(`child process exited with code ${code}`);
            // });
            // term.stdin.write("sudo snap list\n");
            // term.stdin.write("sudo microk8s status --wait-ready\n");
            // term.stdin.write("exit\n");
            // term.disconnect();  // do we need to do this after an exit?
        });
    }
    /** Fires the given shell command into a child process
     * that inherits the parent's stdio (input, output, and error streams),
     * and resolves the promise on exit of that child process.
     * Returns the exit code from the child process.
     * @param env If this gets passed in, it replaces the current env.  That is, no env vars from the parent get carried into the child process.
     *            So if you use this param and you still want the current env vars, you'll have to add process.env to your dictionary (use a spread operator).
     * @param collectStdOut (optional, defaults to false) Prevents piping runtime data directly to stdout and instead renders it after completion. Some processes provide runtime status messages (eg: docker's percent completes, etc), which get obstructed by piping to stdout. If so, set this argument to true.
     * */
    spawnTerminalCommands(terminalCommand, terminalArgs, cmd, cwd, env, logLevel = IBetterLogger_1.LogLevel.debug, collectStdOut = false) {
        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 {
                    // DEV NOTE:   This is a good refresher:  https://www.freecodecamp.org/news/node-js-child-processes-everything-you-need-to-know-e69498fe970a/
                    this.logger.write(`Executing: ${cmd}  from: ${cwd}`, logLevel);
                    let output;
                    let options = {
                        cwd: cwd,
                        // stdio: "inherit",
                        stdio: [process.stdin, process.stdout, process.stderr],
                        shell: true
                    };
                    if (env)
                        options.env = env;
                    // Dim = "\x1b[2m"
                    // process.stdout.write("\x1b[2m")
                    let commandString = cmd.join(" ");
                    commandString = "\"".concat(commandString.replace("\"", "\\\""), "\"");
                    let child = (0, child_process_1.spawn)(terminalCommand, [...terminalArgs, commandString], options);
                    // Can we set child stdout to a chalk stream that colors the output with our single color?
                    // eg:          
                    //      let chalkStrm = new Writable();
                    //      chalkStrm.addListener('pipe', (src) => {
                    //        let chunk = src.read();
                    //        process.stdout.write(chalk.grey(chunk));
                    //      });
                    // NOTE ALSO: child processes tend to lose their own coloring when being piped (see link below)
                    // https://stackoverflow.com/questions/31620298/is-it-possible-for-child-processes-in-node-js-to-preserve-colored-output
                    // For since the child proc is already writing to our own stdio streams, 
                    // logging these error feeds is going to show up as duplicate output in the console. 
                    // child.stderr.on('data', (data) => { this.logger.trace(data)});  
                    if (child.stdout) {
                        child.stdout.on('data', (chunk) => {
                            // this.logger.trace(chunk);
                            // process.stdout.write(chalk.grey(chunk));
                            var _a;
                            if (collectStdOut === true) {
                                output = (_a = output === null || output === void 0 ? void 0 : output.concat(chunk)) !== null && _a !== void 0 ? _a : chunk;
                            }
                            else {
                                // It appears that we lose the runtime status messages 
                                // when we pipe like this (eg: docker's percent completes, etc).
                                // If so, set collectStdOut to true.
                                process.stdout.write(chunk);
                            }
                        });
                    }
                    child.on('data', (chunk) => {
                        var _a;
                        this.logger.trace(chunk);
                        // process.stdout.write(chalk.grey(chunk));
                        if (collectStdOut === true) {
                            output = (_a = output === null || output === void 0 ? void 0 : output.concat(chunk)) !== null && _a !== void 0 ? _a : chunk;
                        }
                        else {
                            // It appears that we lose the runtime status messages 
                            // when we pipe like this (eg: docker's percent completes, etc).
                            // If so, set collectStdOut to true.
                            process.stdout.write(chunk);
                        }
                    });
                    // When we do this, something is keeping the process from exiting.  unpiping afterwards didnt help
                    // child.stdout.pipe(process.stdout);
                    // child.stderr.pipe(process.stderr);
                    // process.stdin.pipe(child.stdin);        
                    child.on('close', (code, signal) => {
                        // this.logger.trace(`Command closed with code: ${code} and signal ${signal}`);
                        if (code == 0) {
                            // this.logger.trace(`Command exited ${(code != undefined) ? 'with code: ' + code : 'without code'} and ${(signal) ? 'with signal ' + signal : 'without signal'}`);
                            resolve(code);
                        }
                        else {
                            // TODO:  Add the stderr output to the error object.
                            reject(new Error(`Error (return code: ${code})`));
                        }
                    });
                    child.on('error', (err) => {
                        this.logger.error(`Command returned error ${err.message || ""}`, err);
                        reject(err);
                    });
                    child.on('exit', (code, signal) => {
                        // process.stdin.unpipe(child.stdin);
                        // child.stderr.unpipe(process.stderr);
                        // child.stdout.unpipe(process.stdout);
                        if (output)
                            this.logger.write(output, logLevel);
                        // reset:  \x1b[0m
                        process.stdout.write("\x1b[0m");
                        // If we're handling both close and error, we dont need this handler, right?
                        if (code == 0) {
                            // this.logger.trace(`Command exited ${(code != undefined) ? 'with code: ' + code : 'without code'} and ${(signal) ? 'with signal ' + signal : 'without signal'}`);
                            resolve(code);
                        }
                        else {
                            // TODO:  Add the stderr output to the error object.
                            reject(new Error(`Error (return code: ${code})`));
                        }
                    });
                    (_a = child.stdin) === null || _a === void 0 ? void 0 : _a.write("exit");
                }
                catch (err) {
                    reject(err);
                }
            }));
        });
    }
}
exports.default = NocClusterCreate;
NocClusterCreate.description = 'Provisions the physical cluster';
NocClusterCreate.aliases = ["cluster:create"];
NocClusterCreate.args = {
    name: core_1.Args.string({ description: 'Proposed name of the cluster' })
};
NocClusterCreate.flags = Object.assign(Object.assign({}, command_base_exec_1.CommandBaseExec.flags), { help: core_1.Flags.help({ char: 'h' }), provider: core_1.Flags.string({ options: [contracts_2.CloudProviderEnum.azure, contracts_2.CloudProviderEnum.aws, contracts_2.CloudProviderEnum.localMachine], description: '(Required in silent mode) Cloud service provider on which to provision the cluster' }), providerSubscription: core_1.Flags.string({ char: 's', description: '(Required in silent mode) The Azure subscription Id under which to create the cluster.' }), resourceGroup: core_1.Flags.string({ char: 'g', description: '(Required in silent mode) The Azure resource group to create for the cluster.' }), location: core_1.Flags.string({ char: 'l', description: '(Required in silent mode) The short name of the azure data center in which the cluster is to reside.' }), nodeQty: core_1.Flags.integer({ char: 'q', default: 2, description: '(Required in silent mode) The starting node count with which to create the cluster.' }), k8sVersion: core_1.Flags.string({ description: '(Required in silent mode) The version of Kubernetes to install on the cluster.' }), 
    // owner: Flags.string({ char: 'p', description: 'OPTIONAL: Tenant code of the private owner of the cluster. Will default to current tennant context.' }),
    subnet: core_1.Flags.string({ char: 'k', description: 'OPTIONAL: The resource id of the subnet in which the cluster is to be created.' }), nodeSpec: core_1.Flags.string({ char: 'n', description: 'The identifier for the type of VM to use for nodes in the cluster.' }), 
    //Are uersName, userId, and password all required together (inter-dependent)?
    identityUserName: core_1.Flags.string({ char: 'u', description: 'OPTIONAL. Name of Svc Principal or Managed Identity to execute as.' }), identityUserId: core_1.Flags.string({ description: 'OPTIONAL. ID of Svc Principal or Managed Identity to execute as.' }), identityUserPassword: core_1.Flags.string({ char: 'c', hidden: true, description: 'OPTIONAL. Password of Svc Principal or Managed Identity to execute as.' }), nodesMin: core_1.Flags.integer({ description: 'OPTIONAL. The minimum number of nodes to maintain in the cluster.' }), nodesMax: core_1.Flags.integer({ description: 'OPTIONAL. The maximum number of nodes to maintain in the cluster.' }), 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.',
        relationships: [
            { type: 'all', flags: ['k8sVersion', 'provider', 'providerSubscription', 'location', 'nodeSpec', 'nodeQty', 'resourceGroup'] }
        ]
    }), 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.' }), 
    //Not sure what to do with this one yet
    "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)." }) });
NocClusterCreate.examples = [
    {
        description: "Create a cluster interatively.",
        command: "cod cluster create my-cluster"
    },
    {
        description: "Create a cluster interatively and assemble a non-interactive command string.",
        command: "cod cluster create my-cluster --echo"
    },
    {
        description: "Assemble a non-interactive command string WITHOUT actually creating a cluster (dry run).",
        command: "cod cluster create my-cluster --to-script"
    },
    {
        description: "Create a cluster silently.",
        command: "cod cluster create my-cluster --provider azure --providerSubscription 12345678-1234-1234-1234-123456789012 --resourceGroup my-rg --location eastus --nodeSpec Standard_DS2_v2 --nodeQty 2 --silent"
    },
    {
        description: "Create a cluster silently with all available options (NOTE: you can generate this command using --to-script)",
        command: `cod cluster create my-comprehensive-cluster --provider azure --providerSubscription 12345678-1234-1234-1234-123456789012 --resourceGroup my-resource-group --location eastus2 --nodeSpec Standard_D2s_v3 --nodeQty 3 --k8sVersion 1.21.2 --nodesMin 2 --nodesMax 5 --owner my-tenant-code --subnet /subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/my-vnet-rg/providers/Microsoft.Network/virtualNetworks/my-vnet/subnets/my-subnet --identityUserName my-service-principal --identityUserId 87654321-4321-4321-4321-210987654321 --identityUserPassword mySecurePassword123! --silent`
    }
];
//# sourceMappingURL=create.js.map