import { Injectable } from '@angular/core';
import { AuthService } from '@core/auth';
import { DomainAvailabilityService } from '@domain/availability';
import {
  OpenStreetMap,
  OpenStreetMapParams,
  PlaceDto,
} from '@external/openstreetmap';
import { ComponentStore } from '@ngrx/component-store';
import { tapResponse } from '@ngrx/operators';

import { BranchDTO, BranchType } from '@generated/swagger/checkout-api';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { geoDistance, GeoLocation } from '@utils/common';
import { debounceTime, switchMap, tap, withLatestFrom } from 'rxjs/operators';

export interface BranchSelectorState {
  query: string;
  fetching: boolean;
  branches: BranchDTO[];
  filteredBranches: BranchDTO[];
  selectedBranch?: BranchDTO;
  online?: boolean;
  orderingEnabled?: boolean;
  shippingEnabled?: boolean;
  filterCurrentBranch?: boolean;
  currentBranchNumber?: string;
  orderBy?: 'name' | 'distance';
  branchType?: number;
}

function branchSorterFn(a: BranchDTO, b: BranchDTO, userBranch: BranchDTO) {
  return (
    geoDistance(userBranch?.address?.geoLocation, a?.address?.geoLocation) -
    geoDistance(userBranch?.address?.geoLocation, b?.address?.geoLocation)
  );
}

function selectBranches(state: BranchSelectorState) {
  if (!state?.branches) {
    return [];
  }

  let branches = state.branches;

  if (typeof state.online === 'boolean') {
    branches = branches.filter((branch) => !!branch?.isOnline === state.online);
  }

  if (typeof state.orderingEnabled === 'boolean') {
    branches = branches.filter(
      (branch) => !!branch?.isOrderingEnabled === state.orderingEnabled,
    );
  }

  if (typeof state.shippingEnabled === 'boolean') {
    branches = branches.filter(
      (branch) => !!branch?.isShippingEnabled === state.shippingEnabled,
    );
  }

  if (
    typeof state.filterCurrentBranch === 'boolean' &&
    typeof state.currentBranchNumber === 'string'
  ) {
    branches = branches.filter(
      (branch) => branch?.branchNumber !== state.currentBranchNumber,
    );
  }

  if (
    typeof state.orderBy === 'string' &&
    typeof state.currentBranchNumber === 'string'
  ) {
    switch (state.orderBy) {
      case 'name':
        branches?.sort((branchA, branchB) =>
          branchA?.name?.localeCompare(branchB?.name),
        );
        break;
      case 'distance': {
        const currentBranch = state.branches?.find(
          (b) => b?.branchNumber === state.currentBranchNumber,
        );
        branches?.sort((a: BranchDTO, b: BranchDTO) =>
          branchSorterFn(a, b, currentBranch),
        );
        break;
      }
    }
  }

  if (typeof state.branchType === 'number') {
    branches = branches.filter(
      (branch) => branch?.branchType === state.branchType,
    );
  }

  return branches;
}

@Injectable()
export class BranchSelectorStore extends ComponentStore<BranchSelectorState> {
  get query() {
    return this.get((s) => s.query);
  }

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

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

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

  get branches() {
    return this.get(selectBranches);
  }

  readonly branches$ = this.select(selectBranches);

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

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

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

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

  constructor(
    private _availabilityService: DomainAvailabilityService,
    private _uiModal: UiModalService,
    private _openStreetMap: OpenStreetMap,
    auth: AuthService,
  ) {
    super({
      query: '',
      fetching: false,
      filteredBranches: [],
      branches: [],
      online: true,
      orderingEnabled: true,
      shippingEnabled: true,
      filterCurrentBranch: undefined,
      currentBranchNumber: auth.getClaimByKey('branch_no'),
      orderBy: 'name',
      branchType: undefined,
    });
  }

  loadBranches = this.effect(($) =>
    $.pipe(
      tap(() => this.setFetching(true)),
      switchMap(() =>
        this._availabilityService.getBranches().pipe(
          withLatestFrom(this.selectedBranch$),
          tapResponse(
            ([response, selectedBranch]) =>
              this.loadBranchesResponseFn({ response, selectedBranch }),
            (error: Error) => this.loadBranchesErrorFn(error),
          ),
        ),
      ),
    ),
  );

  perimeterSearch = this.effect(($) =>
    $.pipe(
      tap(() => this.beforePerimeterSearch()),
      debounceTime(500),
      switchMap(() => {
        const queryToken = {
          country: 'Germany',
          zipCode: this.query,
          limit: 1,
        } as OpenStreetMapParams.Query;
        return this._openStreetMap.query(queryToken).pipe(
          withLatestFrom(this.branches$),
          tapResponse(
            ([response, branches]) =>
              this.perimeterSearchResponseFn({ response, branches }),
            (error: Error) => this.perimeterSearchErrorFn(error),
          ),
        );
      }),
    ),
  );

  beforePerimeterSearch = () => {
    this.setFilteredBranches([]);
    this.setFetching(true);
  };

  perimeterSearchResponseFn = ({
    response,
    branches,
  }: {
    response: PlaceDto[];
    branches: BranchDTO[];
  }) => {
    const place = response?.[0];
    const branch = this._findNearestBranchByPlace({ place, branches });
    const filteredBranches = [...branches]
      ?.sort((a: BranchDTO, b: BranchDTO) => branchSorterFn(a, b, branch))
      ?.slice(0, 10);
    this.setFilteredBranches(filteredBranches ?? []);
  };

  perimeterSearchErrorFn = (error: Error) => {
    this.setFilteredBranches([]);
    console.error('OpenStreetMap Request Failed! ', error);
  };

  loadBranchesResponseFn = ({
    response,
    selectedBranch,
  }: {
    response: BranchDTO[];
    selectedBranch?: BranchDTO;
  }) => {
    this.setBranches(response ?? []);
    if (selectedBranch) {
      this.setSelectedBranch(selectedBranch);
    }
    this.setFetching(false);
  };

  loadBranchesErrorFn = (error: Error) => {
    this.setBranches([]);
    this._uiModal.open({
      title: 'Fehler beim Laden der Filialen',
      content: UiErrorModalComponent,
      data: error,
      config: { showScrollbarY: false },
    });
  };

  setBranches(branches: BranchDTO[]) {
    this.patchState({ branches });
  }

  setFilteredBranches(filteredBranches: BranchDTO[]) {
    this.patchState({ filteredBranches });
  }

  setSelectedBranch(selectedBranch?: BranchDTO) {
    if (selectedBranch) {
      this.patchState({
        selectedBranch,
        query: this.formatBranch(selectedBranch),
      });
    } else {
      this.patchState({
        selectedBranch,
        query: '',
      });
    }
  }

  setQuery(query: string) {
    this.patchState({ query });
  }

  setFetching(fetching: boolean) {
    this.patchState({ fetching });
  }

  formatBranch(branch?: BranchDTO) {
    return branch
      ? branch.key
        ? branch.key + ' - ' + branch.name
        : branch.name
      : '';
  }

  private _findNearestBranchByPlace({
    place,
    branches,
  }: {
    place: PlaceDto;
    branches: BranchDTO[];
  }): BranchDTO {
    const placeGeoLocation = {
      longitude: Number(place?.lon),
      latitude: Number(place?.lat),
    } as GeoLocation;
    return (
      branches?.reduce((a, b) =>
        geoDistance(placeGeoLocation, a.address.geoLocation) >
        geoDistance(placeGeoLocation, b.address.geoLocation)
          ? b
          : a,
      ) ?? {}
    );
  }

  getBranchById(id: number): BranchDTO {
    return this.branches.find((branch) => branch.id === id);
  }

  setOnline(online: boolean) {
    this.patchState({ online });
  }

  setOrderingEnabled(orderingEnabled: boolean) {
    this.patchState({ orderingEnabled });
  }

  setShippingEnabled(shippingEnabled: boolean) {
    this.patchState({ shippingEnabled });
  }

  setFilterCurrentBranch(filterCurrentBranch: boolean) {
    this.patchState({ filterCurrentBranch });
  }

  setOrderBy(orderBy: 'name' | 'distance') {
    this.patchState({ orderBy });
  }

  setBranchType(branchType: BranchType) {
    this.patchState({ branchType });
  }
}
