import { inject, Injectable, signal, WritableSignal } from "@angular/core";
import { Router } from "@angular/router";
import { App, URLOpenListenerEvent } from "@capacitor/app";
import { CookieService } from "ngx-cookie-service";
import { PlatFormService } from "src/app/platform.service";
import { environment } from "src/environments";
import { JsonrpcRequest } from "../../jsonrpc/base";
import { JsonRpcUtils } from "../../jsonrpc/jsonrpcutils";
import { User } from "../../jsonrpc/shared";
import { States } from "../../ngrx-store/states";
import { Language } from "../../type/language";
import { TSignalValue } from "../../type/utility";
import { StringUtils } from "../../utils/string/string.utils";
import { RouteService } from "../route.service";
import { Service } from "../service";
import { UserService } from "../user.service";
import { AuthenticateWithOAuthRequest, AuthenticateWithOAuthResponse } from "./jsonrpc";

export enum AUTHENTICATION_STATE {
    NOT_AUTHENTICATED,
    AUTHENTICATING,
    AUTHENTICATED,
}

@Injectable({ providedIn: "root" })
export class OAuthService {
    public static readonly REFRESH_TOKEN: string = "refresh_token";

    private state: WritableSignal<AUTHENTICATION_STATE> = signal(AUTHENTICATION_STATE.NOT_AUTHENTICATED);
    private service: Service = inject(Service);
    private cookieService: CookieService = inject(CookieService);
    private router: Router = inject(Router);
    private userService: UserService = inject(UserService);
    private platformService: PlatFormService = inject(PlatFormService);
    private readonly routeService: RouteService = inject(RouteService);

    constructor() {
        App.addListener("appUrlOpen", (data: URLOpenListenerEvent) => {
            const device = this.platformService.getDevice();
            const isApp = device.isApp();
            const code = OAuthService.getCode(data);
            if (isApp) {
                this.router.navigate(["/oauthcallback"], {
                    queryParams: { code: code ?? null },
                });
            }
        });
    }

    public static isOAuth(cookieService: CookieService) {
        return cookieService.check(OAuthService.REFRESH_TOKEN) && environment.backend === "OpenEMS Backend";
    }

    public static getRedirectUri(plaformService: PlatFormService) {
        const device = plaformService.getDevice();
        if (device.isApp()) {
            return {};
        }

        return { redirectUri: window.document.baseURI + "oauthcallback" };
    }

    /**
     * Gets the OEM to use.
     *
     * @returns The oem
     */
    public static getOem() {
        return { oem: environment.theme.toLowerCase() };
    }

    /**
     * Gets the code from url queryParams.
     *
     * @param data The data
     * @returns The code if available in event, else null
     */
    private static getCode(data: URLOpenListenerEvent) {
        const dataQueryParams = StringUtils.splitBy(data.url, "?");

        if (dataQueryParams == null || dataQueryParams.length < 1) {
            return null;
        }

        const searchParams = new URLSearchParams(dataQueryParams[1]);
        const code = searchParams.get("code");

        if (code == null || code.length === 0) {
            return null;
        }

        const indexOfFragment = code.indexOf("#") > 0 ? code.indexOf("#") : null;
        return code.substring(0, indexOfFragment ?? code.length);
    }

    /**
     * Executes when websocket is 'online'
     *
     * @returns
     */
    public async startOAuth(): Promise<void> {
        this.state.set(AUTHENTICATION_STATE.AUTHENTICATING);

        const tokenResponse = await this.getTokenByRefreshToken();
        if (tokenResponse == null) {
            this.state.set(AUTHENTICATION_STATE.NOT_AUTHENTICATED);
            return;
        }
        await this.completeAuthentication(tokenResponse);
        this.state.set(AUTHENTICATION_STATE.AUTHENTICATED);
    }

    /**
     * Gets the current oauth state
     *
     * @param cookieService The cookie service
     * @returns State and identifiere if available, else null
     */
    public getOAuthState(): { state: string; identifier: string } | null {
        return this.cookieService.check("oauthredirectstate")
            ? (JSON.parse(this.cookieService.get("oauthredirectstate")) as {
                  state: string;
                  identifier: string;
              })
            : null;
    }

    /**
     * Executes after redirecting from external authentication service back to UI
     *
     * @param code The current code
     * @param oauthState The current oauth state
     * @returns
     */
    public getTokenByCode(code: string, oauthState: { state: string; identifier: string } | null) {
        if (oauthState == null) {
            return;
        }
        this.service.websocket
            .sendRequest<AuthenticateWithOAuthResponse>(
                new AuthenticateWithOAuthRequest({
                    payload: new JsonrpcRequest("getTokenByCode", {
                        identifier: oauthState.identifier,
                        code: code,
                        ...OAuthService.getOem(),
                    }),
                }),
            )
            .then((response: AuthenticateWithOAuthResponse) => {
                const token = response.result;

                this.setTokens(token);
                this.completeAuthentication(response);
            })
            .catch(() => this.service.websocket.logout());
    }

    /**
     * Executes after successfull authentication.
     *
     * @param tokenResponse The token response from authentication
     */
    public completeAuthentication(tokenResponse: AuthenticateWithOAuthResponse): Promise<void> {
        return new Promise<void>((res) => {
            const user = User.from(tokenResponse.result.user);
            if (user == null) {
                this.service.websocket.logout();
                res();
                return;
            }

            this.userService.currentUser.set(user);

            this.service.websocket.state.set(States.AUTHENTICATED);

            this.service.metadata.next({
                user: user,
                edges: {},
            });

            const language =
                Language.getByKey(
                    localStorage.DEMO_LANGUAGE ?? this?.userService?.currentUser()?.language?.toLocaleLowerCase(),
                ) ?? Language.DEFAULT;
            localStorage.LANGUAGE = language.key;
            this.service.setLang(language);

            const currentUrl = this.routeService.currentUrl();
            const isAuthenticatedNavi = (currentUrl?.split("/")?.length ?? 0) > 2;

            if (isAuthenticatedNavi && currentUrl != null) {
                this.router.navigateByUrl(currentUrl);
                res();
                return;
            }

            // Fallback
            this.router.navigateByUrl("/overview");
            res();
        });
    }

    /**
     * Gets the refresh token.
     *
     * @returns The refresh token if existing, else null
     */
    public getRefreshToken(): string | null {
        if (this.cookieService.check("refresh_token") == false) {
            return null;
        }

        return JSON.parse(this.cookieService.get("refresh_token"));
    }

    /**
     * Executes when token and refresh_token is set
     *
     * @param refreshToken The refresh token
     * @returns A authentication response if valid session, else null
     */
    public async getTokenByRefreshToken(): Promise<AuthenticateWithOAuthResponse | null> {
        const refreshToken = this.getRefreshToken();

        if (refreshToken == null && StringUtils.isValidString(refreshToken)) {
            this.service.websocket.logout();
            return null;
        }

        const [err, response] = await JsonRpcUtils.handleResponse<AuthenticateWithOAuthResponse>(
            this.service.websocket.sendRequest(
                new AuthenticateWithOAuthRequest({
                    payload: new JsonrpcRequest("getTokenByRefreshToken", {
                        refreshToken: refreshToken,
                        ...OAuthService.getOem(),
                    }),
                }),
            ),
        );

        if (err || response == null) {
            this.service.websocket.logout();
            throw err;
        }

        this.setTokens(response.result);
        return response;
    }

    public getCurrentState(): TSignalValue<typeof this.state> {
        return this.state.asReadonly()();
    }

    /**
     * Sets the tokens in the cookie service.
     *
     * @param result The authentication response result
     */
    private setTokens(result: AuthenticateWithOAuthResponse["result"]) {
        this.cookieService.set(OAuthService.REFRESH_TOKEN, JSON.stringify(result.refreshToken), 14, "/");
    }
}
