import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { UnsubscribeDirective } from '@msfa-directives/unsubscribe.directive';
import { SortOrder } from '@msfa-enums/sort-order.enum';
import { environment } from '@msfa-environment';
import { EmployeeEditRequest } from '@msfa-models/api/employee-edit.request.model';
import { EmployeeInviteResponse } from '@msfa-models/api/employee-invite.response.model';
import {
  EmployeeCompactResponse,
  EmployeeResponse,
  EmployeesDataResponse,
} from '@msfa-models/api/employee.response.model';
import { Params } from '@msfa-models/api/params.model';
import {
  Employee,
  EmployeesData,
  mapResponseToEmployee,
  mapResponseToEmployeeCompact,
} from '@msfa-models/employee.model';
import { errorToCustomError } from '@msfa-models/error/custom-error';
import { Sort } from '@msfa-models/sort.model';
import { ErrorService } from '@msfa-services/error.service';
import { BehaviorSubject, combineLatest, Observable, of, throwError } from 'rxjs';
import { catchError, distinctUntilChanged, filter, map, switchMap, take, tap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class EmployeeService extends UnsubscribeDirective {
  private _apiBaseUrl = `${environment.api.url}/users`;
  private _currentEmployeeId$ = new BehaviorSubject<string>(null);
  private _limit$ = new BehaviorSubject<number>(10);
  private _page$ = new BehaviorSubject<number>(1);
  private _sort$ = new BehaviorSubject<Sort<keyof EmployeeCompactResponse>>({ key: 'name', order: SortOrder.ASC });
  public sort$: Observable<Sort<keyof EmployeeCompactResponse>> = this._sort$.asObservable();
  private _searchFilter$ = new BehaviorSubject<string>('');
  private _onlyEmployeesWithoutAuthorization$ = new BehaviorSubject<boolean>(false);
  public onlyEmployeesWithoutAuthorization$: Observable<boolean> = this._onlyEmployeesWithoutAuthorization$.asObservable();
  private _employee$ = new BehaviorSubject<Employee>(null);
  public employee$: Observable<Employee> = this._employee$.asObservable();
  private _lastUpdatedEmployeeId$ = new BehaviorSubject<string>(null);
  public lastUpdatedEmployeeId$: Observable<string> = this._lastUpdatedEmployeeId$.asObservable();
  private _lastDeletedEmployee$ = new BehaviorSubject<Employee>(null);
  public lastDeletedEmployee$: Observable<Employee> = this._lastDeletedEmployee$.asObservable();
  private _employeeToDelete$ = new BehaviorSubject<Employee>(null);
  public employeeToDelete$: Observable<Employee> = this._employeeToDelete$.asObservable();
  private _employeesLoading$ = new BehaviorSubject<boolean>(false);
  public employeesLoading$: Observable<boolean> = this._employeesLoading$.asObservable();

  constructor(private httpClient: HttpClient, private errorService: ErrorService) {
    super();
    super.unsubscribeOnDestroy(
      combineLatest([this._currentEmployeeId$, this._lastUpdatedEmployeeId$])
        .pipe(
          distinctUntilChanged(
            ([prevEmployeeId], [currEmployeeId, currLastUpdatedEmployeeId]) =>
              !currLastUpdatedEmployeeId && prevEmployeeId === currEmployeeId
          ),
          filter(([currentEmployeeId]) => !!currentEmployeeId),
          switchMap(([currentEmployeeId]) =>
            this._fetchEmployee$(currentEmployeeId).pipe(filter(employee => !!employee))
          )
        )
        .subscribe(employee => {
          this._employee$.next(employee as Employee);
        })
    );
  }

  public employeesData$: Observable<EmployeesData> = combineLatest([
    this._limit$,
    this._page$,
    this._sort$,
    this._searchFilter$,
    this._onlyEmployeesWithoutAuthorization$,
    this._lastDeletedEmployee$,
  ]).pipe(
    switchMap(([limit, page, sort, searchFilter, onlyEmployeesWithoutAuthorization]) =>
      this._fetchEmployees$(limit, page, sort, searchFilter, onlyEmployeesWithoutAuthorization)
    )
  );

  public setCurrentEmployeeId(currentEmployeeId: string): void {
    if (this._currentEmployeeId$.getValue() !== currentEmployeeId) {
      this._employee$.next(null);
      this._currentEmployeeId$.next(currentEmployeeId);
    }
  }

  public resetLastUpdatedEmployeeId(): void {
    this._lastUpdatedEmployeeId$.next(null);
  }

  private _fetchEmployees$(
    limit: number,
    page: number,
    sort: Sort<keyof EmployeeCompactResponse>,
    searchFilter: string,
    onlyEmployeesWithoutAuthorization?: boolean
  ): Observable<EmployeesData> {
    const params: Params = {
      sort: sort.key as string,
      order: sort.order as string,
      limit: limit.toString(),
      page: page.toString(),
    };

    if (searchFilter) {
      params.search = searchFilter;
    }

    if (onlyEmployeesWithoutAuthorization) {
      params.onlyEmployeesWithoutAuthorization = onlyEmployeesWithoutAuthorization?.toString();
    }
    this._employeesLoading$.next(true);

    return this.httpClient
      .get<EmployeesDataResponse>(this._apiBaseUrl, { params })
      .pipe(
        map(({ data, meta }) => {
          this._employeesLoading$.next(false);
          return { data: data.map(employee => mapResponseToEmployeeCompact(employee)), meta };
        })
      );
  }

  private _fetchEmployee$(id: string): Observable<Employee | Partial<Employee>> {
    return this.httpClient.get<{ data: EmployeeResponse }>(`${this._apiBaseUrl}/${id}`).pipe(
      map(({ data }) => mapResponseToEmployee(data)),
      catchError(error => {
        this.errorService.add(errorToCustomError(error));
        return of({});
      })
    );
  }

  public setSearchFilter(value: string): void {
    this._searchFilter$.next(value);
  }

  public setOnlyEmployeesWithoutAuthorization(value: boolean): void {
    this._onlyEmployeesWithoutAuthorization$.next(value);
  }

  public setEmployeeToDelete(employee: Employee): void {
    this._employeeToDelete$.next(employee);
  }

  public deleteEmployee(employee: Employee): Observable<Employee | Partial<Employee>> {
    return this.httpClient.delete<void>(`${this._apiBaseUrl}/${employee.id}`).pipe(
      tap(() => {
        this._lastDeletedEmployee$.next(employee);
      }),
      map(() => employee),
      catchError(error => throwError(errorToCustomError(error)))
    );
  }

  public setSort(newSortKey: keyof EmployeeCompactResponse): void {
    const currentSort = this._sort$.getValue();
    const order =
      currentSort.key === newSortKey && currentSort.order === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;

    this._sort$.next({ key: newSortKey, order });
  }

  public setPage(page: number): void {
    this._page$.next(page);
  }

  public postEmployeeInvitation(emails: string[]): Observable<EmployeeInviteResponse | null> {
    return this.httpClient
      .patch<{ data: EmployeeInviteResponse }>(`${this._apiBaseUrl}/invite`, { emails })
      .pipe(
        take(1),
        map(({ data }) => data),
        catchError(error => {
          this.errorService.add(errorToCustomError(error));
          return throwError(errorToCustomError(error));
        })
      );
  }

  public updateEmployee$(id: string, data: EmployeeEditRequest): Observable<boolean> {
    return this.httpClient.put<boolean>(`${this._apiBaseUrl}/${id}`, data).pipe(
      take(1),
      tap(() => {
        this._employee$.next(null);
        this._lastUpdatedEmployeeId$.next(id);
      }),
      map(() => true),
      catchError(error => {
        return throwError(errorToCustomError(error));
      })
    );
  }
}
