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

sap.ui.define([
    "sap/base/Log",
    "sap/base/util/isPlainObject",
    "sap/base/util/ObjectPath",
    "sap/ushell/bootstrap/Config/UrlConfigOverwrites"
], (
    Log,
    isPlainObject,
    ObjectPath,
    UrlConfigOverwrites
) => {
    "use strict";

    const OBJECT_PROTOTYPE_KEYS = Object.getOwnPropertyNames(Object.prototype);

    const sConfigMetaPrefix = "sap.ushellConfig";
    const sUshellConfigNamespace = "sap-ushell-config";

    /**
     * @typedef sap.ushell.bootstrap.Config.PreloadBundleConfig
     * Configuration for custom preload bundles, which can be used instead of the standard UI5 preload bundles.
     *
     * @property {object} oPreloadBundleConfig The preload bundle configuration.
     * @property {boolean} [oPreloadBundleConfig.enabled = true] If set to <code>false</code>,
     * the custom preload bundles are not used
     * @property {string[]} [oPreloadBundleConfig.coreResources] The resources containing the initially needed
     *   modules and the UI5 core; the resource path is resolved by standard UI5 loader logic; must contain
     *   the .js file extension
     *
     * @since 1.147.0
     * @private
     */

    /**
     * @typedef {object} MigrationMapping
     * @property {string} from The old config path (dot notation)
     * @property {string} to The new config path (dot notation)
     *
     * @since 1.147.0
     * @private
     */

    /**
     * @alias sap.ushell.bootstrap.Config
     * @namespace
     * @description Singleton responsible for initializing and providing access to the ushell configuration.
     *
     * @since 1.147.0
     * @private
     * @ui5-restricted sap.ushell_abap
     */
    class Config {
        #oUshellConfig = null;

        /**
         * Initializes the ushell configuration by merging the different sources according to this priority order:
         * 1) URL overwrites
         * 2) meta tags
         * 3) provided platform defaults
         * 4) window config
         *
         * @param {object} [oPlatformDefaults] Platform-specific default ushell configuration
         * @throws {Error} If called more than once
         *
         * @private
         * @ui5-restricted sap.ushell_abap
         */
        initialize (oPlatformDefaults) {
            if (this.#oUshellConfig) {
                throw new Error("ushellConfig already initialized. Call reset() to reinitialize.");
            }

            this.#oUshellConfig = this.#mergeConfig(oPlatformDefaults);

            // store merged config as global for backward compatibility
            window[sUshellConfigNamespace] = this.#oUshellConfig;

            Log.info("Initialized ushell Config", JSON.stringify(this.#oUshellConfig), "sap/ushell/bootstrap/Config");
        }

        /**
         * Returns the merged ushell configuration object.
         *
         * @returns {object} The merged ushell configuration
         * @throws {Error} If called before {@link #initialize}
         *
         * @since 1.147.0
         * @private
         * @ui5-restricted sap.ushell_abap
         */
        get () {
            this.#checkInitialized();

            return this.#oUshellConfig;
        }

        /**
         * Merges all sources of configuration into a single ushell config object according to this priority order:
         * 1) URL overwrites
         * 2) meta tags
         * 3) provided platform defaults
         * 4) window config
         *
         * @param {object} [oPlatformDefaults] Platform-specific default ushell configuration
         * @returns {object} The merged ushell configuration
         *
         * @since 1.147.0
         * @private
         */
        #mergeConfig (oPlatformDefaults) {
            const aMetaConfigItems = this.#readMetatags();
            const oWindowConfig = window[sUshellConfigNamespace] || {};

            // URL param sap-ushell-xx-config-values can be used to set single config params
            // this is ONLY for simplified testing and supportability
            const oUrlOverwrites = UrlConfigOverwrites.getConfig() || {};

            const oMergedConfig = [
                oWindowConfig,
                oPlatformDefaults || {},
                ...aMetaConfigItems,
                oUrlOverwrites
            ].reduce((oCurrentConfig, oConfigItem) => {
                /**
                 * Each configuration part might miss a configuration node for v2 services.
                 * Therefore we fetch the migration differences and apply it in addition.
                 *
                 * With this procedure each configuration part is still able to overwrite
                 * previous configurations according to the configuration order.
                 */

                const oMigrationConfig = this.getV2MigrationConfig(oConfigItem);

                this.extendConfig(oCurrentConfig, oMigrationConfig, true);
                this.extendConfig(oCurrentConfig, oConfigItem, true);

                return oCurrentConfig;
            }, {});

            return oMergedConfig;
        }

        /**
         * Reads all meta tags for ushell configuration
         * @returns {object[]} An array of configuration objects
         *
         * @since 1.147.0
         * @private
         */
        #readMetatags () {
            const sSelector = `meta[name^='${sConfigMetaPrefix}']:not([name=''])`;
            const aNodeList = Array.from(document.querySelectorAll(sSelector));

            const aItems = [];

            aNodeList.forEach((oMetaNode) => {
                try {
                    aItems.push(JSON.parse(oMetaNode.content));
                } catch (oError) {
                    Log.error("Metatag Read failed", oError, "sap/ushell/bootstrap/Config");
                }
            });

            return aItems;
        }

        /**
         * Checks if the configuration has been initialized and throws an error if not.
         * @throws {Error} If the configuration has not been initialized
         *
         * @since 1.147.0
         * @private
         */
        #checkInitialized () {
            if (!this.#oUshellConfig) {
                throw new Error("ushellConfig not initialized. Call initialize() first.");
            }
        }

        /**
         * Merge the object oConfigToMerge into oMutatedBaseConfig according to
         * sap-ushell-config merge rules. Note that the JSON serialized content of
         * oConfigToMerge is used, thus JSON serialization restrictions apply (e.g.
         * Infinity -> null ). Note that it is thus not possible to remove a
         * property definition or overriding with  {"propname" : undefined}. One
         * has to override with null or 0 etc.
         *
         * Note: Do not use this method for general merging of other objects, as
         * the rules may be enhanced/altered
         *
         * @param {object} oMutatedBaseConfig
         *     the configuration to merge into, modified in place
         * @param {object} oConfigToMerge
         *     the configuration to be merged with oMutatedBaseConfig
         * @param {boolean} bCloneConfigToMerge
         *     whether the oConfigToMerge must be cloned prior to the merge
         *
         * @since 1.147.0
         * @private
         * @ui5-restricted sap.ushell_abap
         */
        extendConfig (oMutatedBaseConfig, oConfigToMerge, bCloneConfigToMerge) {
            if (!oConfigToMerge) {
                return;
            }

            const oActualConfigToMerge = bCloneConfigToMerge ? JSON.parse(JSON.stringify(oConfigToMerge)) : oConfigToMerge;

            Object.keys(oActualConfigToMerge).forEach((sKey) => {
                // Prevent overriding of object prototype properties to avoid prototype pollution attacks
                if (OBJECT_PROTOTYPE_KEYS.includes(sKey)) {
                    return;
                }

                if (isPlainObject(oMutatedBaseConfig[sKey]) && isPlainObject(oActualConfigToMerge[sKey])) {
                    this.extendConfig(oMutatedBaseConfig[sKey], oActualConfigToMerge[sKey], false);
                    return;
                }

                if (Array.isArray(oMutatedBaseConfig[sKey]) && Array.isArray(oActualConfigToMerge[sKey])) {
                    Log.error(`Merging of arrays is not supported for config key ${sKey}. Overriding the whole array.`, "sap/ushell/bootstrap/Config");
                }

                oMutatedBaseConfig[sKey] = oActualConfigToMerge[sKey];
            });
        }

        /**
         * Returns the mappings for v2 service migrations.
         * @returns {MigrationMapping[]} The list of migration mappings from old to new config paths
         *
         * @since 1.147.0
         * @private
         */
        #getV2MigrationMappings () {
            return [
                { from: "services.Bookmark", to: "services.BookmarkV2" },
                { from: "services.CrossApplicationNavigation", to: "services.Navigation" },
                { from: "services.Notifications", to: "services.NotificationsV2" },
                { from: "services.Personalization", to: "services.PersonalizationV2" },
                { from: "services.LaunchPage", to: "services.FlpLaunchPage" },
                { from: "services.ShellNavigation", to: "services.ShellNavigationInternal" },
                { from: "services.NavTargetResolution", to: "services.NavTargetResolutionInternal" }
            ];
        }

        /**
         * Migrates the config for the v2 services based on their predecessor.
         * Does only work for plain values, not for functions and class instances.
         * @param {object} oUshellConfig The ushell config
         * @returns {object} The migrated config, ONLY the migrated part
         *
         * @since 1.147.0
         * @private
         * @ui5-restricted sap.ushell_abap
         */
        getV2MigrationConfig (oUshellConfig) {
            /**
             * Store whether some migration happened.
             * This allows us to provide a fallback as part of {@link sap.ushell.bootstrap}
             * @deprecated since 1.120
             */
            window["sap-ushell-config-migration"] = true;

            // migrate service config
            const oConfig = {};
            this.#getV2MigrationMappings().forEach((oMigration) => {
                const sServiceFromName = oMigration.from.split(".")[1];
                const oConfigFrom = ObjectPath.get(oMigration.from, oUshellConfig);
                const oConfigTo = ObjectPath.get(oMigration.to, oUshellConfig);
                if (oConfigFrom && !oConfigTo) {
                    Log.warning(`The service config for ${oMigration.from} is migrated to ${oMigration.to}`, "Config");

                    // we only need a shallow copy here, because we only want to change the module property
                    const oConfigFromCopy = { ...oConfigFrom };

                    // the module references the standard service name this might lead to infinite loops
                    // because the old service can depend on the new service
                    // DINC0103566
                    if (oConfigFromCopy.module === `sap.ushell.services.${sServiceFromName}`) {
                        delete oConfigFromCopy.module;
                    }

                    ObjectPath.set(oMigration.to, oConfigFromCopy, oConfig);
                }
            });

            return JSON.parse(JSON.stringify(oConfig));
        }

        /**
         * Migrates the config for the v2 services based on their predecessor.
         * The migration happens INPLACE!
         * @param {object} oUshellConfig The ushell config
         *
         * @since 1.147.0
         * @private
         */
        applyV2Migration (oUshellConfig) {
            /**
             * Store whether some migration happened.
             * This allows us to provide a fallback as part of {@link sap.ushell.bootstrap}
             * @deprecated since 1.120
             */
            window["sap-ushell-config-migration"] = true;

            // migrate service config
            this.#getV2MigrationMappings().forEach((oMigration) => {
                const sServiceFromName = oMigration.from.split(".")[1];
                const oConfigFrom = ObjectPath.get(oMigration.from, oUshellConfig);
                const oConfigTo = ObjectPath.get(oMigration.to, oUshellConfig);
                if (oConfigFrom && !oConfigTo) {
                    Log.warning(`The service config for ${oMigration.from} is migrated to ${oMigration.to}`, "Config");

                    // we only need a shallow copy here, because we only want to change the module property
                    const oConfigFromCopy = { ...oConfigFrom };

                    // the module references the standard service name this might lead to infinite loops
                    // because the old service can depend on the new service
                    // DINC0103566
                    if (oConfigFromCopy.module === `sap.ushell.services.${sServiceFromName}`) {
                        delete oConfigFromCopy.module;
                    }

                    ObjectPath.set(oMigration.to, oConfigFromCopy, oUshellConfig);
                }
            });
        }

        /**
         * Returns the list of preload libs to be preloaded by the UI5 Core.
         * @returns {string} the concatenated list of libs to preload
         *
         * @since 1.147.0
         * @private
         */
        #getPreloadLibs () {
            if (!isPlainObject(this.#oUshellConfig.ui5?.libs)) {
                Log.error("Invalid ushell configuration: /ui5/libs must be an object");
                return "";
            }

            return Object
                .keys(this.#oUshellConfig.ui5.libs)
                .filter(function (sKey) {
                    return this[sKey];
                }, this.#oUshellConfig.ui5.libs)
                .join();
        }

        /**
         * Returns generic UI5 bootstrap configuration derived from the ushell config.
         *
         * @returns {object} An object with camelCase keys: sapUiPreload, sapUiCompatVersion,
         *   sapUiLibs, sapUiActiveTerminologies
         * @throws {Error} If called before {@link #initialize}
         *
         * @since 1.147.0
         * @private
         * @ui5-restricted sap.ushell_abap
         */
        getUI5Config () {
            this.#checkInitialized();

            const sPreloadLibs = this.#getPreloadLibs();

            let aActiveTerminologies = this.#oUshellConfig.ushell?.verticalization?.activeTerminologies;
            if (aActiveTerminologies?.length === 0) {
                aActiveTerminologies = undefined;
            }

            return {
                sapUiPreload: "async",
                sapUiCompatVersion: "edge",
                sapUiLibs: sPreloadLibs,
                sapUiActiveTerminologies: aActiveTerminologies
            };
        }

        /**
         * Applies the configured module paths to the UI5 loader.
         * Converts dot-notation paths from the ushell config to slash-notation.
         *
         * @since 1.147.0
         * @private
         * @ui5-restricted sap.ushell_abap
         */
        applyModulePaths () {
            const oPaths = Object.keys(this.#oUshellConfig.modulePaths || {}).reduce((oResult, sModulePath) => {
                oResult[sModulePath.replace(/\./g, "/")] = this.#oUshellConfig.modulePaths[sModulePath];
                return oResult;
            }, {});

            sap.ui.loader.config({
                paths: oPaths
            });
        }

        /**
         * Returns the configured custom preload bundle configuration.
         * @returns {sap.ushell.bootstrap.Config.PreloadBundleConfig} The configuration for custom preload bundles
         *
         * @since 1.147.0
         * @private
         */
        getCustomPreloads () {
            return this.#oUshellConfig.ushell.customPreload;
        }

        /**
         * The method will fail if the given object has a cyclic reference.
         *
         * @param {object} oInput Object to deep freeze; it should not contain a cyclic reference anywhere in its tree.
         * @returns {object} The given object which is no longer mutable.
         *
         * @since 1.147.0
         * @private
         */
        deepFreeze (oInput) {
            Object.keys(oInput)
                .filter((sProperty) => {
                    return typeof oInput[sProperty] === "object";
                })
                .forEach((sProperty) => {
                    oInput[sProperty] = this.deepFreeze(oInput[sProperty]);
                });

            return Object.freeze(oInput);
        }

        /**
         * Only for testing purposes!
         * Resets the internal configuration state, allowing re-initialization.
         * Clears the ushell config and the window global.
         *
         * @since 1.147.0
         * @private
         */
        reset () {
            this.#oUshellConfig = null;
            delete window[sUshellConfigNamespace];
        }
    }

    return new Config();
});
