/**
 * When `window.myObject` is not defined and we try to recreate it dynamically we will not save it
 * to the original name but a random id. Why? Others could override the prototype of the window object
 * name and make it unusable (e.g. setting via `Object.defineProperty(window, "myObject", { value: undefined, writable: false, configurable: false })`).
 */
const fallbackSlugToKey: Record<string, string> = {};

/**
 * Get the anonymous localized script from the document as object and write it to the window object.
 *
 * Use it in combination with `Assets.php#anonymous_localize_script`.
 *
 * @example
 * ```ts
 * const options = getAnonymousLocalizedScript(process.env.slug, {
 *     makeBase64Encoded: ["codeOptIn", "codeOptOut", "codeOnPageLoad", "contactEmail"],
 *     lazyParse: (obj, parseLazy) => {
 *         parseLazy(obj.others, "frontend", "groups");
 *         parseLazy(obj.others, "frontend", "tcf");
 *         parseLazy(obj, "others", "customizeValuesBanner");
 *     },
 * });
 * ```
 * @param helperScriptOptions - The options for the helper script. This are the same options required as for `Assets.php#anonymous_localize_script`.
 *                              This is used to parse the JSON strings lazily when the helper script is **not** executed.
 */
function getAnonymousLocalizedScript(
    slug: string,
    helperScriptOptions?: {
        makeBase64Encoded: string[];
        lazyParse?: (obj: any, parseLazy: (obj: any, topLevelKeyOfObject: string, property: string) => any) => void;
    },
) {
    const windowObject = (window as any)[slug.replace(/-([a-z])/g, (g) => g[1].toUpperCase())];
    const fallbackKey = fallbackSlugToKey[slug];

    if (!windowObject) {
        if (fallbackKey) {
            return (window as any)[fallbackKey];
        }

        // Case 1: window.myObject is not defined and probably overridden. But `anonymous_localize_script`
        // will set a random id and return the object via `window[randomId]`. We need to search and cache it.
        for (const key in window) {
            // Do not trigger Google PageSpeed Insights warnings about deprecated APIs
            if (["sharedStorage"].includes(key)) {
                continue;
            }
            try {
                const value = window[key] as any;
                if (value?.textDomain === slug) {
                    fallbackSlugToKey[slug] = key;
                    return value;
                }
            } catch (error) {
                // e.g. SecurityError: Failed to read a named property 'value' from 'Window': Blocked a frame with origin "..." from accessing a cross-origin frame.
                // We just ignore this error.
            }
        }

        // Case 2: window.myObject and also the fallback key are not defined. It appears that the helper script which parses the JSON is not yet executed.
        // Let's search for the `<script type="application/json"` and parse the content.
        if (helperScriptOptions) {
            const { makeBase64Encoded, lazyParse = () => {} } = helperScriptOptions;
            const fallbackKey = Math.random().toString(36);

            // Decoder for base64-encoded values by special keys
            const decodeSpecialKeys = function (key: string, value: any) {
                if (
                    makeBase64Encoded.indexOf(key) > -1 &&
                    typeof value === "string" &&
                    value.startsWith("base64-encoded:")
                ) {
                    return window.atob(value.substr(15));
                }
                return value;
            };

            // Creates a Proxy to lazily parse JSON strings at property access time
            const createLazyJsonProxy = (obj: any, property: string) =>
                new Proxy(obj, {
                    get(target, propKey) {
                        let propValue = Reflect.get(target, propKey);
                        if (propKey === property && typeof propValue === "string") {
                            propValue = JSON.parse(propValue, decodeSpecialKeys);
                            Reflect.set(target, propKey, propValue);
                        }
                        return propValue;
                    },
                });

            const foundScripts = document.querySelectorAll<HTMLScriptElement>(
                `script[type="application/json"][id^="a"][id$="1-js-extra"]`,
            );

            try {
                for (const script of foundScripts) {
                    const content = script.innerHTML;
                    const json = JSON.parse(content, decodeSpecialKeys);
                    if (json.textDomain === slug) {
                        (window as any)[fallbackKey] = json;
                        fallbackSlugToKey[slug] = fallbackKey;

                        if (lazyParse) {
                            lazyParse(json, (obj, topLevelKeyOfObject, property) => {
                                try {
                                    const stringValue = obj[topLevelKeyOfObject]?.[property];

                                    if (typeof stringValue === "string") {
                                        if (window.Proxy) {
                                            obj[topLevelKeyOfObject] = createLazyJsonProxy(
                                                obj[topLevelKeyOfObject],
                                                property,
                                            );
                                        } else {
                                            obj[topLevelKeyOfObject][property] = JSON.parse(
                                                stringValue,
                                                decodeSpecialKeys,
                                            );
                                        }
                                    }
                                } catch (error) {
                                    // Silence is golden. The error will raise when accessing the property.
                                    // But, we can ignore when the property is not a string (and therefore not
                                    // available in the original object).
                                }
                            });
                        }

                        return json;
                    }
                }
            } catch (error) {
                // Silence is golden.
            }
        }
    }

    return windowObject;
}

export { getAnonymousLocalizedScript };
