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

/**
 * @fileOverview MenuAdapter for the CEP platform
 *
 * Provides the API functions:
 * - MenuAdapter.prototype.isMenuEnabled
 * - MenuAdapter.prototype.getMenuEntries
 * - MenuAdapter.prototype.getContentNodes
 */
sap.ui.define([
    "sap/base/Log",
    "sap/base/i18n/Localization",
    "sap/ushell/components/tiles/utils",
    "sap/ushell/Config",
    "sap/ushell/library",
    "sap/ushell/utils/HttpClient"
], (
    Log,
    Localization,
    utils,
    Config,
    ushellLibrary,
    HttpClient
) => {
    "use strict";

    // shortcut for sap.ushell.ContentNodeType
    const oContentNodeTypes = ushellLibrary.ContentNodeType;
    // shortcut for sap.ushell.MenuEntryType
    const oMenuEntryTypes = ushellLibrary.MenuEntryType;

    const sCEPMenuAdapterComponent = "sap.ushell.adapters.cep.MenuAdapter";

    /**
     * @typedef {object} MenuServiceEntryTargetParameter
     * @property {string} name The name of the parameter.
     * @property {string} value The value of the parameter.
     *
     * @private
     */

    /**
     * @typedef {object} MenuServiceEntryTarget
     * A target describing the intent: #<semanticObject>-<action>~<contextRaw>?<params>&/<appSpecificRoute>
     * <b>Note:</b> The intent shall not exceed 512 bytes when serialized as UTF-8.
     * @property {string} semanticObject The semanticObject part of the intent.
     * @property {string} action The action part of the intent.
     * @property {MenuServiceEntryTargetParameter[]} parameters[] The parameters of the target
     * <br><b>Note:</b> Parameter values can contain special characters and must be provided
     * unencoded. The APIs takes care of the necessary encodings.
     * @property {string} [innerAppRoute] The appSpecificRoute without the '&/' prefix.
     *
     * @private
     */

    /**
     * @typedef {object} MenuServiceEntry
     * The menu structure required by the menu service.
     * @property {string} title The text of the menu entry
     * @property {string} [help-id] The unique help ID of the menu entry
     * @property {string} [icon] The icon URI of the menu entry
     * @property {string} [description] The description of a the menu entry
     * @property {boolean} [isHomeEntry] Indicates whether the entry is the Home entry
     * @property {sap.ushell.MenuEntryType} type
     *    The type of a the menu entry. May be, for example "IBN" (trigger an intent based navigation when clicked)
     *    or "text" (May be used to display a sub menu).
     * @property {string|MenuServiceEntryTarget|undefined} target Describes the navigation target if type is "IBN" and the url when "URL" otherwise empty
     * @property {MenuServiceEntry[]} menuEntries Contains a list of sub menu entries
     *
     * @private
     */

    /**
     * @typedef {object} ContentNode
     * local type def for {@link sap.ushell.services.BookmarkV2.ContentNode}
     * A content node may be:
     *  - a classic homepage group
     *  - an unselectable node (space) or a selectable node (page) in spaces mode
     *  - or any other containers in the future
     *
     * @property {string} id ID of the content node
     * @property {string} label Human-readable representation of a content node which can be displayed in a control
     * @property {sap.ushell.ContentNodeType} type Specifies the content node type. E.g: space, page, group, etc.
     * @property {boolean} isContainer Specifies if a content can be added eg. via bookmarking.
     * @property {ContentNode[]} [children] Child content nodes of this content node.
     *
     * @private
     */

    /**
     * @typedef {object} GraphQLMenuResponse
     * @property {object} data - The data object from the content api GraphQL response.
     * @property {MenuData[]} data.menu - The menu array containing the menu entries.
     *
     * @private
     */

    /**
     * @typedef {object} MenuData - The menu object provided by the content api.
     * @property {string} id - The id of the menu entry.
     * @property {MenuEntryType} type - The type of the menu entry.
     * @property {string} title - The title of the menu entry.
     * @property {string} [icon] - The icon of the menu entry.
     * @property {MenuData} [subMenu] - The sub menu entries of a space entry.
     * @property {string} [pageType] - The pageType of a workpage entry.
     * @property {NavigationTarget} [navigationTarget] - The navigation target of a visualization entry.
     *
     * @private
     */

    /**
     * @typedef {object} NavigationTarget - The navigation target of a visualization.
     * @property {object} intent - The intent of the navigation target.
     * @property {string} intent.action - The action of the intent.
     * @property {string} intent.businessAppId - The business app id of the intent.
     * @property {string} intent.semanticObject - The semantic object of the intent.
     *
     * @private
     */

    /**
     * @enum {string} MenuEntryType - The type of the menu entry provided by the content api.
     * @private
     */
    const MenuEntryType = {
        Space: "space",
        Workpage: "workpage",
        Visualization: "visualization"
    };

    /**
     * @enum {string} SubMenuPageType - The pageType of the workpage entry provided by the content api.
     * @private
     */
    const SubMenuPageType = {
        Workpage: "workpage",
        Page: "page"
    };

    /**
     * @enum {string} SpaceEntity - The type of the space entity which is used for the log message.
     * @private
     */
    const SpaceEntity = {
        Menu: "FLP menu",
        ContentNodes: "content nodes"
    };

    /**
     * Constructs a new instance of the CEP Menu Adapter.
     *
     * @param {object} oUnused Parameter is not used.
     * @param {string} sUnused Parameter is not used.
     * @param {object} oAdapterConfiguration The adapter specific configuration
     * @param {string} oAdapterConfiguration.serviceUrl The url to the CEP content API, that is a GraphQL service
     * @param {string} oAdapterConfiguration.siteId The relevant site-id
     * @class
     * @see {@link sap.ushell.adapters.cep.MenuAdapter}
     * @since 1.106.0
     */
    function MenuAdapter (oUnused, sUnused, oAdapterConfiguration) {
        if (oAdapterConfiguration
            && oAdapterConfiguration.config
            && oAdapterConfiguration.config.serviceUrl
            && oAdapterConfiguration.config.siteId
        ) {
            this.httpClient = new HttpClient();
            this.serviceUrl = oAdapterConfiguration.config.serviceUrl;
            this.siteId = oAdapterConfiguration.config.siteId;
            this.oSiteDataPromise = this._doRequest(this.serviceUrl, this.siteId);
        } else {
            Log.error("Invalid configuration provided.", "", sCEPMenuAdapterComponent);
        }
    }

    /**
     * Returns whether the menu is enabled.
     * The menu is enabled if there's at least one menu entry and the menu is enabled.
     *
     * @returns {Promise<boolean>} True if the menu is enabled.
     * @since 1.106.0
     * @private
     */
    MenuAdapter.prototype.isMenuEnabled = function () {
        if (!Config.last("/core/menu/enabled")) {
            return Promise.resolve(false);
        }

        return this.getMenuEntries().then((aMenuEntries) => {
            return aMenuEntries.length > 0;
        });
    };

    /**
     * Returns a promise that resolves with the menu entries for the pages.
     *
     * @returns {Promise<MenuServiceEntry[]>} Resolves with the menu entries.
     * @since 1.106.0
     * @private
     */
    MenuAdapter.prototype.getMenuEntries = async function () {
        const aMenu = await this._getMenu();
        return this._buildMenuEntries(aMenu);
    };

    /**
     * Gets the content nodes for the menu.
     *
     * @returns {Promise<ContentNode[]>} The content nodes.
     * @since 1.106.0
     * @private
     */
    MenuAdapter.prototype.getContentNodes = async function () {
        const aMenu = await this._getMenu();
        return this._buildContentNodes(aMenu);
    };

    /**
     * Builds a menu which is accepted by the menu service.
     *
     * @param {MenuData[]} aMenu The menu objects provided by the content api.
     * @returns {Promise<MenuServiceEntry[]>} The menu structure required by the menu service.
     * @since 1.106.0
     * @private
     */
    MenuAdapter.prototype._buildMenuEntries = async function (aMenu) {
        // Create a 1st level menu entry for each space
        // having 2nd level sub menu entries for its pages and visualizations inside if needed.
        return aMenu
            .filter(this._isSpaceNotEmpty.bind(null, SpaceEntity.Menu))
            .map((oMenu) => {
                const oTopMenuEntry = this._buildMenuEntry(oMenu);
                const aSubMenuEntries = [];

                oMenu.subMenu.forEach((oSubMenuNode) => {
                    oSubMenuNode.spaceid = oMenu.id;
                    const oSubMenuEntry = this._buildMenuEntry(oSubMenuNode);
                    aSubMenuEntries.push(oSubMenuEntry);
                });

                if (aSubMenuEntries.length > 1) {
                    oTopMenuEntry.menuEntries = aSubMenuEntries;
                }

                return oTopMenuEntry;
            });
    };

    /**
     * Builds the menu structure for a single menu entry.
     *
     * @param {MenuData} oMenuEntry The menu object provided by the content api.
     * @returns {MenuServiceEntry[]} The menu structure required by the menu service.
     * @since 1.132.0
     * @private
     */
    MenuAdapter.prototype._buildMenuEntry = function (oMenuEntry) {
        let oConfig = {};

        if (oMenuEntry.type === MenuEntryType.Space) {
            oConfig = this._getSpaceConfig(oMenuEntry);
        } else if (oMenuEntry.type === MenuEntryType.Workpage) {
            oConfig = this._getPageConfig(oMenuEntry);
        } else if (oMenuEntry.type === MenuEntryType.Visualization) {
            oConfig = this._getVisualizationConfig(oMenuEntry);
        }

        return {
            title: oConfig.title,
            "help-id": oConfig.helpId,
            description: "",
            icon: utils.isIconURIValid(oConfig.icon) ? oConfig.icon : undefined,
            type: oMenuEntryTypes.IBN,
            target: {
                semanticObject: oConfig.semanticObject,
                action: oConfig.action,
                parameters: [
                    {
                        name: "spaceId",
                        value: oMenuEntry.spaceid || oMenuEntry.id
                    },
                    {
                        name: oConfig.targetName,
                        value: oConfig.targetValue
                    }
                ],
                innerAppRoute: undefined
            },
            menuEntries: []
        };
    };

    /**
     * Builds the menu structure for a menu entry of type space.
     *
     * @param {MenuData} oSpace The space object provided by the menu data.
     * @returns {MenuServiceEntry} The menu structure required by the menu service.
     * @since 1.143.0
     * @private
     */
    MenuAdapter.prototype._getSpaceConfig = function (oSpace) {
        const oFirstSubEntry = oSpace?.subMenu[0];

        const oConfig = {
            title: oSpace.title,
            helpId: `Space-${oSpace.id}`,
            icon: oSpace.icon,
            semanticObject: "Launchpad",
            action: "openFLPPage",
            targetName: "pageId",
            targetValue: oFirstSubEntry.id
        };

        // If the first node is a visualization the target action should be the visualization target action.
        if (oFirstSubEntry.type === MenuEntryType.Visualization) {
            const oVizConfig = this._getVisualizationConfig(oFirstSubEntry);

            Object.assign(oConfig, {
                semanticObject: oVizConfig.semanticObject,
                action: oVizConfig.action,
                targetName: oVizConfig.targetName,
                targetValue: oVizConfig.targetValue
            });

            // In case there is only one sub menu entry, we use that for the top menu entry title and icon.
            if (oSpace.subMenu.length === 1) {
                oConfig.title = oVizConfig.title;
                oConfig.icon = oVizConfig.icon;
                oConfig.helpId = `App-${oSpace.id}`;
            }
        }

        return oConfig;
    };

    /**
     * Builds the menu structure for a menu entry of type page.
     *
     * @param {MenuData} oPage The page object provided by the menu data.
     * @returns {MenuServiceEntry} The menu structure required by the menu service.
     * @since 1.143.0
     * @private
     */
    MenuAdapter.prototype._getPageConfig = function (oPage) {
        return {
            title: oPage.title,
            helpId: `Page-${oPage.id}`,
            icon: oPage.icon,
            semanticObject: "Launchpad",
            action: "openFLPPage",
            targetName: "pageId",
            targetValue: oPage.id
        };
    };

    /**
     * Builds the menu structure for a menu entry of type visualization.
     *
     * @param {MenuData} oViz The visualization object provided by the menu data.
     * @returns {MenuServiceEntry} The menu structure required by the menu service.
     * @since 1.143.0
     * @private
     */
    MenuAdapter.prototype._getVisualizationConfig = function (oViz) {
        const oIntent = oViz?.navigationTarget?.intent;

        const oConfig = {
            helpId: `App-${oViz.id}`,
            targetName: "sap-ui-app-id-hint",
            title: oViz.title,
            icon: oViz.icon
        };

        if (oIntent) {
            oConfig.targetValue = oIntent.businessAppId;
            oConfig.semanticObject = oIntent.semanticObject;
            oConfig.action = oIntent.action;
        } else {
            // Fallback in case no valid navigation target is provided.
            oConfig.targetValue = oViz.id;
            oConfig.semanticObject = "Launchpad";
            oConfig.action = "openFLPPage";
            Log.warning(`Visualization ${oViz.id} does not contain a valid navigationTarget for site-id: ${this.siteId}`, "", sCEPMenuAdapterComponent);
        }

        return oConfig;
    };

    /**
     * Builds content nodes based on the spaces. The property "isContainer" is set to true for pages (which have pageType 'page').
     *
     * @param {MenuData[]} aMenu The menu objects provided by the content api.
     * @returns {ContentNode[]} The content nodes required by the menu service.
     * @private
     * @since 1.106.0
     */
    MenuAdapter.prototype._buildContentNodes = function (aMenu) {
        return aMenu
            .filter(this._isSpaceNotEmpty.bind(null, SpaceEntity.ContentNodes))
            .map((oSpace) => {
                return {
                    id: oSpace.id,
                    label: oSpace.title || oSpace.id,
                    type: oContentNodeTypes.Space,
                    isContainer: false,
                    children: oSpace.subMenu.map((oSubMenuNode) => {
                        const bIsWorkpage = oSubMenuNode.type === MenuEntryType.Workpage;
                        return {
                            id: oSubMenuNode.id,
                            label: oSubMenuNode.title || oSubMenuNode.id,
                            type: bIsWorkpage ? oContentNodeTypes.Page : oContentNodeTypes.Visualization,
                            isContainer: oSubMenuNode?.pageType === SubMenuPageType.Page,
                            children: []
                        };
                    })
                };
            });
    };

    /**
     * Resolves to true if the page with the given sPageId is a WorkPage.
     * This is the case if the type property in the subMenuNode is "workpage" and the pageType is "workpage".
     *
     * @param {string} sPageId The pageId to check.
     * @returns {Promise<boolean>} A promise resolving to true if the pageId is a WorkPage or false if not.
     * @private
     * @since 1.107.0
     */
    MenuAdapter.prototype.isWorkPage = async function (sPageId) {
        const aMenu = await this._getMenu();

        const oFoundMenu = aMenu
            .flatMap((oSpace) => oSpace.subMenu || [])
            .filter((oSubMenuNode) => oSubMenuNode.pageType === SubMenuPageType.Workpage && oSubMenuNode.type === MenuEntryType.Workpage)
            .find((oPage) => oPage.id === sPageId);

        return !!oFoundMenu;
    };

    /**
     * Returns a Promise that sends the request to the GraphQL service to fetch menu data.
     *
     * Example response:
     * <pre>
        {
            "data": {
                "menu": [
                    {
                        "id": "781f3d65-69f2-4a97-b92f-bc2cedf2f8a9",
                        "type": "space",
                        "title": "Test Space",
                        "subMenu": [
                            {
                                "id": "1fac2d11-e9d3-4f23-8ef5-57475030c5c3#Default-VizId",
                                "type": "visualization",
                                "title": "Test App(visualization) 1st-level",
                                "navigationTarget": {
                                    "intent": {
                                        "action": "Launch",
                                        "businessAppId": "app-1",
                                        "semanticObject": "app1"
                                    }
                                }
                            }
                        ]
                    },
                    {
                        "id": "38095617-8298-4f1b-853d-30e0f8b0d58a",
                        "type": "space",
                        "title": "Second Test Space",
                        "subMenu": [
                            {
                                "id": "2f9c360a-6552-4484-8b63-65d8968dbfd0",
                                "type": "workpage",
                                "title": "Test Page"
                            },
                            {
                                "id": "1fac2d11-e9d3-4f23-8ef5-57475030c5c3#Default-VizId",
                                "type": "visualization",
                                "title": "Test App(visualization) 2nd-level",
                                "navigationTarget": {
                                    "intent": {
                                        "action": "Launch",
                                        "businessAppId": "app-2",
                                        "semanticObject": "app2"
                                    }
                                }
                            }
                        ]
                    }
                ]
            }
        }
     * </pre>
     *
     * @param {string} sServiceUrl The GraphQL service URL that represents the CEP content API
     * @param {string} sSiteId The relevant site-id
     * @returns {Promise<GraphQLMenuResponse>} Resolves with the response from the GraphQL service exposing menu data.
     * @since 1.106.0
     * @private
     */
    MenuAdapter.prototype._doRequest = function (sServiceUrl, sSiteId) {
        let sQuery = `{
            menu(siteId: "${sSiteId}")
        }`;

        // Replace line breaks and spaces
        sQuery = sQuery
            .replace(/\n/g, "")
            .replace(/ /g, "");

        const headers = {
            "Content-Type": "application/json",
            Accept: "application/json",
            "Accept-Language": Localization.getLanguageTag().toString()
        };

        if (Config.last("/core/site/sapCdmVersion")) {
            headers["sap-cdm-content-version"] = Config.last("/core/site/sapCdmVersion");
        }

        return this.httpClient.get(`${sServiceUrl}?query=${encodeURIComponent(sQuery)}`, {
            headers
        }).then((oResponse) => {
            if (oResponse.status < 200 || oResponse.status >= 300) {
                Log.error(oResponse.responseText, "", sCEPMenuAdapterComponent);
                throw new Error(`HTTP request to GraphQL service failed with status: ${oResponse.status} - ${oResponse.statusText}`);
            }
            return JSON.parse(oResponse.responseText || "{}");
        }).then((oData) => {
            // Temporary workaround for QESAPSHELL-108
            try {
                oData.data.menu = oData.data.menu.filter((node) => {
                    return node.id !== "default_space";
                });
            } catch {
                // No error handling required
            }
            return oData;
        });
    };

    /**
    * Retrieves the menu from the GraphQL responded JSON object.
    *
    * @param {object} oSiteData The GraphQL response as JSON object
    * @returns {Promise<MenuData[]>} The menu as an array
    * @private
    */
    MenuAdapter.prototype._getMenu = async function () {
        const oSiteData = await this.oSiteDataPromise;
        if (Array.isArray(oSiteData?.data?.menu) && oSiteData.data.menu.length > 0
        ) {
            return oSiteData.data.menu;
        }

        Log.warning(`No menu found for site-id: ${this.siteId}`, "", sCEPMenuAdapterComponent);
        return [];
    };

    /**
     * Filter function that checks if a space is empty to filter it out.
     * If the space is empty, a warning message is logged.
     *
     * @param {string} sEntityName Name of the entity where the space is filtered out for the warning message
     * @param {MenuData} oSpace The space to be filtered
     * @returns {boolean} true = keep, false = remove
     * @private
     * @since 1.106.0
     */
    MenuAdapter.prototype._isSpaceNotEmpty = function (sEntityName, oSpace) {
        if (!(Array.isArray(oSpace?.subMenu) && oSpace.subMenu.length > 0) ||
            !oSpace.subMenu?.some((subMenu) => Object.keys(subMenu).length > 0)) {
            Log.warning(`FLP space ${oSpace.id} without sub menu content omitted in ${sEntityName}.`, "", sCEPMenuAdapterComponent);
            return false;
        }
        return true;
    };

    return MenuAdapter;
});
