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

import {
    Logger,
    createAuthError,
    AuthErrorCodes,
    IPerformanceClient,
    StringDict,
} from "@azure/msal-common/browser";
import {
    DOMExtraParameters,
    PlatformAuthRequest,
    PlatformDOMTokenRequest,
} from "./PlatformAuthRequest.js";
import { PlatformAuthConstants } from "../../utils/BrowserConstants.js";
import {
    PlatformAuthResponse,
    PlatformDOMTokenResponse,
} from "./PlatformAuthResponse.js";
import { createNativeAuthError } from "../../error/NativeAuthError.js";
import { IPlatformAuthHandler } from "./IPlatformAuthHandler.js";

export class PlatformAuthDOMHandler implements IPlatformAuthHandler {
    protected logger: Logger;
    protected performanceClient: IPerformanceClient;
    protected correlationId: string;
    platformAuthType: string;

    constructor(
        logger: Logger,
        performanceClient: IPerformanceClient,
        correlationId: string
    ) {
        this.logger = logger;
        this.performanceClient = performanceClient;
        this.correlationId = correlationId;
        this.platformAuthType = PlatformAuthConstants.PLATFORM_DOM_PROVIDER;
    }

    static async createProvider(
        logger: Logger,
        performanceClient: IPerformanceClient,
        correlationId: string
    ): Promise<PlatformAuthDOMHandler | undefined> {
        logger.trace(
            "PlatformAuthDOMHandler: createProvider called",
            correlationId
        );

        // @ts-ignore
        if (window.navigator?.platformAuthentication) {
            const supportedContracts =
                // @ts-ignore
                await window.navigator.platformAuthentication.getSupportedContracts(
                    PlatformAuthConstants.MICROSOFT_ENTRA_BROKERID
                );
            if (
                supportedContracts?.includes(
                    PlatformAuthConstants.PLATFORM_DOM_APIS
                )
            ) {
                logger.trace(
                    "Platform auth api available in DOM",
                    correlationId
                );
                return new PlatformAuthDOMHandler(
                    logger,
                    performanceClient,
                    correlationId
                );
            }
        }
        return undefined;
    }

    /**
     * Returns the Id for the broker extension this handler is communicating with
     * @returns
     */
    getExtensionId(): string {
        return PlatformAuthConstants.MICROSOFT_ENTRA_BROKERID;
    }

    getExtensionVersion(): string | undefined {
        return "";
    }

    getExtensionName(): string | undefined {
        return PlatformAuthConstants.DOM_API_NAME;
    }

    /**
     * Send token request to platform broker via browser DOM API
     * @param request
     * @returns
     */
    async sendMessage(
        request: PlatformAuthRequest
    ): Promise<PlatformAuthResponse> {
        this.logger.trace(
            `'${this.platformAuthType}' - Sending request to browser DOM API`,
            request.correlationId
        );

        try {
            const platformDOMRequest: PlatformDOMTokenRequest =
                this.initializePlatformDOMRequest(request);
            const response: object =
                // @ts-ignore
                await window.navigator.platformAuthentication.executeGetToken(
                    platformDOMRequest
                );
            return this.validatePlatformBrokerResponse(
                response,
                request.correlationId
            );
        } catch (e) {
            this.logger.error(
                `'${this.platformAuthType}' - executeGetToken DOM API error`,
                request.correlationId
            );
            throw e;
        }
    }

    private initializePlatformDOMRequest(
        request: PlatformAuthRequest
    ): PlatformDOMTokenRequest {
        this.logger.trace(
            `'${this.platformAuthType}' - initializeNativeDOMRequest called`,
            request.correlationId
        );

        const {
            accountId,
            clientId,
            authority,
            scope,
            redirectUri,
            correlationId,
            state,
            storeInCache,
            embeddedClientId,
            extraParameters,
            ...remainingProperties
        } = request;

        const validExtraParameters: DOMExtraParameters = this.getDOMExtraParams(
            remainingProperties,
            correlationId
        );

        const platformDOMRequest: PlatformDOMTokenRequest = {
            accountId: accountId,
            brokerId: this.getExtensionId(),
            authority: authority,
            clientId: clientId,
            correlationId: correlationId || this.correlationId,
            extraParameters: {
                ...extraParameters,
                ...validExtraParameters,
            },
            isSecurityTokenService: false,
            redirectUri: redirectUri,
            scope: scope,
            state: state,
            storeInCache: storeInCache,
            embeddedClientId: embeddedClientId,
        };

        return platformDOMRequest;
    }

    private validatePlatformBrokerResponse(
        response: object,
        correlationId: string
    ): PlatformAuthResponse {
        if (response.hasOwnProperty("isSuccess")) {
            if (
                response.hasOwnProperty("accessToken") &&
                response.hasOwnProperty("idToken") &&
                response.hasOwnProperty("clientInfo") &&
                response.hasOwnProperty("account") &&
                response.hasOwnProperty("scopes") &&
                response.hasOwnProperty("expiresIn")
            ) {
                this.logger.trace(
                    `'${this.platformAuthType}' - platform broker returned successful and valid response`,
                    correlationId
                );
                return this.convertToPlatformBrokerResponse(
                    response as PlatformDOMTokenResponse,
                    correlationId
                );
            } else if (response.hasOwnProperty("error")) {
                const errorResponse = response as PlatformDOMTokenResponse;
                if (
                    errorResponse.isSuccess === false &&
                    errorResponse.error &&
                    errorResponse.error.code
                ) {
                    this.logger.trace(
                        `'${this.platformAuthType}' - platform broker returned error response`,
                        correlationId
                    );
                    throw createNativeAuthError(
                        errorResponse.error.code,
                        correlationId,
                        errorResponse.error.description,
                        {
                            error: parseInt(errorResponse.error.errorCode),
                            protocol_error: errorResponse.error.protocolError,
                            status: errorResponse.error.status,
                            properties: errorResponse.error.properties,
                        }
                    );
                }
            }
        }
        throw createAuthError(
            AuthErrorCodes.unexpectedError,
            correlationId,
            "Response missing expected properties."
        );
    }

    private convertToPlatformBrokerResponse(
        response: PlatformDOMTokenResponse,
        correlationId: string
    ): PlatformAuthResponse {
        this.logger.trace(
            `'${this.platformAuthType}' - convertToNativeResponse called`,
            correlationId
        );
        const nativeResponse: PlatformAuthResponse = {
            access_token: response.accessToken,
            id_token: response.idToken,
            client_info: response.clientInfo,
            account: response.account,
            expires_in: response.expiresIn,
            scope: response.scopes,
            state: response.state || "",
            properties: response.properties || {},
            extendedLifetimeToken: response.extendedLifetimeToken ?? false,
            shr: response.proofOfPossessionPayload,
        };

        return nativeResponse;
    }

    private getDOMExtraParams(
        extraParameters: Record<string, unknown>,
        correlationId: string
    ): DOMExtraParameters {
        try {
            const stringifiedProperties: StringDict = {};
            for (const [key, value] of Object.entries(extraParameters)) {
                if (!value) {
                    continue;
                }
                if (typeof value === "object") {
                    stringifiedProperties[key] = JSON.stringify(value);
                } else {
                    stringifiedProperties[key] = String(value);
                }
            }
            return stringifiedProperties;
        } catch (e) {
            this.logger.error(
                `'${this.platformAuthType}' - Error stringifying extra parameters`,
                correlationId
            );
            this.logger.errorPii(
                `'${this.platformAuthType}' - Error stringifying extra parameters: '${e}'`,
                correlationId
            );
            return {};
        }
    }
}
