"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AuthenticationProvider = void 0;
exports.isOidcError = isOidcError;
const open_1 = __importDefault(require("open"));
const axios_1 = __importDefault(require("axios"));
const jwt_decode_1 = require("jwt-decode");
const env_1 = require("../env/env");
const cliConfigProvider_1 = require("./cliConfigProvider");
const types_1 = require("../api/types");
const mablApiClientFactory_1 = require("../api/mablApiClientFactory");
const loggingProvider_1 = require("./logging/loggingProvider");
const pureUtil_1 = require("../util/pureUtil");
const OktaClient_1 = require("../auth/OktaClient");
const axiosProxyConfig_1 = require("../http/axiosProxyConfig");
const types_2 = require("./types");
const humanizeDuration = require('humanize-duration');
const inquirer = require('inquirer');
const chalk = require('chalk');
class AuthenticationProvider {
    constructor(shouldExitOnError = true) {
        this.verbose = false;
        this.shouldExitOnError = shouldExitOnError;
    }
    exitOrThrow(msg) {
        if (this.shouldExitOnError) {
            process.exit(1);
        }
        else {
            throw new Error(msg);
        }
    }
    async getOktaClient() {
        if (!this.oktaClient) {
            const client = axios_1.default.create(await (0, axiosProxyConfig_1.currentProxyConfig)());
            this.oktaClient = new OktaClient_1.OktaClient(client);
        }
        return this.oktaClient;
    }
    async getAuthConfigWithAutoRenew() {
        const authConfig = (await cliConfigProvider_1.CliConfigProvider.getCliConfig()).authentication;
        if (authConfig.accessToken) {
            if (this.verbose) {
                loggingProvider_1.logger.info('Found existing access token');
            }
            return this.maybeRenewAuthentication(authConfig);
        }
        return authConfig;
    }
    async maybeRenewAuthentication(authConfig) {
        if (!authConfig.refreshToken || authConfig.authType === types_1.AuthType.ApiKey) {
            if (this.verbose) {
                loggingProvider_1.logger.info(`Authentication refresh token ${chalk.red.bold('Not Found')}`);
            }
            return authConfig;
        }
        if (authConfig.authProvider === types_2.AuthProviderType.Auth0) {
            if (this.verbose) {
                loggingProvider_1.logger.info(`Authentication refresh token ${chalk.yellow.bold('not valid')}`);
            }
            return authConfig;
        }
        const nowMilliseconds = Date.now();
        const refreshTokenValidMilliseconds = authConfig.refreshTokenExpirationTimeMilliseconds
            ? authConfig.refreshTokenExpirationTimeMilliseconds - nowMilliseconds
            : undefined;
        if (refreshTokenValidMilliseconds !== undefined &&
            refreshTokenValidMilliseconds <= 0) {
            const msg = `Login has ${chalk.yellow.bold('expired')}.  Please run ${chalk.magenta('mabl auth login')} to log in with your mabl account`;
            loggingProvider_1.logger.info(msg);
            this.exitOrThrow(msg);
        }
        if (this.verbose) {
            loggingProvider_1.logger.info('Found valid refresh token');
        }
        const RefreshAccessTokenWindowMilliseconds = 10 * 60 * 1000;
        const accessTokenValidMilliseconds = authConfig.accessTokenExpirationTimeMilliseconds - nowMilliseconds;
        if (this.verbose) {
            if (accessTokenValidMilliseconds > 0) {
                loggingProvider_1.logger.info(`Existing access token expires in [${chalk.magenta.bold(humanizeExpirationTime(accessTokenValidMilliseconds))}]`);
            }
            else {
                loggingProvider_1.logger.info(`Access token has ${chalk.yellow.bold('expired')}`);
            }
        }
        if (accessTokenValidMilliseconds < RefreshAccessTokenWindowMilliseconds) {
            if (this.verbose) {
                loggingProvider_1.logger.info(`Renewing authorization...`);
            }
            const oktaClient = await this.getOktaClient();
            let newAuthInfo;
            try {
                newAuthInfo = await oktaClient.exchangeRefreshTokenForAccessToken(authConfig.refreshToken);
            }
            catch (error) {
                const msg = 'Token refresh failed.';
                processAuthorizationError(error, true);
                this.exitOrThrow(msg);
            }
            if (newAuthInfo.error) {
                const msg = chalk.red.bold(`Could not refresh authentication due to error: ${newAuthInfo.error_description}`);
                loggingProvider_1.logger.error(msg);
                this.exitOrThrow(msg);
            }
            if (!newAuthInfo.refresh_token_expires_at &&
                authConfig.refreshTokenExpirationTimeMilliseconds) {
                newAuthInfo.refresh_token_expires_at =
                    authConfig.refreshTokenExpirationTimeMilliseconds / 1000;
            }
            await cliConfigProvider_1.CliConfigProvider.setCliAuthInfo(newAuthInfo, nowMilliseconds);
            return (await cliConfigProvider_1.CliConfigProvider.getCliConfig()).authentication;
        }
        return authConfig;
    }
    static async getAuthType() {
        const config = await cliConfigProvider_1.CliConfigProvider.getCliConfig();
        return config.authentication.authType;
    }
    async displayInfo() {
        try {
            const config = await cliConfigProvider_1.CliConfigProvider.getCliConfig();
            if (config.authentication.authType === types_1.AuthType.ApiKey) {
                loggingProvider_1.logger.info(`You are authenticated by ${chalk.magenta.bold('API key')}`);
                return;
            }
            if (!config.email || config.authentication.authType === undefined) {
                loggingProvider_1.logger.info('You are not logged in');
                return;
            }
            loggingProvider_1.logger.info(`Logged in as user [${chalk.magenta.bold(config.email)}]`);
            const expirationMilliseconds = (config.authentication.refreshTokenExpirationTimeMilliseconds ??
                config.authentication.accessTokenExpirationTimeMilliseconds) -
                Date.now();
            if (expirationMilliseconds > 0) {
                loggingProvider_1.logger.info(`Login expires in [${chalk.magenta.bold(humanizeExpirationTime(expirationMilliseconds))}]`);
            }
            else {
                loggingProvider_1.logger.info(`Login has ${chalk.yellow.bold('expired')}`);
            }
        }
        catch (error) {
            loggingProvider_1.logger.error(chalk.red.bold(error));
            this.exitOrThrow(error instanceof Error ? error.message : String(error));
        }
    }
    async clearAuthentication() {
        try {
            const config = await cliConfigProvider_1.CliConfigProvider.getCliConfig();
            if (!config.email && config.authentication.authType !== types_1.AuthType.ApiKey) {
                loggingProvider_1.logger.info('You are not logged in');
                return;
            }
            if (config.authentication.authType === types_1.AuthType.Bearer) {
                await this.revokeTokens(config.authentication);
            }
            await cliConfigProvider_1.CliConfigProvider.clearAuthConfig();
            if (config.email) {
                loggingProvider_1.logger.info(`Logged out user [${chalk.magenta.bold(config.email)}]`);
            }
            else {
                loggingProvider_1.logger.info(`Removed ${chalk.magenta.bold('API key')}`);
            }
        }
        catch (error) {
            loggingProvider_1.logger.error(chalk.red.bold(error));
            this.exitOrThrow(error instanceof Error ? error.message : String(error));
        }
    }
    async revokeTokens(authentication) {
        const revocations = [];
        if (authentication.accessToken) {
            revocations.push(this.revokeMablJwt(authentication.accessToken));
        }
        if (authentication.authProvider === types_2.AuthProviderType.Okta) {
            revocations.push(this.revokeOktaTokens(authentication));
        }
        const results = await Promise.allSettled(revocations);
        for (const result of results) {
            if (result.status === 'rejected') {
                loggingProvider_1.logger.debug(`Token revocation failed; proceeding with local logout: ${result.reason}`);
            }
        }
    }
    async revokeMablJwt(accessToken) {
        const apiClient = await mablApiClientFactory_1.MablApiClientFactory.createApiClientFromBearerToken(accessToken);
        await apiClient.logoutJwt();
    }
    async revokeOktaTokens(authentication) {
        const oktaClient = await this.getOktaClient();
        const revocations = [];
        if (authentication.refreshToken) {
            revocations.push(oktaClient.revokeToken(authentication.refreshToken, 'refresh_token'));
        }
        if (authentication.accessToken) {
            revocations.push(oktaClient.revokeToken(authentication.accessToken, 'access_token'));
        }
        await Promise.all(revocations);
    }
    async activateApiKey(apiKey) {
        const authInfo = {
            auth_provider: types_2.AuthProviderType.Mabl,
            auth_type: types_1.AuthType.ApiKey,
            api_key: apiKey,
        };
        const requestTimeMilliseconds = Date.now();
        await cliConfigProvider_1.CliConfigProvider.setCliAuthInfo(authInfo, requestTimeMilliseconds);
    }
    async authenticate() {
        const oktaClient = await this.getOktaClient();
        try {
            const { codeChallenge, codeVerifier } = oktaClient.generateCodeChallenge();
            const redirectUri = `${env_1.BASE_APP_URL}/app-code`;
            const authUrl = oktaClient.buildAuthorizationUrl(codeChallenge, redirectUri);
            loggingProvider_1.logger.info(`Your browser has been opened to the following URL for obtaining an authorization code:

${authUrl}
          `);
            await this.openUrl(authUrl);
            const response = await inquirer.prompt([
                {
                    type: 'password',
                    name: 'authCode',
                    message: 'Authorization code:',
                    mask: true,
                },
            ]);
            const requestTimeMilliseconds = Date.now();
            let authInfo;
            try {
                authInfo = await oktaClient.validateAuthCode(response.authCode, codeVerifier, redirectUri);
            }
            catch (error) {
                const msg = 'Authorization failed.';
                processAuthorizationError(error, false);
                this.exitOrThrow(msg);
            }
            if (authInfo.error) {
                const msg = chalk.red.bold(`Could not login due to error: ${authInfo.error_description}`);
                loggingProvider_1.logger.error(msg);
                this.exitOrThrow(msg);
            }
            await cliConfigProvider_1.CliConfigProvider.setCliAuthInfo(authInfo, requestTimeMilliseconds);
            const userData = await AuthenticationProvider.requestUserInfo(authInfo.access_token);
            const config = await cliConfigProvider_1.CliConfigProvider.getCliConfig();
            config.userId = userData.id;
            config.email = userData.email;
            config.userFullName = userData.name;
            await cliConfigProvider_1.CliConfigProvider.setCliConfig(config);
            loggingProvider_1.logger.info(chalk.green.bold(`Login successful for: ${userData.email}`));
        }
        catch (error) {
            loggingProvider_1.logger.info(chalk.red.bold('Login failed.'));
            loggingProvider_1.logger.error(chalk.red.bold(error));
            this.exitOrThrow(error instanceof Error ? error.message : String(error));
        }
    }
    async activateJwt(jwt) {
        const nowMillis = Date.now();
        const decodedToken = (0, jwt_decode_1.jwtDecode)(jwt);
        const expiresInSeconds = (decodedToken.exp ?? 0) - nowMillis / 1_000;
        const authInfo = {
            access_token: jwt,
            auth_provider: types_2.AuthProviderType.Mabl,
            auth_type: types_1.AuthType.Bearer,
            expires_in: expiresInSeconds,
            id_token: jwt,
            token_type: 'Bearer',
        };
        await cliConfigProvider_1.CliConfigProvider.setCliAuthInfo(authInfo, nowMillis);
        const userData = await AuthenticationProvider.requestUserInfo(authInfo.access_token);
        const config = await cliConfigProvider_1.CliConfigProvider.getCliConfig();
        config.userId = userData.id;
        config.email = userData.email;
        config.userFullName = userData.name;
        await cliConfigProvider_1.CliConfigProvider.setCliConfig(config);
    }
    openUrl(path) {
        return (0, open_1.default)(path);
    }
    static async requestUserInfo(accessToken) {
        const apiClient = await mablApiClientFactory_1.MablApiClientFactory.createApiClientForAccessToken(accessToken);
        return apiClient.getSelf();
    }
}
exports.AuthenticationProvider = AuthenticationProvider;
function processAuthorizationError(error, isTokenRefresh) {
    const mainErrorMessage = isTokenRefresh ? 'Token refresh' : 'Authorization';
    loggingProvider_1.logger.error(chalk.red.bold(`${mainErrorMessage} failed.`));
    loggingProvider_1.logger.error(chalk.red.bold(error.toString()));
    let hasResponseData = false;
    if (axios_1.default.isAxiosError(error)) {
        const data = error.response?.data;
        if (isOidcError(data)) {
            loggingProvider_1.logger.error(chalk.red.bold(`${mainErrorMessage} failed: ${data.error_description} [${data.error}]`));
            hasResponseData = true;
        }
        else if (data) {
            loggingProvider_1.logger.error(chalk.red.bold(`Response data: ${JSON.stringify(data)}`));
            hasResponseData = true;
        }
    }
    if (!hasResponseData) {
        loggingProvider_1.logger.error(chalk.red.bold('No response data'));
    }
    if (isTokenRefresh) {
        loggingProvider_1.logger.error(`Please try again. If the issue persists authenticate again with ${chalk.magenta('mabl auth login')}.`);
    }
}
function humanizeExpirationTime(milliseconds) {
    return humanizeDuration(milliseconds, {
        maxDecimalPoints: 0,
        units: ['h', 'm'],
    });
}
function isOidcError(err) {
    return ((0, pureUtil_1.isString)(err?.error) &&
        (0, pureUtil_1.isString)(err?.error_description));
}
