import { Injectable, OnDestroy } from '@angular/core';
import { DomainTaskCalendarService } from '@domain/task-calendar';
import { ComponentStore } from '@ngrx/component-store';
import { tapResponse } from '@ngrx/operators';

import { DisplayInfoDTO, InputDTO, QuerySettingsDTO } from '@generated/swagger/eis-api';
import { CalendarIndicator } from '@ui/calendar';
import { DateAdapter } from '@ui/common';
import { Filter, FilterOption, SelectFilter, UiFilter, UiFilterMappingService } from '@ui/filter';
import { UiMessageModalComponent, UiModalRef, UiModalResult, UiModalService } from '@ui/modal';
import { clone } from 'lodash';
import { Observable, Subject, zip } from 'rxjs';
import { debounceTime, map, switchMap, tap, withLatestFrom, first, takeWhile, filter } from 'rxjs/operators';
import { InfoModalComponent } from './modals/info/info-modal.component';
import { PreInfoModalComponent } from './modals/preinfo/preinfo-modal.component';
import { TaskModalComponent } from './modals/task/task-modal.component';

export interface TaskCalendarState {
  mode: 'week' | 'month';
  selectedDate: Date;
  displayedDate: Date;
  displayInfos: DisplayInfoDTO[];
  initialFilter: UiFilter;
  filter: UiFilter;
  message: string;
  searchResults: DisplayInfoDTO[];
  searchTarget: string;
  fetchingFilter: boolean;
  fetchingItems: boolean;
  fetchingSearch: boolean;
  hits: number;
}

@Injectable()
export class TaskCalendarStore extends ComponentStore<TaskCalendarState> implements OnDestroy {
  private readonly _searchbarFocus$ = new Subject<void>();

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

  readonly searchbarFocus$ = this._searchbarFocus$.asObservable();

  readonly selectSelectedDate = this.select((s) => s.selectedDate);

  readonly selectDisplayedDate = this.select((s) => s.displayedDate);

  readonly selectSelectedYear = this.select(this.selectSelectedDate, (selectedDate) =>
    this.dateAdapter.getYear(selectedDate),
  );

  readonly selectSelectedMonth = this.select(this.selectSelectedDate, (selectedDate) =>
    this.dateAdapter.getMonth(selectedDate),
  );

  readonly selectedCalendarWeek = this.select(this.selectSelectedDate, (selectedDate) =>
    this.dateAdapter.getCalendarWeek(selectedDate),
  );

  readonly selectDisplayInfos = this.select((s) => s.displayInfos);

  readonly searchResults$ = this.select((s) => s.searchResults);

  readonly hits$ = this.select((s) => s.hits);

  readonly searchTarget$ = this.select((s) => s.searchTarget);

  readonly selectCalendarIndicators = this.select(this.selectDisplayInfos, (displayItems) =>
    displayItems.reduce<CalendarIndicator[]>((agg, item) => {
      const calendarIndicator = this.mapDisplayInfoToCalendarIndicator(item);
      if (
        !agg.some(
          (s) =>
            this.dateAdapter.equals({ first: s.date, second: new Date(calendarIndicator.date), precision: 'day' }) &&
            s.color === calendarIndicator.color,
        ) &&
        !item.successor // do not show color indicator for items with successor
      ) {
        return [...agg, calendarIndicator];
      }
      return agg;
    }, []),
  );

  readonly selectInitialFilter = this.select((s) => s.initialFilter);

  readonly selectFilter = this.select((s) => s.filter);

  readonly selectFetchingFilter = this.select((s) => s.fetchingFilter);

  readonly selectFetchingItems = this.select((s) => s.fetchingItems);

  readonly selectFetchingSearch = this.select((s) => s.fetchingSearch);

  readonly selectMessage = this.select((s) => s.message);

  readonly selectStartStop = this.select((s) => {
    const { mode, displayedDate } = s;

    if (mode === 'week') {
      const fdow = this.dateAdapter.getFirstDateOfWeek(displayedDate);
      const fdowWithOffset = this.dateAdapter.addCalendarDays(fdow, -7);
      const ldow = this.dateAdapter.getLastDateOfWeek(displayedDate);

      return {
        start: fdowWithOffset,
        stop: ldow,
      };
    } else {
      const month = this.dateAdapter.getDatesAndCalendarWeeksForMonth(displayedDate);
      if (month.length > 0) {
        const firstWeekOfMonth = month[0];
        const lastWeekOfMonth = month[month.length - 1];
        return {
          start: firstWeekOfMonth.dates[0],
          stop: lastWeekOfMonth.dates[lastWeekOfMonth.dates.length - 1],
        };
      }
    }
  });

  hits = this.get((s) => s.hits);

  constructor(
    public domainTaskCalendarService: DomainTaskCalendarService,
    private dateAdapter: DateAdapter,
    private uiModal: UiModalService,
    private uiFilterMappingService: UiFilterMappingService,
  ) {
    super({
      mode: 'month',
      selectedDate: dateAdapter.today(),
      displayedDate: dateAdapter.today(),
      displayInfos: [],
      initialFilter: undefined,
      filter: undefined,
      message: undefined,
      fetchingFilter: false,
      fetchingItems: false,
      fetchingSearch: false,
      searchResults: [],
      hits: undefined,
      searchTarget: '',
    });
  }

  readonly setSelectedDate = this.updater((s, { date }: { date: Date }) => ({
    ...s,
    selectedDate: date,
  }));

  readonly setDisplayedDate = this.updater((s, { date }: { date: Date }) => ({
    ...s,
    displayedDate: date,
  }));

  readonly setFilter = this.updater((s, { filters }: { filters: UiFilter }) => ({
    ...s,
    filter: filters,
  }));

  readonly setMode = this.updater((s, { mode }: { mode: 'week' | 'month' }) => ({
    ...s,
    mode,
  }));

  readonly setMessage = this.updater((s, { message }: { message: string }) => ({
    ...s,
    message,
  }));

  /**
   * Der Key der Datums-Gruppe, zu der initial navigiert werden soll
   */
  readonly setSearchTarget = this.updater((s, { searchTarget }: { searchTarget: string }) => ({
    ...s,
    searchTarget,
  }));

  search = this.effect((options$: Observable<{ clear?: boolean }>) => {
    const currentBranch$ = this.domainTaskCalendarService.currentBranchId$.pipe(filter((f) => !!f));
    const selectFilter$ = this.selectFilter.pipe(filter((f) => !!f));
    return options$.pipe(
      tap(() => this.patchState({ fetchingSearch: true, searchTarget: '' })),
      debounceTime(150),
      switchMap((options) =>
        zip(currentBranch$, selectFilter$).pipe(
          switchMap(([branchId, filter]) => {
            const querytoken = {
              ...filter?.getQueryToken(),
            };

            // Im Zeitraum von 6 Wochen in der Vergangenheit und 2 Wochen in der Zukunft abfragen
            const start = this.dateAdapter.addCalendarDays(this.dateAdapter.today(), -42);
            const stop = this.dateAdapter.addCalendarDays(this.dateAdapter.today(), 14);
            return this.domainTaskCalendarService
              .getInfos({
                ...querytoken,
                filter: {
                  timespan: `"${start.toISOString()}"-"${stop?.toISOString()}"`,
                  ...querytoken?.filter,
                  branch_id: String(branchId),
                },
              })
              .pipe(
                tapResponse(
                  (response) => {
                    if (!response.error) {
                      response = this.preparePreInfos(response);

                      const results = this.get((s) => s.searchResults);
                      const searchResults =
                        results.length > 0 && !options?.clear ? [...results, ...response.result] : [...response.result];
                      const sorted = searchResults.sort((a, b) =>
                        this.dateAdapter.findClosestDate(
                          new Date(a.taskDate || a.publicationDate),
                          new Date(b.taskDate || b.publicationDate),
                        ),
                      );
                      const searchTarget =
                        sorted?.length > 0
                          ? this.domainTaskCalendarService.getDateGroupKey(
                              sorted[0].taskDate || sorted[0].publicationDate,
                            )
                          : '';

                      this.patchState({
                        searchResults,
                        searchTarget,
                        fetchingSearch: false,
                        message: undefined,
                        hits: response.hits,
                      });
                    } else {
                      this.uiModal.open({ content: UiMessageModalComponent, data: response });
                      this.patchState({ searchResults: [], fetchingSearch: false, message: 'Keine Suchergebnisse' });
                    }
                  },
                  (error) => {
                    console.error(error);
                    this.patchState({ searchResults: [], fetchingSearch: false, message: 'Keine Suchergebnisse' });
                  },
                ),
              );
          }),
        ),
      ),
    );
  });

  resetSearch() {
    this.patchState({ searchResults: [], hits: undefined });
  }

  focusSearchbar() {
    this._searchbarFocus$.next();
  }

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

  loadItems = this.effect(($: Observable<void>) => {
    const currentBranch$ = this.domainTaskCalendarService.currentBranchId$.pipe(filter((f) => !!f));
    const selectStartStop$ = this.selectStartStop.pipe(filter((f) => !!f));
    const selectInitialFilter$ = this.selectInitialFilter.pipe(filter((f) => !!f));

    return $.pipe(
      tap(() => this.patchState({ fetchingItems: true })),
      debounceTime(250),
      switchMap((_) =>
        zip(currentBranch$, selectStartStop$, selectInitialFilter$).pipe(
          switchMap(([branchId, date, filter]) => {
            const querytoken = { ...filter?.getQueryToken() };
            return this.domainTaskCalendarService
              .getInfos({
                ...querytoken,
                filter: {
                  ...querytoken?.filter,
                  branch_id: String(branchId),
                  timespan: `"${date?.start?.toISOString()}"-"${date.stop?.toISOString()}"`,
                },
              })
              .pipe(
                tapResponse(
                  (response) => {
                    if (!response.error) {
                      response = this.preparePreInfos(response);
                      this.patchState({ displayInfos: response.result, fetchingItems: false, message: undefined });
                    } else {
                      this.uiModal.open({
                        content: UiMessageModalComponent,
                        data: response,
                      });
                      this.patchState({ displayInfos: [], fetchingItems: false, message: 'Keine Suchergebnisse' });
                    }
                  },
                  (error) => {
                    console.error(error);
                    this.patchState({ displayInfos: [], fetchingItems: false, message: 'Keine Suchergebnisse' });
                  },
                ),
              );
          }),
        ),
      ),
    );
  });

  preparePreInfos(response) {
    const preInfos = response.result.filter((info) => this.domainTaskCalendarService.getInfoType(info) === 'PreInfo');
    preInfos.forEach((info) => {
      const preInfoTask = clone(info);
      delete preInfoTask.publicationDate;
      response.result.push(preInfoTask);
    });
    return response;
  }

  readonly loadFilter = this.effect(($: Observable<void>) =>
    $.pipe(
      tap(() => this.patchState({ fetchingFilter: true })),
      switchMap((_) => this.domainTaskCalendarService.getSettings()),
      tapResponse(
        (response: QuerySettingsDTO) => {
          this.patchState({
            filter: UiFilter.create(response),
            initialFilter: UiFilter.create(response),
            fetchingFilter: false,
          });
        },
        (error) => {
          console.error(error);
          this.patchState({ filter: undefined, initialFilter: undefined, fetchingFilter: false });
        },
      ),
    ),
  );

  open(displayInfoDTO: DisplayInfoDTO) {
    const type = this.domainTaskCalendarService.getInfoType(displayInfoDTO);
    let taskModalRef: UiModalRef<{ successorId: number }, DisplayInfoDTO>;

    switch (type) {
      case 'Info':
        taskModalRef = this.uiModal.open({
          content: InfoModalComponent,
          data: displayInfoDTO,
        });
        break;

      case 'PreInfo':
        taskModalRef = this.uiModal.open({
          content: PreInfoModalComponent,
          data: displayInfoDTO,
        });
        break;

      case 'Task':
        taskModalRef = this.uiModal.open({
          content: TaskModalComponent,
          data: displayInfoDTO,
        });
        break;
    }

    taskModalRef?.afterClosed$.subscribe({
      next: async (result: UiModalResult<{ successorId: number }>) => {
        if (result.data?.successorId) {
          const info = await this.domainTaskCalendarService
            .getInfoById({ infoId: result.data.successorId })
            .pipe(map((res) => res.result))
            .toPromise();
          if (info) {
            this.open(info);
          }
        }
      },
    });

    let hasChanged = false;
    taskModalRef?.afterChanged$.subscribe({
      next: (changed) => (hasChanged = changed),
      complete: async () => {
        if (hasChanged) {
          this.loadItems();
          const searchResults = await this.searchResults$.pipe(first()).toPromise();
          if (searchResults?.length > 0) {
            this.search({ clear: true });
          }
        }
      },
    });
  }

  mapInputArrayToFilterArray(source: InputDTO[]): SelectFilter[] {
    const mappedArr = source.map((input) => {
      return this.uiFilterMappingService.fromInputDto(input as any);
    });
    return mappedArr;
  }

  mapFilterArrayToStringDictionary(source: Filter[]): { [key: string]: string } {
    const dict: { [key: string]: string } = {};
    for (const filter of source) {
      const options: FilterOption[] = filter.options;
      const selected = options.filter((o) => o.selected);
      if (selected.length > 0) {
        dict[filter.key] = selected.map((o) => o.id).join(';');
      }
    }
    return dict;
  }

  mapDisplayInfoToCalendarIndicator(source: DisplayInfoDTO): CalendarIndicator {
    return {
      date: this.domainTaskCalendarService.getDisplayInfoDate(source),
      color: this.domainTaskCalendarService.getProcessingStatusColorCode(source),
    };
  }
}
