import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { ConfigService } from '../config/config.service';
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
import { catchError, finalize, map } from 'rxjs/operators';
import { IUserDataModel } from './models/user-data.model';
import { IAuthResultModel } from './models/auth-result.model';
import { LanguageService } from '../language/language.service';
import { UiFeaturesService } from '../ui-api/ui-features.service';
import { VERSION } from '@environments/version';
import { IClientModel } from '@fleio-api/client-user/model/client.model';
import { TokenStorageService } from '@shared/ui-api/token-storage.service';
import { NotificationService } from '@shared/ui-api/notification.service';
import { Router } from '@angular/router';
import { IFleioToken } from '@shared/auth/models/fleio-token.model';
import * as _moment from 'moment';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private readonly loginEndpoint: string;
  private readonly signupEndpoint: string;
  private readonly logoutEndpoint: string;
  private readonly generateOtpTokenEndpoint: string;
  private readonly extendFrontendTokenEndpoint: string;
  private readonly resetPasswordEndpoint: string;
  private readonly setNewPasswordEndpoint: string;
  private readonly currentUserEndpoint: string;
  private readonly ssoEndpoint: string;
  private readonly saml2Endpoint: string;

  private lastCheckedIfLoggedIn = null;

  // NOTE: user data should be refreshed from time to time
  private userDataBS: BehaviorSubject<IUserDataModel> = new BehaviorSubject<IUserDataModel>(null);
  private isLoggedInBS: BehaviorSubject<boolean> = new BehaviorSubject(false);
  private loggedInCompletedBS: BehaviorSubject<boolean> = new BehaviorSubject(false);

  public readonly observableUserData: Observable<IUserDataModel> = this.userDataBS.asObservable();
  public readonly isLoggedIn: Observable<boolean> = this.isLoggedInBS.asObservable();
  public readonly loggedInCompleted: Observable<boolean> = this.loggedInCompletedBS.asObservable();

  public userData: IUserDataModel = null;
  public currencyDisplayMode;
  public dateFormat: string;
  public dateTimeFormat: string;
  public currencyDigitsInfo = null;
  public logoutStarted = false;
  public newBuildAvailable = false;
  public suspended = false;
  public token_expiry_warn_seconds = 60 * 2; // 2 minutes
  public extendTokenDialogShown = false;

  public feature(featureName: string): boolean {
    featureName = featureName.toLowerCase();
    if (this.userData && this.userData.features) {
      if (featureName.startsWith('ui.')) {
        return this.uiFeatures.feature(featureName);
      } else {
        return !!this.userData.features[featureName];
      }
    }

    // defaults to false if no feature found
    return false;
  }

  constructor(
    private httpClient: HttpClient,
    private config: ConfigService,
    private lang: LanguageService,
    private uiFeatures: UiFeaturesService,
    private tokenStorage: TokenStorageService,
    private notificationService: NotificationService,
    private router: Router,
  ) {
    this.loginEndpoint = this.config.getPanelApiUrl('login');
    this.logoutEndpoint = this.config.getPanelApiUrl('logout');
    this.generateOtpTokenEndpoint = this.config.getPanelApiUrl('auth/generate-otp-token');
    this.extendFrontendTokenEndpoint = this.config.getPanelApiUrl('auth/extend-frontend-token');
    this.resetPasswordEndpoint = this.config.getPanelApiUrl('reset-password/');
    this.currentUserEndpoint = this.config.getPanelApiUrl('current-user');
    this.setNewPasswordEndpoint = this.config.getPanelApiUrl('reset-password/confirm/');
    this.signupEndpoint = this.config.getPanelApiUrl('signup');
    this.ssoEndpoint = this.config.getPanelApiUrl('sso');
    this.saml2Endpoint = this.config.getPanelApiUrl('saml2sso');
    this.config.authService = this;
  }

  public checkIfLoggedIn(avoidDuplicates = false): Observable<boolean> {
    let lastCheckOldEnough = false;
    if (avoidDuplicates && this.lastCheckedIfLoggedIn) {
      lastCheckOldEnough = (
        ((new Date().getTime() - this.lastCheckedIfLoggedIn.getTime()) / 1000) > 5
      ); // check if older than 5s
    }
    if (!avoidDuplicates || !this.lastCheckedIfLoggedIn || (avoidDuplicates && lastCheckOldEnough)) {
      this.lastCheckedIfLoggedIn = new Date();
      return this.httpClient.get(
        this.currentUserEndpoint,
      ).pipe(
        map(currentUserData => {
          this.userData = currentUserData as IUserDataModel;
          this.currencyDisplayMode = this.userData?.formatting?.currency?.display_mode;
          this.dateFormat = this.userData?.formatting?.date.date_format;
          this.dateTimeFormat = this.userData?.formatting?.date.date_time_format;
          if (Number.isInteger(this.userData?.formatting?.currency?.min_fraction_digits)) {
            this.currencyDigitsInfo = `1.${this.userData.formatting.currency.min_fraction_digits}`;
          }
          this.newBuildAvailable = VERSION.version !== this.userData.version.latest_frontend_version;
          this.lang.setUserLanguage(this.userData?.user?.language);
          this.userDataBS.next(this.userData);
          const loggedIn = !!this.userData.user;
          this.isLoggedInBS.next(loggedIn);
          if (this.userData?.user?.active_client) {
            this.config.setActiveClientId(this.userData.user.active_client);
          }
          return loggedIn;
        })
      );
    }
    return of(!!this.userData?.user);
  }

  public setNewPassword(newPasswordData: {
    new_password: string;
    confirm_password: string;
    user_id: string;
    token: string;
  }) {
    return this.httpClient.post(
      this.setNewPasswordEndpoint,
      newPasswordData
    );
  }

  public resetPassword(email: string) {
    return this.httpClient.post(
      this.resetPasswordEndpoint,
      {
        email,
        frontend_url_host: window.location.host,
      }
    );
  }

  public signalLoginCompleted() {
    this.loggedInCompletedBS.next(true);
  }

  public signup(signupData: Record<string, unknown>): Observable<any> {
    return this.httpClient.post(
      this.signupEndpoint,
      signupData,
    ).pipe(map((userData: IUserDataModel) => {
        this.tokenStorage.setToken(
          userData.user.fleio_token.token,
          userData.user.fleio_token.expire_at,
          userData.user.fleio_token.expire_at_browser_close
        );
        return {
          success: true,
          error: null,
        };
      }),
      catchError((error: HttpErrorResponse) => of({
        success: false,
        error
      }))
    );
  }

  public login(email, password, rememberMe, sfaParams = null): Observable<IAuthResultModel> {
    return this.httpClient.post(
      this.loginEndpoint,
      {
        email,
        password,
        remember_me: rememberMe,
        sfa_params: sfaParams
      },
    ).pipe(map((currentUserData: IUserDataModel) => {
        this.userData = currentUserData;
        if (this.userData.user) {
          this.tokenStorage.setToken(
            this.userData.user.fleio_token.token,
            this.userData.user.fleio_token.expire_at,
            this.userData.user.fleio_token.expire_at_browser_close,
          );
          this.lang.setUserLanguage(this.userData.user.language);
          if (this.userData.user.active_client) {
            this.config.setActiveClientId(this.userData.user.active_client);
          }
        }
        this.loggedInCompletedBS.next(true);
        let sfaRequired = false;
        if (currentUserData.sfa_required) {
          sfaRequired = currentUserData.sfa_required;
        }
        let sfaMethods = null;
        if (currentUserData.sfa_methods) {
          sfaMethods = currentUserData.sfa_methods;
        }
        let rememberSFAToken = null;
        if (currentUserData.remember_sfa_token) {
          rememberSFAToken = currentUserData.remember_sfa_token;
        }
        return {
          success: true,
          errorMessage: '',
          rememberSFAToken,
          sfaRequired,
          sfaMethods,
        };
      }),
      catchError((err: HttpErrorResponse) => {
        this.lang.clearUserLanguage();
        // TODO: find a way to use constants instead of codes here
        if (err.status === 401) {
          return of({
            success: false,
            errorMessage: err.error.detail || $localize`Incorrect user name or password`
          });
        }

        return throwError(err);
      })
    );
  }

  public logout(tokenCleared = false): Observable<any> {
    this.logoutStarted = true;
    return this.httpClient.post(
      this.logoutEndpoint, {}
    ).pipe(map(response => {
      if (!tokenCleared) {
        this.tokenStorage.clearToken();
        if (this.feature('ui.staff')) {
          // make sure that impersonation tokens are also removed
          this.tokenStorage.clearPanelToken('enduser');
        }
      }
      this.lang.clearUserLanguage();
      this.userData = null;
      return response;
    }), finalize(() => {
      this.logoutStarted = false;
    }));
  }

  public clearUser() {
    this.lang.clearUserLanguage();
    this.userData = null;
    this.isLoggedInBS.next(false);
  }

  public isImpersonating(): boolean {
    return this.userData && this.userData.impersonated;
  }

  public isSuperUser(): boolean {
    return this.userData && this.userData.user && this.userData.user.is_superuser;
  }

  public stopImpersonating() {
    const staffBackendUrl = localStorage.getItem('fleio.flStaffBackend');
    const staffFrontendUrl = localStorage.getItem('fleio.flStaffUrl');

    this.httpClient.post(
      `${staffBackendUrl}/stop-impersonation`, {},
    ).subscribe(() => {
      this.tokenStorage.clearToken();
      window.location.href = staffFrontendUrl;
    });
  }

  activeClient(newActiveClient = null): IClientModel {
    if (this.userData?.user?.clients?.length) {
      if (!newActiveClient) {
        newActiveClient = this.userData.user.active_client;
      }
      return this.userData.user.clients.find(client => client.id === Number.parseInt(newActiveClient, 10));
    } else {
      return null;
    }
  }

  ssoLogin(hashValue: string) {
    return this.httpClient.post(
      this.ssoEndpoint,
      {hash_val: hashValue},
    ).pipe(map((userData: IUserDataModel) => {
      this.tokenStorage.setToken(
        userData.user.fleio_token.token,
        userData.user.fleio_token.expire_at,
        userData.user.fleio_token.expire_at_browser_close,
      );
      return userData;
    }));
  }

  saml2ssoLogin(otpToken: string) {
    return this.httpClient.post(
      this.saml2Endpoint,
      {fleio_token: otpToken},
    ).pipe(map((userData: IUserDataModel) => {
      this.tokenStorage.setToken(
        userData.user.fleio_token.token,
        userData.user.fleio_token.expire_at,
        userData.user.fleio_token.expire_at_browser_close,
      );
      return userData;
    }));
  }

  generateOtpToken(): Observable<string> {
    return this.httpClient.post(this.generateOtpTokenEndpoint, {})
      .pipe(map((response: any) => response.token));
  }

  showExtendTokenValidityDialog(fleioToken: IFleioToken) {
    if (!this.extendTokenDialogShown) {
      this.extendTokenDialogShown = true;

      const currentMoment = new Date();
      const expirationMomentLong = new Date();
      const expirationMomentShort = new Date();
      expirationMomentLong.setSeconds(currentMoment.getSeconds() + fleioToken.long_session_seconds);
      const humanizedLongTime = _moment.duration(
        // @ts-ignore
        expirationMomentLong - currentMoment
      ).locale(this.config.locale).humanize();
      expirationMomentShort.setSeconds(currentMoment.getSeconds() + fleioToken.short_session_seconds);
      const humanizedShortTime = _moment.duration(
        // @ts-ignore
        expirationMomentShort - currentMoment
      ).locale(this.config.locale).humanize();

      this.notificationService.confirmDialog(
        {
          title: $localize`Session expires soon`,
          message: $localize`Your session is about to expire. You will be automatically logged out shortly.`,
          yesButtonName: $localize`Extend session with ${humanizedLongTime}`,
          secondaryYesButtonName: $localize`Extend session with ${humanizedShortTime}`,
          noButtonName: $localize`Log out`,
        }, true, true,
      ).subscribe(result => {
        if (result?.button === 'yes' || result?.button === 'yes-secondary') {
          this.httpClient.post(
            this.extendFrontendTokenEndpoint, {
              remember_me: result?.button === 'yes',
            }
          ).subscribe((response: {
            fleio_token: IFleioToken;
          }) => {
            this.tokenStorage.setToken(
              response.fleio_token.token,
              response.fleio_token.expire_at,
              response.fleio_token.expire_at_browser_close,
            );
            this.extendTokenDialogShown = false;
          });
        } else if (result?.button === 'no') {
          this.logout().subscribe(() => {
            if (this.config && this.config.current && this.config.current.settings &&
              this.config.current.settings.logoutRedirect) {
              window.location.href = this.config.current.settings.logoutRedirect;
            } else {
              this.router.navigateByUrl(this.config.getPanelUrl('login')).then(() => {
              });
              this.extendTokenDialogShown = false;
            }
          });
        }
      });
    }
  }

}
