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 { EmployeeParams, 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';

const DEFAULT_PARAMS: EmployeeParams = {
  page: 1,
  limit: 10,
  sort: 'name',
  order: SortOrder.ASC,
  search: '',
  onlyEmployeesWithoutAuthorization: false,
};

@Injectable({
  providedIn: 'root',
})
export class EmployeeService extends UnsubscribeDirective {
  private _apiBaseUrl = `${environment.api.url}/users`;
  private _currentEmployeeId$ = new BehaviorSubject<string>(null);
  private _params$ = new BehaviorSubject<EmployeeParams>(DEFAULT_PARAMS);
  public sort$: Observable<Sort<keyof EmployeeCompactResponse>> = this._params$.pipe(
    map(({ sort, order }) => ({ key: sort, order }))
  );
  public onlyEmployeesWithoutAuthorization$: Observable<boolean> = this._params$.pipe(
    map(({ onlyEmployeesWithoutAuthorization }) => onlyEmployeesWithoutAuthorization)
  );
  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._params$, this._lastDeletedEmployee$]).pipe(
    switchMap(([params]) => this._fetchEmployees$(params))
  );

  public resetParams(): void {
    this._params$.next(DEFAULT_PARAMS);
  }

  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$(employeeParams: EmployeeParams): Observable<EmployeesData> {
    const { sort, order, limit, page, search, onlyEmployeesWithoutAuthorization } = employeeParams;
    const params: Params = {
      sort,
      order,
      limit: limit.toString(),
      page: page.toString(),
    };

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

    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 setSearchString(value: string): void {
    this._params$.next({
      ...this._params$.getValue(),
      search: value,
      page: 1,
    });
  }

  public setOnlyEmployeesWithoutAuthorization(value: boolean): void {
    this._params$.next({
      ...this._params$.getValue(),
      onlyEmployeesWithoutAuthorization: value,
      page: 1,
    });
  }

  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(sort: keyof EmployeeCompactResponse): void {
    const params = this._params$.getValue();
    const order = params.sort === sort && params.order === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;

    this._params$.next({ ...params, sort, order });
  }

  public setPage(page: number): void {
    this._params$.next({ ...this._params$.getValue(), 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));
      })
    );
  }
}
