/*
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */

import {
    Constants,
    ResponseMode,
    CLIENT_INFO,
    AuthenticationScheme,
    ClaimsRequestKeys,
    PasswordGrantConstants,
    OIDC_DEFAULT_SCOPES,
    ThrottlingConstants,
    HeaderNames,
} from "../utils/Constants.js";
import * as AADServerParamKeys from "../constants/AADServerParamKeys.js";
import { ScopeSet } from "./ScopeSet.js";
import {
    createClientConfigurationError,
    ClientConfigurationErrorCodes,
} from "../error/ClientConfigurationError.js";
import { StringDict } from "../utils/MsalTypes.js";
import { RequestValidator } from "./RequestValidator.js";
import {
    ApplicationTelemetry,
    LibraryInfo,
} from "../config/ClientConfiguration.js";
import { ServerTelemetryManager } from "../telemetry/server/ServerTelemetryManager.js";
import { ClientInfo } from "../account/ClientInfo.js";
import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js";

function instrumentBrokerParams(
    parameters: Map<string, string>,
    correlationId?: string,
    performanceClient?: IPerformanceClient
) {
    if (!correlationId) {
        return;
    }

    const clientId = parameters.get(AADServerParamKeys.CLIENT_ID);
    if (clientId && parameters.has(AADServerParamKeys.BROKER_CLIENT_ID)) {
        performanceClient?.addFields(
            {
                embeddedClientId: clientId,
                embeddedRedirectUri: parameters.get(
                    AADServerParamKeys.REDIRECT_URI
                ),
            },
            correlationId
        );
    }
}

/** @internal */
export class RequestParameterBuilder {
    private parameters: Map<string, string>;
    private readonly performanceClient?: IPerformanceClient;
    private readonly correlationId?: string;

    constructor(
        correlationId?: string,
        performanceClient?: IPerformanceClient
    ) {
        this.parameters = new Map<string, string>();
        this.performanceClient = performanceClient;
        this.correlationId = correlationId;
    }

    /**
     * add response_type = code
     */
    addResponseTypeCode(): void {
        this.parameters.set(
            AADServerParamKeys.RESPONSE_TYPE,
            encodeURIComponent(Constants.CODE_RESPONSE_TYPE)
        );
    }

    /**
     * add response_type = token id_token
     */
    addResponseTypeForTokenAndIdToken(): void {
        this.parameters.set(
            AADServerParamKeys.RESPONSE_TYPE,
            encodeURIComponent(
                `${Constants.TOKEN_RESPONSE_TYPE} ${Constants.ID_TOKEN_RESPONSE_TYPE}`
            )
        );
    }

    /**
     * add response_mode. defaults to query.
     * @param responseMode
     */
    addResponseMode(responseMode?: ResponseMode): void {
        this.parameters.set(
            AADServerParamKeys.RESPONSE_MODE,
            encodeURIComponent(responseMode ? responseMode : ResponseMode.QUERY)
        );
    }

    /**
     * Add flag to indicate STS should attempt to use WAM if available
     */
    addNativeBroker(): void {
        this.parameters.set(
            AADServerParamKeys.NATIVE_BROKER,
            encodeURIComponent("1")
        );
    }

    /**
     * add scopes. set addOidcScopes to false to prevent default scopes in non-user scenarios
     * @param scopeSet
     * @param addOidcScopes
     */
    addScopes(
        scopes: string[],
        addOidcScopes: boolean = true,
        defaultScopes: Array<string> = OIDC_DEFAULT_SCOPES
    ): void {
        // Always add openid to the scopes when adding OIDC scopes
        if (
            addOidcScopes &&
            !defaultScopes.includes("openid") &&
            !scopes.includes("openid")
        ) {
            defaultScopes.push("openid");
        }
        const requestScopes = addOidcScopes
            ? [...(scopes || []), ...defaultScopes]
            : scopes || [];
        const scopeSet = new ScopeSet(requestScopes);
        this.parameters.set(
            AADServerParamKeys.SCOPE,
            encodeURIComponent(scopeSet.printScopes())
        );
    }

    /**
     * add clientId
     * @param clientId
     */
    addClientId(clientId: string): void {
        this.parameters.set(
            AADServerParamKeys.CLIENT_ID,
            encodeURIComponent(clientId)
        );
    }

    /**
     * add redirect_uri
     * @param redirectUri
     */
    addRedirectUri(redirectUri: string): void {
        RequestValidator.validateRedirectUri(redirectUri);
        this.parameters.set(
            AADServerParamKeys.REDIRECT_URI,
            encodeURIComponent(redirectUri)
        );
    }

    /**
     * add post logout redirectUri
     * @param redirectUri
     */
    addPostLogoutRedirectUri(redirectUri: string): void {
        RequestValidator.validateRedirectUri(redirectUri);
        this.parameters.set(
            AADServerParamKeys.POST_LOGOUT_URI,
            encodeURIComponent(redirectUri)
        );
    }

    /**
     * add id_token_hint to logout request
     * @param idTokenHint
     */
    addIdTokenHint(idTokenHint: string): void {
        this.parameters.set(
            AADServerParamKeys.ID_TOKEN_HINT,
            encodeURIComponent(idTokenHint)
        );
    }

    /**
     * add domain_hint
     * @param domainHint
     */
    addDomainHint(domainHint: string): void {
        this.parameters.set(
            AADServerParamKeys.DOMAIN_HINT,
            encodeURIComponent(domainHint)
        );
    }

    /**
     * add login_hint
     * @param loginHint
     */
    addLoginHint(loginHint: string): void {
        this.parameters.set(
            AADServerParamKeys.LOGIN_HINT,
            encodeURIComponent(loginHint)
        );
    }

    /**
     * Adds the CCS (Cache Credential Service) query parameter for login_hint
     * @param loginHint
     */
    addCcsUpn(loginHint: string): void {
        this.parameters.set(
            HeaderNames.CCS_HEADER,
            encodeURIComponent(`UPN:${loginHint}`)
        );
    }

    /**
     * Adds the CCS (Cache Credential Service) query parameter for account object
     * @param loginHint
     */
    addCcsOid(clientInfo: ClientInfo): void {
        this.parameters.set(
            HeaderNames.CCS_HEADER,
            encodeURIComponent(`Oid:${clientInfo.uid}@${clientInfo.utid}`)
        );
    }

    /**
     * add sid
     * @param sid
     */
    addSid(sid: string): void {
        this.parameters.set(AADServerParamKeys.SID, encodeURIComponent(sid));
    }

    /**
     * add claims
     * @param claims
     */
    addClaims(claims?: string, clientCapabilities?: Array<string>): void {
        const mergedClaims = this.addClientCapabilitiesToClaims(
            claims,
            clientCapabilities
        );
        RequestValidator.validateClaims(mergedClaims);
        this.parameters.set(
            AADServerParamKeys.CLAIMS,
            encodeURIComponent(mergedClaims)
        );
    }

    /**
     * add correlationId
     * @param correlationId
     */
    addCorrelationId(correlationId: string): void {
        this.parameters.set(
            AADServerParamKeys.CLIENT_REQUEST_ID,
            encodeURIComponent(correlationId)
        );
    }

    /**
     * add library info query params
     * @param libraryInfo
     */
    addLibraryInfo(libraryInfo: LibraryInfo): void {
        // Telemetry Info
        this.parameters.set(AADServerParamKeys.X_CLIENT_SKU, libraryInfo.sku);
        this.parameters.set(
            AADServerParamKeys.X_CLIENT_VER,
            libraryInfo.version
        );
        if (libraryInfo.os) {
            this.parameters.set(AADServerParamKeys.X_CLIENT_OS, libraryInfo.os);
        }
        if (libraryInfo.cpu) {
            this.parameters.set(
                AADServerParamKeys.X_CLIENT_CPU,
                libraryInfo.cpu
            );
        }
    }

    /**
     * Add client telemetry parameters
     * @param appTelemetry
     */
    addApplicationTelemetry(appTelemetry: ApplicationTelemetry): void {
        if (appTelemetry?.appName) {
            this.parameters.set(
                AADServerParamKeys.X_APP_NAME,
                appTelemetry.appName
            );
        }

        if (appTelemetry?.appVersion) {
            this.parameters.set(
                AADServerParamKeys.X_APP_VER,
                appTelemetry.appVersion
            );
        }
    }

    /**
     * add prompt
     * @param prompt
     */
    addPrompt(prompt: string): void {
        RequestValidator.validatePrompt(prompt);
        this.parameters.set(
            `${AADServerParamKeys.PROMPT}`,
            encodeURIComponent(prompt)
        );
    }

    /**
     * add state
     * @param state
     */
    addState(state: string): void {
        if (state) {
            this.parameters.set(
                AADServerParamKeys.STATE,
                encodeURIComponent(state)
            );
        }
    }

    /**
     * add nonce
     * @param nonce
     */
    addNonce(nonce: string): void {
        this.parameters.set(
            AADServerParamKeys.NONCE,
            encodeURIComponent(nonce)
        );
    }

    /**
     * add code_challenge and code_challenge_method
     * - throw if either of them are not passed
     * @param codeChallenge
     * @param codeChallengeMethod
     */
    addCodeChallengeParams(
        codeChallenge: string,
        codeChallengeMethod: string
    ): void {
        RequestValidator.validateCodeChallengeParams(
            codeChallenge,
            codeChallengeMethod
        );
        if (codeChallenge && codeChallengeMethod) {
            this.parameters.set(
                AADServerParamKeys.CODE_CHALLENGE,
                encodeURIComponent(codeChallenge)
            );
            this.parameters.set(
                AADServerParamKeys.CODE_CHALLENGE_METHOD,
                encodeURIComponent(codeChallengeMethod)
            );
        } else {
            throw createClientConfigurationError(
                ClientConfigurationErrorCodes.pkceParamsMissing
            );
        }
    }

    /**
     * add the `authorization_code` passed by the user to exchange for a token
     * @param code
     */
    addAuthorizationCode(code: string): void {
        this.parameters.set(AADServerParamKeys.CODE, encodeURIComponent(code));
    }

    /**
     * add the `authorization_code` passed by the user to exchange for a token
     * @param code
     */
    addDeviceCode(code: string): void {
        this.parameters.set(
            AADServerParamKeys.DEVICE_CODE,
            encodeURIComponent(code)
        );
    }

    /**
     * add the `refreshToken` passed by the user
     * @param refreshToken
     */
    addRefreshToken(refreshToken: string): void {
        this.parameters.set(
            AADServerParamKeys.REFRESH_TOKEN,
            encodeURIComponent(refreshToken)
        );
    }

    /**
     * add the `code_verifier` passed by the user to exchange for a token
     * @param codeVerifier
     */
    addCodeVerifier(codeVerifier: string): void {
        this.parameters.set(
            AADServerParamKeys.CODE_VERIFIER,
            encodeURIComponent(codeVerifier)
        );
    }

    /**
     * add client_secret
     * @param clientSecret
     */
    addClientSecret(clientSecret: string): void {
        this.parameters.set(
            AADServerParamKeys.CLIENT_SECRET,
            encodeURIComponent(clientSecret)
        );
    }

    /**
     * add clientAssertion for confidential client flows
     * @param clientAssertion
     */
    addClientAssertion(clientAssertion: string): void {
        if (clientAssertion) {
            this.parameters.set(
                AADServerParamKeys.CLIENT_ASSERTION,
                encodeURIComponent(clientAssertion)
            );
        }
    }

    /**
     * add clientAssertionType for confidential client flows
     * @param clientAssertionType
     */
    addClientAssertionType(clientAssertionType: string): void {
        if (clientAssertionType) {
            this.parameters.set(
                AADServerParamKeys.CLIENT_ASSERTION_TYPE,
                encodeURIComponent(clientAssertionType)
            );
        }
    }

    /**
     * add OBO assertion for confidential client flows
     * @param clientAssertion
     */
    addOboAssertion(oboAssertion: string): void {
        this.parameters.set(
            AADServerParamKeys.OBO_ASSERTION,
            encodeURIComponent(oboAssertion)
        );
    }

    /**
     * add grant type
     * @param grantType
     */
    addRequestTokenUse(tokenUse: string): void {
        this.parameters.set(
            AADServerParamKeys.REQUESTED_TOKEN_USE,
            encodeURIComponent(tokenUse)
        );
    }

    /**
     * add grant type
     * @param grantType
     */
    addGrantType(grantType: string): void {
        this.parameters.set(
            AADServerParamKeys.GRANT_TYPE,
            encodeURIComponent(grantType)
        );
    }

    /**
     * add client info
     *
     */
    addClientInfo(): void {
        this.parameters.set(CLIENT_INFO, "1");
    }

    /**
     * add extraQueryParams
     * @param eQParams
     */
    addExtraQueryParameters(eQParams: StringDict): void {
        Object.entries(eQParams).forEach(([key, value]) => {
            if (!this.parameters.has(key) && value) {
                this.parameters.set(key, value);
            }
        });
    }

    addClientCapabilitiesToClaims(
        claims?: string,
        clientCapabilities?: Array<string>
    ): string {
        let mergedClaims: object;

        // Parse provided claims into JSON object or initialize empty object
        if (!claims) {
            mergedClaims = {};
        } else {
            try {
                mergedClaims = JSON.parse(claims);
            } catch (e) {
                throw createClientConfigurationError(
                    ClientConfigurationErrorCodes.invalidClaims
                );
            }
        }

        if (clientCapabilities && clientCapabilities.length > 0) {
            if (!mergedClaims.hasOwnProperty(ClaimsRequestKeys.ACCESS_TOKEN)) {
                // Add access_token key to claims object
                mergedClaims[ClaimsRequestKeys.ACCESS_TOKEN] = {};
            }

            // Add xms_cc claim with provided clientCapabilities to access_token key
            mergedClaims[ClaimsRequestKeys.ACCESS_TOKEN][
                ClaimsRequestKeys.XMS_CC
            ] = {
                values: clientCapabilities,
            };
        }

        return JSON.stringify(mergedClaims);
    }

    /**
     * adds `username` for Password Grant flow
     * @param username
     */
    addUsername(username: string): void {
        this.parameters.set(
            PasswordGrantConstants.username,
            encodeURIComponent(username)
        );
    }

    /**
     * adds `password` for Password Grant flow
     * @param password
     */
    addPassword(password: string): void {
        this.parameters.set(
            PasswordGrantConstants.password,
            encodeURIComponent(password)
        );
    }

    /**
     * add pop_jwk to query params
     * @param cnfString
     */
    addPopToken(cnfString: string): void {
        if (cnfString) {
            this.parameters.set(
                AADServerParamKeys.TOKEN_TYPE,
                AuthenticationScheme.POP
            );
            this.parameters.set(
                AADServerParamKeys.REQ_CNF,
                encodeURIComponent(cnfString)
            );
        }
    }

    /**
     * add SSH JWK and key ID to query params
     */
    addSshJwk(sshJwkString: string): void {
        if (sshJwkString) {
            this.parameters.set(
                AADServerParamKeys.TOKEN_TYPE,
                AuthenticationScheme.SSH
            );
            this.parameters.set(
                AADServerParamKeys.REQ_CNF,
                encodeURIComponent(sshJwkString)
            );
        }
    }

    /**
     * add server telemetry fields
     * @param serverTelemetryManager
     */
    addServerTelemetry(serverTelemetryManager: ServerTelemetryManager): void {
        this.parameters.set(
            AADServerParamKeys.X_CLIENT_CURR_TELEM,
            serverTelemetryManager.generateCurrentRequestHeaderValue()
        );
        this.parameters.set(
            AADServerParamKeys.X_CLIENT_LAST_TELEM,
            serverTelemetryManager.generateLastRequestHeaderValue()
        );
    }

    /**
     * Adds parameter that indicates to the server that throttling is supported
     */
    addThrottling(): void {
        this.parameters.set(
            AADServerParamKeys.X_MS_LIB_CAPABILITY,
            ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE
        );
    }

    /**
     * Adds logout_hint parameter for "silent" logout which prevent server account picker
     */
    addLogoutHint(logoutHint: string): void {
        this.parameters.set(
            AADServerParamKeys.LOGOUT_HINT,
            encodeURIComponent(logoutHint)
        );
    }

    addBrokerParameters(params: {
        brokerClientId: string;
        brokerRedirectUri: string;
    }): void {
        const brokerParams: StringDict = {};
        brokerParams[AADServerParamKeys.BROKER_CLIENT_ID] =
            params.brokerClientId;
        brokerParams[AADServerParamKeys.BROKER_REDIRECT_URI] =
            params.brokerRedirectUri;

        this.addExtraQueryParameters(brokerParams);
    }

    /**
     * Utility to create a URL from the params map
     */
    createQueryString(): string {
        const queryParameterArray: Array<string> = new Array<string>();

        this.parameters.forEach((value, key) => {
            queryParameterArray.push(`${key}=${value}`);
        });

        instrumentBrokerParams(
            this.parameters,
            this.correlationId,
            this.performanceClient
        );

        return queryParameterArray.join("&");
    }
}
