"use strict";
var CloudProviderContextualizer_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CloudProviderContextualizer = void 0;
const tslib_1 = require("tslib");
const inversify_1 = require("inversify");
const codiac_common_1 = require("@codiac.io/codiac-common");
const contracts_1 = require("@codiac.io/codiac-common/contracts");
const cloud_resource_configurator_factory_1 = require("./cloud-resource-configurator-factory");
const codiac_relay_1 = require("../apis/codiac-relay");
const contracts_2 = require("../apis/codiac-relay/contracts");
const sdk_1 = require("../sdk");
const ops_1 = require("../ops");
const _ = require("lodash");
/** Prompts the user for a cloud resource provider,
 *  their credentials there, attempts a login,
 *  and then persists the credentials into the keyring under that tenant and provider.
 *
 * In subsequent sessions, this logic will allows the user to confirm/change accounts
 * for the given provider if they are already authenticated.
 */
let CloudProviderContextualizer = CloudProviderContextualizer_1 = class CloudProviderContextualizer {
    constructor(relayClient, loginUiFactory, _ui, _styler, logger) {
        this.relayClient = relayClient;
        this.loginUiFactory = loginUiFactory;
        this._ui = _ui;
        this._styler = _styler;
        this.logger = logger;
        this.takeDefaults = false;
    }
    static bootstrap(ioc) {
        if (this.isBootstrapped !== true) {
            let sdk = new sdk_1.CodiacSdkForCli(ioc);
            // COMMENTED OUT BC THEYRE FUTURE AND THEY COLLIDE WITH EXISTING IOC REGISTRATIONS
            // sdk.registerCspAdapter(AzureCliAdapter);
            // sdk.registerCspAdapter(AwsCliAdapter);
            // sdk.registerCspAdapter(DockerHubCliAdapter);
            // sdk.registerCspAdapter(ArtifactHubCliAdapter);
            // sdk.registerCspAdapter(LocalMachineCliAdapter);
            cloud_resource_configurator_factory_1.LoginConfiguratorFactory.bootstrap(ioc);
            if (!ioc.isBound(CloudProviderContextualizer_1)) {
                ioc.bind(CloudProviderContextualizer_1).toSelf();
            }
            this.isBootstrapped = true;
        }
    }
    promptForCloudProvider(allPvds, defaultValue) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            return new Promise((resolve, reject) => tslib_1.__awaiter(this, void 0, void 0, function* () {
                let output;
                try {
                    // let allPvds = this._regClientFactory.getAllProviders();
                    // TODO: support the default selection
                    let keep = this.takeDefaults;
                    if (defaultValue && allPvds.find(x => x == defaultValue) && (!this.takeDefaults)) {
                        keep = yield this._ui.promptConfirm("keepPvd", `Cloud Provider: ${this._styler.hilite(defaultValue)} `);
                    }
                    if (keep && defaultValue !== undefined) {
                        output = defaultValue;
                    }
                    else {
                        output = (yield this._ui.promptSelect("pvdName", "Cloud Provider:", allPvds.map(t => { return { name: t, value: t }; })));
                    }
                    resolve(output);
                }
                catch (err) {
                    // this.logger.error(`${err.name ?? ''} ${err.message ?? ''}`, err.stack);
                    reject(err);
                }
            }));
        });
    }
    /** Suffixes the username with its applicable csp subscriptionId
     *
     * as:
     * ```
     * "<username> (<subscriptionId>)"
     * ```
     * @param defaultSubxId Renders when user.subscriptionId is undefined.
     * */
    userAndSubx(user, defaultSubxId, hilite = false) {
        var _a, _b, _c;
        let userString = (hilite) ? this._styler.hilite(user.username) : user.username;
        return ((_a = user.subscriptionId) !== null && _a !== void 0 ? _a : defaultSubxId) ? userString.concat(" (", user.provider == contracts_2.CloudProviderEnum.aws ? "Acct:" : "Subx:", (_c = (_b = user.subscriptionId) !== null && _b !== void 0 ? _b : defaultSubxId) !== null && _c !== void 0 ? _c : "", ")") : userString;
    }
    /** Simple filter based on existence of filter values.  This method is just for reability. */
    isCspAccountMatch(cspTenantProfile, cspAcct, noFiltersResult = true) {
        var _a, _b;
        if ((((cspAcct === null || cspAcct === void 0 ? void 0 : cspAcct.tenantId) == undefined) || _.isEmpty(cspAcct.tenantId)) && ((cspAcct.subscriptionId == undefined) || _.isEmpty(cspAcct.subscriptionId))) {
            return noFiltersResult;
        }
        else {
            let isMatch = ((cspAcct === null || cspAcct === void 0 ? void 0 : cspAcct.tenantId) == undefined)
                || _.isEmpty(cspAcct.tenantId)
                || (cspTenantProfile.cspTenantId == cspAcct.tenantId.trim());
            isMatch = isMatch && ((cspAcct.subscriptionId == undefined)
                || _.isEmpty(cspAcct.subscriptionId)
                || ((_b = (_a = cspTenantProfile.subscriptionIds) === null || _a === void 0 ? void 0 : _a.includes(cspAcct.subscriptionId.trim())) !== null && _b !== void 0 ? _b : false));
            return isMatch;
        }
    }
    /** Acquires the relevant cspTenantProfile (if Cloud Provider supports it),
     *  forcing the user to select one if the user's organization in Codiac (aka: Codiac tenant) has more than one declared.
     */
    ensureCspTenant(providerCode, codiacApi, currentTenantId, throwNotFound = false) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            try {
                let ultimate;
                // TODO: break this out into ILoginConfigurator classes (inelegant for now, but it's only Azure that does this at the moment...)
                if (providerCode == contracts_2.CloudProviderEnum.azure) {
                    let cspAcct = {};
                    if ((currentTenantId != undefined)) {
                        cspAcct = (typeof currentTenantId == 'string') ? { tenantId: (_.isEmpty(currentTenantId.trim())) ? undefined : currentTenantId } : currentTenantId;
                    }
                    const profiles = yield codiacApi.CspTenantProfile.readMany({ provider: providerCode }, { throwNotFound: false });
                    if (profiles.length <= 0) {
                        throw new Error("You have not declared a Cloud Service Provider profile for your organization (which registers Codiac with your Azure tenant so that you can grant it access to perform work on your Azure resources).  Please contact your administrator to set this up.");
                    }
                    else {
                        let invalidTenant = false;
                        let found;
                        // Caller specified a CspTenant, so validate/retrieve it
                        if (((cspAcct === null || cspAcct === void 0 ? void 0 : cspAcct.tenantId) != undefined && !_.isEmpty(cspAcct.tenantId)) || ((cspAcct === null || cspAcct === void 0 ? void 0 : cspAcct.subscriptionId) != undefined && !_.isEmpty(cspAcct.subscriptionId))) {
                            found = profiles.find(p => this.isCspAccountMatch(p, cspAcct));
                            if (found) {
                                ultimate = found;
                            }
                            else {
                                invalidTenant = true;
                                if (throwNotFound == true) {
                                    throw new Error(`No ${providerCode} tenant profile was found for the given tenantId/subscriptionId combination.`);
                                }
                                else {
                                    this.logger.detail(`No ${providerCode} tenant profile was found matching the given tenantId/subscriptionId combination; user will be prompted for an existing one.`);
                                }
                            }
                        }
                        if (ultimate == undefined) {
                            // if (silentMode && profiles.length > 0) {
                            //   throw new Error("You have registered more than one csp tenant profile under which to authenticate, so you must specify one by providing either tenantId or subscriptionId with that provider in the request.");
                            // }
                            if (profiles.length == 1 && !invalidTenant) { // <-- Dont auto-override invalid input; we have to prompt user to change their input
                                ultimate = profiles[0];
                                this._ui.userMessage("Using ".concat(providerCode, " tenant: ", ultimate.cspTenantId));
                            }
                            else {
                                const choices = profiles.map(p => { return { name: "TenantId: ".concat(p.cspTenantId), value: p.cspTenantId }; });
                                const selected = (yield this._ui.promptSelect("cspTenantId", providerCode.concat(" Tenant Id: "), choices));
                                ultimate = profiles.find(p => p.cspTenantId == selected);
                            }
                        }
                    }
                }
                return ultimate;
            }
            catch (err) {
                throw err;
            }
        });
    }
    ensureCspTenantContext(codiacApi, provider, argsSubxId, argsTenantId, argsSilent = false) {
        var _a, _b;
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            try {
                let output = {};
                // CSP TENANT PROFILE
                // TODO: make this generic enough for a passthru on other csps
                if (argsSubxId != undefined && !_.isEmpty(argsSubxId.trim())) {
                    output.subscriptionId = argsSubxId.trim();
                }
                if (argsTenantId != undefined && !_.isEmpty(argsTenantId.trim())) {
                    output.tenantId = argsTenantId.trim();
                }
                if (provider == contracts_2.CloudProviderEnum.azure) {
                    if (argsSilent == true) {
                        // ensureCspTenant() prompt the user as needed - dont want or need that in silent mode.
                    }
                    else {
                        const cspTenant = yield this.ensureCspTenant(provider, codiacApi, output, false);
                        output.tenantId = cspTenant === null || cspTenant === void 0 ? void 0 : cspTenant.cspTenantId;
                        // Remove invalid subscriptionId (aka: if its not in the cspTenantProfile)
                        if (cspTenant && argsSubxId != undefined && !_.isEmpty(argsSubxId.trim())) {
                            if (((_b = (_a = cspTenant.subscriptionIds) === null || _a === void 0 ? void 0 : _a.includes(argsSubxId.trim())) !== null && _b !== void 0 ? _b : false) != true) {
                                delete output.subscriptionId;
                                // This ^ is consistent, but THIS MAY NOT BE BACKWARD COMPATIBLE!!!! 
                                // It requires all csp tenant profiles to stay up to date with EVERY subscriptionId.  
                                // Warn for now but activate ^ this line above when this feature is solid.
                                // this.logger.warn("Selected CspTenant profile does NOT contain the given subscriptionId: ".concat(argsSubxId));
                            }
                        }
                    }
                }
                return output;
            }
            catch (err) {
                throw err;
            }
        });
    }
    /** Enables the user to enter, use existing, or change the credentials for the given cloud service provider.
     *
     * Throws on authentication failure of cached creds,
     * or loops a login prompt otherwise.
     *
     * TODO: Return an authenticated user object.
     * @returns username
     *
     * @param suppressConfirmations TODO: Remove this argument and apply the non-tty ui provider for silent mode.
     * @param providerUserName TODO: Remove this argument and rely on relay state (via separate login step).
     * @param providerSecret TODO: Remove this argument and rely on relay state (via separate login step).
     * @param proprietaryInput Input specific to the selected cloud provider. (eg: Aws session token).
     */
    promptForCloudProviderCreds(ioc, providerCode, suppressConfirmations, providerUserName, providerSecret, reqSubscriptionId, proprietaryInput) {
        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, _b, _c, _d, _e, _f, _g, _h, _j;
                let output;
                try {
                    // let regUser = await this.relayClient.getCurrentUserFor(providerCode);
                    let cspAcct = {};
                    if (reqSubscriptionId) {
                        if (typeof reqSubscriptionId == 'string') {
                            cspAcct.subscriptionId = reqSubscriptionId;
                        }
                        else {
                            cspAcct = reqSubscriptionId;
                        }
                    }
                    let cachedCred;
                    // if (providerSubscriptionId) {
                    //   let userResults = await this.RelayClient.Call.get_current_users_for({ providerCode: providerCode, providerSubscriptionId: providerSubscriptionId });
                    //   if (userResults.error) throw userResults.error;
                    //   regUser = userResults.output ? userResults.output[0] : undefined;
                    // } else {
                    let userResults = new contracts_1.ListResult(false);
                    if ((providerUserName == undefined || providerUserName.trim() == "") && (providerSecret == undefined || providerSecret.trim() == "")) { // Skip existing creds if they gave us a username and password
                        userResults = yield this.relayClient.Call.get_current_users_for({ providerCode: providerCode, providerSubscriptionId: cspAcct.subscriptionId, providerTenantId: cspAcct.tenantId });
                        if (userResults.error)
                            throw userResults.error;
                    }
                    let keep = this.takeDefaults || (suppressConfirmations !== null && suppressConfirmations !== void 0 ? suppressConfirmations : false);
                    if (userResults.output) {
                        if (userResults.output.length == 1) {
                            cachedCred = userResults.output[0];
                            // 1.a Confirm existing creds
                            if (cachedCred !== undefined) {
                                // if ((!this.takeDefaults) && (!(suppressConfirmations ?? false))) {
                                //   let prompt = `Continue in ${this._styler.hilite(providerCode)} as [${this.userAndSubx(cachedCred, reqSubscriptionId, true)}]?`;
                                //   keep = await this._ui.promptConfirm("reauth", prompt.concat(` (hit "n" to change)`));
                                // } else if (keep == true) {
                                this._ui.userMessage("Continuing in ".concat(this._styler.hilite(providerCode), " as [", this.userAndSubx(cachedCred, cspAcct.subscriptionId, true), "]"));
                                keep = true;
                                // }
                            }
                        }
                        else if (userResults.output.length > 1) {
                            // 1.b Select existing creds
                            let index = 0;
                            let choices = userResults.output.map(x => { return { name: this.userAndSubx(x), value: index++ }; });
                            choices.push({ name: "** enter new credentials **", value: -1 });
                            const selected = (yield this._ui.promptSelect("cspSubxUser", `Select ${providerCode} credentials`, choices));
                            if (selected >= 0) {
                                cachedCred = userResults.output[selected];
                                keep = true;
                            }
                        }
                    }
                    // }
                    // 2. Attempt auth with existing creds
                    let isAuthed = false;
                    if (cachedCred && keep === true) {
                        this._ui.startSpinner(`Authenticating with ${providerCode}...`);
                        try {
                            const loginResult = yield this.relayClient.Call.csp_login({ providerCode: providerCode, providerSubscription: cachedCred.subscriptionId, providerTenant: cachedCred.cspTenantId }); // We already have creds, but let's validate with the service anyway  (az cli is the only one who needs this bc it runs off a machine-user state and has to set the state on the local machine).
                            if (loginResult.error)
                                throw loginResult.error;
                            if (loginResult.output == undefined)
                                throw new Error(`Login failed ${(_a = loginResult.message) !== null && _a !== void 0 ? _a : "[no details provided]"}`);
                            output = loginResult.output.cspUser;
                            this._ui.stopSpinner(`Success!`);
                            isAuthed = true;
                        }
                        catch (err) {
                            this._ui.stopSpinner(`Failed.`);
                            if (err.message)
                                this._ui.userMessage(err.message);
                            isAuthed = false;
                            // throw err;
                        }
                    }
                    // 3. If reqd, Log in interactively
                    if (!isAuthed) {
                        let loginUi = this.loginUiFactory.create(providerCode, ioc);
                        let cspUser;
                        // if (suppressConfirmations)
                        let ultCspAcct = {
                            subscriptionId: (_b = cachedCred === null || cachedCred === void 0 ? void 0 : cachedCred.subscriptionId) !== null && _b !== void 0 ? _b : cspAcct.subscriptionId,
                            tenantId: (_c = cachedCred === null || cachedCred === void 0 ? void 0 : cachedCred.cspTenantId) !== null && _c !== void 0 ? _c : cspAcct.tenantId
                        };
                        let loginOptions = {
                            providerSubscriptionId: ultCspAcct.subscriptionId,
                            providerTenantId: ultCspAcct.tenantId,
                            suppressConfirmations: suppressConfirmations,
                            providedUserName: providerUserName,
                            providedSecret: providerSecret,
                            proprietaryInput: proprietaryInput
                        };
                        [isAuthed, cspUser] = yield loginUi.promptToLogin(loginOptions);
                        if (!isAuthed) {
                            throw new Error(`Login to ${providerCode} failed${((_d = cspUser.username) !== null && _d !== void 0 ? _d : providerUserName) ? " for " + ((_e = cspUser.username) !== null && _e !== void 0 ? _e : providerUserName) : ""}${((_g = (_f = cspUser.subscriptionId) !== null && _f !== void 0 ? _f : ultCspAcct.subscriptionId) !== null && _g !== void 0 ? _g : "(none)") ? " (subx:" + ((_j = (_h = cspUser.subscriptionId) !== null && _h !== void 0 ? _h : ultCspAcct.subscriptionId) !== null && _j !== void 0 ? _j : "(none)") + ")" : ""}.`);
                        }
                        output = cspUser;
                    }
                    resolve(output); // assertion operator ok bc logic forces output to be defined.
                }
                catch (err) {
                    // this.logger.error(`${err.name ?? ''} ${err.message ?? ''}`, err.stack);
                    reject(err);
                }
            }));
        });
    }
    /** Used when cloud service provider has special proprietary input
     * required for csp login that breaks the standard patterns.
     * This method allows that provider a chance to build the login request however they see fit. */
    assembleLoginRequest(ioc, providerCode, user, secret, subscriptionId, proprietaryInput) {
        let loginUi = this.loginUiFactory.create(providerCode, ioc);
        return loginUi.assembleLoginRequest(user, secret, subscriptionId, proprietaryInput);
    }
    toCspAccountFilter(cspUser) {
        const cspAccount = {};
        if (cspUser === null || cspUser === void 0 ? void 0 : cspUser.cspTenantId)
            cspAccount.tenantId = cspUser.cspTenantId;
        if (cspUser === null || cspUser === void 0 ? void 0 : cspUser.subscriptionId)
            cspAccount.subscriptionId = cspUser.subscriptionId;
        return cspAccount;
    }
    toAccountDesc(acct) {
        let output = "";
        if ((acct === null || acct === void 0 ? void 0 : acct.subscriptionId) || (acct === null || acct === void 0 ? void 0 : acct.tenantId)) {
            const parts = [];
            if (acct.tenantId)
                parts.push("tenant: ".concat(acct.tenantId)); // TODO: Much better with a tenant Name
            if (acct.subscriptionId)
                parts.push("subx: ".concat(acct.subscriptionId)); // TODO: Much better with a subx Name
            if (parts.length > 0) {
                output = parts.join(", ");
            }
        }
        return output;
    }
    toCspUserDesc(cspUser) {
        let acct = this.toCspAccountFilter(cspUser);
        const output = this.toAccountDesc(acct);
        return output;
    }
    toIndex(things) {
        const byIndex = {};
        things.forEach((x, index) => byIndex[index.toString()] = x);
        return byIndex;
    }
    /** Signs the user out from the given cloud service provider.
     *
     * @returns username
     *
     * @param suppressConfirmations TODO: Remove this argument and apply the non-tty ui provider for silent mode.
     * @param providedUserName TODO: Remove this argument and rely on relay state (via separate login step).
     */
    promptToLogout(ioc, providerCode, accountFilter, suppressConfirmations, providedUserName) {
        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, _b, _c;
                let output;
                try {
                    let regUsers = yield this.relayClient.Call.get_current_users_for({
                        providerCode: providerCode,
                        providerSubscriptionId: accountFilter === null || accountFilter === void 0 ? void 0 : accountFilter.subscriptionId,
                        providerTenantId: accountFilter === null || accountFilter === void 0 ? void 0 : accountFilter.tenantId
                    });
                    let regUser; // Q: whats the undefined scenario?
                    let cspAccount;
                    if (regUsers.output && (regUsers.output.length > 0)) {
                        if (regUsers.output.length == 1) {
                            regUser = regUsers.output[0];
                        }
                        else {
                            // SELECT the Csp Creds
                            const usersByIndex = this.toIndex(regUsers.output);
                            const accountChoices = regUsers.output.map((u, index) => ({ name: this.toCspUserDesc(u), value: index.toString() }));
                            const selected = (yield this._ui.promptSelect("cspAcct", "Account: ", accountChoices));
                            regUser = usersByIndex[selected];
                        }
                        cspAccount = this.toCspAccountFilter(regUser);
                    }
                    // // 1. Confirm existing creds
                    // let keep = this.takeDefaults || (suppressConfirmations ?? false);
                    // if (regUser !== undefined && (!this.takeDefaults) && (!(suppressConfirmations ?? false))) {
                    //   keep = await this._ui.promptConfirm("reauth", `Continue in ${this._styler.hilite(providerCode)} as [${this._styler.hilite(regUser!)}]? (hit "n" to change)`);
                    // }
                    // 3. Sign out interactively
                    let cspLoginUi = this.loginUiFactory.create(providerCode, ioc);
                    let succeeded;
                    let userName;
                    // if (suppressConfirmations)
                    [succeeded, userName] = yield cspLoginUi.promptToLogout(cspAccount);
                    // TODO: Support a tri-state: 1) Completed successfully, 2) Failed with an error, and 3) tokens purged or not (and we arent waiting for popup)
                    if (!succeeded)
                        throw new Error(`Logout from ${providerCode} failed${providedUserName ? (_b = (_a = " for " + userName) !== null && _a !== void 0 ? _a : regUser) !== null && _b !== void 0 ? _b : providedUserName : ""}.`);
                    // while (!isAuthed && !suppressConfirmations) {
                    //   loginUi = this.loginUiFactory.create(providerCode, ioc);
                    //   [isAuthed, userName] = await loginUi.promptToLogin(undefined, suppressConfirmations, providedUserName, providedSecret);
                    // }
                    output = { username: (_c = userName !== null && userName !== void 0 ? userName : regUser === null || regUser === void 0 ? void 0 : regUser.username) !== null && _c !== void 0 ? _c : "" }; // assertion operator ok; logic forces it.
                    resolve(output.username); // operator ok bc logic forces output to be defined.
                }
                catch (err) {
                    // this.logger.error(`${err.name ?? ''} ${err.message ?? ''}`, err.stack);
                    reject(err);
                }
            }));
        });
    }
};
CloudProviderContextualizer.isBootstrapped = false;
CloudProviderContextualizer = CloudProviderContextualizer_1 = tslib_1.__decorate([
    (0, inversify_1.injectable)(),
    tslib_1.__param(0, (0, inversify_1.inject)(codiac_relay_1.RelayClient)),
    tslib_1.__param(1, (0, inversify_1.inject)(cloud_resource_configurator_factory_1.LoginConfiguratorFactory)),
    tslib_1.__param(2, (0, inversify_1.inject)(ops_1.INTERFACES_CLI.IUserInputProvider)),
    tslib_1.__param(3, (0, inversify_1.inject)(ops_1.INTERFACES_CLI.IOutputStyler)),
    tslib_1.__param(4, (0, inversify_1.inject)(codiac_common_1.INTERFACES.IBetterLogger)),
    tslib_1.__metadata("design:paramtypes", [codiac_relay_1.RelayClient,
        cloud_resource_configurator_factory_1.LoginConfiguratorFactory, Object, Object, Object])
], CloudProviderContextualizer);
exports.CloudProviderContextualizer = CloudProviderContextualizer;
//# sourceMappingURL=cloud-provider-contextualizer.js.map