"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const core_1 = require("@oclif/core");
const _ = require("lodash");
const YAML = require("yaml");
const command_base_exec_tenant_1 = require("../../../command-base-exec-tenant");
const CONFIGURE_MODES = ["overlay", "replace"];
/** Flat dot-path schema for the known leaf-level settings in CodiacInClusterSettingsOverride.
 *  Each key is a dot-path; each value is a human-readable description of that setting.
 *  Paths containing `*` represent dictionary keys — the user will be prompted to supply the key name. */
const AGENT_SETTINGS_PATHS = {
    // Codiac cluster agent chart settings
    "codiac-cluster-agent-chart.fullnameOverride": "Override for the full Helm release name of the cluster agent chart.",
    "codiac-cluster-agent-chart.configMap.create": "Whether to create the agent ConfigMap (true/false).",
    "codiac-cluster-agent-chart.env.*": "An environment variable on the cluster agent container. Replace * with the variable name.",
    "codiac-cluster-agent-chart.keel.policy": "Keel image watch policy (e.g. all, major, minor, patch, force).",
    "codiac-cluster-agent-chart.keel.pollSchedule": "Keel poll schedule as a cron expression (e.g. @every 3m).",
    "codiac-cluster-agent-chart.keel.trigger": "Keel trigger type (e.g. poll).",
    // Keel chart settings
    "keel.image.repository": "Repository for the Keel image.",
    "keel.image.tag": "Tag (version) of the Keel image to run.",
    "keel.image.pullPolicy": "Image pull policy (e.g. Always, IfNotPresent, Never).",
    "keel.helmProvider.enabled": "Whether to enable the Helm provider for Keel (true/false).",
    "keel.helmProvider.version": "Version of the Helm binary for Keel to use.",
    "keel.resources.limits.cpu": "CPU limit for Keel (e.g. 100m).",
    "keel.rbac.enabled": "Whether to enable RBAC for Keel (true/false).",
    "keel.rbac.role.create": "Whether to create the Keel RBAC role (true/false).",
    "keel.rbac.serviceAccount.create": "Whether to create the Keel service account (true/false).",
    "keel.nodeSelector.*": "A node selector label for the Keel pod. Replace * with the label key.",
};
class ClusterAgentConfigure extends command_base_exec_tenant_1.CommandBaseExecTenant {
    constructor() {
        // static hidden = true;
        super(...arguments);
        this.adminOnly = true;
    }
    captureArgs() {
        var _a, _b;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const { args, flags } = yield this.parse(ClusterAgentConfigure);
            let partial = {};
            if (args.cluster)
                partial.cluster = args.cluster;
            if (flags.mode)
                partial.mode = flags.mode;
            // Stdin takes precedence over --settings when both are provided
            const rawSettings = (_b = (_a = this.pipedInput) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : flags.settings;
            if (rawSettings)
                partial.settings = this.parseSettingsString(rawSettings);
            // ---------------------------------------------------------------
            // ---------------------------------------------------------------
            const takeDefaults = flags['take-defaults'];
            return [partial, takeDefaults];
        });
    }
    buildCandidate_silently(partial, rootMap) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (partial.cluster == undefined || _.isEmpty(partial.cluster))
                throw new Error("Must specify cluster.");
            if (partial.mode == undefined)
                partial.mode = "overlay";
            const found = rootMap.clusters.find(c => c.name == partial.cluster);
            if (found == undefined)
                throw new Error(`No cluster found with name [${partial.cluster}].`);
            if (!CONFIGURE_MODES.includes(partial.mode))
                throw new Error(`Invalid mode [${partial.mode}].  Must be one of: ${CONFIGURE_MODES.join(", ")}.`);
            return Object.assign({ cluster: partial.cluster, mode: partial.mode }, (partial.settings ? { settings: partial.settings } : {}));
        });
    }
    buildCandidate_interactively(partial, takeDefaults, rootMap) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let clusters = rootMap.clusters.map(c => c);
            if (partial.cluster) {
                const found = clusters.find(c => c.name == partial.cluster);
                if (found == undefined) {
                    this._ui.userMessage(this._styler.colorWarning(`No cluster found with name [${partial.cluster}]; please select from the list below.`));
                    partial.cluster = undefined;
                }
            }
            let selected;
            if (clusters.length == 1 && takeDefaults) {
                selected = clusters[0];
            }
            else {
                selected = yield this.rootMapUtils.promptForCluster(clusters, partial.cluster);
            }
            partial.cluster = selected.name;
            partial.mode = yield this.setMode(partial.mode, takeDefaults);
            // If no settings were provided via stdin or flag, prompt for a single key-value pair
            if (partial.settings == undefined) {
                partial.settings = yield this.promptForSettings(partial.cluster);
            }
            return yield this.buildCandidate_silently(partial, rootMap);
        });
    }
    buildCommandString(candidate, cmd) {
        cmd.push("--silent");
        cmd.push(candidate.cluster);
        cmd.push("--mode", candidate.mode);
        if (candidate.settings)
            cmd.push("--settings", JSON.stringify(candidate.settings));
        return cmd;
    }
    executeTask(candidate) {
        var _a, _b, _c;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            try {
                const result = yield this.watcher.triggerAndWatch((correlationId, socketId) => this.genRelayClient.Call.clusterEnvoyConfigure({
                    spec: Object.assign({ clusterName: candidate.cluster, mode: candidate.mode }, (candidate.settings ? { settings: candidate.settings } : {})),
                    socketId: socketId,
                    correlationId: correlationId,
                }));
                if (result.error) {
                    const msg = `Error configuring Codiac agent on cluster [${candidate.cluster}]: ${(_a = result.message) !== null && _a !== void 0 ? _a : ""} ${(_b = result.error.message) !== null && _b !== void 0 ? _b : ""}`.trimEnd();
                    this.logger.error(msg, result.error);
                    this._ui.userMessage(`${this._styler.colorCritical("ERROR!")} ${msg}`);
                }
                else if (!result.success) {
                    const msg = `Codiac agent configure failed on cluster [${candidate.cluster}]: ${(_c = result.message) !== null && _c !== void 0 ? _c : ""}`.trimEnd();
                    this.logger.notice(msg, result);
                    this._ui.userMessage(`${this._styler.colorCritical("FAILED!")} ${msg}`);
                }
                else {
                    const msg = `${this._styler.colorGood("SUCCESS!")} Codiac agent configured on cluster [${this._styler.hilite(candidate.cluster)}] using ${this._styler.hilite(candidate.mode)} mode.`;
                    this.logger.notice(msg, result);
                    this._ui.userMessage(msg);
                }
            }
            catch (err) {
                throw err;
            }
            finally {
                return;
            }
        });
    }
    /** Prompts the user to select a single setting from the known schema and enter its value.
     *  Reads the current agent config from the cluster to show existing values alongside each option.
     *  Fails gracefully if the config read is unavailable — prompting continues without current values. */
    promptForSettings(clusterName) {
        var _a, _b;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            // Read current config — fail gracefully so prompting still works without it
            const currentFlat = yield this.fetchAgentConfig(clusterName);
            // Build choices from schema, with current values shown dimly
            const schemaPaths = Object.keys(AGENT_SETTINGS_PATHS);
            const choices = schemaPaths.map(dotPath => {
                const hasWildcard = dotPath.includes("*");
                const currentValue = hasWildcard ? undefined : currentFlat[dotPath];
                const valueSuffix = currentValue != undefined
                    ? this._styler.dim(` = ${currentValue}`)
                    : "";
                const desc = this._styler.dim(`  — ${AGENT_SETTINGS_PATHS[dotPath]}`);
                return { name: `${dotPath}${valueSuffix}${desc}`, value: dotPath };
            });
            // Let the user pick a setting path
            let dotPath = (yield this._ui.promptSelect("agentSettingKey", "Select a setting to configure:", choices));
            if (dotPath == undefined)
                return undefined;
            // If the path contains a wildcard, prompt for the actual key name
            if (dotPath.includes("*")) {
                const prefix = dotPath.substring(0, dotPath.lastIndexOf(".*"));
                const keyName = yield this._ui.prompt("agentSettingWildcard", `${prefix}.<key name>:`, true);
                if (!keyName)
                    return undefined;
                dotPath = dotPath.replace("*", String(keyName).trim());
            }
            // Show the current value if there is one
            const currentValue = currentFlat[dotPath];
            if (currentValue != undefined) {
                this._ui.userMessage(`Current value: ${this._styler.dim(String(currentValue))}`);
            }
            // Prompt for the value
            const rawValue = yield this._ui.prompt("agentSettingValue", `${dotPath}:`, true, currentValue != undefined ? String(currentValue) : undefined);
            if (rawValue == undefined)
                return undefined;
            // Coerce to boolean if the schema path description mentions true/false
            const description = (_b = AGENT_SETTINGS_PATHS[(_a = Object.keys(AGENT_SETTINGS_PATHS).find(k => k === dotPath || dotPath.match(new RegExp("^" + k.replace(/\./g, "\\.").replace(/\*/g, "[^.]+") + "$")))) !== null && _a !== void 0 ? _a : ""]) !== null && _b !== void 0 ? _b : "";
            const isBooleanSetting = description.includes("true/false");
            const typedValue = isBooleanSetting
                ? (rawValue.trim().toLowerCase() === "true")
                : rawValue;
            // Build the nested settings object from the dot-path
            const settings = {};
            _.set(settings, dotPath, typedValue);
            return settings;
        });
    }
    setMode(userInput, takeDefaults) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            let output;
            if (userInput != undefined) {
                if (CONFIGURE_MODES.includes(userInput)) {
                    // Valid and explicitly provided — accept it
                    output = userInput;
                    this._ui.userMessage("Continuing with mode ".concat(this._styler.hilite(output)));
                }
                else {
                    // Invalid — warn and fall through to prompt
                    this._ui.userMessage(this._styler.colorWarning(`Invalid mode [${userInput}]; please select from the list below.`));
                }
            }
            if (output == undefined) {
                const defaultMode = "overlay";
                if (takeDefaults) {
                    output = defaultMode;
                    this._ui.userMessage("Continuing with mode ".concat(this._styler.hilite(output)));
                }
                else {
                    const choice = yield this._ui.promptSelect("configureMode", "Select configure mode:", CONFIGURE_MODES.map(m => ({ name: m, value: m })));
                    output = choice;
                }
            }
            return output;
        });
    }
    /** Reads the current agent config for the given cluster and returns it as a flattened dot-path dictionary.
     *  Fails gracefully — returns an empty object if the read is unavailable. */
    fetchAgentConfig(clusterName) {
        var _a, _b;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            try {
                const socketId = this.msgClient.isConnected()
                    ? ((_a = this.msgClient.connectionId) !== null && _a !== void 0 ? _a : yield this.msgClient.connect())
                    : yield this.msgClient.connect();
                const readResult = yield this.genRelayClient.Call.clusterEnvoyConfigRead({
                    spec: { clusterName },
                    socketId,
                });
                if (readResult.success && readResult.output) {
                    return this.flattenObject(readResult.output);
                }
                else if (readResult.message) {
                    this.logger.debug(`Could not read current agent config: ${readResult.message}`);
                }
            }
            catch (err) {
                this.logger.debug(`Could not read current agent config: ${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`);
            }
            return {};
        });
    }
    /** Parses a JSON or YAML string into a settings override object.  Tries JSON first; falls back to YAML.  Throws if neither parses successfully. */
    parseSettingsString(input) {
        try {
            return JSON.parse(input);
        }
        catch (_a) {
            // Not valid JSON — try YAML
        }
        try {
            return YAML.parse(input);
        }
        catch (_b) {
            throw new Error(`Settings could not be parsed as JSON or YAML.`);
        }
    }
    /** Recursively flattens a nested object into a dot-path keyed dictionary.
     *  Arrays are omitted — only scalar leaf values are included. */
    flattenObject(obj, prefix = "") {
        const result = {};
        for (const key of Object.keys(obj !== null && obj !== void 0 ? obj : {})) {
            const val = obj[key];
            const fullKey = prefix ? `${prefix}.${key}` : key;
            if (val !== null && typeof val === "object" && !Array.isArray(val)) {
                Object.assign(result, this.flattenObject(val, fullKey));
            }
            else if (!Array.isArray(val)) {
                result[fullKey] = val;
            }
        }
        return result;
    }
}
exports.default = ClusterAgentConfigure;
ClusterAgentConfigure.description = 'Applies configuration settings to the Codiac agent on the given cluster.  Use "overlay" mode (default) to merge settings on top of the existing configuration, or "replace" mode to overwrite the configuration entirely.  Settings can be provided as a JSON or YAML string via --settings, piped in via stdin, or entered interactively as a single key-value pair.';
ClusterAgentConfigure.aliases = ['cluster:envoy:configure'];
ClusterAgentConfigure.examples = [
    {
        description: 'Patch inline JSON settings into the existing agent configuration.',
        command: '<%= config.bin %> <%= command.id %> my-cluster --settings \'{"keel": {"image": {"repository": "my-repo", "tag": "latest", "pullPolicy": "Always"}}}\'',
    },
    {
        description: 'Replace the entire agent configuration with inline JSON settings.',
        command: '<%= config.bin %> <%= command.id %> my-cluster --mode replace --settings \'{"keel": {"image": {"repository": "my-repo", "tag": "latest", "pullPolicy": "Always"}}}\'',
    },
    {
        description: 'Pipe a YAML settings file into the command.',
        command: 'cat mysettings.yaml | <%= config.bin %> <%= command.id %> my-cluster',
    },
    {
        description: 'Non-interactive (scripted) invocation.',
        command: '<%= config.bin %> <%= command.id %> my-cluster --settings \'{"keel": {"image": {"tag": "v1.2.3"}}}\' --silent',
    },
    {
        description: 'Renders the equivalent silent command and then executes.',
        command: '<%= config.bin %> <%= command.id %> my-cluster --echo',
    },
    {
        description: 'Renders the equivalent silent command without executing.',
        command: '<%= config.bin %> <%= command.id %> my-cluster --to-script',
    },
];
ClusterAgentConfigure.flags = {
    mode: core_1.Flags.string({ char: 'm', options: CONFIGURE_MODES, description: 'How to apply the provided settings: "overlay" (default) merges the settings on top of the existing configuration; "replace" overwrites the configuration entirely.' }),
    settings: core_1.Flags.string({ description: 'JSON or YAML string of settings to apply to the agent configuration.  Alternatively, pipe settings content via stdin.' }),
    // 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: "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 where the command needs to override invalid user input; such cases will always prompt for confirmation." }),
};
ClusterAgentConfigure.args = {
    cluster: core_1.Args.string({ description: 'Name of the cluster in Codiac on which to configure the Codiac agent.  You can use `cod cluster list` to see available cluster names.' }),
};
//# sourceMappingURL=configure.js.map