import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import {
  ALL_LOCAL_STORAGE_KEYS,
  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, isBefore, sub } from 'date-fns';
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
import { catchError, distinctUntilChanged, filter, 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)
  );

  // We set token validity at 1 minute prior expiration time.
  // Refresh will be possible until expiration time.
  // TODO: Refesh solution
  isTokenValid$: Observable<{ isValid: boolean; isRefreshable: boolean }> = combineLatest([
    this._token$,
    this._expiresAt$,
  ]).pipe(
    filter(([token, expiresAt]) => !!(token && expiresAt)),
    map(([, expiresAt]) => {
      return {
        isValid: isBefore(new Date(), sub(expiresAt, { minutes: 1 })),
        isRefreshable: false,
        // isRefreshable: isBefore(new Date(), expiresAt),
      };
    })
  );

  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(): { id_token: string; expires_at: number; expires_in: number } {
    const id_token = localStorage.getItem(AUTH_TOKEN_KEY);
    const expiresAt = localStorage.getItem(AUTH_TOKEN_EXPIRE_KEY);
    const expiresIn = localStorage.getItem(AUTH_TOKEN_EXPIRES_IN_KEY);

    return id_token && expiresAt && expiresIn
      ? {
          id_token,
          expires_at: +JSON.parse(expiresAt),
          expires_in: +expiresIn,
        }
      : 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.id_token);
      this._expiresAt$.next(localStorageData.expires_at);
      this._expiresIn$.next(localStorageData.expires_in);
    }
  }

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

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