import {
  Component,
  OnInit,
  ChangeDetectionStrategy,
  Input,
  OnDestroy,
  ViewChild,
  AfterViewInit,
} from '@angular/core';
import {
  Subject,
  BehaviorSubject,
  Observable,
  of,
  NEVER,
  asyncScheduler,
  combineLatest,
} from 'rxjs';
import {
  ShelfSearchFacadeService,
  ShelfNavigationService,
} from '../../../shared/services';
import { AutocompleteDTO, OrderItemListItemDTO } from '@swagger/oms';
import {
  distinctUntilChanged,
  debounceTime,
  switchMap,
  map,
  takeUntil,
  filter,
  shareReplay,
  withLatestFrom,
  startWith,
  take,
} from 'rxjs/operators';
import { SHELF_SCROLL_INDEX } from 'apps/sales/src/app/core/utils/app.constants';
import { ShelfSearchbarComponent } from '../../../components';
import { ShelfFilterService } from '../../../services/shelf-filter.service';
import { SearchStateFacade } from '@shelf-store';
import { AutocompleteOptions } from '../../../defs';
import { isNullOrUndefined, isString } from 'util';
import { ProcessSelectors } from 'apps/sales/src/app/core/store/selectors/process.selectors';
import { Select } from '@ngxs/store';
import { ScrollPositionService } from 'apps/sales/src/app/core/services/scroll-position.service';
import { ORDER_DETAILS_PREFIX } from '../../shelf-search-results/order-details-prefix';
import { NativeContainerService } from 'native-container';

@Component({
  selector: 'app-shelf-search-input',
  templateUrl: 'search-input.component.html',
  styleUrls: ['./search-input.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShelfSearchInputComponent
  implements OnInit, AfterViewInit, OnDestroy {
  NO_RESULT_ERROR_MESSAGE = 'Keine Suchergebnisse';
  GENERAL_ERROR_MESSAGE = 'Ein Fehler ist aufgetreten';

  @Input() allowScan = false;
  @Input() hasAutocomplete = true;

  // Called before search is initiated
  @Input() preSearchCallback: () => Observable<void>;

  // Called after search has been initiated
  @Input() searchCallback: () => void;

  @ViewChild(ShelfSearchbarComponent, { static: true })
  searchbar: ShelfSearchbarComponent;

  @Select(ProcessSelectors.getCurrentProcessId) currentProcessId$: Observable<
    number
  >;

  destroy$ = new Subject();

  isFetching$: Observable<boolean>;
  errorMessage$: Observable<string>;
  hits$ = new Observable();

  autocompleteQueryString$ = new BehaviorSubject<string>(null);
  autocompleteResult$: Observable<AutocompleteDTO[]>;
  showAutocompleteResults$ = new BehaviorSubject<boolean>(true);

  searchMode$: Observable<'standalone' | 'autocomplete'>;

  selectedItem$ = new BehaviorSubject<AutocompleteDTO & AutocompleteOptions>(
    null
  );
  selectedFilters$: Observable<{
    [key: string]: string;
  }>;

  constructor(
    private shelfSearchService: ShelfSearchFacadeService,
    private searchStateFacade: SearchStateFacade,
    private shelfNavigationService: ShelfNavigationService,
    private filterService: ShelfFilterService,
    private scrollPositionService: ScrollPositionService,
    private nativeContainer: NativeContainerService
  ) {}

  ngOnInit() {
    if (this.hasAutocomplete) {
      this.setupAutocompletion();
    }

    this.setUpSearchMode();
    this.initSelectedFilters();
    this.initStore();
  }

  ngAfterViewInit() {
    this.setUpInputFocus();
    this.setUpSearchbarSync();
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }

  reset() {
    this.resetQueryString();
    this.setSelectedAutocompleteItem(null);
    this.searchStateFacade.clearError();
  }

  triggerBarcodeSearch(barcode: string) {
    this.searchStateFacade.setPendingInput(barcode);

    this.triggerSearch({ type: 'scan', value: barcode });
  }

  async triggerSearch(
    {
      type,
      value,
      bypassValidation,
    }: {
      type: 'search' | 'scan';
      value: string;
      bypassValidation?: boolean;
    } = {
      type: 'search',
      value: '',
      bypassValidation: false,
    }
  ) {
    if (this.preSearchCallback) {
      await this.preSearchCallback().pipe(take(1)).toPromise();
    }
    let searchQuery = value;

    if (this.isAutocompleteSelected()) {
      searchQuery = this.selectedItem$.value.query;
      await this.shelfSearchService.search(searchQuery, { bypassValidation });
      this.updateSearchbarValue(searchQuery);
    } else if (type === 'search') {
      await this.shelfSearchService.search(value, {
        bypassValidation,
        allowCustomerCardSearch: true,
      });
    } else if (type === 'scan') {
      await this.shelfSearchService.searchWithBarcode(value, {
        allowCustomerCardSearch: true,
      });
    }

    this.setUpNavigation();
  }

  openScanner() {
    if (this.nativeContainer.isUiWebview()) {
      this.nativeContainer
        .openScanner('scanShelf')
        .pipe(takeUntil(this.destroy$), this.filterScanEvents)
        .subscribe((result) => this.triggerBarcodeSearch(result.data));
    }
  }

  updateAutocomplete(searchQuery: string | Event) {
    if (this.isEvent(searchQuery)) {
      if (this.isClearSearchbarEvent(searchQuery)) {
        return this.setSelectedAutocompleteItem(null);
      }

      return;
    }

    this.updateSearchQuery(searchQuery);
  }

  handleAutocompleteAction(selectItem: AutocompleteDTO & AutocompleteOptions) {
    const oldValue = this.selectedItem$.value && this.selectedItem$.value.query;
    const newValue = selectItem.query;

    if (this.isAutocompleteOverwritten(oldValue, newValue)) {
      return this.setSelectedAutocompleteItem(null);
    }

    this.setSelectedAutocompleteItem(selectItem);
    if (selectItem.query) {
      this.updateSearchbarValue(selectItem.query);
      this.updateSearchQuery(selectItem.query);
    }
  }

  onClickOutside() {
    this.showAutocompleteResults$.next(false);
  }

  private updateSearchQuery(searchQuery: string) {
    this.showAutocompleteResults$.next(true);
    this.autocompleteQueryString$.next(searchQuery);
  }

  private setUpSavePendingInput() {
    this.autocompleteQueryString$
      .pipe(
        takeUntil(this.destroy$),
        map(this.getSearchQuery),
        filter(this.isSearchQuery),
        distinctUntilChanged(this.filterDistinctStrings),
        debounceTime(1000)
      )
      .subscribe((pendingInput) =>
        this.searchStateFacade.setPendingInput(pendingInput)
      );
  }

  private resetSessionStorage(key: string = SHELF_SCROLL_INDEX) {
    sessionStorage.removeItem(key);
  }

  private setupAutocompletion() {
    this.autocompleteResult$ = this.autocompleteQueryString$.pipe(
      debounceTime(250),
      filter(this.isSearchQuery),
      distinctUntilChanged(this.filterDistinctStrings),
      withLatestFrom(this.searchStateFacade.selectedFilter$),
      switchMap(([queryString, selectedFilters]) => {
        if (this.isAutocompleteSelected()) {
          return NEVER;
        }

        if (this.isValidQuery(queryString)) {
          return this.shelfSearchService
            .searchForAutocomplete(queryString, {
              selectedFilters: selectedFilters,
            })
            .pipe(map((result) => result.result));
        }

        return of([]);
      }),
      shareReplay()
    );
  }

  private setUpSearchMode() {
    this.searchMode$ = combineLatest([
      this.autocompleteResult$,
      this.showAutocompleteResults$,
    ]).pipe(
      map(([result, showResult]) => {
        if (!result || !result.length || !showResult) {
          return 'standalone';
        }
        return 'autocomplete';
      })
    );
  }

  private initSelectedFilters() {
    this.selectedFilters$ = this.searchStateFacade.selectedFilter$;
  }

  private initStore() {
    this.isFetching$ = this.searchStateFacade.fetching$.pipe(debounceTime(25));
    this.errorMessage$ = this.searchStateFacade.showNoResultError$.pipe(
      map((showError) => (showError ? this.NO_RESULT_ERROR_MESSAGE : '')),
      startWith('')
    );

    this.setUpErrorCleaner();
    this.setUpSavePendingInput();
  }

  private setUpNavigation() {
    this.searchStateFacade.hasResults$
      .pipe(
        takeUntil(this.destroy$),
        debounceTime(25),
        filter((hasResults) => !!hasResults),
        withLatestFrom(
          this.searchStateFacade.hits$,
          this.searchStateFacade.input$,
          this.searchStateFacade.result$,
          this.currentProcessId$
        ),
        take(1)
      )
      .subscribe(([_, numberOfHits, searchQuery, result, processId]) => {
        this.shelfNavigationService.updateResultPageBreadcrumb({
          numberOfHits,
          searchQuery,
        });

        const key = ORDER_DETAILS_PREFIX + processId;
        this.resetScrollPosition(key);

        if (this.shouldNavigateToResultList(numberOfHits)) {
          this.resetSessionStorage();

          this.shelfNavigationService.navigateToResultList({
            searchQuery,
            numberOfHits,
          });
        }

        if (this.shouldNavigateToDetails(numberOfHits)) {
          this.shelfNavigationService.navigateToDetails(
            this.getDetails(result)
          );
        }

        if (this.searchCallback) {
          this.searchCallback();
        }
      });
  }

  private setUpErrorCleaner() {
    this.autocompleteQueryString$
      .pipe(
        takeUntil(this.destroy$),
        withLatestFrom(this.isFetching$, this.errorMessage$),
        filter(([_, isFetching, errorMessage]) =>
          this.shouldClearError(isFetching, errorMessage)
        )
      )
      .subscribe(() => this.searchStateFacade.clearError());
  }

  private resetScrollPosition(key: string) {
    this.scrollPositionService.deletePosition(key);
  }

  private setUpInputFocus() {
    this.filterService.overlayClosed$
      .pipe(takeUntil(this.destroy$))
      .subscribe(() => this.setFocus());
  }

  private setUpSearchbarSync() {
    this.searchStateFacade.pendingInput$
      .pipe(
        takeUntil(this.destroy$),
        filter((pendingInput) => !isNullOrUndefined(pendingInput))
      )
      .subscribe((pendingInput) =>
        asyncScheduler.schedule(() => this.updateSearchbarValue(pendingInput))
      );
  }

  private setFocus() {
    if (this.searchbar) {
      this.searchbar.setFocus('input');
    }
  }

  private resetQueryString() {
    this.autocompleteQueryString$.next('');
  }

  private setSelectedAutocompleteItem(
    item: (AutocompleteDTO & AutocompleteOptions) | null
  ) {
    this.selectedItem$.next(item);
  }

  private isValidQuery(queryString: string): boolean {
    return !!queryString && queryString.length >= 3;
  }

  private filterDistinctStrings(oldQuery: string, newQuery: string): boolean {
    return (oldQuery || '').toLowerCase() === (newQuery || '').toLowerCase();
  }

  private isAutocompleteSelected() {
    return !!this.selectedItem$.value;
  }

  private isAutocompleteOverwritten(
    oldQuery: string,
    newQuery: string
  ): boolean {
    return this.isEqual(oldQuery, newQuery);
  }

  private getSearchQuery = (queryString: string | Event) => {
    if (isNullOrUndefined(queryString)) {
      return;
    }

    if (this.isString(queryString)) {
      return queryString;
    }

    const targetValue = (<HTMLInputElement>queryString.target).value;

    if (targetValue) {
      return targetValue;
    }
  };

  private isSearchQuery(queryString: string | Event): boolean {
    return typeof queryString === 'string';
  }

  private isString(test: any): test is string {
    return typeof test === 'string';
  }

  private isEqual<T>(val1: T, val2: T): boolean {
    return val1 === val2;
  }

  private isEvent(value: any): value is Event {
    return value && value instanceof Event;
  }

  private isClearSearchbarEvent(event: Event): boolean {
    return event.target instanceof HTMLInputElement;
  }

  private inputOutOfFocus(): boolean {
    return (
      document.activeElement &&
      document.activeElement.tagName &&
      document.activeElement.tagName.toLowerCase() === 'body'
    );
  }

  private updateSearchbarValue(queryString: string) {
    if (this.searchbar) {
      this.searchbar.setInputValue(queryString);
    }
    if (this.inputOutOfFocus()) {
      this.setFocus();
    }
  }

  private shouldNavigateToResultList(hits: number): boolean {
    return hits > 1;
  }

  private shouldNavigateToDetails(hits: number): boolean {
    return hits === 1;
  }

  private shouldClearError(isFetching: boolean, errorMessage: string): boolean {
    return !isFetching && !!errorMessage;
  }

  private getDetails(result: OrderItemListItemDTO[]) {
    return result && result[0];
  }

  private filterScanEvents(
    scanEvent$: Observable<{
      status: string;
      data?: any;
    }>
  ): Observable<{
    status: string;
    data?: any;
  }> {
    return scanEvent$.pipe(
      filter((scanEvent) => {
        if (!scanEvent) {
          return false;
        }
        const barcode = String(scanEvent.data).toLowerCase().trim();
        return barcode && isString(barcode) && barcode !== 'scan started';
      })
    );
  }
}
