import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import {
  ALL_LOCAL_STORAGE_KEYS,
  APPLICATION_CLOSED_TIME_STAMP,
  AUTH_TOKEN_EXPIRES_IN_KEY,
  AUTH_TOKEN_EXPIRE_KEY,
  AUTH_TOKEN_KEY,
} from '@msfa-constants/local-storage-keys';
import { environment } from '@msfa-environment';
import { AuthenticationResponse } from '@msfa-models/api/authentication.response.model';
import { Authentication, mapAuthApiResponseToAuthenticationResult } from '@msfa-models/authentication.model';
import { add, isAfter, isBefore, sub } from 'date-fns';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, distinctUntilChanged, map, tap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class AuthenticationService {
  private readonly _refreshBaseUrl = environment.ciam.refreshUrl;
  private _token$ = new BehaviorSubject<string>(null);
  private _expiresAt$ = new BehaviorSubject<number>(null);
  private _expiresIn$ = new BehaviorSubject<number>(null);

  isLoggedIn$: Observable<boolean> = this._token$.pipe(
    distinctUntilChanged(),
    map(token => !!token)
  );

  get isLoggedIn(): boolean {
    return !!this._token$.getValue();
  }

  // We set token validity at 1 minute prior expiration time.
  // Refresh will be possible until expiration time.
  // TODO: Refesh solution
  get isTokenValid(): boolean {
    const now = new Date();
    const expiresAt = this._expiresAt$.getValue();
    const applicationClosedTimeStamp = localStorage.getItem(APPLICATION_CLOSED_TIME_STAMP);
    // Checking to see if the user has been active on the page within the last hour
    const applicationClosedWithin1Hour =
      !applicationClosedTimeStamp || isAfter(+applicationClosedTimeStamp, sub(now, { hours: 1 }));
    return isBefore(now, sub(expiresAt, { minutes: 1 })) && applicationClosedWithin1Hour;
  }

  get isTokenRefreshable(): boolean {
    // const expiresAt = this._expiresAt$.getValue();
    // return isBefore(new Date(), expiresAt);
    return false;
  }

  get isLoggedInWithValidToken(): boolean {
    return this.isLoggedIn && this.isTokenValid;
  }

  private static _authTokenApiUrl(code: string): string {
    return `${environment.api.url}/auth/token?accessCode=${code}`;
  }

  public get expiresAtDate(): number {
    return this._expiresAt$.getValue();
  }

  refreshToken$(): Observable<boolean> {
    return this.httpClient.get(`${this._refreshBaseUrl}`, { responseType: 'blob' }).pipe(
      tap(() => {
        this._setSession({
          idToken: this._token$.getValue(),
          expiresIn: 3600,
        });
      }),
      map(() => true),
      catchError((error: HttpErrorResponse) => {
        // TODO: Information dialog that the user is logged out?
        console.error('Could not refresh token... User needs to login again: ', error);
        this._setSession(null);
        return of(false);
      })
    );
  }

  private _setSession(authenticationResult: Authentication | null): void {
    if (!authenticationResult) {
      this.removeLocalStorageData();
      this._token$.next(null);
      this._expiresAt$.next(null);
      this._expiresIn$.next(null);
    } else {
      const expiresAt = add(new Date(), { seconds: authenticationResult.expiresIn });

      this._expiresIn$.next(authenticationResult.expiresIn);
      localStorage.setItem(AUTH_TOKEN_EXPIRES_IN_KEY, authenticationResult.expiresIn.toString());
      this._token$.next(authenticationResult.idToken);
      localStorage.setItem(AUTH_TOKEN_KEY, authenticationResult.idToken);
      this._expiresAt$.next(expiresAt.valueOf());
      localStorage.setItem(AUTH_TOKEN_EXPIRE_KEY, JSON.stringify(expiresAt.valueOf()));
    }
  }

  login$(authorizationCodeFromCiam: string): Observable<Authentication> {
    this.removeLocalStorageData();

    return this.httpClient
      .get<{ data: AuthenticationResponse }>(AuthenticationService._authTokenApiUrl(authorizationCodeFromCiam))
      .pipe(
        map(({ data }) => mapAuthApiResponseToAuthenticationResult(data)),
        tap(authenticationResult => {
          this._setSession(authenticationResult);
        })
      );
  }

  private get _localStorageData(): { idToken: string; expiresAt: number; expiresIn: number; lastActive: number } {
    const idToken = localStorage.getItem(AUTH_TOKEN_KEY);
    const expiresAt = localStorage.getItem(AUTH_TOKEN_EXPIRE_KEY);
    const expiresIn = localStorage.getItem(AUTH_TOKEN_EXPIRES_IN_KEY);
    const lastActive = localStorage.getItem(APPLICATION_CLOSED_TIME_STAMP);

    return idToken && expiresAt && expiresIn
      ? {
          idToken,
          expiresAt: +expiresAt,
          expiresIn: +expiresIn,
          lastActive: +lastActive,
        }
      : null;
  }

  public removeLocalStorageData(): void {
    ALL_LOCAL_STORAGE_KEYS.forEach(key => {
      localStorage.removeItem(key);
    });
  }

  get currentAuthorizationToken(): string {
    return this._token$.getValue();
  }

  constructor(private httpClient: HttpClient, private router: Router) {
    const localStorageData = this._localStorageData;

    if (localStorageData) {
      this._token$.next(localStorageData.idToken);
      this._expiresAt$.next(localStorageData.expiresAt);
      this._expiresIn$.next(localStorageData.expiresIn);
    }
  }

  logout(): void {
    this.removeLocalStorageData();

    if (environment.ciam.clientId) {
      window.location.href = environment.ciam.logoutUrl;
    } else {
      void this.router.navigateByUrl(environment.ciam.logoutUrl);
    }
  }
}
