/*
 * Copyright 2020-2025 VMware, Inc. All rights reserved. VMware Confidential
 */

import { finalize, take } from "rxjs/operators";
import { Injectable } from "@angular/core";
import { combineLatest, from, merge, Observable, of, Subject } from "rxjs";
import { ExtensionLibScope } from "../extensibility.model";
import { ExternalExtensionLoaderState } from "../loader/extension-loader.model";
import { SystemJsStub } from "../loader/external/load-external-extension-bundle.step";

export const UIPluginsExternalScopes = {
    BUNDLED: "bundled",
    EXTERNAL: "external",
    ISOLATED: "isolated",
};

@Injectable()
export class PluginsManagerService {
    private readonly LIBS_IN_PARALLEL = 5;
    private readonly systemJs: SystemJsStub = window["System"];

    private pendingLibraries: {
        [libNamePlusVersion: string]: {
            scope: ExtensionLibScope;
            pending: boolean;
            request: Observable<any>;
        };
    } = {};
    private extensonsLibrariesRegistry: ExtensonsLibraries;

    constructor() {
        this.extensonsLibrariesRegistry = (window as any).extensonsLibraries = {};
    }

    load(state: ExternalExtensionLoaderState): Promise<unknown> {
        const manifest = state.manifest;

        if (manifest?.extensionPoints?.length) {
            if (!manifest.extensionPoints[0]?.scripts?.length) {
                return Promise.resolve();
            }

            return this.loadScriptsAsHTML(state, manifest.extensionPoints[0].scripts);
        }

        if (!manifest.externals || !manifest.externals.libs || !manifest.externals.jsonpFunction) {
            return Promise.resolve();
        }

        const manifestLibs = manifest.externals.libs;
        const libsToLoad = Object.keys(manifestLibs)
            .map((libName) => {
                return {
                    name: libName,
                    version: manifestLibs[libName].version,
                    scope: manifestLibs[libName].scope,
                    absLocation: `${state.spec.resourcePath}/${manifestLibs[libName].location}`,
                };
            })
            .filter((lib) => {
                // Self provided
                if (this.isIsolatedProviding(lib.scope)) {
                    return true;
                }

                // Has Pending Dependency
                const pendingLib = this.checkForPendingDependency(lib);
                if (pendingLib) {
                    pendingLib.request.pipe(take(1)).subscribe(() => {
                        this.borrowDependecy(manifest.externals.jsonpFunction, lib);
                    });
                    return false;
                }

                // Already bootstrapped lib
                if (this.isExternalProvided(lib.scope) || this.checkForDependency(lib)) {
                    this.borrowDependecy(manifest.externals.jsonpFunction, lib);
                    return false;
                }

                // Bootstrap for re-use.
                if (this.isBundledProvided(lib.scope)) {
                    return true;
                }

                throw new Error(`Un-recognized scope, please check your plugin: ${state.manifest.urn}.`);
            });

        return this.loadDependencies(state, libsToLoad);
    }

    checkForDependency(lib: { name: string; version: string }): boolean {
        return (
            Object.keys(this.extensonsLibrariesRegistry).indexOf(
                this.getLibNameRegistrySearchKey(lib.name, lib.version)
            ) !== -1
        );
    }

    checkForPendingDependency(lib: {
        name: string;
        version: string;
    }): {
        scope: ExtensionLibScope;
        pending: boolean;
        request: Observable<any>;
    } {
        const pendingLib = this.pendingLibraries[this.getLibNameRegistrySearchKey(lib.name, lib.version)];

        if (pendingLib) {
            return pendingLib;
        }

        return null;
    }

    loadDependencies(state: ExternalExtensionLoaderState, libs: UIExtensonLibrary[]): Promise<unknown> {
        const requests = libs.map((lib) => {
            const libNamePlusVersion = this.getLibNameRegistrySearchKey(lib.name, lib.version);
            const request = from(this.systemJs.import(lib.absLocation)).pipe(
                finalize(() => {
                    if (!this.isBundledProvided(lib.scope)) {
                        return;
                    }

                    this.registerDependencies(state, lib);

                    if (this.pendingLibraries[libNamePlusVersion]) {
                        this.pendingLibraries[libNamePlusVersion].pending = false;
                    }
                })
            );

            if (this.isBundledProvided(lib.scope)) {
                this.pendingLibraries[libNamePlusVersion] = {
                    scope: lib.scope,
                    pending: true,
                    request,
                };
            }

            return request;
        });

        return merge(...requests, this.LIBS_IN_PARALLEL).toPromise();
    }

    /**
     * Load script file as an script element and block the further plugin loading process
     * until the script is ready.
     *
     * @param state
     * @param scripts list of script file names to load
     * @returns Promise
     */
    loadScriptsAsHTML(state: ExternalExtensionLoaderState, scripts: string[]): Promise<unknown> {
        const requests = scripts.map((script) => {
            const path = `${state.spec.resourcePath}/${script}`;

            const scriptEl = document.createElement("script");
            scriptEl.src = path;
            document.body.appendChild(scriptEl);

            const onLoadSubject = new Subject<void>();
            scriptEl.onload = scriptEl.onerror = () => {
                onLoadSubject.complete();
            };

            return onLoadSubject.asObservable();
        });

        return merge(...requests, this.LIBS_IN_PARALLEL).toPromise();
    }

    registerDependencies(state: ExternalExtensionLoaderState, lib: UIExtensonLibrary): void {
        this.extensonsLibrariesRegistry[`${this.getLibNameRegistrySearchKey(lib.name, lib.version)}`] = {
            bootstrappedBy: state.manifest.urn,
            jsonpFunctionName: state.manifest.externals.jsonpFunction,
        };
    }

    borrowDependecy(jsonpFunctionName: string, lib: UIExtensonLibrary): void {
        // Get or create the target AMD vendor registry
        const targetUIExtensionLibraries = ((window as any)[jsonpFunctionName] =
            (window as any)[jsonpFunctionName] || []);
        const library: (string[] | WebpackAMDJsonpMap)[] = [];

        // Find the source AMD vendor registry
        const libName = `${this.getLibNameRegistrySearchKey(lib.name, lib.version)}`;
        const bootstrappedLib = this.extensonsLibrariesRegistry[libName];

        if (!bootstrappedLib) {
            console.error(`Library ${lib.name} with version ${lib.version} not found!`);
            return;
        }

        const sourceUIExtensionLibraries = (window as any)[bootstrappedLib.jsonpFunctionName] as any[];

        // Check for chached AMD Lib in the registry or find and register it
        let dependency: WebpackAMDJsonpMap;
        if (bootstrappedLib.jsonpFunctionCache) {
            dependency = bootstrappedLib.jsonpFunctionCache;
        } else {
            dependency = sourceUIExtensionLibraries.find((dependencies: ArrayLike<string[] | WebpackAMDJsonpMap>) => {
                const depName = dependencies[0][0];

                return depName === libName;
            })[1];
            bootstrappedLib.jsonpFunctionCache = dependency;
        }

        // Assign the existing dependency to the empty target AMD vendor registry
        library.push([libName]);
        library.push(dependency);
        targetUIExtensionLibraries.push(library);
    }

    hasPendingDepencies(state: ExternalExtensionLoaderState): Observable<any> {
        const externals = state.manifest.externals;
        if (!externals || !externals.libs || !externals.jsonpFunction) {
            return of(null);
        }

        const pendingLibs = Object.keys(externals.libs)
            .filter((libName) => {
                return (
                    this.isBundledProvided(externals.libs[libName].scope) ||
                    this.isExternalProvided(externals.libs[libName].scope)
                );
            })
            .map((libName) => {
                return this.pendingLibraries[
                    this.getLibNameRegistrySearchKey(libName, externals.libs[libName].version)
                ];
            })
            .filter((lib) => {
                if (!lib) {
                    return false;
                }
                return lib.pending && this.isBundledProvided(lib.scope);
            })
            .map((lib) => lib.request);

        if (!pendingLibs.length) {
            return of(null);
        }

        return combineLatest(pendingLibs);
    }

    isBundledProvided(scope: ExtensionLibScope): boolean {
        return scope === UIPluginsExternalScopes.BUNDLED;
    }

    isExternalProvided(scope: ExtensionLibScope): boolean {
        return scope === UIPluginsExternalScopes.EXTERNAL;
    }

    isIsolatedProviding(scope: ExtensionLibScope): boolean {
        return scope === UIPluginsExternalScopes.ISOLATED;
    }

    getLibNameRegistrySearchKey(libName: string, libVersion: string): string {
        return `${this.parseLibName(libName)}@${libVersion}`;
    }

    /**
     * This is needed because in the manifest the lib names defined
     * like @clr/angular, but we want to remove the "/" to avoid path
     * traversing issues.
     */
    parseLibName(libName): any {
        return libName.replace("/", ":");
    }
}

interface UIExtensonLibrary {
    name: string;
    version: string;
    scope: ExtensionLibScope;
    absLocation: string;
}

interface ExtensonsLibraries {
    /**
     * Library name and version separated by "@".
     */
    [namePlusVersion: string]: {
        jsonpFunctionName: string;
        /**
         * The URN of the plugin which bootstrapped this library.
         */
        bootstrappedBy: string;
        jsonpFunctionCache?: WebpackAMDJsonpMap;
    };
}

interface WebpackAMDJsonpMap {
    [fileName: string]: WebpackAMDJsonpFunction;
}

/**
 * Webpack AMD jsonFunction details https://webpack.js.org/configuration/output/#outputjsonpfunction
 */
// eslint-disable-next-line @typescript-eslint/naming-convention, no-underscore-dangle, id-blacklist, id-match
export type WebpackAMDJsonpFunction = (module: any, __webpack_exports__: any, __webpack_require__: any) => void;
