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

import {
    ServerTelemetryManager,
    AuthorizationCodeClient,
    ClientConfiguration,
    UrlString,
    CommonEndSessionRequest,
    ProtocolUtils,
    IdTokenClaims,
    AccountInfo,
    AzureCloudOptions,
    invokeAsync,
    BaseAuthRequest,
    StringDict,
    CommonAuthorizationUrlRequest,
    ICrypto,
    Logger,
    IPerformanceClient,
    Authority,
} from "@azure/msal-common/browser";
import {
    BaseInteractionClient,
    getDiscoveredAuthority,
    getRedirectUri,
} from "./BaseInteractionClient.js";
import * as BrowserPerformanceEvents from "../telemetry/BrowserPerformanceEvents.js";
import {
    BrowserConstants,
    InteractionType,
} from "../utils/BrowserConstants.js";
import { version } from "../packageMetadata.js";
import { BrowserStateObject } from "../utils/BrowserProtocolUtils.js";
import { EndSessionRequest } from "../request/EndSessionRequest.js";
import * as BrowserUtils from "../utils/BrowserUtils.js";
import { RedirectRequest } from "../request/RedirectRequest.js";
import { PopupRequest } from "../request/PopupRequest.js";
import { SsoSilentRequest } from "../request/SsoSilentRequest.js";
import { createNewGuid } from "../crypto/BrowserCrypto.js";
import { BrowserConfiguration } from "../config/Configuration.js";
import { BrowserCacheManager } from "../cache/BrowserCacheManager.js";
import {
    initializeBaseRequest,
    validateRequestMethod,
} from "../request/RequestHelpers.js";

/**
 * Defines the class structure and helper functions used by the "standard", non-brokered auth flows (popup, redirect, silent (RT), silent (iframe))
 */
export abstract class StandardInteractionClient extends BaseInteractionClient {
    /**
     * Initializer for the logout request.
     * @param logoutRequest
     */
    protected initializeLogoutRequest(
        logoutRequest?: EndSessionRequest
    ): CommonEndSessionRequest {
        this.logger.verbose(
            "initializeLogoutRequest called",
            this.correlationId
        );

        const validLogoutRequest: CommonEndSessionRequest = {
            correlationId: this.correlationId,
            ...logoutRequest,
        };

        /**
         * Set logout_hint to be login_hint from ID Token Claims if present
         * and logoutHint attribute wasn't manually set in logout request
         */
        if (logoutRequest) {
            // If logoutHint isn't set and an account was passed in, try to extract logoutHint from account loginHint or ID Token Claims
            if (!logoutRequest.logoutHint) {
                if (logoutRequest.account) {
                    const logoutHint =
                        logoutRequest.account.loginHint ||
                        this.getLogoutHintFromIdTokenClaims(
                            logoutRequest.account
                        );
                    if (logoutHint) {
                        this.logger.verbose(
                            "Setting logoutHint value to loginHint of the account provided",
                            this.correlationId
                        );
                        validLogoutRequest.logoutHint = logoutHint;
                    }
                } else {
                    this.logger.verbose(
                        "logoutHint was not set and account was not passed into logout request, logoutHint will not be set",
                        this.correlationId
                    );
                }
            } else {
                this.logger.verbose(
                    "logoutHint has already been set in logoutRequest",
                    this.correlationId
                );
            }
        } else {
            this.logger.verbose(
                "logoutHint will not be set since no logout request was configured",
                this.correlationId
            );
        }

        /*
         * Only set redirect uri if logout request isn't provided or the set uri isn't null.
         * Otherwise, use passed uri, config, or current page.
         */
        if (!logoutRequest || logoutRequest.postLogoutRedirectUri !== null) {
            if (logoutRequest && logoutRequest.postLogoutRedirectUri) {
                this.logger.verbose(
                    "Setting postLogoutRedirectUri to uri set on logout request",
                    validLogoutRequest.correlationId
                );
                validLogoutRequest.postLogoutRedirectUri =
                    UrlString.getAbsoluteUrl(
                        logoutRequest.postLogoutRedirectUri,
                        BrowserUtils.getCurrentUri()
                    );
            } else if (this.config.auth.postLogoutRedirectUri === null) {
                this.logger.verbose(
                    "postLogoutRedirectUri configured as null and no uri set on request, not passing post logout redirect",
                    validLogoutRequest.correlationId
                );
            } else if (this.config.auth.postLogoutRedirectUri) {
                this.logger.verbose(
                    "Setting postLogoutRedirectUri to configured uri",
                    validLogoutRequest.correlationId
                );
                validLogoutRequest.postLogoutRedirectUri =
                    UrlString.getAbsoluteUrl(
                        this.config.auth.postLogoutRedirectUri,
                        BrowserUtils.getCurrentUri()
                    );
            } else {
                this.logger.verbose(
                    "Setting postLogoutRedirectUri to current page",
                    validLogoutRequest.correlationId
                );
                validLogoutRequest.postLogoutRedirectUri =
                    UrlString.getAbsoluteUrl(
                        BrowserUtils.getCurrentUri(),
                        BrowserUtils.getCurrentUri()
                    );
            }
        } else {
            this.logger.verbose(
                "postLogoutRedirectUri passed as null, not setting post logout redirect uri",
                validLogoutRequest.correlationId
            );
        }

        return validLogoutRequest;
    }

    /**
     * Parses login_hint ID Token Claim out of AccountInfo object to be used as
     * logout_hint in end session request.
     * @param account
     */
    protected getLogoutHintFromIdTokenClaims(
        account: AccountInfo
    ): string | null {
        const idTokenClaims: IdTokenClaims | undefined = account.idTokenClaims;
        if (idTokenClaims) {
            if (idTokenClaims.login_hint) {
                this.logger.verbose(
                    "Extracted login_hint claim from account ID Token Claims to be used as logoutHint",
                    this.correlationId
                );
                return idTokenClaims.login_hint;
            } else {
                this.logger.verbose(
                    "The ID Token Claims tied to the provided account do not contain a login_hint claim, logoutHint will not be added to logout request",
                    this.correlationId
                );
            }
        } else {
            this.logger.verbose(
                "The provided account does not contain ID Token Claims, logoutHint will not be added to logout request",
                this.correlationId
            );
        }

        return null;
    }

    /**
     * Creates an Authorization Code Client with the given authority, or the default authority.
     * @param params {
     *         serverTelemetryManager: ServerTelemetryManager;
     *         authorityUrl?: string;
     *         requestAzureCloudOptions?: AzureCloudOptions;
     *         requestExtraQueryParameters?: StringDict;
     *         account?: AccountInfo;
     *        }
     */
    protected async createAuthCodeClient(params: {
        serverTelemetryManager: ServerTelemetryManager;
        requestAuthority?: string;
        requestAzureCloudOptions?: AzureCloudOptions;
        requestExtraQueryParameters?: StringDict;
        account?: AccountInfo;
        authority?: Authority;
    }): Promise<AuthorizationCodeClient> {
        // Create auth module.
        const clientConfig = await invokeAsync(
            this.getClientConfiguration.bind(this),
            BrowserPerformanceEvents.StandardInteractionClientGetClientConfiguration,
            this.logger,
            this.performanceClient,
            this.correlationId
        )(params);

        return new AuthorizationCodeClient(
            clientConfig,
            this.performanceClient
        );
    }

    /**
     * Creates a Client Configuration object with the given request authority, or the default authority.
     * @param params {
     *         serverTelemetryManager: ServerTelemetryManager;
     *         requestAuthority?: string;
     *         requestAzureCloudOptions?: AzureCloudOptions;
     *         requestExtraQueryParameters?: boolean;
     *         account?: AccountInfo;
     *        }
     */
    protected async getClientConfiguration(params: {
        serverTelemetryManager: ServerTelemetryManager;
        requestAuthority?: string;
        requestAzureCloudOptions?: AzureCloudOptions;
        requestExtraQueryParameters?: StringDict;
        account?: AccountInfo;
        authority?: Authority;
    }): Promise<ClientConfiguration> {
        const {
            serverTelemetryManager,
            requestAuthority,
            requestAzureCloudOptions,
            requestExtraQueryParameters,
            account,
        } = params;

        const discoveredAuthority =
            params.authority ||
            (await invokeAsync(
                getDiscoveredAuthority,
                BrowserPerformanceEvents.StandardInteractionClientGetDiscoveredAuthority,
                this.logger,
                this.performanceClient,
                this.correlationId
            )(
                this.config,
                this.correlationId,
                this.performanceClient,
                this.browserStorage,
                this.logger,
                requestAuthority,
                requestAzureCloudOptions,
                requestExtraQueryParameters,
                account
            ));
        const logger = this.config.system.loggerOptions;

        return {
            authOptions: {
                clientId: this.config.auth.clientId,
                authority: discoveredAuthority,
                clientCapabilities: this.config.auth.clientCapabilities,
                redirectUri: this.config.auth.redirectUri,
                isMcp: this.config.auth.isMcp,
            },
            systemOptions: {
                tokenRenewalOffsetSeconds:
                    this.config.system.tokenRenewalOffsetSeconds,
                preventCorsPreflight: true,
            },
            loggerOptions: {
                loggerCallback: logger.loggerCallback,
                piiLoggingEnabled: logger.piiLoggingEnabled,
                logLevel: logger.logLevel,
                correlationId: this.correlationId,
            },
            cryptoInterface: this.browserCrypto,
            networkInterface: this.networkClient,
            storageInterface: this.browserStorage,
            serverTelemetryManager: serverTelemetryManager,
            libraryInfo: {
                sku: BrowserConstants.MSAL_SKU,
                version: version,
                cpu: "",
                os: "",
            },
            telemetry: this.config.telemetry,
        };
    }
}

/**
 * Helper to initialize required request parameters for interactive APIs and ssoSilent().
 *
 * @param request - The authentication request object (RedirectRequest, PopupRequest, or SsoSilentRequest).
 * @param interactionType - The type of interaction (e.g., redirect, popup, silent).
 * @param config - The browser configuration object.
 * @param browserCrypto - The cryptographic interface for browser operations.
 * @param browserStorage - The browser storage manager instance.
 * @param logger - The logger instance for logging messages.
 * @param performanceClient - The performance client for telemetry.
 * @param correlationId - The correlation ID for the request.
 * @returns A promise that resolves to a CommonAuthorizationUrlRequest object with initialized parameters.
 */
export async function initializeAuthorizationRequest(
    request: RedirectRequest | PopupRequest | SsoSilentRequest,
    interactionType: InteractionType,
    config: BrowserConfiguration,
    browserCrypto: ICrypto,
    browserStorage: BrowserCacheManager,
    logger: Logger,
    performanceClient: IPerformanceClient,
    correlationId: string
): Promise<CommonAuthorizationUrlRequest> {
    const redirectUri = getRedirectUri(
        request.redirectUri,
        config.auth.redirectUri,
        logger,
        correlationId
    );
    if (new URL(redirectUri).origin !== new URL(window.location.href).origin) {
        logger.warning(
            "The origin of the redirect URI does not match the origin of the current page. This is likely to cause issues with authentication.",
            correlationId
        );
        performanceClient.addFields(
            { isRedirectUriCrossOrigin: true },
            correlationId
        );
    }
    const browserState: BrowserStateObject = {
        interactionType: interactionType,
    };
    const state = ProtocolUtils.setRequestState(
        browserCrypto,
        (request && request.state) || "",
        browserState
    );

    const baseRequest: BaseAuthRequest = await invokeAsync(
        initializeBaseRequest,
        BrowserPerformanceEvents.InitializeBaseRequest,
        logger,
        performanceClient,
        correlationId
    )(
        { ...request, correlationId: correlationId },
        config,
        performanceClient,
        logger,
        correlationId
    );

    const interactionRequest: CommonAuthorizationUrlRequest = {
        ...baseRequest,
        redirectUri: redirectUri,
        state: state,
        nonce: request.nonce || createNewGuid(),
        responseMode: config.auth.OIDCOptions.responseMode,
    };

    const validatedRequest = {
        ...interactionRequest,
        httpMethod: validateRequestMethod(
            interactionRequest,
            config.system.protocolMode
        ),
    };

    // Skip active account lookup if either login hint or session id is set
    if (request.loginHint || request.sid) {
        return validatedRequest;
    }

    const account =
        request.account || browserStorage.getActiveAccount(correlationId);
    if (account) {
        logger.verbose("Setting validated request account", correlationId);
        logger.verbosePii(
            `Setting validated request account: '${account.homeAccountId}'`,
            correlationId
        );
        validatedRequest.account = account;
    }

    return validatedRequest;
}
