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

/**
 * @fileOverview Configuration Provider for the Fiori Launchpad Sandbox.
 *
 * This module serves as the central configuration provider for the FLP Sandbox environment.
 * It loads application-specific configuration from `fioriSandboxAppConfig.json`, processes
 * application manifests, and dynamically builds the ushell configuration including site data,
 * visualizations, and navigation targets.
 *
 * @version 1.147.1
 * @since 1.147
 */
sap.ui.define([
    "sap/base/Log",
    "sap/base/util/ObjectPath",
    "sap/ui/core/Manifest",
    "sap/ushell/sandbox/sandboxConfig",
    "sap/ushell/sandbox/SandboxSite",
    "sap/ui/fl/initial/_internal/FlexConfiguration"
], (
    Log,
    ObjectPath,
    Manifest,
    SandboxConfig,
    SandboxSite,
    FlexConfiguration
) => {
    "use strict";

    /**
     * Describes a custom tile configuration entry.
     *
     * @typedef {object} sap.ushell.sandbox.ConfigurationProvider.TileConfig
     * @property {string} semanticObject The semantic object for navigation
     * @property {string} action The action for navigation
     * @property {object} [parameters] Optional navigation parameters
     * @property {string} [rootPath] Optional root path to the application
     * @since 1.147
     * @private
     */

    /**
     * The ConfigurationProvider implements a configuration provider interface for UI5's
     * base configuration system. It supplies sandbox-specific configuration values and
     * manages the ushell configuration lifecycle.
     *
     * @namespace sap.ushell.sandbox.ConfigurationProvider
     * @alias module:sap/ushell/sandbox/ConfigurationProvider
     * @since 1.147
     * @private
     */
    class ConfigurationProvider {
        /**
         * Internal sandbox configuration object storing runtime configuration values.
         * Properties are populated from `fioriSandboxAppConfig.json` with `sapUshellSandbox` prefix.
         *
         * @type {object}
         * @since 1.147
         * @private
         */
        #sandboxConfig = Object.create(null);

        /**
         * Promise that resolves when the application configuration has been loaded from
         * `fioriSandboxAppConfig.json`. Configuration properties are normalized and prefixed
         * with `sapUshellSandbox`.
         *
         * @type {Promise<undefined>}
         * @since 1.147
         * @private
         */
        #pAppConfig;

        constructor () {
            /**
             * Default tile configuration used when no custom tiles are specified.
             *
             * @type {Array<sap.ushell.sandbox.ConfigurationProvider.TileConfig>}
             * @since 1.147
             * @private
             */
            this.#sandboxConfig.sapUshellSandboxTiles = [{
                semanticObject: "MyApp",
                action: "display"
            }];

            this.#pAppConfig = fetch("./fioriSandboxAppConfig.json", {
                headers: {
                    "Content-Type": "application/json"
                }
            }).then(async (oResponse) => {
                if (!oResponse.ok) {
                    Log.info(`Failed to load Fiori Launchpad Sandbox configuration: '${oResponse.status}'`);
                    return;
                }
                const oParsedResponse = await oResponse.json();
                for (const key in oParsedResponse) {
                    if (Object.hasOwn(oParsedResponse, key)) {
                        this.#sandboxConfig[`sapUshellSandbox${key.charAt(0).toUpperCase() + key.slice(1)}`] = oParsedResponse[key];
                    }
                }

                // Normalize tiles to array
                this.#sandboxConfig.sapUshellSandboxTiles = Array.isArray(this.#sandboxConfig.sapUshellSandboxTiles)
                    ? this.#sandboxConfig.sapUshellSandboxTiles
                    : [this.#sandboxConfig.sapUshellSandboxTiles];
            });
        }

        /**
         * Loads and processes an application manifest including i18n resources.
         *
         * @param {string} [rootPath="../"] The root path to the application manifest
         * @returns {Promise<sap.ui.core.Manifest|undefined>} The processed manifest or undefined on error
         * @since 1.147
         * @private
         */
        async #loadAndProcessApplicationManifest (rootPath = "../") {
            try {
                const oManifest = await Manifest.load({
                    manifestUrl: `${rootPath}manifest.json`,
                    async: true
                });
                await oManifest._processI18n(true);
                return oManifest;
            } catch (error) {
                Log.error("Manifest for Application could not be loaded from default path.");
            }
        }

        /**
         * Creates an application entry and visualization in the CDM site data.
         *
         * @param {sap.ushell.sandbox.ConfigurationProvider.TileConfig} oCustomApp Custom application configuration
         * @param {sap.ui.core.Manifest|undefined} oAppManifest The application manifest
         * @since 1.147
         * @private
         */
        #createApp (oCustomApp, oAppManifest) {
            const {
                semanticObject,
                action,
                parameters
            } = oCustomApp;
            const oManifestAppInfo = oAppManifest?.getEntry("sap.app") || {};
            const oManifestUiInfo = oAppManifest?.getEntry("sap.ui") || {};
            const sAppKey = `${oCustomApp.semanticObject}-${oCustomApp.action}`;
            const sVizId = `custom_viz_${sAppKey}`;
            const sApplicationTitle = oManifestAppInfo.title || oManifestAppInfo.id?.split(".").pop() || sAppKey;
            const signature = {
                parameters: {},
                additionalParameters: "allowed"
            };

            let sUrlParam = "";

            for (const sName in parameters) {
                if (Object.hasOwn(parameters, sName)) {
                    signature.parameters[sName] = {
                        defaultValue: {
                            value: parameters[sName]
                        }
                    };
                    sUrlParam += `${sUrlParam ? "&" : "?"}${sName}=${parameters[sName]}`;
                }
            }

            /* eslint-disable quote-props */
            const oDeviceTypes = Object.assign({}, oManifestUiInfo.deviceTypes, {
                "desktop": true,
                "phone": true,
                "tablet": true
            });

            const oApp = {
                "sap.app": {
                    "id": oManifestAppInfo.id || "sap.ushell.sandbox.BusinessApp",
                    "title": sApplicationTitle,
                    "subTitle": oManifestAppInfo.description || "",
                    "ach": oManifestAppInfo.ach,
                    "applicationVersion": {
                        "version": "1.0.0"
                    },
                    "crossNavigation": {
                        "inbounds": {
                            "target": {
                                "semanticObject": semanticObject,
                                "action": action,
                                "signature": signature
                            }
                        }
                    },
                    "tags": {
                        "keywords": sApplicationTitle.split(" ")
                    }
                },
                "sap.flp": {
                    "type": "application"
                },
                "sap.ui": {
                    "technology": "UI5",
                    "icons": {
                        "icon": oManifestUiInfo.icons?.icon || "sap-icon://Fiori2/F0018"
                    },
                    "deviceTypes": oDeviceTypes
                },
                "sap.ui5": {
                    "componentName": oManifestAppInfo.id || "sap.ushell.sandbox.BusinessApp"
                },
                "sap.platform.runtime": {
                    "componentProperties": {
                        "url": `${oCustomApp.rootPath || "../"}${sUrlParam}`
                    }
                }
            };

            const oViz = {
                "vizType": "sap.ushell.StaticAppLauncher",
                "businessApp": oManifestAppInfo.id || "sap.ushell.sandbox.BusinessApp",
                "vizConfig": {
                    "sap.app": {
                        "title": sApplicationTitle,
                        "subTitle": oManifestAppInfo.description || ""
                    },
                    "sap.ui": {
                        "icons": {
                            "icon": oManifestUiInfo.icons?.icon || "sap-icon://Fiori2/F0018"
                        },
                        "deviceTypes": oDeviceTypes
                    },
                    "sap.flp": {
                        "target": {
                            "type": "URL",
                            "url": `#${sAppKey}`
                        }
                    }
                }
            };
            /* eslint-enable quote-props */

            const oSiteData = ObjectPath.get("services.CommonDataModel.adapter.config.siteData", SandboxConfig) || structuredClone(SandboxSite);
            const oSection = ObjectPath.get("pages.page1.payload.sections.APPS", oSiteData);
            oSiteData.applications[sAppKey] = oApp;
            oSiteData.visualizations[sAppKey] = oViz;
            oSection.layout.vizOrder.push(sVizId);
            oSection.viz[sVizId] = {
                id: sVizId,
                vizId: sAppKey
            };
            ObjectPath.set("services.CommonDataModel.adapter.config.siteData", oSiteData, SandboxConfig);
        }

        /**
         * Applies site data for custom tiles by creating application and visualization entries.
         *
         * @param {Array<sap.ushell.sandbox.ConfigurationProvider.TileConfig>} aCustomTiles Array of custom tile configurations
         * @returns {Promise<undefined>} A promise that resolves when site data is applied
         * @since 1.147
         * @private
         */
        async #applySiteData (aCustomTiles) {
            await Promise.all(aCustomTiles.map(async (oCustomApp) => {
                const oAppManifest = await this.#loadAndProcessApplicationManifest(oCustomApp.rootPath);
                this.#createApp(oCustomApp, oAppManifest);
            }));
        }

        /**
         * Creates the complete ushell configuration by processing application manifests
         * and building the CDM site data structure.
         *
         * @returns {Promise<object>} A promise resolving to the complete ushell configuration object
         * @since 1.147
         * @private
         */
        async #createUshellConfig () {
            // Apply root intent configuration if specified
            if (this.#sandboxConfig.sapUshellSandboxRootIntent) {
                ObjectPath.set("renderers.fiori2.componentData.config.rootIntent", this.#sandboxConfig.sapUshellSandboxRootIntent, SandboxConfig);
            }

            // Apply AppState transient configuration if specified
            if (Object.hasOwn(this.#sandboxConfig, "sapUshellSandboxAppStateMode")) {
                ObjectPath.set("services.AppState.config.transient", this.#sandboxConfig.sapUshellSandboxAppStateMode, SandboxConfig);
            }

            // Merge custom plugins with default plugins
            if (this.#sandboxConfig.sapUshellSandboxPlugins) {
                const oPlugins = Object.assign(ObjectPath.get("bootstrapPlugins", SandboxConfig), this.#sandboxConfig.sapUshellSandboxPlugins);
                ObjectPath.set("bootstrapPlugins", oPlugins, SandboxConfig);
            }

            await this.#applySiteData(this.#sandboxConfig.sapUshellSandboxTiles);

            return SandboxConfig;
        }

        /**
         * Retrieves a configuration value by key.
         *
         * Special keys handled:
         * - `sapUiOnInit`: Returns the sandbox initialization module path
         * - `sapUiFlexibilityServices`: Returns flexibility services configuration including
         *   LocalStorageConnector and optional ObjectPathConnector for RTA
         *
         * @param {string} sKey The configuration key to retrieve
         * @returns {*} The configuration value or undefined if not found
         * @since 1.147
         * @private
         */
        get (sKey) {
            switch (sKey) {
                case "sapUiOnInit":
                    return "module:sap/ushell/sandbox/SandboxModule";
                case "sapUiFlexibilityServices": {
                    const aFlexibilityServices = [
                        {connector: "LocalStorageConnector"}
                    ];
                    if (this.#sandboxConfig.sapUshellSandboxRta) {
                        aFlexibilityServices.push({
                            connector: "ObjectPathConnector",
                            path: this.#sandboxConfig.sapUshellSandboxRta
                        });
                    }
                    return aFlexibilityServices;
                }
                default:
                    return this.#sandboxConfig[sKey];
            }
        }

        /**
         * Returns a promise that resolves when the application configuration has been loaded.
         *
         * @returns {Promise<undefined>} A promise resolving when configuration loading is complete
         * @since 1.147
         * @private
         */
        loaded () {
            return this.#pAppConfig;
        }

        /**
         * Applies the ushell configuration to the global `sap-ushell-config` object.
         *
         * This method performs validation to ensure no ushell configuration has been set
         * before the sandbox starts, then creates the complete configuration including
         * site data, applications, and visualizations.
         *
         * For UI5 1.x compatibility, it also explicitly sets the flexibility services
         * configuration since the boot task runs after flex config evaluation.
         *
         * @returns {Promise<undefined>} A promise resolving when configuration is applied
         * @throws {Error} If `sap-ushell-config` is already set or ushell modules were loaded prematurely
         * @since 1.147
         * @private
         */
        async applyUshellConfig () {
            // Validate that no ushell config was set before sandbox start
            if (globalThis["sap-ushell-config"]) {
                throw Error("It's not allowed to configure the sandbox providing configuration directly to the ushell config object 'sap-ushell-config'. " +
                    "Please provide sandbox specific configuration using 'fioriSandboxAppConfig.json'.");
            }
            // Validate that ushell modules haven't been loaded prematurely
            if (sap.ui.require("sap/ushell/Config")) {
                throw Error("It's not allowed to use 'sap/ushell' modules before the sandbox has been started.");
            }

            /**
             * In UI5 1.x the xx-bootTask is executed after the flex config has already been evaluated,
             * therefore we need to explicitly set the flex configuration.
             *
             * @deprecated since 1.120
             */
            if (true) { // eslint-disable-line no-constant-condition
                const aFlexibilityServices = [
                    {connector: "LocalStorageConnector"}
                ];
                if (this.#sandboxConfig.sapUshellSandboxRta) {
                    aFlexibilityServices.push({
                        connector: "ObjectPathConnector",
                        path: this.#sandboxConfig.sapUshellSandboxRta
                    });
                }
                FlexConfiguration.setFlexibilityServices(aFlexibilityServices);
            }

            const oUshellConfig = await this.#createUshellConfig();

            globalThis["sap-ushell-config"] = oUshellConfig;
        }
    }

    return new ConfigurationProvider();
});
