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

import * as Constants 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 {
    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";

export function instrumentBrokerParams(
    parameters: Map<string, string>,
    correlationId?: string,
    performanceClient?: IPerformanceClient
): void {
    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
        );
    }
}

/**
 * Add the given response_type
 * @param parameters
 * @param responseType
 */
export function addResponseType(
    parameters: Map<string, string>,
    responseType: Constants.OAuthResponseType
): void {
    parameters.set(AADServerParamKeys.RESPONSE_TYPE, responseType);
}

/**
 * add response_mode. defaults to query.
 * @param responseMode
 */
export function addResponseMode(
    parameters: Map<string, string>,
    responseMode?: Constants.ResponseMode
): void {
    parameters.set(
        AADServerParamKeys.RESPONSE_MODE,
        responseMode ? responseMode : Constants.ResponseMode.QUERY
    );
}

/**
 * Add flag to indicate STS should attempt to use WAM if available
 */
export function addNativeBroker(parameters: Map<string, string>): void {
    parameters.set(AADServerParamKeys.NATIVE_BROKER, "1");
}

/**
 * add scopes. set addOidcScopes to false to prevent default scopes in non-user scenarios
 * @param scopeSet
 * @param addOidcScopes
 */
export function addScopes(
    parameters: Map<string, string>,
    scopes: string[],
    correlationId: string,
    addOidcScopes: boolean = true,
    defaultScopes: Array<string> = Constants.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, correlationId);
    parameters.set(AADServerParamKeys.SCOPE, scopeSet.printScopes());
}

/**
 * add clientId
 * @param clientId
 */
export function addClientId(
    parameters: Map<string, string>,
    clientId: string
): void {
    parameters.set(AADServerParamKeys.CLIENT_ID, clientId);
}

/**
 * add redirect_uri
 * @param redirectUri
 */
export function addRedirectUri(
    parameters: Map<string, string>,
    redirectUri: string
): void {
    parameters.set(AADServerParamKeys.REDIRECT_URI, redirectUri);
}

/**
 * add post logout redirectUri
 * @param redirectUri
 */
export function addPostLogoutRedirectUri(
    parameters: Map<string, string>,
    redirectUri: string
): void {
    parameters.set(AADServerParamKeys.POST_LOGOUT_URI, redirectUri);
}

/**
 * add id_token_hint to logout request
 * @param idTokenHint
 */
export function addIdTokenHint(
    parameters: Map<string, string>,
    idTokenHint: string
): void {
    parameters.set(AADServerParamKeys.ID_TOKEN_HINT, idTokenHint);
}

/**
 * add domain_hint
 * @param domainHint
 */
export function addDomainHint(
    parameters: Map<string, string>,
    domainHint: string
): void {
    parameters.set(AADServerParamKeys.DOMAIN_HINT, domainHint);
}

/**
 * add login_hint
 * @param loginHint
 */
export function addLoginHint(
    parameters: Map<string, string>,
    loginHint: string
): void {
    parameters.set(AADServerParamKeys.LOGIN_HINT, loginHint);
}

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

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

/**
 * add sid
 * @param sid
 */
export function addSid(parameters: Map<string, string>, sid: string): void {
    parameters.set(AADServerParamKeys.SID, sid);
}

/**
 * Adds claims to request parameters, conditionally excluding clientCapabilities
 * when skipBrokerClaims is true and a brokered flow is in effect.
 * @param parameters - The request parameters map
 * @param correlationId - The request correlation id
 * @param claims - The claims string from the request
 * @param clientCapabilities - The client capabilities from configuration
 * @param skipBrokerClaims - When true and BROKER_CLIENT_ID is present, excludes clientCapabilities from claims
 */
export function addClaims(
    parameters: Map<string, string>,
    correlationId: string,
    claims?: string,
    clientCapabilities?: Array<string>,
    skipBrokerClaims?: boolean
): void {
    // Skip clientCapabilities if skipBrokerClaims is set to true and this is a brokered authentication flow
    const configClaims =
        skipBrokerClaims && parameters.has(AADServerParamKeys.BROKER_CLIENT_ID)
            ? undefined
            : clientCapabilities;

    const mergedClaims = buildMergedClaims(claims, configClaims, correlationId);
    parameters.set(AADServerParamKeys.CLAIMS, mergedClaims);
}

/**
 * add correlationId
 * @param correlationId
 */
export function addCorrelationId(
    parameters: Map<string, string>,
    correlationId: string
): void {
    parameters.set(AADServerParamKeys.CLIENT_REQUEST_ID, correlationId);
}

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

/**
 * Add client telemetry parameters
 * @param appTelemetry
 */
export function addApplicationTelemetry(
    parameters: Map<string, string>,
    appTelemetry: ApplicationTelemetry
): void {
    if (appTelemetry?.appName) {
        parameters.set(AADServerParamKeys.X_APP_NAME, appTelemetry.appName);
    }

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

/**
 * add prompt
 * @param prompt
 */
export function addPrompt(
    parameters: Map<string, string>,
    prompt: string
): void {
    parameters.set(AADServerParamKeys.PROMPT, prompt);
}

/**
 * add state
 * @param state
 */
export function addState(parameters: Map<string, string>, state: string): void {
    if (state) {
        parameters.set(AADServerParamKeys.STATE, state);
    }
}

/**
 * add nonce
 * @param nonce
 */
export function addNonce(parameters: Map<string, string>, nonce: string): void {
    parameters.set(AADServerParamKeys.NONCE, nonce);
}

/**
 * add code_challenge and code_challenge_method
 * - throw if either of them are not passed
 * @param codeChallenge
 * @param codeChallengeMethod
 */
export function addCodeChallengeParams(
    parameters: Map<string, string>,
    codeChallenge?: string,
    codeChallengeMethod?: string
): void {
    if (codeChallenge && codeChallengeMethod) {
        parameters.set(AADServerParamKeys.CODE_CHALLENGE, codeChallenge);
        parameters.set(
            AADServerParamKeys.CODE_CHALLENGE_METHOD,
            codeChallengeMethod
        );
    } else {
        throw createClientConfigurationError(
            ClientConfigurationErrorCodes.pkceParamsMissing,
            ""
        );
    }
}

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

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

/**
 * add the `refreshToken` passed by the user
 * @param refreshToken
 */
export function addRefreshToken(
    parameters: Map<string, string>,
    refreshToken: string
): void {
    parameters.set(AADServerParamKeys.REFRESH_TOKEN, refreshToken);
}

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

/**
 * add client_secret
 * @param clientSecret
 */
export function addClientSecret(
    parameters: Map<string, string>,
    clientSecret: string
): void {
    parameters.set(AADServerParamKeys.CLIENT_SECRET, clientSecret);
}

/**
 * add clientAssertion for confidential client flows
 * @param clientAssertion
 */
export function addClientAssertion(
    parameters: Map<string, string>,
    clientAssertion: string
): void {
    if (clientAssertion) {
        parameters.set(AADServerParamKeys.CLIENT_ASSERTION, clientAssertion);
    }
}

/**
 * add clientAssertionType for confidential client flows
 * @param clientAssertionType
 */
export function addClientAssertionType(
    parameters: Map<string, string>,
    clientAssertionType: string
): void {
    if (clientAssertionType) {
        parameters.set(
            AADServerParamKeys.CLIENT_ASSERTION_TYPE,
            clientAssertionType
        );
    }
}

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

/**
 * add grant type
 * @param grantType
 */
export function addRequestTokenUse(
    parameters: Map<string, string>,
    tokenUse: string
): void {
    parameters.set(AADServerParamKeys.REQUESTED_TOKEN_USE, tokenUse);
}

/**
 * add grant type
 * @param grantType
 */
export function addGrantType(
    parameters: Map<string, string>,
    grantType: string
): void {
    parameters.set(AADServerParamKeys.GRANT_TYPE, grantType);
}

/**
 * add client info
 *
 */
export function addClientInfo(parameters: Map<string, string>): void {
    parameters.set(Constants.CLIENT_INFO, "1");
}

/**
 * add clidata=1 to request to indicate client data support
 */
export function addCliData(parameters: Map<string, string>): void {
    parameters.set(AADServerParamKeys.CLI_DATA, "1");
}

export function addInstanceAware(parameters: Map<string, string>): void {
    if (!parameters.has(AADServerParamKeys.INSTANCE_AWARE)) {
        parameters.set(AADServerParamKeys.INSTANCE_AWARE, "true");
    }
}

/**
 * Add extraParameters
 * @param extraParams - String dictionary containing extra parameters to be added.
 */
export function addExtraParameters(
    parameters: Map<string, string>,
    extraParams: StringDict
): void {
    Object.entries(extraParams).forEach(([key, value]) => {
        if (!parameters.has(key) && value) {
            parameters.set(key, value);
        }
    });
}

/**
 * Default optional idToken claims requested on all auth requests.
 * signin_state enables KMSI detection; login_hint enables login hint propagation.
 */
const DEFAULT_ID_TOKEN_CLAIMS: Record<string, { essential: false }> = {
    [Constants.ClaimsRequestKeys.SIGNIN_STATE]: { essential: false },
    [Constants.ClaimsRequestKeys.LOGIN_HINT]: { essential: false },
};

/**
 * Parses claims JSON, merges default optional idToken claims (signin_state, login_hint),
 * and appends client capabilities (xms_cc) to the access_token section.
 * Does not overwrite idToken claims already specified by the caller.
 * @param claims - Existing claims JSON string from the request (may be undefined)
 * @param clientCapabilities - Client capabilities array from configuration
 * @returns Merged claims JSON string
 */
export function buildMergedClaims(
    claims?: string,
    clientCapabilities?: Array<string>,
    correlationId: string = ""
): string {
    let mergedClaims: object;

    // Parse provided claims into JSON object or initialize empty object
    if (!claims) {
        mergedClaims = {};
    } else {
        try {
            const parsed = JSON.parse(claims);
            if (
                typeof parsed !== "object" ||
                parsed === null ||
                Array.isArray(parsed)
            ) {
                throw new Error("Claims must be a JSON object");
            }
            mergedClaims = parsed;
        } catch (e) {
            throw createClientConfigurationError(
                ClientConfigurationErrorCodes.invalidClaims,
                correlationId
            );
        }
    }

    // Add default optional idToken claims
    if (
        !Object.prototype.hasOwnProperty.call(
            mergedClaims,
            Constants.ClaimsRequestKeys.ID_TOKEN
        )
    ) {
        mergedClaims[Constants.ClaimsRequestKeys.ID_TOKEN] = {};
    }
    const idTokenClaims = mergedClaims[Constants.ClaimsRequestKeys.ID_TOKEN];
    for (const [key, value] of Object.entries(DEFAULT_ID_TOKEN_CLAIMS)) {
        if (!(key in idTokenClaims)) {
            idTokenClaims[key] = value;
        }
    }

    // Add client capabilities
    if (clientCapabilities && clientCapabilities.length > 0) {
        if (
            !Object.prototype.hasOwnProperty.call(
                mergedClaims,
                Constants.ClaimsRequestKeys.ACCESS_TOKEN
            )
        ) {
            // Add access_token key to claims object
            mergedClaims[Constants.ClaimsRequestKeys.ACCESS_TOKEN] = {};
        }

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

    return JSON.stringify(mergedClaims);
}

/**
 * adds `username` for Password Grant flow
 * @param username
 */
export function addUsername(
    parameters: Map<string, string>,
    username: string
): void {
    parameters.set(Constants.PasswordGrantConstants.username, username);
}

/**
 * adds `password` for Password Grant flow
 * @param password
 */
export function addPassword(
    parameters: Map<string, string>,
    password: string
): void {
    parameters.set(Constants.PasswordGrantConstants.password, password);
}

/**
 * add pop_jwk to query params
 * @param cnfString
 */
export function addPopToken(
    parameters: Map<string, string>,
    cnfString: string
): void {
    if (cnfString) {
        parameters.set(
            AADServerParamKeys.TOKEN_TYPE,
            Constants.AuthenticationScheme.POP
        );
        parameters.set(AADServerParamKeys.REQ_CNF, cnfString);
    }
}

/**
 * add SSH JWK and key ID to query params
 */
export function addSshJwk(
    parameters: Map<string, string>,
    sshJwkString: string
): void {
    if (sshJwkString) {
        parameters.set(
            AADServerParamKeys.TOKEN_TYPE,
            Constants.AuthenticationScheme.SSH
        );
        parameters.set(AADServerParamKeys.REQ_CNF, sshJwkString);
    }
}

/**
 * add server telemetry fields
 * @param serverTelemetryManager
 * @internal
 */
export function addServerTelemetry(
    parameters: Map<string, string>,
    serverTelemetryManager: ServerTelemetryManager
): void {
    parameters.set(
        AADServerParamKeys.X_CLIENT_CURR_TELEM,
        serverTelemetryManager.generateCurrentRequestHeaderValue()
    );
    parameters.set(
        AADServerParamKeys.X_CLIENT_LAST_TELEM,
        serverTelemetryManager.generateLastRequestHeaderValue()
    );
}

/**
 * Adds parameter that indicates to the server that throttling is supported
 */
export function addThrottling(parameters: Map<string, string>): void {
    parameters.set(
        AADServerParamKeys.X_MS_LIB_CAPABILITY,
        Constants.X_MS_LIB_CAPABILITY_VALUE
    );
}

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

export function addBrokerParameters(
    parameters: Map<string, string>,
    brokerClientId: string,
    brokerRedirectUri: string
): void {
    if (!parameters.has(AADServerParamKeys.BROKER_CLIENT_ID)) {
        parameters.set(AADServerParamKeys.BROKER_CLIENT_ID, brokerClientId);
    }
    if (!parameters.has(AADServerParamKeys.BROKER_REDIRECT_URI)) {
        parameters.set(
            AADServerParamKeys.BROKER_REDIRECT_URI,
            brokerRedirectUri
        );
    }
}

/**
 * Add EAR (Encrypted Authorize Response) request parameters
 * @param parameters
 * @param jwk
 */
export function addEARParameters(
    parameters: Map<string, string>,
    jwk: string
): void {
    parameters.set(AADServerParamKeys.EAR_JWK, encodeURIComponent(jwk));

    // ear_jwe_crypto will always have value: {"alg":"dir","enc":"A256GCM"} so we can hardcode this
    const jweCryptoB64Encoded = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0";
    parameters.set(AADServerParamKeys.EAR_JWE_CRYPTO, jweCryptoB64Encoded);
}

export function addResource(
    parameters: Map<string, string>,
    resource?: string
): void {
    if (resource) {
        parameters.set(AADServerParamKeys.RESOURCE, resource);
    }
}
