// @ts-strict-ignore
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, model, OnDestroy, OnInit, viewChild, } from "@angular/core";
import { ActivatedRoute, NavigationEnd, NavigationExtras, Router, RouterModule } from "@angular/router";
import { IonPopover, ModalController } from "@ionic/angular";
import { TranslateService } from "@ngx-translate/core";
import { NgxSpinnerComponent } from "ngx-spinner";
import { Subject } from "rxjs";
import { filter, switchMap, takeUntil } from "rxjs/operators";
import { ChosenFilter, Filter, FilterComponent, FilterOption } from "src/app/index/filter/filter.component";
import { NavigationService } from "src/app/shared/components/navigation/service/navigation.service";
import { NavigationTree } from "src/app/shared/components/navigation/shared";
import { DomChangeDirective } from "src/app/shared/directive/oe-dom-change";
import { ComponentJsonApiRequest } from "src/app/shared/jsonrpc/request/componentJsonApiRequest";
import { PipeComponentsModule } from "src/app/shared/pipe/pipe.module";
import { Role } from "src/app/shared/type/role";
import { StringUtils } from "src/app/shared/utils/string/string.utils";
import { Environment, environment } from "src/environments";
import { CommonUiModule } from "../../../shared/common-ui.module";
import { Edge, Service, Websocket } from "../../../shared/shared";
import { ExecuteSystemUpdate } from "../system/executeSystemUpdate";
import { InstallAppComponent } from "./install.component";
import { Flags } from "./jsonrpc/flag/flags";
import { GetApps } from "./jsonrpc/getApps";
import { App } from "./keypopup/app";
import { AppCenter } from "./keypopup/appCenter";
import { AppCenterGetPossibleApps } from "./keypopup/appCenterGetPossibleApps";
import { AppCenterGetRegisteredKeys } from "./keypopup/appCenterGetRegisteredKeys";
import { Key } from "./keypopup/key";
import { KeyModalComponent, KeyValidationBehaviour } from "./keypopup/modal.component";
import { canEnterKey } from "./permissions";

@Component({
    selector: IndexComponent.SELECTOR,
    templateUrl: "./index.component.html",
    styleUrls: ["./index.component.scss"],
    standalone: true,
    imports: [
        CommonUiModule,
        PipeComponentsModule,
        NgxSpinnerComponent,
        RouterModule,
        DomChangeDirective,
        FilterComponent,
    ],
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IndexComponent implements OnInit, OnDestroy {
    private static readonly SELECTOR = "app-index";
    /** E. g. if more than 4 apps are in a list the apps are displayed in their categories */
    private static readonly MAX_APPS_IN_LIST: number = 4;
    public readonly spinnerId: string = IndexComponent.SELECTOR;

    public apps: GetApps.App[] = [];

    public installedApps: AppList = {
        name: "EDGE.CONFIG.APP.INSTALLED",
        appCategories: [],
        shouldBeShown: () => this.key === null, // only show installed apps when the user is not currently selecting an app from a key
    };
    public availableApps: AppList = {
        name: "EDGE.CONFIG.APP.AVAILABLE",
        appCategories: [],
        shouldBeShown: () => true, // always show available apps
    };
    public incompatibleApps: AppList = {
        name: "EDGE.CONFIG.APP.INCOMPATIBLE",
        appCategories: [],
        shouldBeShown: () => this.edge.roleIsAtLeast(Role.ADMIN), // only show incompatible apps for admins
    };

    public appLists: AppList[] = [this.installedApps, this.availableApps, this.incompatibleApps];
    public categories: { val: GetApps.Category; isChecked: boolean }[] = [];

    protected readonly environment: Environment = environment;
    protected edge: Edge | null = null;
    protected filters: Filter[] = [];
    protected key: Key | null = null;
    protected selectedBundle: number | null = null;
    protected isUpdateAvailable: boolean = false;
    protected canEnterKey: boolean = false;
    protected numberOfUnusedRegisteredKeys: number = 0;
    protected showPopover = model<boolean>(false);
    protected searchParams: Map<string, ChosenFilter["value"]> = new Map();
    protected hasKeyPopover = viewChild<IonPopover>("hasKeyPopover");

    private useMasterKey: boolean = false;
    private hasSeenPopover: boolean = false;
    private stopOnDestroy: Subject<void> = new Subject<void>();
    private inputValue: string;

    private readonly navigationService: NavigationService = inject(NavigationService);
    private readonly cdRef: ChangeDetectorRef = inject(ChangeDetectorRef);

    public constructor(
        private route: ActivatedRoute,
        private service: Service,
        private websocket: Websocket,
        private translate: TranslateService,
        private router: Router,
        private modalController: ModalController,
    ) {}

    public ngOnInit() {
        this.init();
        this.router.events
            .pipe(
                filter((event) => event instanceof NavigationEnd),
                switchMap(() => this.route.url),
                takeUntil(this.stopOnDestroy),
            )
            .subscribe(() => {
                const navigationExtras = this.router.currentNavigation()?.extras as NavigationExtras;
                const appInstanceChange = navigationExtras?.state?.appInstanceChange;
                if (appInstanceChange != null && appInstanceChange) {
                    this.init();
                }
            });
    }

    public ngOnDestroy(): void {
        this.stopOnDestroy.next();
        this.stopOnDestroy.complete();
    }

    public searchOnChange(event: InstanceType<typeof FilterComponent>["searchParams"]): void {
        this.searchParams = event;
        this.updateSelection();
    }
    protected onValueChanged(event: any) {
        this.inputValue = event.detail.value;
        this.updateSelection();
    }

    protected onDomChange(mutation: MutationRecord, appId: string) {
        mutation.addedNodes.forEach((node) => {
            if (!(node instanceof HTMLElement && node.classList.contains("open-modal-thirdpartyaccess"))) {
                return;
            }
        });
    }

    /**
     * Updates the selected categories.
     *
     * @param event The event of a click on a 'ion-fab-list' to stop it from closing
     */
    protected updateSelection(event?: PointerEvent) {
        this.installedApps.appCategories = [];
        this.availableApps.appCategories = [];
        this.incompatibleApps.appCategories = [];

        const sortedApps = [];
        this.apps.forEach((app) => {
            app.categorys.forEach((category) => {
                if (this.selectedBundle >= 0 && this.key) {
                    if (!this.key.bundles[this.selectedBundle].some((a) => app.appId === a.appId)) {
                        return false;
                    }
                } else {
                    if (
                        Flags.getByType(app.flags, Flags.SHOW_AFTER_KEY_REDEEM) &&
                        environment.production &&
                        app.instanceIds.length === 0
                    ) {
                        return false;
                    }
                }
                const cat = this.categories.find((c) => c.val.name === category.name);
                if (cat?.isChecked == false) {
                    return false;
                }
                if (
                    this.inputValue &&
                    !(
                        app.name.toLowerCase().includes(this.inputValue.toLowerCase()) ||
                        category.readableName.toLowerCase().includes(this.inputValue.toLowerCase())
                    )
                ) {
                    return false;
                }
                const selectedCategories =
                    this.searchParams.get("categories") ?? this.categoryFilter().options.map((el) => el.option.value);
                if (selectedCategories != null && selectedCategories != undefined) {
                    if (Array.isArray(selectedCategories)) {
                        if (!selectedCategories.includes(category.name)) {
                            return false;
                        }
                    }
                }
                sortedApps.push(app);
                return true;
            });
        });

        sortedApps.forEach((a) => {
            if (a.instanceIds.length > 0) {
                this.pushIntoCategory(a, this.installedApps);
                if (a.cardinality === "MULTIPLE" && a.status.name !== AppStatusName.INCOMPATIBLE) {
                    this.pushIntoCategory(a, this.availableApps);
                }
            } else {
                if (a.status.name === AppStatusName.INCOMPATIBLE) {
                    this.pushIntoCategory(a, this.incompatibleApps);
                } else {
                    this.pushIntoCategory(a, this.availableApps);
                }
            }
        });

        this.appLists = [];
        const selectedStatuses =
            this.searchParams.get("appStatus") ?? this.statusFilter().options.map((el) => el.option.value);
        if (Array.isArray(selectedStatuses)) {
            if (StringUtils.isInArr(AppStatusName.INSTALLABLE, selectedStatuses)) {
                this.appLists.push(this.installedApps);
            }
            if (StringUtils.isInArr(AppStatusName.COMPATIBLE, selectedStatuses)) {
                this.appLists.push(this.availableApps);
            }
            if (StringUtils.isInArr(AppStatusName.INCOMPATIBLE, selectedStatuses)) {
                this.appLists.push(this.incompatibleApps);
            }
        } else {
            this.appLists = [this.installedApps, this.availableApps, this.incompatibleApps];
        }
    }

    protected showCategories(app: AppList): boolean {
        return this.sum(app) > IndexComponent.MAX_APPS_IN_LIST;
    }

    protected isEmpty(app: AppList): boolean {
        return this.sum(app) === 0;
    }

    /** Opens a popup to select a key. */
    protected async redeemKey(): Promise<void> {
        const modal = await this.modalController.create({
            component: KeyModalComponent,
            componentProps: {
                edge: this.edge,
                behaviour: KeyValidationBehaviour.SELECT,
                knownApps: this.apps,
                route: this.route,
            },
            cssClass: "auto-height",
        });
        modal.onDidDismiss().then((data) => {
            if (!data.data) {
                this.key = null;
                this.useMasterKey = false;
                this.updateSelection();
                return; // no key selected
            }
            if (data.data?.useMasterKey) {
                this.selectedBundle = 0;
                // set dummy key for available apps to install
                this.key = {
                    keyId: null,
                    bundles: [
                        this.apps
                            .filter((e) => !Flags.getByType(e.flags, Flags.SHOW_AFTER_KEY_REDEEM))
                            .map<App>((d) => {
                                return { id: 0, appId: d.appId };
                            }),
                    ],
                };
                this.useMasterKey = true;
                this.updateSelection();
                return;
            }
            this.useMasterKey = false;
            this.key = data.data.key;
            if (!this.key.bundles) {
                // load bundles
                this.edge
                    .sendRequest(
                        this.websocket,
                        new AppCenter.Request({
                            payload: new AppCenterGetPossibleApps.Request({
                                key: this.key.keyId,
                            }),
                        }),
                    )
                    .then((response) => {
                        const result = (response as AppCenterGetPossibleApps.Response).result;
                        this.key.bundles = result.bundles;
                        this.selectedBundle = 0;
                        this.updateSelection();
                    });
            } else {
                this.selectedBundle = 0;
                this.updateSelection();
            }
        });
        return await modal.present();
    }

    protected onAppClicked(app: GetApps.App): void {
        // navigate
        this.navigationService.setChildToCurrentNavigation(
            new NavigationTree(
                "single",
                { baseString: "single", queryParams: { appId: app.appId } },
                { name: "add-outline" },
                "AppCenter",
                "label",
                [],
                null,
            ),
        );

        if (this.key != null || this.useMasterKey) {
            this.router.navigate(["single"], {
                queryParams: { name: app.name, appId: app.appId },
                state: {
                    app: app,
                    appKey: this.key.keyId,
                    useMasterKey: this.useMasterKey,
                },
                relativeTo: this.route,
            });
        } else {
            this.router.navigate(["single"], {
                queryParams: { name: app.name, appId: app.appId },
                state: app,
                relativeTo: this.route,
            });
        }
        // reset keys
        this.key = null;
        this.useMasterKey = false;
    }

    /** Opens a popup to register a key. */
    protected async registerKey(): Promise<void> {
        const modal = await this.modalController.create({
            component: KeyModalComponent,
            componentProps: {
                edge: this.edge,
                behaviour: KeyValidationBehaviour.REGISTER,
                route: this.route,
            },
            cssClass: "auto-height",
        });

        return await modal.present();
    }

    private updateHasUnusedKeysPopover() {
        if (this.hasSeenPopover) {
            return;
        }
        if (!this.canEnterKey) {
            return;
        }
        if (this.numberOfUnusedRegisteredKeys === 0) {
            return;
        }

        this.hasSeenPopover = true;
        const popover = this.hasKeyPopover();

        if (popover == null) {
            return;
        }

        popover.event = {
            type: "willPresent",
            target: document.querySelector("#redeemKeyCard"),
        };
        this.showPopover.set(true);
    }

    private pushIntoCategory(app: GetApps.App, list: AppList): void {
        app.categorys.forEach((category) => {
            let catList = list.appCategories.find((l) => l.category.name === category.name);
            if (catList === undefined) {
                catList = { category: category, apps: [] };
                list.appCategories.push(catList);
            }
            catList.apps.push(app);
        });
    }

    private statusFilter(): Filter {
        const filterOptions: FilterOption<string>[] = [
            {
                name: this.translate.instant("EDGE.CONFIG.APP.STATUS_INSTALLED"),
                option: { value: AppStatusName.INSTALLABLE, default: true },
            },
            {
                name: this.translate.instant("EDGE.CONFIG.APP.STATUS_AVAILABLE"),
                option: { value: AppStatusName.COMPATIBLE, default: true },
            },
            ...(this.edge.roleIsAtLeast(Role.ADMIN)
                ? [
                      {
                          name: this.translate.instant("EDGE.CONFIG.APP.STATUS_INCOMPATIBLE"),
                          option: { value: AppStatusName.INCOMPATIBLE },
                      },
                  ]
                : []),
        ];
        return {
            placeholder: this.translate.instant("EDGE.CONFIG.APP.STATUS_FILTER"),
            category: "appStatus",
            multiple: true,
            options: filterOptions,
        };
    }
    private categoryFilter(): Filter {
        return {
            placeholder: this.translate.instant("EDGE.CONFIG.APP.CATEGORY_FILTER"),
            category: "categories",
            multiple: true,
            options: this.categories.map((t) => ({
                name: t.val.readableName,
                option: { value: t.val.name },
            })),
        };
    }

    private sum(app: AppList): number {
        return app.appCategories.reduce((p, c) => p + c.apps.length, 0);
    }

    private init() {
        this.service.startSpinner(this.spinnerId);
        this.key = null;
        this.selectedBundle = null;

        this.appLists.forEach((element) => {
            element.appCategories = [];
        });

        this.service
            .setCurrentComponent(
                {
                    languageKey: "EDGE.CONFIG.APP.NAME_WITH_EDGE_NAME",
                    interpolateParams: {
                        edgeShortName: environment.edgeShortName,
                    },
                },
                this.route,
            )
            .then((edge) => {
                this.edge = edge;

                this.service.metadata.pipe(takeUntil(this.stopOnDestroy)).subscribe((entry) => {
                    this.canEnterKey = canEnterKey(edge, entry.user);
                    this.updateHasUnusedKeysPopover();
                });
                edge.sendRequest(
                    this.websocket,
                    new ComponentJsonApiRequest({
                        componentId: "_appManager",
                        payload: new GetApps.Request(),
                    }),
                )
                    .then((response) => {
                        this.service.stopSpinner(this.spinnerId);

                        this.apps = (response as GetApps.Response).result.apps.map((app) => {
                            app.imageUrl = environment.links.APP_CENTER.APP_IMAGE(
                                this.translate.getCurrentLang(),
                                app.appId,
                            );
                            return app;
                        });

                        // init categories
                        const statusFilter = this.statusFilter();
                        this.apps.forEach((a) => {
                            a.categorys.forEach((category) => {
                                const isCategoryAlreadyExisting =
                                    this.categories.filter((c) => c.val.name === category.name)?.length > 0;
                                const isCategoryAllowed = StringUtils.isInArr(
                                    a.status.name,
                                    statusFilter.options.map((el) => el.option.value),
                                );
                                if (!isCategoryAlreadyExisting && isCategoryAllowed) {
                                    this.categories.push({
                                        val: category,
                                        isChecked: true,
                                    });
                                }
                            });
                        });

                        this.filters = [this.categoryFilter(), this.statusFilter()];
                        this.updateSelection();

                        edge.sendRequest(
                            this.websocket,
                            new AppCenter.Request({
                                payload: new AppCenterGetRegisteredKeys.Request({}),
                            }),
                        )
                            .then((response) => {
                                const result = (response as AppCenterGetRegisteredKeys.Response).result;
                                this.numberOfUnusedRegisteredKeys = result.keys.length;
                                this.updateHasUnusedKeysPopover();
                            })
                            .catch(this.service.handleError);
                    })
                    .catch(
                        InstallAppComponent.errorToast(
                            this.service,
                            (error) => "Error while receiving available apps: " + error,
                        ),
                    )
                    .finally(() => {
                        this.cdRef.markForCheck();
                    });

                const systemUpdate = new ExecuteSystemUpdate(edge, this.websocket);
                systemUpdate.systemUpdateStateChange = (updateState) => {
                    if (updateState.available) {
                        this.isUpdateAvailable = true;
                    }
                };
                systemUpdate.start();
            });
    }
}

interface AppList {
    name: string;
    appCategories: AppListByCategorie[];
    shouldBeShown: () => boolean;
}

interface AppListByCategorie {
    category: GetApps.Category;
    apps: GetApps.App[];
}

enum AppStatusName {
    INSTALLABLE = "INSTALLABLE",
    COMPATIBLE = "COMPATIBLE",
    INCOMPATIBLE = "INCOMPATIBLE",
}
