// @ts-strict-ignore
import { registerLocaleData } from "@angular/common";
import { effect, inject, Injectable, Injector, runInInjectionContext, signal, untracked, WritableSignal, } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { ToastController } from "@ionic/angular";
import { LangChangeEvent, TranslateService } from "@ngx-translate/core";
import { NgxSpinnerService, Spinner } from "ngx-spinner";
import { BehaviorSubject, Subject } from "rxjs";
import { take } from "rxjs/operators";
import { PlatFormService } from "src/app/platform.service";
import { environment } from "src/environments";
import { ChartConstants } from "../components/chart/chart.constants";
import { Edge } from "../components/edge/edge";
import { EdgeConfig } from "../components/edge/edgeconfig";
import { JsonrpcResponseError } from "../jsonrpc/base";
import { GetEdgeRequest } from "../jsonrpc/request/getEdgeRequest";
import { GetEdgesRequest } from "../jsonrpc/request/getEdgesRequest";
import { QueryHistoricTimeseriesEnergyRequest } from "../jsonrpc/request/queryHistoricTimeseriesEnergyRequest";
import { GetEdgeResponse } from "../jsonrpc/response/getEdgeResponse";
import { GetEdgesResponse } from "../jsonrpc/response/getEdgesResponse";
import { QueryHistoricTimeseriesEnergyResponse } from "../jsonrpc/response/queryHistoricTimeseriesEnergyResponse";
import { User } from "../jsonrpc/shared";
import { States } from "../ngrx-store/states";
import { ChannelAddress } from "../shared";
import { DefaultTypes } from "../type/defaulttypes";
import { Language } from "../type/language";
import { Role } from "../type/role";
import { DateUtils } from "../utils/date/dateutils";
import { AbstractService } from "./abstractservice";
import { RouteService } from "./route.service";
import { Websocket } from "./websocket";

@Injectable()
export class Service extends AbstractService {
    public static readonly TIMEOUT = 15_000;

    public notificationEvent: Subject<DefaultTypes.Notification> =
        new Subject<DefaultTypes.Notification>();

    /** Currently selected history period */
    public historyPeriod: BehaviorSubject<DefaultTypes.HistoryPeriod>;

    /**
     * Currently selected history period string
     *
     * Initialized as day, is getting changed by pickdate component
     */
    public periodString: DefaultTypes.PeriodString =
        DefaultTypes.PeriodString.DAY;

    /**
     * Represents the resolution of used device Checks if smartphone resolution
     * is used
     */
    public deviceHeight: number = 0;
    public deviceWidth: number = 0;
    public activeQueryData: string;

    /** Holds the currenty selected Page Title. */
    public currentPageTitle: string;

    /** Holds reference to Websocket. This is set by Websocket in constructor. */
    public websocket: Websocket = null;
    /** Holds the currently selected Edge. */
    public readonly currentEdge: WritableSignal<Edge> = signal(null);

    /** Holds references of Edge-IDs (=key) to Edge objects (=value) */
    public readonly metadata: BehaviorSubject<{
        user: User;
        edges: { [edgeId: string]: Edge };
    }> = new BehaviorSubject(null);

    /** Holds the current Activated Route */
    private currentActivatedRoute: ActivatedRoute | null = null;

    private queryEnergyQueue: {
        fromDate: Date;
        toDate: Date;
        channels: ChannelAddress[];
        promises: { resolve; reject }[];
    }[] = [];
    private queryEnergyTimeout: any = null;
    private injector = inject(Injector);

    /** @deprecated */
    private isSmartphoneResolution = signal<boolean>(
        this.computeIsSmartphoneResolution(),
    );

    constructor(
        private router: Router,
        public spinnerService: NgxSpinnerService,
        private toaster: ToastController,
        public translate: TranslateService,
        private _injector: Injector,
        private routeService: RouteService,
    ) {
        super();
        // add language
        translate.addLangs(Language.ALL.map((l) => l.key));
        // this language will be used as a fallback when a translation isn't found in the current language
        translate.setFallbackLang(Language.DEFAULT.key);
        // translate.use(Language.DEFAULT.key);

        // initialize history period
        this.historyPeriod = new BehaviorSubject(
            new DefaultTypes.HistoryPeriod(new Date(), new Date()),
        );

        // React on Language Change and update language
        translate.onLangChange.subscribe((event: LangChangeEvent) => {
            registerLocaleData(
                Language.getLocale(
                    Language.getByKey(event.lang)?.key ?? Language.DEFAULT.key,
                ),
            );
        });
    }

    public getIsSmartphoneResolution() {
        return this.isSmartphoneResolution();
    }

    public setLang(language: Language) {
        if (language !== null) {
            registerLocaleData(Language.getLocale(language.key));

            /** Reload global translation files */
            this.translate.setTranslation(language.key, language.json, true);
            this.translate.use(language.key);
        } else {
            this.translate.use(Language.DEFAULT.key);
        }
        // TODO set locale for date-fns: https://date-fns.org/docs/I18n
    }

    public getDocsLang(): string {
        if (this.translate.getCurrentLang() == "de") {
            return "de";
        } else {
            return "en";
        }
    }

    public notify(notification: DefaultTypes.Notification) {
        this.notificationEvent.next(notification);
    }

    // https://v16.angular.io/api/core/ErrorHandler#errorhandler

    public override handleError(error: any) {
        console.error(error);
        // TODO: show notification
        // let notification: Notification = {
        //     type: "error",
        //     message: error
        // };
        // this.notify(notification);
    }

    public setCurrentComponent(
        currentPageTitle:
            | string
            | { languageKey: string; interpolateParams?: {} },
        activatedRoute: ActivatedRoute,
    ): Promise<Edge> {
        return new Promise((resolve, reject) => {
            // Set the currentPageTitle only once per ActivatedRoute
            if (this.currentActivatedRoute != activatedRoute) {
                if (typeof currentPageTitle === "string") {
                    // Use given page title directly
                    if (
                        currentPageTitle == null ||
                        currentPageTitle.trim() === ""
                    ) {
                        this.currentPageTitle = environment.uiTitle;
                    } else {
                        this.currentPageTitle = currentPageTitle;
                    }
                } else {
                    // Translate from key
                    this.translate
                        .get(
                            currentPageTitle.languageKey,
                            currentPageTitle.interpolateParams,
                        )
                        .pipe(take(1))
                        .subscribe((title) => (this.currentPageTitle = title));
                }
            }
            this.currentActivatedRoute = activatedRoute;

            this.getCurrentEdge()
                .then((edge) => {
                    resolve(edge);
                })
                .catch(reject);
        });
    }

    public getCurrentEdge(): Promise<Edge> {
        return new Promise<Edge>((resolve) => {
            let isResolved = false; // Flag to ensure the Promise resolves only once

            // Use runInInjectionContext to provide the injector context
            const dispose = runInInjectionContext(this.injector, () => {
                return untracked(() => {
                    return effect(() => {
                        const edge = this.currentEdge();
                        if (edge != null && !isResolved) {
                            isResolved = true; // Mark as resolved
                            resolve(edge); // Resolve the Promise with the non-null value
                            dispose.destroy();
                        }
                    });
                });
            });
        });
    }

    public getConfig(): Promise<EdgeConfig> {
        return new Promise<EdgeConfig>((resolve, reject) => {
            this.getCurrentEdge()
                .then((edge) => {
                    edge.getFirstValidConfig(this.websocket)
                        .then(resolve)
                        .catch(reject);
                })
                .catch((reason) => reject(reason));
        });
    }

    public getNextConfig(): Promise<EdgeConfig> {
        return new Promise<EdgeConfig>((resolve, reject) => {
            this.getCurrentEdge()
                .then((edge) => {
                    edge.getFirstValidConfig(this.websocket)
                        .then(resolve)
                        .catch(reject);
                })
                .catch((reason) => reject(reason));
        });
    }

    public onLogout() {
        this.currentEdge.set(null);

        this.metadata.next(null);
        this.websocket.state.set(States.NOT_AUTHENTICATED);
        this.router.navigate(["/login"]);
    }

    public getChannelAddresses(
        edge: Edge,
        channels: ChannelAddress[],
    ): Promise<ChannelAddress[]> {
        return new Promise((resolve) => {
            resolve(channels);
        });
    }

    public queryEnergy(
        fromDate: Date,
        toDate: Date,
        channels: ChannelAddress[],
    ): Promise<QueryHistoricTimeseriesEnergyResponse> {
        // keep only the date, without time
        fromDate.setHours(0, 0, 0, 0);
        toDate.setHours(0, 0, 0, 0);
        const promise = { resolve: null, reject: null };
        const response = new Promise<QueryHistoricTimeseriesEnergyResponse>(
            (resolve, reject) => {
                promise.resolve = resolve;
                promise.reject = reject;
            },
        );
        this.queryEnergyQueue.push({
            fromDate: fromDate,
            toDate: toDate,
            channels: channels,
            promises: [promise],
        });

        if (this.queryEnergyTimeout == null) {
            this.queryEnergyTimeout = setTimeout(() => {
                this.queryEnergyTimeout = null;

                const mergedRequests: {
                    fromDate: Date;
                    toDate: Date;
                    channels: ChannelAddress[];
                    promises: { resolve; reject }[];
                }[] = [];

                let request;
                while ((request = this.queryEnergyQueue.pop())) {
                    if (mergedRequests.length === 0) {
                        mergedRequests.push(request);
                    } else {
                        let merged = false;
                        for (const mergedRequest of mergedRequests) {
                            if (
                                mergedRequest.fromDate.valueOf() ===
                                    request.fromDate.valueOf() &&
                                mergedRequest.toDate.valueOf() ===
                                    request.toDate.valueOf()
                            ) {
                                // same date -> merge
                                mergedRequest.promises =
                                    mergedRequest.promises.concat(
                                        request.promises,
                                    );
                                for (const newChannel of request.channels) {
                                    if (
                                        !mergedRequest.channels.some(
                                            (existingChannel) =>
                                                existingChannel.channelId ===
                                                    newChannel.channelId &&
                                                existingChannel.componentId ===
                                                    newChannel.componentId,
                                        )
                                    ) {
                                        mergedRequest.channels.push(newChannel);
                                    }
                                }
                                merged = true;
                            }
                        }
                        if (!merged) {
                            mergedRequests.push(request);
                        }
                    }
                }

                // send merged requests
                this.getCurrentEdge().then((edge) => {
                    for (const source of mergedRequests) {
                        // Jump to next request for empty channelAddresses
                        if (!source?.channels?.length) {
                            continue;
                        }

                        const request =
                            new QueryHistoricTimeseriesEnergyRequest(
                                DateUtils.maxDate(
                                    source.fromDate,
                                    edge?.firstSetupProtocol,
                                ),
                                source.toDate,
                                source.channels,
                            );

                        this.activeQueryData = request.id;
                        edge.sendRequest(this.websocket, request)
                            .then((response) => {
                                if (this.activeQueryData !== response.id) {
                                    return;
                                }

                                const result = (
                                    response as QueryHistoricTimeseriesEnergyResponse
                                ).result;

                                if (Object.keys(result.data).length === 0) {
                                    for (const promise of source.promises) {
                                        promise.reject(
                                            new JsonrpcResponseError(
                                                response.id,
                                                {
                                                    code: 0,
                                                    message: "Result was empty",
                                                },
                                            ),
                                        );
                                    }
                                    return;
                                }

                                for (const promise of source.promises) {
                                    promise.resolve(
                                        response as QueryHistoricTimeseriesEnergyResponse,
                                    );
                                }
                            })
                            .catch(async (reason) => {
                                for (const promise of source.promises) {
                                    promise.reject(
                                        new JsonrpcResponseError(
                                            (await response).id,
                                            {
                                                code: 0,
                                                message: "Result was empty",
                                            },
                                        ),
                                    );
                                }
                            });
                    }
                });
            }, ChartConstants.REQUEST_TIMEOUT);
        }
        return response;
    }

    /**
     * Gets the page for the given number.
     *
     * @param req The get edges request
     * @returns A promise with the resulting edges
     */
    public getEdges(req: GetEdgesRequest): Promise<Edge[]> {
        return new Promise<Edge[]>((resolve, reject) => {
            this.websocket
                .sendRequest<GetEdgesResponse>(req)
                .then((response) => {
                    const result = (response as GetEdgesResponse).result;

                    // TODO change edges-map to array or other way around
                    const value = this.metadata.value;
                    const mappedResult = [];
                    for (const edge of result.edges) {
                        const mappedEdge = new Edge(
                            edge.id,
                            this.updateCommentForDemoUser(
                                value.user.id,
                                edge.id,
                                edge.comment,
                            ),
                            edge.producttype,
                            "version" in edge ? edge["version"] : "0.0.0",
                            Role.getRole(edge.role.toString()),
                            edge.isOnline,
                            edge.lastmessage,
                            edge.sumState,
                            DateUtils.stringToDate(
                                edge.firstSetupProtocol?.toString(),
                            ),
                            edge.settings ?? null,
                        );
                        value.edges[edge.id] = mappedEdge;
                        mappedResult.push(mappedEdge);
                    }

                    this.metadata.next(value);
                    resolve(mappedResult);
                })
                .catch((err) => {
                    reject(err);
                });
        });
    }

    /**
     * Updates the currentEdge in metadata
     *
     * @param edgeId The edgeId
     * @returns A empty Promise
     */
    public updateCurrentEdge(edgeId: string): Promise<Edge> {
        // TODO INTERSOLAR
        if (this.metadata.getValue()?.user.id === "intersolar@fenecon.de") {
            if (edgeId === "fems17289") {
                this.setLang(Language.EN);
            } else {
                this.setLang(Language.DE);
            }
        }

        return new Promise<Edge>((resolve, reject) => {
            const existingEdge = this.metadata.value?.edges[edgeId];
            if (existingEdge) {
                this.currentEdge.set(existingEdge);
                resolve(existingEdge);
                return;
            }

            this.websocket
                .sendStateFullRequest<GetEdgeResponse>(
                    new GetEdgeRequest({ edgeId: edgeId }),
                )
                .then((response) => {
                    const edgeData = (response as GetEdgeResponse).result.edge;
                    const value = this.metadata.value;
                    const currentEdge = new Edge(
                        edgeData.id,
                        this.updateCommentForDemoUser(
                            value.user.id,
                            edgeData.id,
                            edgeData.comment,
                        ),
                        edgeData.producttype,
                        "version" in edgeData ? edgeData["version"] : "0.0.0",
                        Role.getRole(edgeData.role.toString()),
                        edgeData.isOnline,
                        edgeData.lastmessage,
                        edgeData.sumState,
                        DateUtils.stringToDate(
                            edgeData.firstSetupProtocol?.toString(),
                        ),
                        edgeData.settings ?? null,
                    );
                    this.currentEdge.set(currentEdge);
                    value.edges[edgeData.id] = currentEdge;
                    this.metadata.next(value);
                    resolve(currentEdge);
                })
                .catch(reject);
        });
    }

    /**
     * Starts a spinner.
     *
     * @param selector The unique selector
     * @param spinner The spinner to show
     */
    public startSpinner(selector: string, spinner?: Spinner) {
        this.spinnerService.show(selector, {
            type: "ball-clip-rotate-multiple",
            fullScreen: false,
            bdColor: "rgba(0, 0, 0, 0.8)",
            size: "medium",
            color: "#fff",
            ...spinner,
        });
    }

    public startSpinnerTransparentBackground(selector: string) {
        this.spinnerService.show(selector, {
            type: "ball-clip-rotate-multiple",
            fullScreen: false,
            bdColor: "rgba(0, 0, 0, 0)",
            size: "medium",
            color: "var(--ion-color-primary)",
        });
    }

    public stopSpinner(selector: string) {
        this.spinnerService.hide(selector);
    }

    public async toast(
        message: string,
        level: "success" | "warning" | "danger",
        duration?: number,
    ) {
        const toast = await this.toaster.create({
            message: message,
            color: level,
            duration: duration ?? 4000,
            id: "toast-container",
        });
        toast.present();
    }

    private computeIsSmartphoneResolution() {
        const platFormService = this.injector.get(PlatFormService);
        const device = platFormService.getDevice();
        return device.isSmartphone();
    }

    // TODO INTERSOLAR remove
    private updateCommentForDemoUser(
        userId: string,
        edgeId: string,
        comment: string,
    ): string {
        if (userId !== "intersolar@fenecon.de") {
            return comment;
        }
        switch (edgeId) {
            case "fems31372":
                return "C 100 - dyn. RSE";
            case "fems888":
                return "H 10 - WB + Dyn. Tarif";
            case "fems4":
                return "H 30 - Vollausbau";
            case "fems82085":
                return "H 10 - FP-EEG 25 + Heizstab";
            case "fems666":
                return "C 50 - 2x Hardy + my-PV";
            case "fems10303":
                return "H 10 - Keba + Askoma";
            case "fems72127":
                return "H 10 - FP + 60 % Abregelung";
            case "fems17289":
                return "H 10 - Schweden + LSK";
            case "fems65456":
                return "H 30 - Direktvermarktung";
            case "fems33006":
                return "XL - Vermarktungsspeicher";
            case "fems31113":
                return "C 92 - Ladepark";
            case "fems30776":
                return "S - Fahrplan - dyn. Tarif";
            case "fems3625":
                return "M  - Multiuse";
            default:
                return comment;
        }
    }
}
