import { Injectable, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { CacheService } from '@core/cache';
import { Config } from '@core/config';
import { DomainRemissionService, RemissionListItem } from '@domain/remission';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { ReturnItemDTO, ReturnSuggestionDTO, SupplierDTO } from '@swagger/remi';
import { UiFilter } from '@ui/filter';
import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
import { debounceTime, filter, switchMap, takeUntil, tap, withLatestFrom } from 'rxjs/operators';

export interface RemissionState {
  suppliers: SupplierDTO[];
  selectedSupplierId?: number;
  sources: string[];
  selectedSource?: string;
  filter?: UiFilter;
  hits: number;
  searchOptions?: { take?: number; skip?: number };
  items?: RemissionListItem[];
  requiredCapacities: [];
  fetching?: boolean;
}

@Injectable()
export class RemissionListComponentStore extends ComponentStore<RemissionState> implements OnDestroy {
  private _onDestroy$ = new Subject<void>();

  private _searchCompleted = new BehaviorSubject<RemissionState>({
    suppliers: [],
    sources: [],
    hits: 0,
    items: [],
    requiredCapacities: undefined,
  });

  searchCompleted = this._searchCompleted.asObservable();
  _triggerReload$ = new BehaviorSubject<boolean>(false);

  private _filterChange$ = new BehaviorSubject<boolean>(false);

  get processId() {
    return this._config.get('process.ids.remission');
  }

  get suppliers() {
    return this.get((s) => s.suppliers);
  }

  get suppliers$() {
    return this.select((s) => s.suppliers);
  }

  get selectedSupplier() {
    return this.get((s) => s.suppliers?.find((si) => si.id === s.selectedSupplierId));
  }

  get selectedSupplier$() {
    return this.select((s) => s.suppliers?.find((si) => si.id === s.selectedSupplierId));
  }

  get sources() {
    return this.get((s) => s.sources);
  }

  get sources$() {
    return this.select((s) => s.sources);
  }

  get selectedSource() {
    return this.get((s) => s.selectedSource);
  }

  get selectedSource$() {
    return this.select((s) => s.selectedSource);
  }

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

  get filter$() {
    return this.select((s) => s.filter);
  }

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

  get items$() {
    return this.select((s) => s.items);
  }

  get requiredCapacities$() {
    return this.select((s) => s.requiredCapacities);
  }

  get fetching() {
    return this.get((s) => s.fetching);
  }

  get fetching$() {
    return this.select((s) => s.fetching);
  }

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

  get hits$() {
    return this.select((s) => s.hits);
  }

  get searchOptions() {
    return this.get((s) => s.searchOptions);
  }

  get searchOptions$() {
    return this.select((s) => s.searchOptions);
  }

  constructor(
    private readonly _domainRemissionService: DomainRemissionService,
    private readonly _config: Config,
    private readonly _cache: CacheService,
    private readonly _activatedRoute: ActivatedRoute,
    private readonly _router: Router
  ) {
    super({
      suppliers: [],
      sources: [],
      hits: 0,
      items: [],
      requiredCapacities: undefined,
    });
    this.loadSuppliers();
    this.loadSources();
    this.initLoadFilter();
    this.initSearch();
  }

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

  initLoadFilter() {
    combineLatest([this.selectedSource$, this.selectedSupplier$])
      .pipe(takeUntil(this._onDestroy$))
      .subscribe(() => {
        this.loadFilter();
      });
  }

  initSearch() {
    combineLatest([this._triggerReload$, this._filterChange$])
      .pipe(debounceTime(500), takeUntil(this._onDestroy$))
      .subscribe(([reload, filter]) => {
        const data = this.getCachedData();
        if (reload || data.items?.length === 0 || filter) {
          this.search({ newSearch: true });
        }
      });
  }

  // tslint:disable: member-ordering
  loadSuppliers = this.effect(($) =>
    $.pipe(
      // tap((_) => (this.getCachedData()?.hits === 0 ? this.setFetching(true) : null)),
      switchMap((_) =>
        this._domainRemissionService.getSuppliers().pipe(
          tapResponse(
            (suppliers) => this.setSuppliers(suppliers),
            (err) => {}
          )
        )
      )
    )
  );

  loadSources = this.effect(($) =>
    $.pipe(
      // tap((_) => (this.getCachedData()?.hits === 0 ? this.setFetching(true) : null)),
      switchMap((_) =>
        this._domainRemissionService.getSources().pipe(
          tapResponse(
            (sources) => this.setSources(sources),
            (err) => {}
          )
        )
      )
    )
  );

  loadRequiredCapacities = this.effect(($) =>
    $.pipe(
      // tap((_) => (this.getCachedData()?.hits === 0 ? this.setFetching(true) : null)),
      withLatestFrom(this.selectedSupplier$, this.filter$),
      switchMap(([_, supplier, filter]) => {
        let departments = [];
        if (filter?.getQueryParams()?.filter_abteilungen) {
          departments = filter?.getQueryParams()?.filter_abteilungen?.split(';') || [];
        }

        return this._domainRemissionService.getRequiredCapacities({ supplierId: supplier?.id, departments }).pipe(
          tapResponse(
            (response) => this.setRequiredCapacities(response),
            (err) => {}
          )
        );
      })
    )
  );

  loadFilter = this.effect(($) =>
    $.pipe(
      // tap((_) => (this.getCachedData()?.hits === 0 ? this.setFetching(true) : null)),
      withLatestFrom(this.selectedSupplier$, this.selectedSource$, this._activatedRoute.queryParams),
      filter(([, selectedSupplier, selectedSource]) => !!selectedSupplier?.id && !!selectedSource),
      switchMap(([, selectedSupplier, selectedSource, queryParams]) =>
        this._domainRemissionService
          .getQuerySettings({
            supplierId: selectedSupplier.id,
            source: selectedSource,
          })
          .pipe(
            tapResponse(
              (settings) => {
                // Filtergruppen ausblenden, wenn keine Werte innerhalb des Filters für die Anzeige vorhanden sind
                settings?.filter?.forEach((filter) => (filter.input = filter.input?.filter((input) => input.options?.values?.length > 0)));

                const filter = UiFilter.create(settings);
                if (!!queryParams) {
                  filter?.fromQueryParams(queryParams);
                }

                this.setFilter(filter);

                // Only trigger search with _filterChange$ if no cached data available
                // If cached data is available, there is no need to trigger the search
                const data = this.getCachedData();
                if (data.items?.length === 0) {
                  this._filterChange$.next(true);
                }
              },
              (err) => {}
            )
          )
      )
    )
  );

  search = this.effect((options$: Observable<{ newSearch?: boolean }>) =>
    options$.pipe(
      withLatestFrom(this.filter$, this.selectedSource$, this.selectedSupplier$, this.searchOptions$, this.items$),
      filter(([, filter, source, supplier]) => !!supplier?.id && !!source && !!filter),
      filter(([, filter, source]) => (source === 'Abteilungsremission' ? !!filter.getQueryToken().filter.abteilungen : true)),
      tap(([options]) => {
        this.setFetching(true);
        options?.newSearch ? this.setSearchResult({ result: [], hits: 0 }) : null;
      }),
      switchMap(([options, filter, source, supplier, searchOptions, items]) =>
        this._domainRemissionService
          .getItems({
            queryToken: {
              ...filter?.getQueryToken(),
              skip: options?.newSearch ? 0 : searchOptions?.skip ?? items?.length,
              take: searchOptions?.take ?? 40,
            },
            source,
            supplierId: supplier.id,
          })
          .pipe(
            // Nochmal letzten Stand der Items holen, da diese während dem Nachladen manipuliert werden können (remittieren)
            withLatestFrom(this.items$),
            tapResponse(
              ([res, currentItems]) => {
                const results = options?.newSearch
                  ? { result: [...res.result], hits: res.hits ?? 0 }
                  : { result: [...(currentItems ?? []), ...(res.result ?? [])], hits: res.hits ?? 0 };

                // find items that are already in the list and remove the first duplicate
                // why do we need it? when we use the action item not found we will add this item to the end of the list
                // if we load more items this item appears again at the end of the list
                // to keep the list clean we remove all duplicates and just keep the last one
                const itemsToRemove: number[] = [];
                results.result.forEach((item) => {
                  const duplicates = results.result.filter((i) => i.dto.id === item.dto.id);
                  if (duplicates.length > 1 && !itemsToRemove.includes(duplicates.find((_) => true)?.dto?.id)) {
                    // skip the first duplicate
                    // and add ids of the duplicates to the list of items to remove
                    itemsToRemove.push(...duplicates.slice(1).map((i) => i.dto.id));
                  }
                });

                itemsToRemove.forEach((id) => {
                  const index = results.result.findIndex((i) => i.dto.id === id);
                  if (index > -1) {
                    results.result.splice(index, 1);
                    // Hits anpassen, da sonst beim runterscrollen versucht wird nachzuladen obwohl evtl. nichts mehr kommt
                    results.hits--;
                  }
                });

                // move all items with impediment to the end of the list
                const itemsToMoveToTheEnd = results.result.filter((i) => i.dto.impediment);
                itemsToMoveToTheEnd.forEach((item) => {
                  const index = results.result.findIndex((i) => i.dto.id === item.dto.id);
                  if (index > -1) {
                    results.result.splice(index, 1);
                    results.result.push(item);
                  }
                });

                this.setSearchResult(results);
                this.setFetching(false);
                this._searchCompleted.next(this.get());
              },
              (err) => {}
            )
          )
      )
    )
  );

  setFetching = this.updater<boolean>((state, fetching) => ({
    ...state,
    fetching,
  }));

  setSearchResult = this.updater<{ hits: number; result: RemissionListItem[] }>((state, { hits, result }) => ({
    ...state,
    items: result,
    hits,
  }));

  setSuppliers = this.updater<SupplierDTO[]>((state, suppliers) => ({
    ...state,
    suppliers,
    selectedSupplierId: state.selectedSupplierId ?? suppliers[0]?.id,
  }));

  setSelectedSupplierId = this.updater<number>((state, supplierId) => {
    if (state.selectedSupplierId === supplierId) {
      return { ...state };
    }
    return {
      ...state,
      selectedSupplierId: supplierId ?? state.selectedSupplierId ?? state.suppliers[0]?.id,
    };
  });

  setSources = this.updater<string[]>((state, sources) => {
    return {
      ...state,
      sources,
      selectedSource: state.selectedSource ? sources.find((source) => source === state.selectedSource) : sources[0],
    };
  });

  setSelectedSource = this.updater<string>((state, source) => ({
    ...state,
    selectedSource: state.sources.find((s) => s === source) ?? state.selectedSource,
  }));

  setFilter = this.updater<UiFilter>((state, filter) => ({
    ...state,
    filter,
  }));

  setItems = this.updater<RemissionListItem[]>((state, items) => ({
    ...state,
    items,
  }));

  setHits = this.updater<number>((state, hits) => ({
    ...state,
    hits,
  }));

  addItems = this.updater<RemissionListItem[]>((state, items) => ({
    ...state,
    items: [...state.items, ...items],
    hits: state.hits + items.length,
  }));

  removeItem = this.updater<RemissionListItem>((state, item) => ({
    ...state,
    items: state.items.filter((i) => i.dto?.id !== item.dto?.id),
    hits: state.hits - 1,
  }));

  updateItem = this.updater<RemissionListItem>((state, item) => {
    return {
      ...state,
      items: state.items.map((i) => (i.dto.id === item.dto.id ? { ...item } : i)),
    };
  });

  updateItemDto = this.updater<ReturnItemDTO | ReturnSuggestionDTO>((state, itemDto) => {
    const itemToUpdate = state.items.find((i) => i.dto?.id === itemDto.id);

    return {
      ...state,
      items: [...state.items.filter((i) => i.dto?.id !== itemDto?.id), { ...itemToUpdate, dto: itemDto }],
    };
  });

  setRequiredCapacities = this.updater<any>((state, requiredCapacities) => ({
    ...state,
    requiredCapacities,
  }));

  cacheCurrentData() {
    this._cache.set(
      { processId: String(this.processId) },
      {
        items: this.items,
        hits: this.hits,
      }
    );
  }

  getCachedData() {
    return (
      this._cache.get<{
        items: RemissionListItem[];
        hits: number;
      }>({ processId: String(this.processId) }) || { items: [], hits: 0 }
    );
  }

  clearCache() {
    this._cache.delete({ processId: String(this.processId) });
  }

  updateCache() {
    this.clearCache();
    this.cacheCurrentData();
  }

  async applyFilter(filter: UiFilter) {
    this._filterChange$.next(true);
    this.setFilter(filter);
    this.clearCache();
    await this._router.navigate([], {
      queryParams: {
        ...filter.getQueryParams(),
        scroll_position: 0,
      },
      queryParamsHandling: 'merge',
    });
  }

  async setSupplier(supplier: SupplierDTO) {
    if (supplier.id === this.selectedSupplier?.id) {
      return;
    }
    this._triggerReload$.next(true);
    this.setSearchResult({ result: [], hits: 0 });
    this.clearCache();
    await this._router.navigate([], {
      queryParams: {
        supplier: supplier.id,
        scroll_position: 0,
      },
      queryParamsHandling: 'merge',
    });
  }

  async setSource(source: string) {
    if (source === this.selectedSource) {
      return;
    }
    this._triggerReload$.next(true);
    this.setSearchResult({ result: [], hits: 0 });
    this.clearCache();
    await this._router.navigate([], {
      queryParams: {
        source,
        scroll_position: 0,
      },
      queryParamsHandling: 'merge',
    });
  }
}
