// Copyright (c) 2009-2026 SAP SE, All Rights Reserved

/**
 * @fileoverview This file contains the PostMessageManager class.
 * It handles the postMessage communication between the applications and the framework.
 */
sap.ui.define([
    "sap/base/Log",
    "sap/base/util/Deferred",
    "sap/base/util/isPlainObject",
    "sap/base/util/ObjectPath",
    "sap/base/util/uid"
], (
    Log,
    Deferred,
    isPlainObject,
    ObjectPath,
    uid
) => {
    "use strict";

    const oLogger = Log.getLogger("sap.ushell.appIntegration.PostMessageManager");

    /* eslint-disable max-len */
    /**
     * @typedef {sap.ushell.appIntegration.PostMessageManager.PostMessageRequest|sap.ushell.appIntegration.PostMessageManager.PostMessageResponse} sap.ushell.appIntegration.PostMessageManager.ParsedMessage
     * The parsed message from the postMessage event.
     *
     * @private
     */
    /* eslint-enable max-len */

    /**
     * @alias sap.ushell.appIntegration.PostMessageManager.PostMessageType
     * @enum {string}
     *
     * @private
     */
    const PostMessageType = {
        /**
         * Indicates that the message is a request message.
         * @private
         */
        Request: "request",
        /**
         * Indicates that the message is a response message.
         * @private
         */
        Response: "response"
    };

    /**
     * @typedef {object} sap.ushell.appIntegration.PostMessageManager.PostMessageRequest
     * @property {sap.ushell.appIntegration.PostMessageManager.PostMessageType} type the type of the message
     * @property {string} request_id the request id
     * @property {sap.ushell.appIntegration.PostMessageManager.ServiceRequestId} service the service name
     * @property {object} body the message body
     *
     * @since 1.134.0
     * @private
     */

    /**
     * @alias sap.ushell.appIntegration.PostMessageManager.ResponseStatus
     * @enum {string}
     *
     * @private
     */
    const ResponseStatus = {
        /**
         * Indicates that the service request was handled successfully.
         * @private
         */
        Success: "success",
        /**
         * Indicates that an error occurred while handling the service request.
         * @private
         */
        Error: "error"
    };

    /**
     * @typedef {object} sap.ushell.appIntegration.PostMessageManager.PostMessageResponse
     * @property {sap.ushell.appIntegration.PostMessageManager.PostMessageType} type the type of the message
     * @property {string} request_id the request id
     * @property {sap.ushell.appIntegration.PostMessageManager.ServiceRequestId} service the service name
     * @property {sap.ushell.appIntegration.PostMessageManager.ResponseStatus} status the status of the response
     * @property {object} body the message body
     *
     * @since 1.134.0
     * @private
     */

    /**
     * @callback sap.ushell.appIntegration.PostMessageManager.CustomValidator
     * A custom validator function to validate incoming messages.
     * It is called with the parsed message and the original message event and should return a boolean value indicating whether the message is valid or not.
     * @param {MessageEvent} oMessageEvent The original message event from the postMessage event.
     * @returns {Promise<boolean>} Whether the message is valid or not.
     *
     * @private
     */

    /**
     * @typedef {object} sap.ushell.appIntegration.PostMessageManager.RequestHandlerOptions
     * Defines the options for a service request handler.
     * These options determine how the PostMessageManager handles incoming service requests and how the handler function is called.
     * @property {boolean} [provideApplicationContext=false] A flag to indicate if the application context should be used.
     * @property {boolean} [allowInactive=false] A flag to indicate if the request should be allowed from inactive applications.
     * @property {sap.ushell.appIntegration.PostMessageManager.CustomValidator} [isValidRequest] An async function to validate the request if not
     * provided the request is validated by default checks. This requires that the request comes from a known application iframe.
     *
     * @see {@link sap.ushell.appIntegration.PostMessageManager#setRequestHandler}
     *
     * @since 1.134.0
     * @private
     */

    const oDefaultRequestHandlerOptions = {
        provideApplicationContext: false,
        allowInactive: false
    };

    /* eslint-disable max-len */
    /**
     * @typedef {sap.ushell.appIntegration.PostMessageManager.DefaultRequestHandler|sap.ushell.appIntegration.PostMessageManager.ApplicationContextRequestHandler} sap.ushell.appIntegration.PostMessageManager.RequestHandler
     * The request handler for incoming service requests.
     * It handles the incoming service request and returns an asynchronous response.
     *
     * @private
     */
    /* eslint-enable max-len */

    /**
     * @callback sap.ushell.appIntegration.PostMessageManager.DefaultRequestHandler
     * The default request handler when {@link sap.ushell.appIntegration.PostMessageManager.RequestHandlerOptions.provideApplicationContext} is false or not provided.
     * @param {object} oMessageBody The message body of the incoming request message
     * @param {MessageEvent} oMessageEvent The original message event of the incoming request message
     * @return {Promise<any>|any} The response for the incoming request message.
     * If a Promise is returned, the PostMessageManager will wait for the Promise to resolve before sending the response message.
     *
     * @private
     */

    /**
     * @callback sap.ushell.appIntegration.PostMessageManager.ApplicationContextRequestHandler
     * The request handler when {@link sap.ushell.appIntegration.PostMessageManager.RequestHandlerOptions.provideApplicationContext} is true.
     * @param {object} oMessageBody The message body of the incoming request message
     * @param {sap.ushell.appIntegration.ApplicationContainer} oApplicationContainer The application container of the application that sent the incoming request message
     * @param {MessageEvent} oMessageEvent The original message event of the incoming request message
     * @return {Promise<any>|any} The response for the incoming request message.
     * If a Promise is returned, the PostMessageManager will wait for the Promise to resolve before sending the response message.
     *
     * @private
     */

    /**
     * @typedef {string} sap.ushell.appIntegration.PostMessageManager.ServiceRequestId
     * The service request id is the name of the service request that is used to identify the service request handler for an incoming request message.
     *
     * @private
     */

    /**
     * @typedef {object} sap.ushell.appIntegration.PostMessageManager.RequestHandlerEntry
     * Defines the request handler for a service request.
     * @property {sap.ushell.appIntegration.PostMessageManager.RequestHandler} handler The handler function for the service request
     * @property {sap.ushell.appIntegration.PostMessageManager.RequestHandlerOptions} options The options for the service request handler
     *
     * @private
     */

    /**
     * @typedef {object} sap.ushell.appIntegration.PostMessageManager.DistributionPolicy
     * Defines the distribution policy for a service request.
     * The distribution policy determines how the PostMessageManager handles to what applications and how a request is sent.
     * @property {boolean} [preventBroadcast=false] A flag to indicate if the message should not be sent to all applications
     * @property {boolean} [onlyCurrentApplication=false] A flag to indicate if the message should be sent only to the current application
     * @property {boolean} [ignoreCapabilities=false] A flag to indicate if the capabilities should be ignored
     * @property {function(sap.ushell.appIntegration.ApplicationContainer):boolean} [isValidRequestTarget] a function to validate the request target
     *
     * @see {@link sap.ushell.appIntegration.PostMessageManager#setDistributionPolicy}
     * @see {@link sap.ushell.appIntegration.PostMessageManager#sendRequestToAllApplications}
     *
     * @since 1.134.0
     * @private
     */

    const oDefaultDistributionPolicy = {
        preventBroadcast: false,
        onlyCurrentApplication: false,
        ignoreCapabilities: false
    };

    /**
     * @typedef {object} sap.ushell.appIntegration.PostMessageManager.PreprocessingResult
     * @property {MessageEvent} event The preprocessed message event
     * @property {sap.ushell.appIntegration.PostMessageManager.ParsedMessage} message The preprocessed message
     *
     * @private
     */

    /**
     * @callback sap.ushell.appIntegration.PostMessageManager.Preprocessor
     * @param {MessageEvent} oOriginalMessageEvent The original message event
     * @param {sap.ushell.appIntegration.PostMessageManager.ParsedMessage} oOriginalMessage The original parsed message
     * @returns {sap.ushell.appIntegration.PostMessageManager.PreprocessingResult|undefined}
     * The preprocessed message event and message. If undefined is returned, the original event and message are used.
     *
     * @since 1.145.0
     * @private
     */

    /**
     * @callback sap.ushell.appIntegration.PostMessageManager.ResponseHandler
     * @param {MessageEvent} oMessageEvent The message event of the incoming response message
     * @param {sap.ushell.appIntegration.PostMessageManager.PostMessageResponse} oMessage The parsed incoming response message
     *
     * @private
     */

    /**
     * @typedef {object} sap.ushell.appIntegration.PostMessageManager.ResponseHandlerEntry
     * @property {string} requestId The request id to handle
     * @property {Window} expectedContentWindow The expected content window to receive the response from
     * @property {sap.ushell.appIntegration.PostMessageManager.ResponseHandler} handler The handler function for the response
     *
     * @private
     */

    /**
     * @alias sap.ushell.appIntegration.PostMessageManager.BuiltInResponses
     * @enum {any}
     * A set of built-in responses that can be returned by service request handlers to indicate special handling of the response by the PostMessageManager.
     *
     * @private
     */
    const BuiltInResponses = {
        /**
         * Indicates that the service request was handled successfully, but no response message needs to be sent.
         * @private
         */
        DoNotReply: Symbol("DoNotReply")
    };

    /**
     * @typedef {object} sap.ushell.appIntegration.PostMessageManager.InitOptions
     * @property {function():sap.ushell.appIntegration.ApplicationContainer} [getCurrentApplication]
     * A function that returns the current application container.
     * This is used to provide the application context for service request handlers that have the `provideApplicationContext` option enabled.
     * @property {function():sap.ushell.appIntegration.ApplicationContainer[]} [getAllApplications]
     * A function that returns all application containers.
     * This is used to provide the application context for service request handlers that have the `provideApplicationContext` option enabled
     * and for distribution policies that need to check all applications.
     * @property {boolean} [skipReplay=false]
     * A flag to indicate whether messages that are received before the PostMessageManager is initialized should be kept for later processing
     * or should be ignored. This can be used to prevent replaying messages that are received during the initialization of the PostMessageManager,
     * which can cause issues if the messages are related to services that are not yet available.
     * @property {boolean} [skipSourceValidation=false]
     * A flag to indicate whether the source validation should be skipped for incoming messages.
     * This can be used for testing purposes to allow sending messages from iframes that are not registered as applications.
     *
     * @private
     */

    /**
     * @alias sap.ushell.appIntegration.PostMessageManager
     * @namespace
     * @classdesc This class is used to manage postMessage communication between the applications and the framework.
     *
     * @since 1.134.0
     * @private
     */
    class PostMessageManager {
        #fnPostMessageEventHandler = () => {
            throw new Error("PostMessageManager is not initialized");
        };
        #fnGetCurrentApplication = () => {
            throw new Error("PostMessageManager is not initialized");
        };
        #fnGetAllApplications = () => {
            throw new Error("PostMessageManager is not initialized");
        };

        #bInitialized = false;
        #bDisabled = false;
        #bSkipSourceValidation = false;

        /**
         * The list of preprocessor functions for incoming messages.
         * Each preprocessor is called in the order they were added.
         * The first preprocessor that returns a result will determine the final message and event that is handled.
         * @type {sap.ushell.appIntegration.PostMessageManager.Preprocessor[]}
         */
        #aEventPreprocessor = [];
        /**
         * The list of pending expected responses.
         * @type {sap.ushell.appIntegration.PostMessageManager.ResponseHandlerEntry[]}
         */
        #aResponseHandlers = [];
        /**
         * The map of service request handlers. The key is the service request name and the value is an object containing the handler function and the handler options.
         * @type {Object<sap.ushell.appIntegration.PostMessageManager.ServiceRequestId, sap.ushell.appIntegration.PostMessageManager.RequestHandlerEntry>}
         */
        #oRequestHandlers = {};
        /**
         * The map of distribution policies. The key is the service request name and the value is the distribution policy for the service request.
         * @type {Object<sap.ushell.appIntegration.PostMessageManager.ServiceRequestId, sap.ushell.appIntegration.PostMessageManager.DistributionPolicy>}
         */
        #oDistributionPolicies = {};

        // Plugins might add more handlers. Therefore, we need to keep track of them.
        #bKeepMessagesForLaterProcessing = true;
        /**
         * The list of messages that are received before the PostMessageManager is initialized or while the PostMessageManager is still waiting for the plugins to register their handlers.
         * @type {MessageEvent[]}
         */
        #aUnprocessedMessages = [];

        // Expose enums
        BuiltInResponses = BuiltInResponses;
        ResponseStatus = ResponseStatus;
        PostMessageType = PostMessageType; // Only for Testing

        /**
         * Initializes the PostMessageManager by setting up the message event listener and providing necessary methods for handling messages.
         * @param {sap.ushell.appIntegration.PostMessageManager.InitOptions} [oOptions] The initialization options for the PostMessageManager.
         *
         * @private
         */
        init (oOptions = {}) {
            if (this.#bInitialized) {
                throw new Error("PostMessageManager is already initialized.");
            }

            const oConfig = ObjectPath.get("sap-ushell-config.services.PostMessage.config") || {};

            if (oConfig.enabled === false) {
                this.#bDisabled = true;
                this.#bInitialized = true;
                oLogger.warning("PostMessageAPI is disabled by configuration.");
                return;
            }

            // Use provided methods or create fallbacks. The fallbacks are only relevant for tests
            const fnGetCurrentApplication = oOptions.getCurrentApplication || function () {};
            const fnGetAllApplications = oOptions.getAllApplications || function () {
                const oCurrentApplication = fnGetCurrentApplication();
                return oCurrentApplication ? [oCurrentApplication] : [];
            };

            this.#fnGetCurrentApplication = fnGetCurrentApplication;
            this.#fnGetAllApplications = fnGetAllApplications;

            if (oOptions.skipReplay) {
                this.#bKeepMessagesForLaterProcessing = false;
            }
            if (oOptions.skipSourceValidation) {
                // only for testing
                this.#bSkipSourceValidation = true;
            }

            this.#fnPostMessageEventHandler = this.#handlePostMessageEvent.bind(this);
            addEventListener("message", this.#fnPostMessageEventHandler);
            this.#bInitialized = true;
        }

        /**
         * Handles the postMessage event.
         *  - Parses the message from the event.
         *  - Preprocesses the event and message.
         *  - Run checks
         *  - actual message handling (response, request)
         * @param {MessageEvent} oMessageEvent The incoming message event.
         * @returns {Promise} A promise that resolves when the message is handled.
         *
         * @since 1.134.0
         * @private
         */
        async #handlePostMessageEvent (oMessageEvent) {
            let oMessage;
            try {
                oMessage = this.#parseMessage(oMessageEvent);
            } catch {
                oLogger.debug("Received Post Message with invalid JSON. Ignoring the message.", oMessageEvent.data);
                return;
            }

            const oPreprocessingResult = this.#preprocessMessageEvent(oMessageEvent, oMessage);
            oMessageEvent = oPreprocessingResult.event;
            oMessage = oPreprocessingResult.message;

            if (!oMessage.service) {
                oLogger.debug("Received Post Message without a service name. Ignoring the message.");
                return;
            }

            if (![PostMessageType.Response, PostMessageType.Request].includes(oMessage.type)) {
                oLogger.warning(`Received Post Message with an invalid type: '${oMessage.type}'. Ignoring the message.`);
                return;
            }

            const oSourceWindow = oMessageEvent.source;
            const sSourceDomain = oMessageEvent.origin;
            oLogger.debug(`Received postMessage ${oMessage.service} (${oMessage.request_id}) from iframe with domain '${sSourceDomain}'`, JSON.stringify(oMessage, null, 2));

            const bSchemaFulfilled = !!oMessage.request_id;

            if (!oMessage.request_id) {
                // generate a request_id if not provided. This ensures that the FLP is sending messages with the correct schema.
                // This enables other teams to identify FLP responses as such and ignore them if they are not relevant for their scenario.
                const sNewRequestId = uid();
                oMessage.request_id = sNewRequestId;
                oLogger.warning(`Received Post Message without a request_id for ${oMessage.service}. This message might be not handled correctly. Assigned a new request_id: ${sNewRequestId}`);
            }

            // first check if the message is a response to a request
            if (oMessage.type === PostMessageType.Response) {
                this.#handleResponseMessage(oMessageEvent, oMessage);
                return;
            }

            // Initialize the body. It can be undefined which is unexpected for the service request handlers.
            oMessage.body = oMessage.body || {};

            let oResponseMessageBody;
            let bSuccess = true;
            try {
                if (oSourceWindow === window && bSchemaFulfilled) {
                    // Be less strict on messages without request_id as this is a contract violation.
                    // This message might come from a different integration scenario and might not meant to be handled by the FLP.

                    // This is a workaround for the FieldHelpEndpoint integration scenario
                    // This communication scenario
                    //  * uses the same window for messaging
                    //  * is not meant to be processed by the FLP
                    //  * does not provide a request_id
                    //  * expects the code=-1 as criteria to exclude FLP responses
                    // Once they have migrated to a check for the request_id the additional check bSchemaFulfilled can be removed.

                    throw new Error(`Received Post Message Request from the same window for ${oMessage.service} (${oMessage.request_id}). This is not supported by the FLP.`);
                }

                // Handle the message as Request
                const vResult = await this.#handleServiceRequests(oMessage, oSourceWindow, sSourceDomain, oMessageEvent);
                oLogger.debug(`Service request for ${oMessage.service} (${oMessage.request_id}) was handled successfully.`);

                if (vResult === BuiltInResponses.DoNotReply) {
                    oLogger.debug(`Service request for ${oMessage.service} (${oMessage.request_id}) was handled successfully, but no response is needed.`);
                    return;
                }

                oResponseMessageBody = { result: vResult };
            } catch (oError) {
                bSuccess = false;
                oLogger.error(`Service request for ${oMessage.service} (${oMessage.request_id}) failed.`, oError);
                // some services reject with a string instead of an Error object
                let sStack;
                let sMessage = oError.toString();
                if (oError instanceof Error) {
                    sMessage = oError.message;
                    sStack = oError.stack;
                }

                oResponseMessageBody = {
                    message: sMessage,
                    stack: sStack
                };
            }

            // always reply with the original service request
            const sServiceRequest = oMessage.originalMessage?.service || oMessage.service;

            return this.sendResponse(
                oMessage.request_id,
                sServiceRequest,
                oResponseMessageBody,
                bSuccess,
                oSourceWindow,
                sSourceDomain
            );
        }

        /**
         * Parses the message from the postMessage event.
         * @param {MessageEvent} oMessageEvent The incoming message event.
         * @returns {sap.ushell.appIntegration.PostMessageManager.ParsedMessage} The parsed message from the postMessage event.
         * @throws {Error} If the message could not be parsed.
         *
         * @since 1.139.0
         * @private
         */
        #parseMessage (oMessageEvent) {
            let oMessage;
            // we support stringified JSON and plain objects
            if (typeof oMessageEvent.data === "string") {
                oMessage = JSON.parse(oMessageEvent.data);
            } else if (isPlainObject(oMessageEvent.data)) {
                oMessage = oMessageEvent.data;
            } else {
                // e.g. Error, transferable objects, ...
                throw new Error("Invalid message format");
            }

            return oMessage;
        }

        /**
         * Helper function for PostMessage handler which want to send a custom formatted response.
         * @param {MessageEvent} oMessageEvent The incoming message event.
         * @returns {string|undefined} The request id from the message event.
         *
         * @since 1.140.0
         * @private
         */
        getRequestId (oMessageEvent) {
            try {
                const oMessage = this.#parseMessage(oMessageEvent);
                return oMessage.request_id;
            } catch { /* fail silently */ }
        }

        /**
         * Preprocesses the message event and message.
         * The preprocessing might change the event.
         * However, a message is only changed once.
         *
         * @param {MessageEvent} oMessageEvent The incoming message event.
         * @param {sap.ushell.appIntegration.PostMessageManager.ParsedMessage} oMessage The parsed message from the postMessage event.
         * @returns {{ event: MessageEvent, message: sap.ushell.appIntegration.PostMessageManager.ParsedMessage }} The preprocessed message event and message.
         *
         * @since 1.134.0
         * @private
         */
        #preprocessMessageEvent (oMessageEvent, oMessage) {
            // Event properties are read-only, so we need to create a new event object if we want to change them.
            let oProcessingResult;
            this.#aEventPreprocessor.forEach((fnProcessor) => {
                if (oProcessingResult) {
                    return;
                }

                oProcessingResult = fnProcessor(oMessageEvent, oMessage);
            });

            return {
                event: oProcessingResult?.event || oMessageEvent,
                message: oProcessingResult?.message || oMessage
            };
        }

        /**
         * Handles the response handlers for the postMessage event.
         * @param {MessageEvent} oMessageEvent The incoming message event.
         * @param {sap.ushell.appIntegration.PostMessageManager.PostMessageResponse|undefined} oMessage
         * The parsed message from the postMessage event or undefined if the message could not be parsed.
         *
         * @since 1.134.0
         * @private
         */
        #handleResponseMessage (oMessageEvent, oMessage) {
            if (oMessageEvent.source === window) {
                oLogger.error(`Received Post Message Response from the same window for ${oMessage.service} (${oMessage.request_id}). This is not supported by the FLP. Ignoring the message.`);
                return;
            }

            const iIndex = this.#aResponseHandlers.findIndex((oResponseHandler) => {
                if (oResponseHandler.requestId !== oMessage.request_id) {
                    return false;
                }

                if (oResponseHandler.expectedContentWindow !== oMessageEvent.source) {
                    return false;
                }

                return true;
            });

            if (iIndex < 0) {
                // We only handle awaited responses. Additionally, we log error responses for debugging purposes.
                if (oMessage.status === ResponseStatus.Error) {
                    const oError = this.#parseErrorMessage(oMessage);
                    oLogger.error(`Received error response for ${oMessage.service} (${oMessage.request_id}):`, oError);
                }
                return;
            }

            const oResponseHandler = this.#aResponseHandlers.splice(iIndex, 1)[0];

            try {
                oResponseHandler.handler(oMessageEvent, oMessage);

                oLogger.debug(`Response for ${oMessage.service} (${oMessage.request_id}) was handled by a response handler.`);
            } catch (oError) {
                oLogger.error(`Error while handling response for ${oMessage.service} (${oMessage.request_id}).`, oError);
            }
        }

        /**
         * Handles the service requests.
         * Tries to apply the RequestHandlerOptions before calling the handler.
         * Calls the handler with the expected context and returns the result.
         *
         * @param {sap.ushell.appIntegration.PostMessageManager.PostMessageRequest} oMessage The parsed message from the postMessage event.
         * @param {Window} oSourceWindow The source window of the postMessage event.
         * @param {string} sSourceDomain The origin of the postMessage event.
         * @param {MessageEvent} oMessageEvent The incoming message event.
         * @returns {Promise<any>} Resolves with the result of the service request handler.
         *
         * @since 1.134.0
         * @private
         */
        async #handleServiceRequests (oMessage, oSourceWindow, sSourceDomain, oMessageEvent) {
            const sServiceRequest = oMessage.service;
            const fnServiceRequestHandler = this.#getServiceRequestHandler(sServiceRequest);

            if (!fnServiceRequestHandler) {
                if (this.#bKeepMessagesForLaterProcessing) {
                    this.#aUnprocessedMessages.push(oMessageEvent);
                    return BuiltInResponses.DoNotReply; // event will be answered later
                }
                await this.sendResponse(
                    oMessage.request_id,
                    sServiceRequest,
                    {
                        code: -1,
                        message: `Unknown service name: '${sServiceRequest}'`
                    },
                    false, // bSuccess
                    oSourceWindow,
                    sSourceDomain
                );
                return BuiltInResponses.DoNotReply;
            }

            await this.#validateServiceRequest(oMessageEvent, sServiceRequest);

            const oRequestHandlerOptions = this.#getServiceRequestHandlerOptions(sServiceRequest);

            if (oRequestHandlerOptions.provideApplicationContext) {
                const oApplicationContainer = this.#getApplicationContainerContext(oSourceWindow);
                if (!oApplicationContainer) {
                    throw new Error("Cannot find the related application for the service request.");
                }

                oLogger.debug(`Handling service request for ${sServiceRequest} (${oMessage.request_id}) with application context ${oApplicationContainer.getId()}.`);

                return fnServiceRequestHandler(oMessage.body, oApplicationContainer, oMessageEvent);
            }

            oLogger.debug(`Handling service request for ${sServiceRequest} (${oMessage.request_id}) without application context.`);

            return fnServiceRequestHandler(oMessage.body, oMessageEvent);
        }

        /**
         * Validates the service request by applying the custom validation function if provided or by performing default checks.
         * @param {MessageEvent} oMessageEvent The incoming message event.
         * @param {sap.ushell.appIntegration.PostMessageManager.ServiceRequestId} sServiceRequest The service request name.
         * @returns {Promise} Resolves if the service request is valid, otherwise rejects with an error.
         *
         * @private
         */
        async #validateServiceRequest (oMessageEvent, sServiceRequest) {
            // only for testing
            if (this.#bSkipSourceValidation) {
                return;
            }

            const oRequestHandlerOptions = this.#getServiceRequestHandlerOptions(sServiceRequest);
            const bUseCustomValidation = typeof oRequestHandlerOptions.isValidRequest === "function";

            if (bUseCustomValidation) {
                const bIsValidRequest = await oRequestHandlerOptions.isValidRequest(oMessageEvent);
                if (!bIsValidRequest) {
                    throw new Error(`Invalid request for service ${sServiceRequest}.`);
                }
            } else {
                const oApplicationContainer = this.#getApplicationContainerContext(oMessageEvent.source);
                /**
                 * For most post messages we expect that we can find the related application container.
                 * There are certain scenario where this is not the case:
                 *  1) NWBC Compatibility mode: The message is coming from a nested iframe
                 *  2) Standalone iframes created by plugins
                 *
                 * For case 1) we assume that message is coming from the current application.
                 *  - should be active
                 *  - should be trusted (same domain)
                 *
                 * Case 2) is not supported.
                 */
                if (!oApplicationContainer) {
                    throw new Error("Cannot find the related application for the service request.");
                }

                if (!oApplicationContainer.getActive() && !oRequestHandlerOptions.allowInactive) {
                    throw new Error("Received Post Message from an inactive application.");
                }

                if (!oApplicationContainer.isTrustedPostMessageSource(oMessageEvent)) {
                    throw new Error("Received Post Message from an untrusted source.");
                }
            }
        }

        /**
         * Returns the request handler for the given service request.
         * @param {sap.ushell.appIntegration.PostMessageManager.ServiceRequestId} sServiceRequest The service request name.
         * @returns {sap.ushell.appIntegration.PostMessageManager.RequestHandler|undefined} The request handler function for the given service request.
         *
         * @since 1.134.0
         * @private
         */
        #getServiceRequestHandler (sServiceRequest) {
            return this.#oRequestHandlers[sServiceRequest]?.handler;
        }

        /**
         * Returns the options for the given service request handler.
         * @param {sap.ushell.appIntegration.PostMessageManager.ServiceRequestId} sServiceRequest The service request name.
         * @returns {sap.ushell.appIntegration.PostMessageManager.RequestHandlerOptions} The options for the given service request handler.
         *
         * @since 1.134.0
         * @private
         */
        #getServiceRequestHandlerOptions (sServiceRequest) {
            return this.#oRequestHandlers[sServiceRequest]?.options;
        }

        /**
         * Retrieves the application container context for the given source window.
         *  1) tries to match by iframe reference
         *  2) falls back to the current application container
         * @param {Window} oSourceWindow The source window of the postMessage event.
         * @returns {sap.ushell.appIntegration.IframeApplicationContainer|undefined} The application container context for the given source window.
         *
         * @since 1.134.0
         * @private
         */
        #getApplicationContainerContext (oSourceWindow) {
            let oApplicationContainer;
            this.#fnGetAllApplications().forEach((oContainer) => {
                if (oApplicationContainer || !oContainer.isA("sap.ushell.appIntegration.IframeApplicationContainer")) {
                    return;
                }

                const oApplicationWindow = oContainer.getPostMessageTarget();
                if (oApplicationWindow === oSourceWindow) {
                    oApplicationContainer = oContainer;
                }
            });

            if (oApplicationContainer) {
                return oApplicationContainer;
            }

            oLogger.error("Cannot find the related application for the service request; using the current application as fallback. This fallback will be removed in future");

            const oCurrentApplicationContainer = this.#fnGetCurrentApplication();
            if (oCurrentApplicationContainer && oCurrentApplicationContainer.isA("sap.ushell.appIntegration.IframeApplicationContainer")) {
                return oCurrentApplicationContainer;
            }
        }

        /**
         * Adds a preprocessor function to the PostMessageManager.
         * Preprocessors are called before the message is handled.
         * They can modify the message event and the message itself and even  try to "fix" the message.
         * Each preprocessor is called in the order they were added.
         * The first preprocessor that returns a result will determine the final message and event that is handled.
         * @param {sap.ushell.appIntegration.PostMessageManager.Preprocessor} fnPreprocessor The preprocessor function.
         *
         * @since 1.134.0
         * @private
         */
        addEventPreprocessor (fnPreprocessor) {
            this.#aEventPreprocessor.push(fnPreprocessor);
        }

        /**
         * Sets a request handler for the given service request.
         * The options are mixed with the default options.
         * If a handler for the service request already exists, it will be overwritten.
         *
         * @param {sap.ushell.appIntegration.PostMessageManager.ServiceRequestId} sServiceRequest The service request name.
         * @param {sap.ushell.appIntegration.PostMessageManager.RequestHandler} fnHandler The handler function for the service request.Callback
         * @param {sap.ushell.appIntegration.PostMessageManager.RequestHandlerOptions} oOptions The options for the service request handler.
         *
         * @since 1.134.0
         * @private
         */
        setRequestHandler (sServiceRequest, fnHandler, oOptions = {}) {
            if (this.#oRequestHandlers[sServiceRequest]) {
                oLogger.error(`Request handler for service ${sServiceRequest} is already defined. Overwriting the existing handler.`);
            }

            this.#oRequestHandlers[sServiceRequest] = {
                handler: fnHandler,
                options: {
                    ...oDefaultRequestHandlerOptions,
                    ...oOptions
                }
            };
        }

        /**
         * Sends a request to the given iframe.
         * Constructs a message based on the provided parameters and sends it as a postMessage event.
         *
         * @param {sap.ushell.appIntegration.PostMessageManager.ServiceRequestId} sServiceRequest The service request name.
         * @param {any} oMessageBody The message body to send with the request.
         * @param {Window} oContentWindow The content window to send the request to.
         * @param {string} sTargetOrigin The target origin for the postMessage request.
         * @param {boolean} bWaitForResponse Whether to wait for a response from the service request.
         * @returns {Promise<any>} A promise that resolves with the response from the service request if bWaitForResponse is true, otherwise resolves immediately.
         *
         * @since 1.134.0
         * @private
         */
        async sendRequest (sServiceRequest, oMessageBody, oContentWindow, sTargetOrigin, bWaitForResponse) {
            if (this.#bDisabled) {
                throw new Error("PostMessageAPI is disabled by configuration.");
            }
            if (!this.#bInitialized) {
                throw new Error("PostMessageManager is not initialized.");
            }

            const sRequestId = uid();

            const oMessage = {
                type: PostMessageType.Request,
                request_id: sRequestId,
                service: sServiceRequest,
                body: oMessageBody || {}
            };

            return this.#sendMessage(oMessage, oContentWindow, sTargetOrigin, bWaitForResponse);
        }

        /**
         * Sends a response to the given iframe.
         * Constructs a message based on the provided parameters and sends it as a postMessage event.
         *
         * @param {string} sRequestId The request ID of the service request to respond to.
         * @param {sap.ushell.appIntegration.PostMessageManager.ServiceRequestId} sServiceRequest The service request name to respond to.
         * @param {any} oMessageBody The message body to send with the response.
         * @param {boolean} bSuccess Whether the response is successful or not.
         * @param {Window} oContentWindow The content window to send the response to.
         * @param {string} sTargetOrigin The target origin for the postMessage response.
         * @returns {Promise} A promise that resolves when the response is sent.
         *
         * @since 1.134.0
         * @private
         */
        async sendResponse (sRequestId, sServiceRequest, oMessageBody, bSuccess, oContentWindow, sTargetOrigin) {
            if (this.#bDisabled) {
                throw new Error("PostMessageAPI is disabled by configuration.");
            }
            if (!this.#bInitialized) {
                throw new Error("PostMessageManager is not initialized.");
            }
            if (!sRequestId) {
                throw new Error("No request Id provided.");
            }

            const oMessage = {
                type: PostMessageType.Response,
                request_id: sRequestId,
                service: sServiceRequest,
                status: bSuccess ? ResponseStatus.Success : ResponseStatus.Error,
                body: oMessageBody || {}
            };

            return this.#sendMessage(oMessage, oContentWindow, sTargetOrigin, false);
        }

        /**
         * Sends a message to the content window.
         * Adds the bWaitForResponse handling and actually sends the postMessage.
         * @param {sap.ushell.appIntegration.PostMessageManager.ParsedMessage} oMessage The message to send.
         * @param {Window} oContentWindow The content window to send the message to.
         * @param {string} sTargetOrigin The target origin for the postMessage request.
         * @param {boolean} bWaitForResponse Whether to wait for a response from the service request.
         * @returns {Promise<any>} A promise that resolves with the response from the service request if bWaitForResponse is true, otherwise resolves immediately.
         *
         * @since 1.134.0
         * @private
         */
        async #sendMessage (oMessage, oContentWindow, sTargetOrigin, bWaitForResponse) {
            if (!oContentWindow) {
                throw new Error("No content window provided.");
            }

            const sRequestId = oMessage.request_id;

            let oResponsePromise = Promise.resolve();
            if (bWaitForResponse) {
                oLogger.debug(`Waiting for response on ${oMessage.service} (${oMessage.request_id}) from iframe with domain '${sTargetOrigin}'`);
                oResponsePromise = this.#waitForClientResponse(sRequestId, oContentWindow);
            }

            const sMessage = JSON.stringify(oMessage);
            oLogger.debug(`Sending post message on ${oMessage.service} (${oMessage.request_id}) to iframe with domain '${sTargetOrigin}'`, JSON.stringify(oMessage, null, 2));

            oContentWindow.postMessage(sMessage, sTargetOrigin);

            return oResponsePromise;
        }

        /**
         * Waits for a response from the client for the given request ID.
         * Adds a response handler to the list of response handlers.
         * The response handler will be executed when the expected response is received.
         * @param {string} sRequestId The request ID to wait for a response.
         * @param {Window} oExpectedContentWindow The expected content window to receive the response from.
         * @returns {Promise<any>} A promise that resolves with the response from the client.
         *
         * @since 1.134.0
         * @private
         */
        #waitForClientResponse (sRequestId, oExpectedContentWindow) {
            const oDeferred = new Deferred();

            this.#aResponseHandlers.push({
                requestId: sRequestId,
                expectedContentWindow: oExpectedContentWindow,

                /**
                 * Generic handler for client responses.
                 * It is called on every response and has to match the request ID and content window.
                 * @param {MessageEvent} oMessageEvent The incoming message event.
                 * @param {sap.ushell.appIntegration.PostMessageManager.PostMessageResponse} oMessage The parsed message from the postMessage event.
                 *
                 * @since 1.134.0
                 * @private
                 */
                handler: (oMessageEvent, oMessage) => {
                    oLogger.debug(`Received response for ${oMessage.service} (${oMessage.request_id}) from iframe with domain '${oMessageEvent.origin}'`);

                    if (oMessage.status === "success") {
                        oDeferred.resolve(oMessage.body);
                    } else {
                        const oError = this.#parseErrorMessage(oMessage);
                        oDeferred.reject(oError);
                    }
                }
            });

            return oDeferred.promise;
        }

        /**
         * Parses an error message from the postMessage event.
         * @param {sap.ushell.appIntegration.PostMessageManager.PostMessageResponse} oMessage The parsed message from the postMessage event.
         * @returns {Error} The parsed error from the postMessage event.
         *
         * @since 1.145.0
         * @private
         */
        #parseErrorMessage (oMessage) {
            if (oMessage.status !== "error") {
                throw new Error("Cannot parse error message from a successful response.");
            }

            let oError;
            if (oMessage.body?.message) {
                oError = new Error(oMessage.body.message);
            } else {
                oError = new Error("Unknown error");
            }

            if (oMessage.body?.stack) {
                oError.stack = oMessage.body.stack;
            }

            return oError;
        }

        /**
         * Sends a request to the specified application container.
         * Applies the distribution policy for the service request and checks if the application container is a valid target.
         * Helper method to implement {@link sap.ushell.appIntegration.ApplicationContainer#sendRequest}
         *
         * @param {sap.ushell.appIntegration.PostMessageManager.ServiceRequestId} sServiceRequest The service request name to send the request to.
         * @param {any} oMessageBody The message body to send with the request.
         * @param {sap.ushell.appIntegration.ApplicationContainer} oApplicationContainer The application container to send the request to.
         * @param {boolean} bWaitForResponse Whether to wait for a response from the service request.
         * @returns {Promise<any>} A promise that resolves with the response from the service request.
         *
         * @since 1.139.0
         * @private
         */
        async sendRequestToApplication (sServiceRequest, oMessageBody, oApplicationContainer, bWaitForResponse) {
            if (this.#bDisabled) {
                throw new Error("PostMessageAPI is disabled by configuration.");
            }
            if (!this.#bInitialized) {
                throw new Error("PostMessageManager is not initialized.");
            }

            const oDistributionPolicy = this.#getDistributionPolicy(sServiceRequest);

            if (oDistributionPolicy.onlyCurrentApplication) {
                const oCurrentApplicationContainer = this.#fnGetCurrentApplication();
                if (oApplicationContainer !== oCurrentApplicationContainer) {
                    throw new Error("Application is not the current application.");
                }
            }

            const bMatchesCapabilities = oDistributionPolicy.ignoreCapabilities || oApplicationContainer.supportsCapabilities([sServiceRequest]);
            if (!bMatchesCapabilities) {
                throw new Error(`Application does not support the service request: ${sServiceRequest}`);
            }

            if (typeof oDistributionPolicy?.isValidRequestTarget === "function") {
                const bIsValidRequestTarget = oDistributionPolicy.isValidRequestTarget(oApplicationContainer);
                if (!bIsValidRequestTarget) {
                    throw new Error("Application is not a valid request target.");
                }
            }

            return this.sendRequest(
                sServiceRequest,
                oMessageBody,
                oApplicationContainer.getPostMessageTarget(),
                oApplicationContainer.getPostMessageTargetOrigin(),
                bWaitForResponse
            );
        }

        /**
         * Sends a request to all applications that match the distribution policy for the given service request.
         *
         * @param {sap.ushell.appIntegration.PostMessageManager.ServiceRequestId} sServiceRequest The service request name to send the request to.
         * @param {any} oMessageBody The message body to send with the request.
         * @param {boolean} bWaitForResponse Whether to wait for a response from the service request.
         * @returns {Promise<any[]>} A promise that resolves with an array of responses from the service request.
         *
         * @since 1.134.0
         * @private
         */
        async sendRequestToAllApplications (sServiceRequest, oMessageBody, bWaitForResponse) {
            if (this.#bDisabled) {
                throw new Error("PostMessageAPI is disabled by configuration.");
            }
            if (!this.#bInitialized) {
                throw new Error("PostMessageManager is not initialized.");
            }

            const oDistributionPolicy = this.#getDistributionPolicy(sServiceRequest);

            if (oDistributionPolicy.preventBroadcast) {
                throw new Error(`Cannot send request to all applications for service request: ${sServiceRequest}. The distribution policy prevents broadcasting.`);
            }

            const aRelevantContainers = [];
            if (oDistributionPolicy.onlyCurrentApplication) {
                const oCurrentApplicationContainer = this.#fnGetCurrentApplication();
                if (!oCurrentApplicationContainer) {
                    return []; // might be undefined
                }

                aRelevantContainers.push(oCurrentApplicationContainer);
            } else {
                this.#fnGetAllApplications().forEach((oContainer) => {
                    aRelevantContainers.push(oContainer);
                });
            }

            const aFilteredContainers = aRelevantContainers
                .filter((oContainer) => {
                    if (!oContainer.isA("sap.ushell.appIntegration.IframeApplicationContainer")) {
                        return false;
                    }

                    if (!oContainer.getPostMessageTarget()) {
                        Log.warning(`Application container '${oContainer.getId()}' does not have (yet) a valid content window. Skipping the container.`);
                        return false; // container is not rendered yet
                    }

                    const bMatchesCapabilities = oDistributionPolicy.ignoreCapabilities || oContainer.supportsCapabilities([sServiceRequest]);
                    if (!bMatchesCapabilities) {
                        return false;
                    }

                    if (typeof oDistributionPolicy?.isValidRequestTarget === "function") {
                        const bIsValidRequestTarget = oDistributionPolicy.isValidRequestTarget(oContainer);
                        return bIsValidRequestTarget;
                    }

                    return true;
                });

            const aPromises = aFilteredContainers.map((oContainer) => {
                return this.sendRequest(
                    sServiceRequest,
                    oMessageBody,
                    oContainer.getPostMessageTarget(),
                    oContainer.getPostMessageTargetOrigin(),
                    bWaitForResponse
                );
            });

            return Promise.allSettled(aPromises).then((aResults) => {
                const aErrors = aResults
                    .filter((oResult) => oResult.status === "rejected")
                    .map((oResult) => oResult.reason);

                if (aErrors.length > 0) {
                    throw new Error(`Some requests failed: ${aErrors.map((oError) => oError.message).join(", ")}`);
                }

                // all requests were successful, return the results
                return aResults.map((oResult) => {
                    return oResult.value;
                });
            });
        }

        /**
         * Returns the distribution policy for the given service request.
         * @param {sap.ushell.appIntegration.PostMessageManager.ServiceRequestId} sServiceRequest The service request name to get the distribution policy for.
         * @returns {sap.ushell.appIntegration.PostMessageManager.DistributionPolicy} The distribution policy for the given service request.
         *
         * @since 1.134.0
         * @private
         */
        #getDistributionPolicy (sServiceRequest) {
            return this.#oDistributionPolicies[sServiceRequest] || oDefaultDistributionPolicy;
        }

        /**
         * Sets the distribution policy for the given service request.
         * If a policy for the service request already exists, it will be overwritten.
         * @param {sap.ushell.appIntegration.PostMessageManager.ServiceRequestId} sServiceRequest The service request name to set the distribution policy for.
         * @param {sap.ushell.appIntegration.PostMessageManager.DistributionPolicy} oPolicy The distribution policy to set for the service request.
         *
         * @since 1.134.0
         * @private
         */
        setDistributionPolicy (sServiceRequest, oPolicy) {
            if (this.#oDistributionPolicies[sServiceRequest]) {
                oLogger.error(`Distribution policy for service ${sServiceRequest} is already defined. Overwriting the existing policy.`);
            }

            this.#oDistributionPolicies[sServiceRequest] = {
                ...oDefaultDistributionPolicy,
                ...oPolicy
            };
        }

        /**
         * Replays all stored messages that were not processed yet.
         * This is useful for plugins that were added after the messages were received.
         * It will process all messages that were stored while the PostMessageManager was not initialized.
         *
         * @since 1.134.0
         * @private
         */
        replayStoredMessages () {
            this.#bKeepMessagesForLaterProcessing = false;
            this.#aUnprocessedMessages.forEach((oMessageEvent) => {
                this.#handlePostMessageEvent(oMessageEvent);
            });
            this.#aUnprocessedMessages = [];
        }

        /**
         * For testing only
         */
        reset () {
            removeEventListener("message", this.#fnPostMessageEventHandler);
            this.#fnPostMessageEventHandler = () => {
                throw new Error("PostMessageManager is not initialized");
            };
            this.#fnGetCurrentApplication = () => {
                throw new Error("PostMessageManager is not initialized");
            };
            this.#fnGetAllApplications = () => {
                throw new Error("PostMessageManager is not initialized");
            };

            this.#bInitialized = false;
            this.#bDisabled = false;
            this.#bSkipSourceValidation = false;

            this.#aEventPreprocessor = [];
            this.#oRequestHandlers = {};
            this.#aResponseHandlers = [];
            this.#oDistributionPolicies = {};

            this.#bKeepMessagesForLaterProcessing = true;
            this.#aUnprocessedMessages = [];
        }
    }

    return new PostMessageManager();
});
