import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
import { HttpErrorInfo, HttpErrorType, IAuthToken } from "@shared/models";
import { AppData, CommunicationService, HttpErrorService, IdentityService, appUrls } from "@shared/services";
import { BehaviorSubject, Observable, throwError } from "rxjs";
import { catchError, filter, finalize, switchMap, take } from "rxjs/operators";

function getCookie(cookieName: string) {
    const name = cookieName + "=";
    const decodedCookie = decodeURIComponent(document.cookie);
    const ca = decodedCookie.split(";");
    for (let i = 0; i < ca.length; i++) {
        let c = ca[i];
        while (c.charAt(0) === " ") {
            c = c.substring(1);
        }
        if (c.indexOf(name) === 0) {
            return c.substring(name.length, c.length);
        }
    }
    return "";
}

@Injectable()
export class RequestInterceptor implements HttpInterceptor {
    isRefreshingToken = false;
    tokenSubject = new BehaviorSubject<string>(null);

    constructor(
        private readonly router: Router,
        private readonly appData: AppData,
        private readonly identityService: IdentityService,
        private readonly httpErrorService: HttpErrorService,
        private readonly communicationService: CommunicationService,
    ) { }

    addHeaders(req: HttpRequest<any>): HttpRequest<any> {
        let headers = req.headers;
        headers = headers.set("Access-Control-Allow-Origin", "*");
        headers = headers.set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
        headers = headers.set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT");
        headers = headers.set("Accept", "application/json");
        headers = headers.set("Access-Control-Allow-Credentials", "true");

        if (!req.reportProgress) {
            headers = headers.set("Content-Type", "application/json");
        }

        const cookieName = "X-XSRF-TOKEN";
        const token = getCookie(cookieName);
        if (token) {
            headers = headers.set(cookieName, token);
        }

        return req.clone({ headers: headers });
    }

    addLocationHeader(req: HttpRequest<any>, locationId: number, createdBy: number): HttpRequest<any> {
        let headers = req.headers;
        headers = headers.set("LocationId", locationId ? locationId.toString() : "0");
        headers = headers.set("CreatedBy", createdBy ? createdBy.toString() : "0");
        return req.clone({ headers: headers });
    }

    addToken(req: HttpRequest<any>, token: string): HttpRequest<any> {
        return req.clone({ setHeaders: { Authorization: token } });
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // Add default headers
        let transformedReq = this.addHeaders(req);

        // Check whether the request is Allow Anonymous or not
        const skipAuthentication = transformedReq.headers.get("Auth") === "False";
        if (skipAuthentication) {
            return next.handle(transformedReq);
        }

        // Get token from Identity
        let token = "";
        let locationId = 0;
        let createdBy = 0;
        this.appData.userAccount.subscribe(userAccount => {
            if (userAccount) {
                token = userAccount.token;
                locationId = userAccount.locationId;
                createdBy = userAccount.accountId;
            } else {
                token = "";
                locationId = 0;
                createdBy = 0;
            }
        });

        // Add Location Header
        transformedReq = this.addLocationHeader(req, locationId, createdBy);

        // Add token
        transformedReq = this.addToken(transformedReq, token);
        return next.handle(transformedReq).pipe(
            catchError(
                (error: HttpErrorResponse) => {
                    switch (error.status) {
                        case 401:
                            return this.handle401Error(transformedReq, next);
                        case 403:
                            return this.handle403Error();
                        default:
                            return throwError(error);
                    }
                }
            )
        );
    }

    handle401Error(req: HttpRequest<any>, next: HttpHandler) {

        const errorInfo = new HttpErrorInfo();
        errorInfo.message = "401 Unauthorized";
        errorInfo.type = HttpErrorType.E401;
        this.httpErrorService.add(errorInfo);

        if (!this.isRefreshingToken) {
            this.isRefreshingToken = true;

            // Reset here so that the following requests wait until the token
            // comes back from the refreshToken call.
            this.tokenSubject.next(null);

            // Get reference(refresh token) from Identity
            let referenceToken = "";
            this.appData.userAccount.subscribe(userAccount => {
                if (userAccount) {
                    referenceToken = userAccount.referenceToken;
                }
            });

            return this.identityService.refreshToken(referenceToken).pipe(
                switchMap((authToken: IAuthToken) => {
                    if (authToken) {
                        this.tokenSubject.next(authToken.token);
                        this.identityService.update(authToken);
                        return next.handle(this.addToken(req, authToken.token));
                    } else {
                        return this.logout();
                    }
                }),
                catchError(() => {
                    return this.logout();
                }),
                finalize(() => {
                    this.isRefreshingToken = false;
                })
            );
        }

        return this.tokenSubject.pipe(
            filter(token => token !== null),
            take(1),
            switchMap((token: string) => {
                return next.handle(this.addToken(req, token));
            })
        );
    }

    handle403Error() {
        this.router.navigateByUrl(appUrls.forbidden);
        return throwError("");
    }

    logout() {
        this.identityService.signOut().subscribe(() => {
            this.appData.setAccount(null);
            localStorage.removeItem("menus");
            try {
                this.communicationService.stopConnection();
            } catch (e) {
                console.error(e)
            }
            this.router.navigateByUrl(appUrls.login);
        });
        return throwError("");
    }
}