"use strict";
const Logger = require('./../logger').default;
const authUtil = require('../authentication/authentication_util');
const { getFreePort, format } = require('../util');
const { withBrowserActionTimeout, writeToCache, removeFromCache, readCache, } = require('./authentication_util');
const querystring = require('querystring');
const open = require('open');
const Util = require('../util');
const AuthenticationTypes = require('./authentication_types');
const { coordinateAuth } = require('./auth_coordinator');
/**
 * Creates an oauth authenticator.
 *
 * @param {Object} connectionConfig
 * @param {Object} httpClient
 *
 * @returns {Object}
 * @constructor
 */
function AuthOauthAuthorizationCode(connectionConfig, httpClient) {
    const DEFAULT_REDIRECT_HOST = 'http://127.0.0.1';
    const browserActionTimeout = connectionConfig.getBrowserActionTimeout();
    let oauth;
    let token;
    const clientId = connectionConfig.getOauthClientId();
    const clientSecret = connectionConfig.getOauthClientSecret();
    const authorizationUrl = getAuthorizationUrl(connectionConfig);
    const tokenUrl = authUtil.getTokenUrl(connectionConfig);
    const accessTokenKey = authUtil.buildOauthAccessTokenCacheKey(authorizationUrl.host, connectionConfig.username, AuthenticationTypes.OAUTH_AUTHORIZATION_CODE);
    const refreshTokenKey = authUtil.buildOauthRefreshTokenCacheKey(tokenUrl.host, connectionConfig.username, AuthenticationTypes.OAUTH_AUTHORIZATION_CODE);
    /**
     * Update JSON body with token.
     * @param {JSON} body
     * @returns {null}
     */
    this.updateBody = function (body) {
        if (token) {
            body.data.TOKEN = token;
        }
        body.data.AUTHENTICATOR = AuthenticationTypes.OAUTH_AUTHENTICATOR;
        body.data.CLIENT_ENVIRONMENT.OAUTH_TYPE = AuthenticationTypes.OAUTH_AUTHORIZATION_CODE;
    };
    this.loadOauth4webapi = async function () {
        if (!oauth) {
            oauth = await import('oauth4webapi');
        }
    };
    this.authenticate = async function () {
        globalThis.crypto ??= require('node:crypto').webcrypto;
        token = await coordinateAuth(connectionConfig.host, connectionConfig.username, AuthenticationTypes.OAUTH_AUTHORIZATION_CODE, async () => {
            const accessTokenFromCache = await readCache(accessTokenKey);
            const refreshTokenFromCache = await readCache(refreshTokenKey);
            if (accessTokenFromCache && connectionConfig.getClientStoreTemporaryCredential()) {
                return accessTokenFromCache;
            }
            else if (refreshTokenFromCache && connectionConfig.getClientStoreTemporaryCredential()) {
                return this.getAccessTokenUsingRefreshToken(refreshTokenFromCache);
            }
            else {
                return this.executeFullAuthorizationCodeFlow();
            }
        });
    };
    // Wipes the cached access token. Called by the connection-failure dispatch
    // in `sf.js` for every GS `success: false` response so we never resend a
    // stale access token, regardless of whether a full reauthenticate follows.
    this.clearAccessTokenCache = async function () {
        await removeFromCache(accessTokenKey);
    };
    this.reauthenticate = async function (body) {
        const refreshToken = await readCache(refreshTokenKey);
        if (refreshToken) {
            try {
                token = await this.getAccessTokenUsingRefreshToken(refreshToken);
                this.updateBody(body);
            }
            catch (error) {
                await removeFromCache(refreshTokenKey);
                Logger().warn(format('Error while getting access token using refresh token. Message: %s. The refresh token is removed form cache - authentication must be proceed from the beginning', error.message));
                await this.authenticate();
                this.updateBody(body);
            }
        }
        else {
            await this.authenticate();
            this.updateBody(body);
        }
    };
    this.executeFullAuthorizationCodeFlow = async function () {
        await this.loadOauth4webapi(); // import module using the dynamic import
        const codeChallengeMethod = connectionConfig.getOauthChallengeMethod() || 'S256'; // TODO: should be verified with "discovery" response
        //An issuer is a obligatory parameter in validation processed by oauth4webapi library, even when it isn't used
        const issuer = connectionConfig.issuer || 'UNKNOWN';
        const codeVerifier = oauth.generateRandomCodeVerifier();
        const codeChallenge = await oauth.calculatePKCECodeChallenge(codeVerifier);
        const as = { issuer: issuer };
        // oxlint-disable-next-line camelcase
        const client = { client_id: clientId };
        const clientAuth = oauth.ClientSecretPost(clientSecret);
        const redirectUri = await buildRedirectUri(connectionConfig);
        const scope = await authUtil.prepareScope(connectionConfig);
        const authorizationUrlWithParams = await prepareAuthorizationUrl(authorizationUrl, client, redirectUri, codeChallenge, codeChallengeMethod, as, scope);
        const authorizationCodeResponse = await requestAuthorizationCode(authorizationUrlWithParams, browserActionTimeout);
        const params = oauth.validateAuthResponse(as, client, authorizationUrlWithParams, authorizationCodeResponse.state);
        params.set('code', authorizationCodeResponse.code);
        Logger().trace('Requesting token');
        const token = await requestToken(as, tokenUrl, client, clientAuth, params, redirectUri, codeVerifier);
        return token;
    };
    this.getAccessTokenUsingRefreshToken = async function (refreshToken) {
        globalThis.crypto ??= require('node:crypto').webcrypto;
        await this.loadOauth4webapi(); // import module using the dynamic import
        const issuer = connectionConfig.issuer || 'UNKNOWN';
        const as = { issuer: issuer };
        const clientId = connectionConfig.getOauthClientId();
        const clientSecret = connectionConfig.getOauthClientSecret();
        // oxlint-disable-next-line camelcase
        const client = { client_id: clientId };
        const clientAuth = oauth.ClientSecretPost(clientSecret);
        // Refresh Token Grant Request & Response
        const tokenUrl = authUtil.getTokenUrl(connectionConfig);
        Logger().trace(`Receiving new OAuth access token from: Host: ${tokenUrl.host} Path: ${tokenUrl.pathname}`);
        as['token_endpoint'] = tokenUrl.href;
        const response = await oauth.refreshTokenGrantRequest(as, client, clientAuth, refreshToken, {
            [oauth.allowInsecureRequests]: connectionConfig.getOauthHttpAllowed(),
            [oauth.customFetch]: async (url, options) => await convertToResponseType(httpClient, url, options),
        });
        const result = await oauth.processRefreshTokenResponse(as, client, response);
        if (result.access_token) {
            //cache access token
            Logger().debug(`Received new OAuth access token from: Host: ${tokenUrl.host} Path: ${tokenUrl.pathname}`);
            await writeToCache(accessTokenKey, result.access_token);
            //cache refreshToken if exists
            if (result.refresh_token) {
                //cache refresh token
                Logger().debug(`Received new OAuth refresh token from: Host: ${tokenUrl.host} Path: ${tokenUrl.pathname}`);
                await writeToCache(refreshTokenKey, result.refresh_token);
            }
        }
        else {
            throw Error(`Response doesn't contain OAuth access token. Requested URI: Host: ${tokenUrl.host} Path: ${tokenUrl.pathname}`);
        }
        return result.access_token;
    };
    async function prepareAuthorizationUrl(authorizationUrl, client, redirectUri, codeChallenge, codeChallengeMethod, as, scope) {
        authorizationUrl.searchParams.set('client_id', client.client_id);
        authorizationUrl.searchParams.set('redirect_uri', redirectUri);
        authorizationUrl.searchParams.set('response_type', 'code');
        if (scope) {
            authorizationUrl.searchParams.set('scope', scope);
        }
        authorizationUrl.searchParams.set('code_challenge', codeChallenge);
        authorizationUrl.searchParams.set('code_challenge_method', codeChallengeMethod);
        /**
         * We cannot be sure PKCE is supported then the state should be used.
         */
        if (as.code_challenge_methods_supported?.includes('S256') !== true) {
            const state = oauth.generateRandomState();
            authorizationUrl.searchParams.set('state', state);
        }
        return authorizationUrl;
    }
    async function verifyPortIsAvailable(server, redirectPort) {
        return Util.isPortOpen(redirectPort).catch((rejected) => {
            server.close();
            throw new Error(`Cannot run server using provided redirect url. ${rejected}`);
        });
    }
    async function requestAuthorizationCode(authorizationUrl, browserActionTimeout) {
        if (!Util.number.isPositiveInteger(browserActionTimeout)) {
            throw new Error(`Invalid value for browser action timeout: ${browserActionTimeout}`);
        }
        let server;
        const receiveData = new Promise((resolve, reject) => {
            server = authUtil.createServer(resolve, reject);
        }).then((result) => {
            return result;
        });
        const redirectUri = new URL(authorizationUrl.searchParams.get('redirect_uri'));
        await verifyPortIsAvailable(server, redirectUri.port);
        server.listen(redirectUri.port || 0, 0);
        const codeProvider = connectionConfig.openExternalBrowserCallback || open;
        Logger().debug(`Opening your browser to obtain the authorization code: ${authorizationUrl.href}`);
        const result = codeProvider(authorizationUrl.href);
        if (result && typeof result.catch === 'function') {
            // Fire and forget, the actual result handler is in codeResponse
            // .catch() is used to avoid unhandled promise rejection warning
            result.catch(() => { });
        }
        const codeResponse = await withBrowserActionTimeout(browserActionTimeout, receiveData).catch((rejected) => {
            server.close();
            throw new Error(rejected);
        });
        const autorizationCodeResponseParameters = querystring.parse(codeResponse.substring(codeResponse.indexOf('?') + 1));
        const code = autorizationCodeResponseParameters['code'];
        const state = autorizationCodeResponseParameters['state'].replace(new RegExp('\\sHTTP/.*'), '');
        Logger().debug(`Received new OAuth authorization code from: Host: ${authorizationUrl.host} Path: ${authorizationUrl.pathname}`);
        return { code: code, state: state };
    }
    async function convertToResponseType(httpClient, url, options) {
        function asResponseType(response) {
            return new Response(response.json, {
                status: response.statusCode,
                statusText: response.statusText,
                headers: response.headers,
            });
        }
        options.url = url;
        return asResponseType(await httpClient.requestAsync(options));
    }
    async function requestToken(as, tokenUrl, client, clientAuth, params, redirectUri, codeVerifier) {
        Logger().trace(`Receiving new OAuth access token from: Host: ${tokenUrl.host} Path: ${tokenUrl.pathname}`);
        as['token_endpoint'] = tokenUrl.href;
        const response = await oauth.authorizationCodeGrantRequest(as, client, clientAuth, params, redirectUri, codeVerifier, {
            [oauth.allowInsecureRequests]: connectionConfig.getOauthHttpAllowed(),
            [oauth.customFetch]: async (url, options) => await convertToResponseType(httpClient, url, options),
            additionalParameters: connectionConfig.oauthEnableSingleUseRefreshTokens
                ? {
                    // oxlint-disable-next-line camelcase
                    enable_single_use_refresh_tokens: 'true',
                }
                : undefined,
        });
        const result = await oauth.processAuthorizationCodeResponse(as, client, response);
        if (result.access_token) {
            //cache access token
            Logger().debug(`Received new OAuth access token from: Host: ${tokenUrl.host} Path: ${tokenUrl.pathname}`);
            await writeToCache(accessTokenKey, result.access_token);
            //cache refreshToken if exists
            if (result.refresh_token) {
                //cache refresh token
                Logger().debug(`Received new OAuth refresh token from: Host: ${tokenUrl.host} Path: ${tokenUrl.pathname}`);
                await writeToCache(refreshTokenKey, result.refresh_token);
            }
        }
        else {
            throw Error(`Response doesn't contain OAuth access token. Requested URI: Host: ${tokenUrl.host} Path: ${tokenUrl.pathname}`);
        }
        return result.access_token;
    }
    function getAuthorizationUrl(options) {
        const authCodeUrl = options.getOauthAuthorizationUrl();
        Logger().debug(`Url used for receiving authorization code: ${authCodeUrl}`);
        return new URL(authCodeUrl);
    }
    async function buildRedirectUri(options) {
        const redirectUri = options.getOauthRedirectUri() || (await createDefaultRedirectUri());
        Logger().debug(`Authorization code redirect URL: ${redirectUri}`);
        return redirectUri;
    }
    async function createDefaultRedirectUri() {
        const redirectPort = await getFreePort();
        return `${DEFAULT_REDIRECT_HOST}:${redirectPort}`;
    }
}
module.exports = AuthOauthAuthorizationCode;
//# sourceMappingURL=auth_oauth_authorization_code.js.map