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

import { Injectable } from "@angular/core";
import { Observable, of, throwError as _throw } from "rxjs";
import { catchError, mergeMap } from "rxjs/operators";
import { ComponentCreatedData } from "../../../extensions/vdcs/resources/ComponentCreatedData";
import { JsonObjectHelper } from "../../lib/json-object.helper";
import { WizardExtensionComponent } from "./extension-point-container.interface";
import { ExtensibilityEvent, INTERENAL_EXTENSION_POINT_TYPE } from "./extension-point-events";

@Injectable()
export class ExtensionPointOrchcestrator {
    private extensionComponentRegistry: {
        internalExtensionPointType: INTERENAL_EXTENSION_POINT_TYPE;
        extensionPointComponent: ComponentCreatedData;
    }[] = [];

    /**
     * Holds events triggered by Wizard/Modal extensibility that doesn't have yet
     * payload or returnValue determined. Later when the events have all required data
     * they will be executed.
     */
    private eventsQueue: CustomEvent<ExtensibilityEvent<unknown, unknown>>[] = [];

    constructor() {
        this.listenForEvent();
    }

    public emitEvent<T>(event: T): void {
        try {
            window.dispatchEvent(
                new CustomEvent<T>("extensibilityEvent", {
                    detail: {
                        ...event,
                    },
                })
            );
        } catch (e) {
            console.error(
                `[Extension Point Action Orchestrator]
            Unable to fire event:`,
                event,
                e
            );
        }
    }

    /**
     * Listen for events triggered by Wizard/Modal extensibility.
     * If the events are incomplete, i.e. doesn't have payload or returnValue
     * write them down in eventsQueue list.
     *
     * Later if the same event is triggered with payload or returnValue assign
     * these values the incompleteEvent, check if it's complete and if it's
     * then trigger the event.
     * Otherwise, the event will remain in the queue.
     */
    private listenForEvent(): void {
        window.addEventListener("extensibilityEvent", (event: CustomEvent<ExtensibilityEvent<unknown, unknown>>) => {
            const incompleteEventIndex = this.eventsQueue.findIndex((e) => {
                if (event.detail["id"]) {
                    return e.detail["id"] === event.detail["id"];
                }

                return e.detail.extensionPointType === event.detail.extensionPointType;
            });
            const incompleteEvent = this.eventsQueue[incompleteEventIndex];

            if (incompleteEvent) {
                if (!incompleteEvent.detail.payload) {
                    incompleteEvent.detail.payload = event.detail.payload;
                }

                if (!incompleteEvent.detail.returnValue) {
                    incompleteEvent.detail.returnValue = event.detail.returnValue;
                }

                if (incompleteEvent.detail.payload && incompleteEvent.detail.returnValue) {
                    this.startEvent(incompleteEvent.detail);
                    this.eventsQueue.splice(incompleteEventIndex, 1);
                }
            } else {
                this.addEventToExecutionStack(event);
            }
        });
    }

    /**
     * This method has to be executed by us in the right time, chosen by us.
     * It will loop through the registered extensions, filter them and finally execute them after the
     * observable or promise you passed.
     *
     * @param event - Pass your Extensibility event which contains the extension point type which will be
     * executed plus payload and return value.
     */
    startEvent<P, R>(event: ExtensibilityEvent<P, R>): R | Observable<R> | Promise<R> {
        // Execute on start
        const toBeExecutedAfter: ComponentCreatedData[] = [];

        try {
            // Split extensions to execute after lists
            for (let i = 0; i < this.extensionComponentRegistry.length; i++) {
                const extensionResources = this.extensionComponentRegistry[i];
                if (event.extensionPointType === extensionResources.internalExtensionPointType) {
                    toBeExecutedAfter.push(extensionResources.extensionPointComponent);
                }
            }

            // Execute each extension action after the completion of the event
            if (event.returnValue instanceof Observable) {
                const patchedObservable = event.returnValue.pipe(
                    mergeMap((data: any) => {
                        toBeExecutedAfter.forEach((extPoint) => {
                            // Passing event payload to the extension and data
                            this.executeExtensionPointAction(extPoint, event.payload, data, null);
                        });
                        return of(data);
                    }),
                    catchError((e) => {
                        toBeExecutedAfter.forEach((extPoint) => {
                            // Passing error to extension component
                            this.executeExtensionPointAction(extPoint, event.payload, null, e);
                        });

                        return _throw(e);
                    })
                );
                return (event.returnValue = patchedObservable as Observable<R>);
            }

            // Execute each extension action after the completion of the event
            if (event.returnValue instanceof Promise) {
                const patchedPromise = event.returnValue
                    .then((data) => {
                        toBeExecutedAfter.forEach((extPoint) => {
                            // Passing event payload to the extension and data
                            this.executeExtensionPointAction(extPoint, event.payload, data, null);
                        });
                        return data;
                    })
                    .catch((e) => {
                        toBeExecutedAfter.forEach((extPoint) => {
                            // Passing error to extension component
                            this.executeExtensionPointAction(extPoint, event.payload, null, e);
                        });

                        return Promise.reject(e);
                    });
                return (event.returnValue = patchedPromise as Promise<R>);
            }

            toBeExecutedAfter.forEach((extPoint) => {
                // Passing event payload to the extension
                this.executeExtensionPointAction(extPoint, event.payload, event.returnValue, null);
            });
            return event.returnValue;
        } catch (e) {
            console.error(
                `[Extension Point Action Orchestrator] Unable to trigger event for: ${event.extensionPointType}`
            );
            return event.returnValue;
        }
    }

    addExtensionPointCompnent(
        extensionPointComponent: ComponentCreatedData,
        internalExtensionPointType: INTERENAL_EXTENSION_POINT_TYPE
    ): void {
        try {
            const foundIndex = this.extensionComponentRegistry.findIndex((extPoint) => {
                return (
                    extPoint.extensionPointComponent.registration.manifest.urn ===
                    extensionPointComponent.registration.manifest.urn
                );
            });

            if (foundIndex === -1) {
                this.extensionComponentRegistry.push({
                    internalExtensionPointType,
                    extensionPointComponent,
                });
            } else {
                this.extensionComponentRegistry.splice(foundIndex, 1, {
                    internalExtensionPointType,
                    extensionPointComponent,
                });
            }
        } catch (e) {
            console.error(`[Extension Point Action Orchestrator]
            Unable to add Extension Point component and metadata.`);
        }
    }

    private executeExtensionPointAction(
        extensionComponentData: ComponentCreatedData,
        payload: any,
        response: any,
        error: any
    ): void {
        new Promise<void>((resolve, reject) => {
            const timeout = setTimeout(() => {
                try {
                    (extensionComponentData.component.instance as WizardExtensionComponent<
                        any,
                        any,
                        any
                    >).performAction(
                        JsonObjectHelper.clone(payload),
                        JsonObjectHelper.clone(response),
                        error ? new Error(error.message) : null
                    );
                    resolve();
                    clearTimeout(timeout);
                } catch (e) {
                    reject(`[Extension Point Action Orchestrator]
                    The Extension Point Action of ${extensionComponentData?.registration?.manifest?.urn} failed.`);
                    clearTimeout(timeout);
                }
            }, 0);
        }).catch((e) => {
            console.error(e);
        });
    }

    private addEventToExecutionStack(event: CustomEvent<ExtensibilityEvent<unknown, unknown>>): void {
        if (!event.detail.extensionPointType) {
            return;
        }

        if (!event.detail.payload || !event.detail.returnValue) {
            this.eventsQueue.push(event);
        } else {
            this.startEvent(event.detail);
        }
    }
}
