/*
 * Copyright 2017-2023 VMware, Inc. All rights reserved. VMware Confidential
 */

import { mergeMap, catchError, combineAll, flatMap, map, tap, concatMap, filter } from "rxjs/operators";
import { Injectable } from "@angular/core";
import { concat, merge, Observable, of } from "rxjs";
import { ExtensionSpec } from "../../extensibility.model";
import { ExternalExtensionLoaderState } from "../extension-loader.model";
import { BootstrapExtensionPointsStep } from "./bootstrap-extension-points.step";
import { CompileExternalExtensionModuleStep } from "./compile-external-extension-module.step";
import { CreateExternalExtensionRouteStep } from "./create-external-extension-route.step";
import { InstantiateExternalExtensionModuleStep } from "./instantiate-external-extension-module.step";
import { LoadExternalExtensionBundleStep } from "./load-external-extension-bundle.step";
import { LoadExternalExtensionI18nStep } from "./load-external-extension-i18n.step";
import { LoadExternalExtensionManifestStep } from "./load-external-extension-manifest.step";
import { RegisterExternalExtensionRoutesStep } from "./register-external-extension-routes.step";
import { RegisterExternalExtensionTranslationsStep } from "./register-external-extension-translations.step";
import { ExternalExtensionLoaderErrorObservable } from "./external-extension-loader-error";
import { ANGULAR_APP } from "../../angular-apps/angular-apps.extension-point";

export const PLUGIN_IN_PARALLEL = 5;

/**
 * Service that deals with loading external (third-party) extensions.
 * Follows a step-based system so that each phase is a unit that can be tested for correctness.
 */
@Injectable()
export class ExternalExtensionLoader {
    constructor(
        private loadExternalExtensionManifestStep: LoadExternalExtensionManifestStep,
        private loadExternalExtensionI18nStep: LoadExternalExtensionI18nStep,
        private createExternalExtensionRouteStep: CreateExternalExtensionRouteStep,
        private loadExternalExtensionBundleStep: LoadExternalExtensionBundleStep,
        private registerExternalExtensionTranslationsStep: RegisterExternalExtensionTranslationsStep,
        private registerExternalExtensionRoutesStep: RegisterExternalExtensionRoutesStep,
        private bootstrapExtensionPointsStep: BootstrapExtensionPointsStep,
    ) {}

    /**
     * Load a third party extension.
     */
    public loadExternalExtension(specs: ExtensionSpec[]): Observable<any> {
        const activations = specs.map((spec) => {
            console.log(`[${spec.id}] Activating third party extension`);

            return of(spec).pipe(
                tap(() => console.log(`[${spec?.id}] Loading`)),
                map(this.createState.bind(this)),
                map((state: ExternalExtensionLoaderState) => this.loadExternalExtensionManifestStep.perform(state)),
                mergeMap((e) => this.handleError(e))
            );
        });

        return merge(activations, PLUGIN_IN_PARALLEL).pipe(
            combineAll(),
            flatMap((specs: ExternalExtensionLoaderState[]) => {
                const seeders: Observable<ExternalExtensionLoaderState>[] = [];
                const regularPlugins: Observable<ExternalExtensionLoaderState>[] = [];
                const angularApps: Observable<ExternalExtensionLoaderState>[] = [];

                specs.forEach((spec: ExternalExtensionLoaderState) => {
                    // Is it angular app
                    if (this.isItAngularApp(spec)) {
                        angularApps.push(this.loadAngularApp(spec));
                        return;
                    }

                    regularPlugins.push(of(spec));
                });

                return concat(
                    // Setup plugin dependencies
                    merge(seeders, PLUGIN_IN_PARALLEL).pipe(mergeMap((seeder) => seeder)),
                    // Setup plugins
                    merge(regularPlugins, PLUGIN_IN_PARALLEL).pipe(mergeMap((regular) => regular)),
                    // Setup Angular Apps
                    concat(angularApps).pipe(concatMap((angularApp) => angularApp))
                )
                    .pipe(
                        // Ignore Angular Apps
                        filter((state: ExternalExtensionLoaderState) => {
                            return !this.isItAngularApp(state);
                        }),
                        map((state: ExternalExtensionLoaderState) => this.loadExternalExtensionI18nStep.perform(state)),
                        mergeMap((e) => this.handleError(e)),
                        map((state: ExternalExtensionLoaderState) => this.createExternalExtensionRouteStep.perform(state)),
                        mergeMap((e) => this.handleError(e)),
                        tap((state: ExternalExtensionLoaderState) =>
                            console.log(`[${state?.spec?.id}] Successfully loaded`)
                        )
                    )
                    .pipe(
                        tap((state: ExternalExtensionLoaderState) => console.log(`[${state?.spec?.id}] Activating`)),
                        map((state: ExternalExtensionLoaderState) =>
                            this.registerExternalExtensionTranslationsStep.perform(state)
                        ),
                        mergeMap((e) => this.handleError(e)),
                        map((state: ExternalExtensionLoaderState) =>
                            this.registerExternalExtensionRoutesStep.perform(state)
                        ),
                        mergeMap((e) => this.handleError(e)),
                        map((state: ExternalExtensionLoaderState) => this.bootstrapExtensionPointsStep.perform(state)),
                        mergeMap((e) => this.handleError(e)),
                        tap((state: ExternalExtensionLoaderState) =>
                            console.log(`[${state?.spec?.id}] Successfully activated`)
                        )
                    )
                    .pipe(
                        catchError((err) => {
                            console.error(`Not successfully activated`, err);
                            return of(null);
                        })
                    );
            }),
            catchError((err) => {
                console.error(`UI Plugins Not successfully activated`, err);
                return of(null);
            })
        );
    }

    /**
     * Prepare the state that is passed through each step.
     */
    private createState(spec: ExtensionSpec): ExternalExtensionLoaderState {
        return {
            spec,
            manifest: null,
            angularModuleFactories: null,
            bundle: null,
            angularModules: null,
            translations: null,
            routes: null,
        };
    }

    private handleError(newState: Observable<ExternalExtensionLoaderState>): Observable<ExternalExtensionLoaderState> {
        if (newState instanceof ExternalExtensionLoaderErrorObservable) {
            return of(null);
        }

        return newState;
    }

    private isItAngularApp(state: ExternalExtensionLoaderState): boolean {
        if (
            !state ||
            !state.manifest ||
            !state.manifest.extensionPoints ||
            state.manifest.extensionPoints.length === 0
        ) {
            return false;
        }

        return state.manifest.extensionPoints[0].type.includes(ANGULAR_APP);
    }

    private loadAngularApp(state: ExternalExtensionLoaderState): Observable<ExternalExtensionLoaderState> {
        return of(state).pipe(
            // Load bundle js
            map((state: ExternalExtensionLoaderState) => this.loadExternalExtensionBundleStep.perform(state)),
            mergeMap((e) => this.handleError(e)),
            // Register Angular Application
            map((state: ExternalExtensionLoaderState) => this.bootstrapExtensionPointsStep.perform(state)),
            mergeMap((e) => this.handleError(e))
        );
    }
}
