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

import { Component, DoCheck, ElementRef, Inject, Input, OnDestroy, Optional, Self, ViewChild } from "@angular/core";
import { DomSanitizer, SafeHtml } from "@angular/platform-browser";
import { ActivatedRoute } from "@angular/router";
import { filter, interval, lastValueFrom, map, Subscription, take } from "rxjs";
import { ANGULAR_APP_PRIMARY_NAVIGATION } from "./angular-apps/angular-apps.extension-point";
import { ExtensionComponentRegistration, ExtensionComponentRegistry } from "./extension-component.registry";
import { HYPER_PLUGIN_CONFIG, HYPER_PLUGIN_THEME, HYPER_PLUGIN_THEME_TYPE, HyperPluginViewEncapsulation, HyperpluginConfig, THEME_MODE } from "../../../lib-interfaces";
import { BootstrapDetails } from "../models/bootstrap-details";
import { BOOTSTRAP_DETAILS } from "../tokens";
import { SubscriptionTracker } from "../../../subscription-tracker.service";

/**
 * Component wrapper for Angular Applications initialization.
 *
 * Example Usage:
 * In your component HTML add:
 *
 * <vcd-extension-application [vcdExtensionApplications]="ANGULAR_APP_VRO_EXECUTION"></vcd-extension-application>
 *
 * Where vcdExtensionApplications is the type of extensions you want to setup.
 */
@Component({
    selector: "vcd-extension-application",
    templateUrl: "./extension-application.component.html",
    providers: [SubscriptionTracker],
})
export class ExtensionApplicationsComponent implements OnDestroy, DoCheck {
    @ViewChild("host", { static: false }) el: ElementRef<HTMLSpanElement>;

    private _scope: string;

    @Input("vcdExtensionApplications")
    set scope(scope: string) {
        if (scope == this._scope) {
            return;
        }

        this.clear().then(() => {
            this._scope = scope;
            this.registrationSubscription = this.registry
                .scopedRegistrationsObservable(scope)
                .subscribe(() => (this.shouldUpdate = true));
            this.shouldUpdate = true;
        });
    }

    /**
     * Get the scope of the applications to be displayed.
     */
    get scope(): string {
        return this._scope;
    }

    htmls: {
        html: SafeHtml;
        id: string;
        extension: ExtensionComponentRegistration;
    }[] = [];

    /**
     * Subscription used to listen for new registrations with the ExtensionComponentRegistry.
     */
    private registrationSubscription: Subscription;

    /**
     * Flag set to determine whether we should update our items on the next check cycle.  Angular can freak
     * out if structures change outside of check cycles.
     */
    private shouldUpdate = false;

    private angularApps: ExtensionComponentRegistration[];

    /**
     * Holds loaded CSS files for given theme for each hyper plugin.
     * The links in that list must be removed from the DOM on each
     * `theme` change.
     */
    private linkElementsPerHyperPlugin: {
        [hyperPluginUrn: string]: HTMLLinkElement[]
    } = {};

    constructor(
        @Inject(HYPER_PLUGIN_CONFIG) private HYPER_PLUGIN_CONFIG: HyperpluginConfig,
        @Inject(BOOTSTRAP_DETAILS) private BOOTSTRAP_DETAILS: BootstrapDetails,
        private registry: ExtensionComponentRegistry,
        private sanitizer: DomSanitizer,
        private route: ActivatedRoute,
        private elRef: ElementRef,
        @Self() private subscriptionTracker: SubscriptionTracker,
        @Optional() @Inject(HYPER_PLUGIN_THEME) public HYPER_PLUGIN_THEME: HYPER_PLUGIN_THEME_TYPE,
    ) {}

    ngOnDestroy(): void {
        this.clear().catch((e) => {
            console.error(e);
        });
    }

    ngDoCheck(): void {
        if (this.shouldUpdate) {
            if (this.scope === ANGULAR_APP_PRIMARY_NAVIGATION) {
                const hyperPlugin = this.registry.getRegisteredComponents(this._scope);

                if (hyperPlugin) {
                    this.loadAngularApps([...hyperPlugin]);
                }

                return;
            }

            this.loadAngularApps(this.registry.getRegisteredComponents(this._scope));
        }
    }

    /**
     * Clear subscriptions
     * Call De-Activate lifecycle hook for each Activated Angular Application
     */
    private clear(): Promise<void> {
        if (this.registrationSubscription) {
            this.registrationSubscription.unsubscribe();
        }

        this.htmls = [];

        if (!this.angularApps) {
            return Promise.resolve();
        }

        const activeAngularApps = this.angularApps.filter(this.getActiveAngularApps.bind(this));

        const deactivations = activeAngularApps.map((angularApp) =>
            angularApp.angularAppDetails.angularAppBundle.deactivate(angularApp.manifest)
        );

        return Promise.all(deactivations)
            .then(() => {
                activeAngularApps.forEach((angularApp) => {
                    angularApp.angularAppDetails.bootstrapped = false;
                    angularApp.angularAppDetails.activated = false;
                });

                return Promise.resolve();
            })
            .catch((e) => {
                activeAngularApps.forEach((angularApp) => {
                    angularApp.angularAppDetails.bootstrapped = false;
                    angularApp.angularAppDetails.activated = false;
                });
                return Promise.reject(e);
            });
    }

    /**
     * Activate Angular Applications for certain extension scope.
     */
    private loadAngularApps(plugins: ExtensionComponentRegistration[]): void {
        this.angularApps = plugins;

        this.angularApps
            .filter(this.getInActiveAngularApps.bind(this))
            .forEach((angularApp: ExtensionComponentRegistration) => {
                this.htmls.push({
                    id: angularApp.manifest.urn,
                    extension: angularApp,
                    html: this.sanitizer.bypassSecurityTrustHtml(
                        angularApp.angularAppDetails.angularAppBundle.template
                    ),
                });

                this.shouldUpdate = false;

                if (angularApp.angularAppDetails.angularAppBundle.customWindow) {
                    this.elRef.nativeElement.customElements =
                        angularApp.angularAppDetails.angularAppBundle.customWindow["customElements"];
                }

                angularApp.angularAppDetails.angularAppBundle
                    .bootstrap({ name: angularApp.manifest.urn })
                    .then(() => {
                        angularApp.angularAppDetails.bootstrapped = true;
                        return angularApp.angularAppDetails.angularAppBundle.activate({
                            name: angularApp.manifest.urn,
                            HYPER_PLUGIN_CONFIG: this.HYPER_PLUGIN_CONFIG,
                            BOOTSTRAP_DETAILS: this.BOOTSTRAP_DETAILS,
                        });
                    })
                    .then((ngModuleRef: unknown) => {
                        const anotherAngularNgModuleRef = ngModuleRef as AnotherAngularNgModuleRef;
                        // Assume HyperPlugin is activated
                        if (!ngModuleRef) {
                            return true;
                        }

                        // readiness probe is undefined, assume HyperPlugin is activated
                        if (anotherAngularNgModuleRef.instance.ready === undefined) {
                            return true;
                        }

                        const readinessProbe = interval(1000).pipe(
                            filter(() => {
                                return anotherAngularNgModuleRef.instance.ready;
                            }),
                            take(1),
                            map(() => {
                                return true;
                            })
                        );

                        return lastValueFrom(readinessProbe);
                    })
                    .then((readinessProbe) => {
                        angularApp.angularAppDetails.activated = readinessProbe;

                        const tagName = angularApp.angularAppDetails.angularAppBundle.template
                            .replace("<", "")
                            .replace("/>", "");
                        const angularAppShadowRoot = document.querySelector(tagName).shadowRoot;

                        let darkThemeStyles: string[] = [];
                        let lightThemeStyles: string[] = [];
                        if (angularApp.manifest.styles) {
                            darkThemeStyles = angularApp.manifest.styles.filter((stylesFile) => {
                                return stylesFile.startsWith("dark");
                            });
                            lightThemeStyles = angularApp.manifest.styles.filter((stylesFile) => {
                                return !stylesFile.startsWith("dark");
                            });
                        }

                        this.linkElementsPerHyperPlugin[angularApp.manifest.urn] = [];
                        if (this.HYPER_PLUGIN_THEME) {
                            this.subscriptionTracker.subscribe(
                                this.HYPER_PLUGIN_THEME,
                                (theme) => {
                                    this.loadStyles(theme, darkThemeStyles, lightThemeStyles, angularAppShadowRoot, angularApp);
                                }
                            );
                        } else {
                            this.loadStyles("light", darkThemeStyles, lightThemeStyles, angularAppShadowRoot, angularApp);
                        }

                        if (angularAppShadowRoot && angularApp.manifest.scripts) {
                            angularApp.manifest.scripts.forEach((scriptsFile) => {
                                const scriptsElem = document.createElement("script");
                                scriptsElem.setAttribute("src", `${angularApp.resourcePath}/${scriptsFile}`);
                                angularAppShadowRoot.insertBefore(scriptsElem, angularAppShadowRoot.firstChild);
                            });
                        }
                    })
                    .catch((e) => {
                        this.shouldUpdate = false;
                        console.error(e);
                    });
            });
    }

    private getActiveAngularApps(angularApp: ExtensionComponentRegistration): boolean {
        return angularApp.angularAppDetails.bootstrapped || angularApp.angularAppDetails.activated;
    }

    private getInActiveAngularApps(angularApp: ExtensionComponentRegistration): boolean {
        return !angularApp.angularAppDetails.bootstrapped;
    }

    private loadStyles(theme: THEME_MODE, darkThemeStyles: string[], lightThemeStyles: string[], angularAppShadowRoot: ShadowRoot, angularApp: ExtensionComponentRegistration) {
        this.linkElementsPerHyperPlugin[angularApp.manifest.urn].forEach((link) => {
            link.remove();
        });
        this.linkElementsPerHyperPlugin[angularApp.manifest.urn] = [];

        let stylesToLoad: string[];

        if (theme === "dark" && darkThemeStyles.length > 0) {
            stylesToLoad = darkThemeStyles;
        } else {
            stylesToLoad = lightThemeStyles;
        }

        if (this.HYPER_PLUGIN_CONFIG.encapsulation === HyperPluginViewEncapsulation.ShadowDom &&
            angularAppShadowRoot && angularApp.manifest.styles) {
            stylesToLoad.forEach((stylesFile) => {
                if (!stylesFile.includes("shadowdom")) {
                    return;
                }
                const linkElem = document.createElement("link");
                linkElem.setAttribute("rel", "stylesheet");
                linkElem.setAttribute("href", `${angularApp.resourcePath}/${stylesFile}`);
                angularAppShadowRoot.insertBefore(linkElem, angularAppShadowRoot.firstChild);
                this.linkElementsPerHyperPlugin[angularApp.manifest.urn].push(linkElem);
            });
        }

        if (this.HYPER_PLUGIN_CONFIG.encapsulation === HyperPluginViewEncapsulation.Emulated &&
            angularApp.manifest.styles) {
            stylesToLoad.forEach((stylesFile) => {
                if (!stylesFile.includes("emulated")) {
                    return;
                }
                const linkElem = document.createElement("link");
                linkElem.setAttribute("rel", "stylesheet");
                linkElem.setAttribute("href", `${angularApp.resourcePath}/${stylesFile}`);
                document.head.append(linkElem);
                this.linkElementsPerHyperPlugin[angularApp.manifest.urn].push(linkElem);
            });
        }
    }
}

interface AnotherAngularNgModuleRef {
    instance: {
        ready: boolean;
    }
}