import { Injectable } from '@angular/core';
import { Observable, BehaviorSubject, Subject, combineLatest } from 'rxjs';
import { SelectFilter, SelectFilterOption, Filter } from '../../filter';
import { RemissionSelectors } from '../../../core/store/selectors/remission.selectors';
import { RemissionSelectedFilters, RemissionProcess, RemissionService, RemissionFilter } from '@isa/remission';
import { Select, Store } from '@ngxs/store';
import { filter, take, switchMap, withLatestFrom, map, tap, startWith, debounceTime } from 'rxjs/operators';
import { isNullOrUndefined } from 'util';
import { RequestUpdateRemissionFilter } from '../../../core/store/actions/remission.actions';
import { flatten } from '../../../shared/utils';
import { RemissionResourceType } from '../models';

@Injectable({ providedIn: 'root' })
export class RemissionFilterService {
  @Select(RemissionSelectors.getRemissionProcess)
  remissionProcess$: Observable<RemissionProcess>;
  @Select(RemissionSelectors.getRemissionFilters)
  remissionFilter$: Observable<RemissionSelectedFilters>;
  @Select(RemissionSelectors.getRemissionActiveFilters)
  getRemissionActiveFilters$: Observable<{
    [filterId: string]: string[];
  }>;
  @Select(RemissionSelectors.getRemissiontarget)
  remissionTarget$: Observable<string>;
  @Select(RemissionSelectors.getRemissionSource)
  remissionSource$: Observable<RemissionResourceType>;

  private lastFilterGroupChangedState = new BehaviorSubject<Map<string, Filter>>(new Map());
  public filtersByGroup$: Observable<SelectFilter[]>;
  public filters$: Observable<SelectFilter[]>;
  public pendingFilters$ = new BehaviorSubject<Filter[]>(null);
  public resetFilters$ = new Subject<void>();

  public set lastFilterGroupChanged(filterGroup: Filter) {
    this.getSupplierAndFilter$()
      .pipe(take(1))
      .subscribe(([target, supplier]) => this.lastFilterGroupChangedState.value.set(this.getFilterGroupKey(target, supplier), filterGroup));
  }

  public get lastFilterGroupChanged$(): Observable<Filter> {
    return this.getSupplierAndFilter$().pipe(
      map(([target, supplier]) => this.lastFilterGroupChangedState.value.get(this.getFilterGroupKey(target, supplier)))
    );
  }

  constructor(private remissionService: RemissionService, private store: Store) {
    this.filtersByGroup$ = this.getFilters();
    this.filters$ = this.getFlattenedFilters();
  }

  getFilters(): Observable<SelectFilter[]> {
    return combineLatest([
      this.remissionProcess$,
      this.resetFilters$.pipe(
        startWith(true),
        tap(() => this.pendingFilters$.next(null))
      ),
    ]).pipe(
      debounceTime(100),
      filter(([process, _]) => !isNullOrUndefined(process)),
      map(([process, _]) => process.id),
      switchMap((remissionProcessId) =>
        this.remissionService
          .getFilters({
            remissionProcessId,
          })
          .pipe(map((fltr) => fltr.filter((f) => f.options && f.options.length && f.id !== 'sortiment')))
      ),
      withLatestFrom(
        this.getRemissionActiveFilters$.pipe(
          filter((activeFilters) => !isNullOrUndefined(activeFilters)),
          startWith({})
        )
      ),
      map(([filters, activeFilters], index) => {
        // tslint:disable-next-line: no-shadowed-variable
        return filters.map((filter) => {
          const activeFilterMatch = activeFilters[filter.id];

          if (!activeFilterMatch) {
            return {
              ...filter,
              type: 'select',
              key: filter.id,
              options: this.traverse(filter.options as SelectFilterOption[], 'options', []),
            };
          }

          return {
            ...filter,
            type: 'select',
            key: filter.id,
            options: this.traverse(filter.options as SelectFilterOption[], 'options', activeFilterMatch, !index),
          };
        });
      }),
      map((filters) => (filters as unknown) as SelectFilter[]),
      tap((filters) => {
        this.pendingFilters$.next(filters);
        this.setInitialFilterGroupLastChanged(filters);
      }),
      switchMap(() => this.pendingFilters$)
    );
  }

  getFlattenedFilters(): Observable<SelectFilter[]> {
    return this.getFilters().pipe(map((filters) => flatten(filters, 'options')));
  }
  private groupBy(selectedFilter: SelectFilter[]): SelectFilter[] {
    const result: Filter[] = [];

    const uniqueKeys = Array.from(new Set(selectedFilter.map((f) => f.key)));

    function getLabel(key: string | number): string {
      const match = (selectedFilter.find((f) => f.key === key) as unknown) as SelectFilterOption;
      return match.id;
    }

    uniqueKeys.forEach((key) => {
      const obj: SelectFilter = {
        key: getLabel(key),
        name: String(key),
        options: [],
        type: 'select',
      };
      obj.options = (selectedFilter.filter((f) => f.key === key) as unknown) as SelectFilterOption[];

      result.push(obj);
    });

    return result;
  }

  mapToRemissionFilter(
    filters: SelectFilter[]
  ): {
    [filterId: string]: string[];
  } {
    const result: {
      [filterId: string]: string[];
    } = {};

    for (const f of filters) {
      if (f.key) {
        const filterIds = flatten(f.options, 'options')
          .filter((option) => !!option.selected)
          .map((selectedOption) => selectedOption.id);

        if (Array.isArray(filterIds) && filterIds.length) {
          result[f.key] = filterIds;
        }
      }
    }

    return result;
  }

  updateFilters(filters: SelectFilter[]) {
    const changes: Partial<RemissionFilter> = this.mapToRemissionFilter(filters);

    this.remissionProcess$
      .pipe(
        withLatestFrom(this.remissionFilter$),
        take(1),
        // tslint:disable-next-line: no-shadowed-variable
        switchMap(([remissionProcess, selectedFilters]) =>
          this.store.dispatch(
            new RequestUpdateRemissionFilter(remissionProcess.id, {
              skip: 0,
              filter: {
                ...selectedFilters,
                [remissionProcess.filter.source]: {
                  ...selectedFilters[remissionProcess.filter.source],
                  [remissionProcess.filter.target.name]: changes,
                },
              },
            })
          )
        )
      )
      .subscribe((_) => this.resetFilters$.next());
  }

  getGroupedFilters(selectedFilter: SelectFilter[]): SelectFilter[] {
    return this.groupBy(selectedFilter);
  }

  groupGroupedFilters$(filter$: Observable<SelectFilter[]>): Observable<SelectFilter[]> {
    return filter$.pipe(map((fltrs) => this.groupBy(fltrs)));
  }

  traverse<T extends SelectFilterOption>(arr: T[], prop: keyof T, match: string[], setExpanded?: boolean): SelectFilter[] {
    return (arr.map((item) => {
      item.selected = match.includes(item.name) || match.includes(item.id);
      const hasNestedOptions = Array.isArray(item[prop]);

      if (!!hasNestedOptions) {
        item[prop] = this.traverse((item[prop] as unknown) as T[], 'options', match) as any;

        const childFilters = item[prop];
        item.selected = ((childFilters as unknown) as SelectFilterOption[]).every((f) => !!f.selected);

        if (!!setExpanded) {
          const hasNestedOptionsSelected = ((childFilters as unknown) as SelectFilterOption[]).some((f) => !!f.selected);
          item.expanded = !item.selected && !!hasNestedOptionsSelected;
        }
      }

      return item;
    }) as unknown) as SelectFilter[];
  }

  private setInitialFilterGroupLastChanged(filters: SelectFilter[]) {
    this.lastFilterGroupChanged$.pipe(take(1)).subscribe((lastFilterGroupChanged) => {
      if (!lastFilterGroupChanged) {
        this.lastFilterGroupChanged = filters[0];
      }
    });
  }

  private getSupplierAndFilter$(): Observable<[string, RemissionResourceType]> {
    return combineLatest([this.remissionTarget$, this.remissionSource$]).pipe(
      filter(([target, supplier]) => !isNullOrUndefined(target) && !isNullOrUndefined(supplier))
    );
  }

  private getFilterGroupKey(target: string, supplier: RemissionResourceType): string {
    return target.toLowerCase() + supplier.toLowerCase();
  }

  getRemissionFilters(
    f: Filter[]
  ): {
    [filterId: string]: string[];
  } {
    const result: { [filterId: string]: string[] } = {};

    f.forEach((fltr) => {
      const key = fltr.key;
      if (!Array.isArray(fltr.options)) {
        return [];
      }

      const flattened = flatten(fltr.options, 'options');

      result[key] = flattened.map((selectFilterOption) => selectFilterOption.id);
    });

    return result;
  }

  flattenPendingFilters(pendingFilters: Filter[]): SelectFilterOption[] {
    return (flatten(pendingFilters, 'options') as unknown) as SelectFilterOption[];
  }
}
