import { Inject, Injectable, OnDestroy } from '@angular/core';
import { CacheService } from '@core/cache';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { UISettingsDTO } from '@swagger/cat';
import { UiFilter } from '@ui/filter';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { from, Observable, of, Subject } from 'rxjs';
import { switchMap, withLatestFrom, finalize, tap, filter, takeUntil } from 'rxjs/operators';
import { SearchState, SearchStateSearchResult, SearchStateSearchService, SearchStateSettingsLoader } from './defs';
import { SEARCH_STATE_SEARCH_SERVICE, SEARCH_STATE_SETTINGS_LOADER } from './tokens';

@Injectable()
export class SearchComponentStoreService<T = any> extends ComponentStore<SearchState<T>> implements OnDestroy {
  private _onDestroy$ = new Subject();

  get state() {
    return this.get();
  }

  state$ = this.select((state) => state);

  get filter() {
    return this.get((state) => state.filter);
  }

  filter$ = this.select((state) => state.filter);

  get status() {
    return this.get((state) => state.status);
  }

  status$ = this.select((state) => state.status);

  get fetching() {
    return this.status === 'fetching';
  }

  fetching$ = this.select((state) => state.status === 'fetching');

  get error() {
    return this.status === 'error';
  }

  error$ = this.select((state) => state.status === 'error');

  get message() {
    return this.get((state) => state.message);
  }

  message$ = this.select((state) => state.message);

  get items() {
    return this.get((state) => state.items);
  }

  items$ = this.select((state) => state.items);

  get hits() {
    return this.get((state) => state.hits);
  }

  hits$ = this.select((state) => state.hits);

  searchCompleted = new Subject<SearchState<T>>();

  constructor(
    @Inject(SEARCH_STATE_SETTINGS_LOADER) private readonly _settingsLoader: SearchStateSettingsLoader,
    @Inject(SEARCH_STATE_SEARCH_SERVICE) private readonly _searchService: SearchStateSearchService<T>,
    private readonly _cache: CacheService,
    private readonly _uiModal: UiModalService
  ) {
    super({
      items: [],
      hits: 0,
    });

    this.filter$
      .pipe(
        takeUntil(this._onDestroy$),
        filter((filter) => !!filter)
      )
      .subscribe((filter) => {
        const cachedResult = this._cache.get(filter?.getQueryParams());
        if (cachedResult) {
          this.setHits(cachedResult.hits);
          this.setItems(cachedResult.items);
        }
      });
  }

  ngOnDestroy(): void {
    this._onDestroy$.next();
    this._onDestroy$.complete();
  }

  setItems(items: T[]) {
    this.patchState({ items });
  }

  appendItems(items: T[]) {
    this.patchState({
      items: [...this.items, ...items],
    });
  }

  clearItems() {
    this.patchState({ items: [] });
  }

  setStatus(status: undefined | 'fetching' | 'error') {
    this.patchState({ status });
  }

  setHits(hits: number) {
    this.patchState({ hits });
  }

  setMessage(message: string) {
    this.patchState({ message });
  }

  reset(defaults: Partial<SearchState<T>> = {}) {
    this.setState({
      items: [],
      hits: 0,
      ...defaults,
    });
  }

  async setDefaultFilter() {
    this.setStatus('fetching');
    const filter = await this.getDefaultSettings();
    this.setFilter(filter);
    this.setStatus(undefined);
  }

  async resetFilter(queryParams?: Record<string, string>) {
    const settings = await this.getDefaultSettings();
    const filter = UiFilter.create(settings);

    if (queryParams) {
      filter.fromQueryParams(queryParams);
    }
    this.reset({ filter });
  }

  setFilter(filter: UiFilter | UISettingsDTO) {
    let _filter: UiFilter;

    if (filter instanceof UiFilter) {
      _filter = filter;
    } else {
      _filter = UiFilter.create(filter);
    }

    this.patchState({ filter: _filter });
  }

  getDefaultSettings(): Promise<UISettingsDTO> {
    const settings = this._settingsLoader.load();

    // check if settings is an Observable
    if (settings instanceof Observable) {
      return settings.toPromise();
    }

    // check if setting is a Promise
    if (settings instanceof Promise) {
      return settings;
    }

    return Promise.resolve(settings);
  }

  search = this.effect((options$: Observable<{ clear?: boolean }>) =>
    options$.pipe(
      tap((options) => {
        this.setStatus('fetching');
        this.setMessage(undefined);
        if (options.clear) {
          this.clearItems();
        }
      }),
      withLatestFrom(this.filter$, this.items$),
      switchMap(([_, filter, items]) => {
        return this.searchRequest(filter, items).pipe(
          tapResponse(
            (res) => {
              this.handleSearchResponse(res);
              this.cacheResults(filter, { items: [...items, ...res.items], hits: res.hits });
            },
            (err) => this.handleSearchError(err)
          ),
          finalize(() => {
            this.searchCompleted.next(this.state);
          })
        );
      })
    )
  );

  searchRequest(filter: UiFilter, items: T[]): Observable<SearchStateSearchResult<T>> {
    const result = this._searchService.search(filter, { skip: items.length });

    // check if result is an Observable
    if (result instanceof Observable) {
      return result;
    }

    // check if result is a Promise
    if (result instanceof Promise) {
      return from(result);
    }

    return of(result);
  }

  handleSearchResponse(res: SearchStateSearchResult<T>) {
    if (res.hits === 0) {
      this.setStatus('error');
      this.setMessage('Keine Suchergebnisse');
      this.setHits(0);
      this.clearItems();
    } else {
      this.setStatus(undefined);
      this.appendItems(res.items);
      this.setHits(res.hits);
      this.setMessage(undefined);
    }
  }

  handleSearchError(err: any) {
    this.setStatus('error');
    this._uiModal.open({
      content: UiErrorModalComponent,
      title: 'Suche Fehlgeschlagen',
      data: err,
    });
  }

  cacheResults(filter: UiFilter, data: { items: T[]; hits: number }) {
    this._cache.set(filter.getQueryParams(), data);
  }

  restoreResults() {
    const cachedResult = this._cache.get(this.filter.getQueryParams());

    if (cachedResult) {
      this.setHits(cachedResult.hits);
      this.setItems(cachedResult.items);
    }

    return cachedResult;
  }
}
