import { Injectable } from '@angular/core';
import { RemissionService } from './remission.service';
import { Observable, of, combineLatest, BehaviorSubject, forkJoin, throwError, merge, concat, NEVER } from 'rxjs';
import {
  map,
  shareReplay,
  flatMap,
  tap,
  take,
  switchMap,
  distinctUntilChanged,
  takeWhile,
  withLatestFrom,
  debounceTime,
  filter,
  mergeMap,
  catchError,
  first,
  retry,
  delay,
} from 'rxjs/operators';
import { ProcessStoreService } from './process-store.service';
import { UidGeneratorService } from '../generators/uid-generator.service';
import { RestFilterService } from '../rest/rest-filter.service';
import { MappingService } from '../mappings/mapping.service';
import { RestRemissionProductsService } from '../rest/rest-remission-products.service';
import { RemissionProcess } from '../models/remission-process';
import { RemissionFilter } from '../models/remission-filter';
import { RemissionProduct } from '../models/remission-product';
import { Printer } from '../models/printer';
import { Product } from '../models/product';
import { Mapper } from '../mappings/mapper';
import { RemissionSourceType } from '../types/remission-source.type';
import { Filter } from '../models/filter';
import { RemissionPlacementType } from '../types/remission-placement-types';
import { RemissionSupplier } from '../models/remission-supplier';
import { ShippingDocument } from '../models/shipping-document';
import { ActionResult, CapacityType } from '../models';
import { compare } from '../utils/compare';
import { addDays } from '../utils/add-days';
import {
  RemiService,
  SupplierService,
  ReturnService,
  ReceiptDTO,
  SupplierDTO,
  ProductDTO,
  PriceDTO,
  ReturnItemDTO,
  ReceiptItemDTO,
  ReturnSuggestionValues,
  ReturnQueryTokenDTO,
  ReturnDTO,
  ResponseArgsOfValueTupleOfReceiptItemDTOAndReturnItemDTO,
  ResponseArgsOfValueTupleOfReceiptItemDTOAndReturnSuggestionDTO,
  StockService,
} from '@swagger/remi';
import { ItemDTO, SearchService } from '@swagger/cat';
import { PrintService, InventoryPrintService, PrintRequestOfString } from '@swagger/print';
import { isNullOrUndefined, memorize } from '@utils/common';
import { HttpErrorResponse } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class RestRemissionService extends RemissionService {
  reloadRemissionSub = new BehaviorSubject<void>(undefined);
  reloadProductsSub = new BehaviorSubject<void>(undefined);
  reloadCapacitiesSub = new BehaviorSubject<void>(undefined);
  currentRemissionSource$ = new BehaviorSubject<RemissionSourceType>(undefined);
  remissionProducts$ = new Map<
    number,
    Observable<{
      skip?: number;
      take?: number;
      hits?: number;
      items: RemissionProduct[];
      completed: boolean;
    }>
  >();

  stock$ = this.stockService.StockCurrentStock().pipe(
    map((response) => response.result),
    shareReplay()
  );

  targets$ = this.getRemissionTargets().pipe(take(1), shareReplay());

  constructor(
    private uidGenerator: UidGeneratorService,
    private processStore: ProcessStoreService,
    private remiService: RemiService,
    private stockService: StockService,
    private supplierAPI: SupplierService,
    private returnApi: ReturnService,
    private restFilterService: RestFilterService,
    private mappingService: MappingService,
    private restRemissionProcessService: RestRemissionProductsService,
    private searchApi: SearchService,
    private printApi: PrintService,
    private inventoryPrintService: InventoryPrintService
  ) {
    super();
  }

  createProcess(source?: RemissionProcess): Observable<RemissionProcess> {
    const process$: Observable<RemissionProcess> = of({
      id: this.uidGenerator.generate({ type: 'number' }),
      returnGroup: this.uidGenerator.generate({ type: 'number' }),
      capacities: [],
      filter: {
        skip: 0,
        take: 25,
        source: 'zentral',
        target: undefined,
        filter: {
          ueberlauf: {
            Blank: {},
            ZL: {},
          },
          zentral: {
            Blank: {},
            ZL: {},
          },
        },
      } as RemissionFilter,
      stockId: undefined,
    });

    return combineLatest([process$, this.stock$, this.targets$]).pipe(
      take(1),
      map(([process, stock, targets]) => ({
        ...process,
        stockId: stock.id,
        filter: {
          ...process.filter,
          target: targets[0],
        },
      })),
      flatMap((process) => this.processStore.set(process))
    );
  }

  continueProcess(source: RemissionProcess): Observable<RemissionProcess> {
    const receiptMapper = this.mappingService.get<Mapper<ReceiptDTO, ShippingDocument>>('ReceiptDTO', 'ShippingDocument');

    let process$ = this.processStore.get(source.id);

    if (process$ === undefined) {
      process$ = this.processStore.set(source);
    }

    if (!source.externalId) {
      return process$.asObservable();
    }

    const remissionFromBackend$ = this.returnApi.ReturnGetReturn({ returnId: source.externalId }).pipe(
      map((response) => response.result),
      mergeMap((returnDto) => {
        const remission = {
          ...source,
          externalId: returnDto.id,
          completed: !!returnDto['completed'],
          filter: {
            ...(source.filter || {}),
            filter: {
              ...((source.filter && source.filter.filter) || {
                ueberlauf: {
                  Blank: {},
                  ZL: {},
                },
                zentral: {
                  Blank: {},
                  ZL: {},
                },
              }),
            },
          },
        };

        // Do not fetch shipping documents if not set on remission
        if (!returnDto.receipts) {
          remission.shippingDocuments = [];
          return of(remission as RemissionProcess);
        }

        // for remissions with old filter settings
        if (!this.restFilterService.checkFilterValidity(remission.filter.filter)) {
          remission.filter.filter = this.restFilterService.resetRemissionSelectedFilters();
        }

        return this.returnApi
          .ReturnGetReturnReceipts({
            eagerLoading: 3,
            returnId: returnDto.id,
          })
          .pipe(
            map((response) => response.result),
            take(1),
            map((receipts) => receipts.map((receipt) => receiptMapper.map(receipt))),
            map((shippingDocuments) => ({
              ...remission,
              shippingDocuments,
              filter: {
                ...remission.filter,
                ...source.filter,
                filter: {
                  ...remission.filter.filter,
                  ...source.filter.filter,
                },
              },
            })),
            withLatestFrom(this.stock$),
            mergeMap(([remi, stock]) => {
              const shippingDocumentsWithProductGroupNames$ = forkJoin(
                // tslint:disable-next-line: no-shadowed-variable
                remi.shippingDocuments.map((shippingDocument) => {
                  const productsWithGroupNames$ = this.restRemissionProcessService.addGroupnameToProducts(
                    shippingDocument.products,
                    stock.id
                  );

                  return productsWithGroupNames$.pipe(
                    map((products) => ({
                      ...shippingDocument,
                      products,
                    }))
                  );
                })
              );

              return shippingDocumentsWithProductGroupNames$.pipe(
                map((shippingDocuments) => ({
                  ...remi,
                  shippingDocuments,
                }))
              );
            }),
            tap((continuedRemission) =>
              this.updateRemissionFilter({
                remissionProcessId: continuedRemission.id,
                changes: continuedRemission.filter,
              })
            )
          );
      })
    );

    return combineLatest([process$, remissionFromBackend$]).pipe(
      map(([process, remissionFromBackend]) => ({
        ...process,
        ...remissionFromBackend,
      })),
      take(1),
      flatMap((continuedRemission) => this.processStore.set(continuedRemission))
    );
  }

  continueProcessFromScan(containerId: string): Observable<RemissionProcess> {
    return this.getAllRemissions({
      receiptNumber: containerId,
    }).pipe(
      map((results) => results[0]),
      filter((firstHit) => !!firstHit),
      take(1),
      flatMap((newProcess) => this.processStore.set(newProcess))
    );
  }

  startRemission(params: { remissionProcessId: number; createShippingDocument?: boolean }): Observable<RemissionProcess> {
    const process$ = this.processStore.get(params.remissionProcessId);

    if (process$ === undefined) {
      throw new Error('Prozess nicht verfügbar.');
    }

    // prozess starten
    let nextProcess$ = this.returnApi
      .ReturnCreateReturn({
        data: {
          supplier: {
            id: process$.value.filter.target.id,
          },
          returnGroup: String(process$.value.returnGroup),
        },
      })
      .pipe(
        map((response) => {
          return ({
            ...process$.value,
            externalId: response.result.id,
          } as unknown) as RemissionProcess;
        }),
        tap((result) => process$.next(result))
      );

    if (params.createShippingDocument) {
      nextProcess$ = nextProcess$.pipe(
        flatMap((process) =>
          this.createShippingDocument({
            remissionProcessId: process.id,
          }).pipe(
            map(
              (document) =>
                (({
                  ...process,
                  shippingDocuments: process.shippingDocuments ? [...process.shippingDocuments, document] : [document],
                } as unknown) as RemissionProcess)
            )
          )
        ),
        tap((result) => process$.next(result))
      );
    }

    return nextProcess$.pipe(take(1));
  }

  // Schließt eine Remission ab (externalId)
  protected completeRemission(params: { remissionProcessId: number }): Observable<boolean> {
    const process = this.processStore.get(params.remissionProcessId);
    console.warn('Remission Completed');

    if (process === undefined) {
      throw new Error('Prozess nicht verfügbar.');
    }

    return this.returnApi
      .ReturnFinalizeReturn({
        returnId: process.value.externalId,
      })
      .pipe(map((response) => !response.error));
  }

  // Schließt alle Remissionen unter einer id (returnGroup) ab
  completeRemissions(params: { remissionProcessId: number }): Observable<boolean> {
    const process = this.processStore.get(params.remissionProcessId);
    console.warn('Remission Completed');

    if (process === undefined) {
      throw new Error('Prozess nicht verfügbar.');
    }

    return this.returnApi
      .ReturnFinalizeReturnGroup({
        returnGroup: String(process.value.returnGroup),
      })
      .pipe(map((response) => !response.error));
  }

  getRemission(params: { remissionProcessId: number }): Observable<RemissionProcess> {
    return this.processStore.get(params.remissionProcessId).asObservable();
  }

  getRemissionProducts(params: {
    remissionProcessId: number;
  }): Observable<{
    skip?: number;
    take?: number;
    hits?: number;
    items: RemissionProduct[];
    completed: boolean;
  }> {
    const process$ = this.processStore.get(params.remissionProcessId).pipe(distinctUntilChanged((x, y) => compare(x.filter, y.filter)));

    if (process$ === undefined) {
      throw new Error('Prozess nicht verfügbar.');
    }

    this.reloadProducts(params);

    if (isNullOrUndefined(this.remissionProducts$.get(params.remissionProcessId))) {
      this.remissionProducts$.set(
        params.remissionProcessId,
        combineLatest([process$.pipe(distinctUntilChanged(compare)), this.stock$, this.reloadProductsSub]).pipe(
          debounceTime(50),
          switchMap(([process, stock]) =>
            this.restRemissionProcessService.getRemissionproducts({
              filter: process.filter,
              stockId: stock.id,
            })
          ),
          shareReplay()
        )
      );
    }

    return this.remissionProducts$.get(params.remissionProcessId);
  }

  updateRemissionFilter(changes: { remissionProcessId: number; changes: Partial<RemissionFilter> }): Observable<RemissionFilter> {
    const processSubject = this.processStore.get(changes.remissionProcessId);

    if (processSubject === undefined) {
      throw new Error('Prozess nicht verfügbar.');
    }
    const process = processSubject.value;

    const nextProcess: RemissionProcess = {
      ...process,
      filter: {
        ...process.filter,
        ...changes.changes,
        filter: { ...process.filter.filter, ...changes.changes.filter },
      },
    };

    const selectedFilters: {
      [filterId: string]: string[];
    } = this.restFilterService.getSelectedFilters(nextProcess.filter);

    // Avoid multiple emissions on multiple updateRemissionFilter calls
    this.currentRemissionSource$.next(nextProcess.filter.source);

    if (changes.changes.source === 'ueberlauf' || (process.filter.source === 'ueberlauf' && !!changes.changes.filter)) {
      const supplierId = nextProcess.filter.target && nextProcess.filter.target.id;
      return this.updateCapacities({
        selectedFilters,
        supplierId,
      }).pipe(
        map((capacities) => ({ ...nextProcess, capacities })),
        takeWhile((newProcess) => newProcess.filter.source === this.currentRemissionSource$.value),
        take(1),
        flatMap((newProcess) => this.processStore.set(newProcess)),
        tap((_) => !!changes.changes.filter && this.reloadProductsSub.next()),
        map((m) => m.filter)
      );
    }

    this.processStore.set(nextProcess);

    return processSubject.pipe(
      takeWhile((newProcess) => newProcess.filter.source === this.currentRemissionSource$.value),
      take(1),
      tap((_) => !!changes.changes.filter && this.reloadProductsSub.next()),
      map((m) => m.filter)
    );
  }

  isPrintingRequired(params: { remissionProcessId: number }): Observable<boolean> {
    const process$ = this.processStore.get(params.remissionProcessId);

    if (process$ === undefined) {
      throw new Error('Prozess nicht verfügbar.');
    }

    // fail-safe in case process is already completed
    return process$.value.filter.target.id === 1 ? of(true) : of(false);
  }

  getPrinters(): Observable<Printer[]> {
    return this.printApi.PrintOfficePrinters().pipe(map((response) => response.result.map((printer) => (printer as unknown) as Printer)));
  }

  printRemissionList(params: { remissionProcessId: number; printerKey: string }): Observable<boolean> {
    console.warn('Method not available');
    return of(false);
  }

  printShippingDocument(params: { remissionProcessId: number; shippingDocumentId: number; printerKey: string }): Observable<boolean> {
    const process$ = this.processStore.get(params.remissionProcessId);

    if (process$ === undefined) {
      throw new Error('Prozess nicht verfügbar.');
    }

    const printRequest: PrintRequestOfString = {
      printer: params.printerKey,
      data: String(process$.value.returnGroup),
    };

    return this.inventoryPrintService.InventoryPrintPrintReturnGroup(printRequest).pipe(map((response) => !response.error));
  }

  searchProduct(params: { ean: string }): Observable<Product> {
    const mapper = this.mappingService.get<Mapper<ItemDTO, Product>>('ItemDTO', 'Product');

    const products$ = this.stock$.pipe(
      flatMap((stock) =>
        this.searchApi
          .SearchSearch({
            queryToken: {
              input: { qs: params.ean },
              stockId: stock.id,
            },
            stockId: null,
          })
          .pipe(
            catchError((_) =>
              of({
                hits: 0,
                completed: true,
                result: [],
              })
            )
          )
      ),
      map((response) => {
        if (!response.hits) {
          return null;
        }
        return mapper.map(response.result[0]);
      })
    );

    const productWithGroupName$ = combineLatest([this.stock$, products$]).pipe(
      flatMap(([stock, product]) => {
        if (!product || !product.ean) {
          return of(null as RemissionProduct);
        }

        if (!product.productGroup) {
          return of(product);
        }

        return this.restRemissionProcessService.getProductGroupNames(stock.id).pipe(
          map((groupNames) => {
            return {
              ...product,
              productGroupName: groupNames.find((groupName) => groupName.key === product.productGroup).value,
            };
          })
        );
      })
    );

    return productWithGroupName$;
  }

  getFilters(params: { remissionProcessId: number }): Observable<Filter[]> {
    const process$ = this.processStore.get(params.remissionProcessId);

    if (process$ === undefined) {
      throw new Error('Prozess nicht verfügbar.');
    }

    return combineLatest([this.stock$, process$]).pipe(
      distinctUntilChanged((x, y) => compare(x[1].filter.source, y[1].filter.source) && compare(x[1].filter.target, y[1].filter.target)),
      switchMap(([stock, process]) =>
        concat(
          of([]),
          this.restFilterService.getFilter({
            remissionSourceType: process.filter.source || null,
            supplierId: process.filter.target.id || null,
            stockId: stock.id,
          })
        )
      )
    );
  }

  getPlacementTypes(): Observable<RemissionPlacementType[]> {
    const remissionPlacementTypes: RemissionPlacementType[] = ['Stapel', 'Leistung'];
    return of<RemissionPlacementType[]>(remissionPlacementTypes);
  }
  getRemissionReasons(): Observable<string[]> {
    return this.stock$.pipe(
      flatMap((stock) => this.returnApi.ReturnGetReturnReasons({ stockId: stock.id })),
      map((response) => response.result.map((reason) => reason.value))
    );
  }

  @memorize()
  getRemissionTargets(): Observable<RemissionSupplier[]> {
    const supplierMapper = this.mappingService.get<Mapper<SupplierDTO, RemissionSupplier>>('SupplierDTO', 'RemissionSupplier');

    return this.stock$.pipe(
      mergeMap((stock) => this.supplierAPI.SupplierGetSuppliers({ stockId: stock.id })),
      map((suppliers) => suppliers.result.map((supplier) => supplierMapper.map(supplier))),
      mergeMap((suppliers) => {
        return !!suppliers?.length ? of(suppliers) : throwError(new Error('Keine Supplier'));
      }),
      retry(2),
      shareReplay()
    );
  }

  getRemissionSources(): Observable<RemissionSourceType[]> {
    const remissionSources: RemissionSourceType[] = ['zentral', 'ueberlauf'];
    return of(remissionSources);
  }

  getShippingDocuments(params: { remissionProcessId: number; shippingDocumentId: number }): Observable<ShippingDocument> {
    const process$ = this.processStore.get(params.remissionProcessId);

    if (process$ === undefined) {
      throw new Error('Prozess nicht verfügbar.');
    }

    return process$.pipe(
      filter((p) => !!p.shippingDocuments),
      map((p) => p.shippingDocuments.find((f) => f.id === params.shippingDocumentId))
    );
  }

  deleteProductFromRemissionList(input: { remissionProcessId: number; remissionProductId: number }): Observable<ActionResult<boolean>> {
    const process = this.processStore.get(input.remissionProcessId);

    if (!process) {
      return of({
        error: true,
        result: false,
        message: 'Die Remission konnte nicht gefunden werden',
      });
    }

    const supplierId = process.value.filter.target && process.value.filter.target.id;

    // TODO: Fehlerhandling überprüfen

    return this.returnApi
      .ReturnDeleteReturnItem({
        itemId: input.remissionProductId,
      })
      .pipe(
        map((response) => {
          const hasError = response.error;

          if (!hasError) {
            this.restRemissionProcessService.removePriorityProducts(input.remissionProductId, supplierId);
            this.reloadProducts({
              remissionProcessId: input.remissionProcessId,
            });
          }

          return {
            error: hasError,
            errorReasons: response.invalidProperties,
            // httpError: response.httpError,
            message: response.message,
            result: !hasError,
          } as ActionResult<boolean>;
        })
      );
  }

  addProductToRemit(input: { product: Product; remissionReason: string; remissionQuantity: number }): Observable<RemissionProduct> {
    const productMapper = this.mappingService.get<Mapper<Product, ProductDTO>>('Product', 'ProductDTO');
    const priceMapper = this.mappingService.get<Mapper<Product, PriceDTO>>('Product', 'PriceDTO');

    const assortmentMapper = this.mappingService.get<Mapper<{ key: string; name: string; value?: string }[], string>>(
      'Features',
      'Assortment'
    );

    const remissionProductMapper = this.mappingService.get<Mapper<ReturnItemDTO, RemissionProduct>>('ReturnItemDTO', 'RemissionProduct');

    const supplierMapper = this.mappingService.get<Mapper<SupplierDTO, RemissionSupplier>>('SupplierDTO', 'RemissionSupplier');

    return this.stock$.pipe(
      first(),
      mergeMap((stock) =>
        this.returnApi.ReturnCreateReturnItem({
          data: {
            product: productMapper.map(input.product),
            assortment: assortmentMapper.map(input.product.features),
            stock: { id: stock.id },
            predefinedReturnQuantity: input.remissionQuantity,
            returnReason: input.remissionReason,
            retailPrice: priceMapper.map(input.product),
            source: 'manually-added',
          },
        })
      ),
      withLatestFrom(this.targets$),
      map(([returnItem, targets]) => {
        const result = returnItem.result;
        const product = remissionProductMapper.map(result);

        if (!!result.returnTo) {
          let newTarget: SupplierDTO;
          if (result.returnTo.id) {
            newTarget = targets.find((t) => t.id === result.returnTo.id);
          }
          const target = supplierMapper.map(newTarget || result.returnTo);

          product.target = target;
        }

        return {
          newProduct: product,
          supplier: result.returnTo,
        };
      }),
      tap(({ newProduct, supplier }) => {
        this.restRemissionProcessService.updatePriorityProducts(newProduct.id, supplier.id || 1);
        this.reloadProductsSub.next();
      }),
      map(({ newProduct }) => newProduct)
    );
  }

  getCapacities(params: { selectedFilters?: { [filterId: string]: string[] }; supplierId: number }): Observable<CapacityType[]> {
    return this.fetchCapacities$(params);
  }

  private fetchCapacities$(params: { selectedFilters?: { [filterId: string]: string[] }; supplierId: number }): Observable<CapacityType[]> {
    return this.stock$.pipe(
      switchMap((stock) =>
        this.remiService.RemiGetRequiredCapacities({
          stockId: stock.id,
          payload: {
            departments: params.selectedFilters.abteilungen || params.selectedFilters.department || [],
            supplierId: params.supplierId,
          },
        })
      ),
      map((response) => response.result),
      map((capacities) => {
        if (isNullOrUndefined(capacities) || !capacities.length) {
          return [
            {
              name: 'Leistungsplätze',
              utilized: 0,
              available: 0,
              label: 'Exemplaren',
            },
            { name: 'Stapelplätze', utilized: 0, available: 0, label: 'Titel' },
          ];
        }

        return capacities.map((capacity, index) => ({
          name: capacity.item5,
          utilized: capacity.item3 > capacity.item2 ? capacity.item2 : capacity.item3,
          available:
            (capacity.item4 || 0) < capacity.item2
              ? capacity.item4 || (capacity.item3 > capacity.item2 ? capacity.item2 : capacity.item3) || 0
              : capacity.item2,
          label: index === 0 ? 'Exemplaren' : 'Titel',
        }));
      }),
      take(1)
    );
  }

  private updateCapacities(params: { selectedFilters?: { [filterId: string]: string[] }; supplierId: number }): Observable<CapacityType[]> {
    return this.fetchCapacities$(params);
  }

  private updateShippingDocumentProducts(params: {
    remissionProcessId: number;
    placementType: string;
    // TODO: Typen prüfen
    response: ResponseArgsOfValueTupleOfReceiptItemDTOAndReturnSuggestionDTO | ResponseArgsOfValueTupleOfReceiptItemDTOAndReturnItemDTO;
  }) {
    if (!params.response.result || !params.response.result.item1) {
      return;
    }

    const process = this.processStore.get(params.remissionProcessId);
    const mapper = this.mappingService.get<Mapper<ReceiptItemDTO, RemissionProduct>>('ReceiptItemDTO', 'RemissionProduct');

    this.stock$
      .pipe(
        flatMap((stock) =>
          this.restRemissionProcessService.getProductGroupNames(stock.id).pipe(
            map((groupNames) => {
              const product = mapper.map(params.response.result.item1);
              return {
                ...product,
                placementType: params.placementType,
                productGroupName: groupNames.find((groupName) => groupName.key === product.productGroup).value,
              } as RemissionProduct;
            })
          )
        ),
        map((product) => {
          const shippingDocuments = process.value.shippingDocuments;
          const shippingDocumentToUpdate = shippingDocuments.find((doc) => doc.id === params.response.result.item1.receipt.id);
          const updatedShippingDoc: ShippingDocument = {
            ...shippingDocumentToUpdate,
            products: [...shippingDocumentToUpdate.products, product],
          };
          return updatedShippingDoc;
        })
      )
      .subscribe((updatedShippingDoc) =>
        process.next({
          ...process.value,
          shippingDocuments: [...process.value.shippingDocuments.filter((doc) => doc.id !== updatedShippingDoc.id), updatedShippingDoc],
        })
      );
  }

  addProductToShippingDocument(params: {
    remissionProcessId: number;
    shippingDocumentId: number;
    remissionProductId: number;
    placementType?: string;
    quantity?: number;
    inStock?: number;
    predefinedRemissionQuantity?: number;
  }): Observable<ActionResult<boolean>> {
    const process = this.processStore.get(params.remissionProcessId);

    if (process === undefined) {
      return of<ActionResult<boolean>>({
        error: true,
        message: 'Prozess nicht verfügbar.',
        result: false,
      });
    }

    const source = process.value.filter && process.value.filter.source;

    if (source === undefined) {
      return of<ActionResult<boolean>>({
        error: true,
        message: 'Unbekannter Artikel-Ursprung (zentrales Sortiment oder Überlauf)',
        result: false,
      });
    }

    if (params.quantity <= 0) {
      switch (source) {
        case 'ueberlauf':
          return this.returnApi
            .ReturnReturnSuggestionImpediment({
              itemId: params.remissionProductId,
              data: {
                comment: 'Produkt nicht gefunden',
              },
            })
            .pipe(
              tap((_) =>
                this.reloadProducts({
                  remissionProcessId: params.remissionProcessId,
                })
              ),
              catchError(this.handleHttpError),
              map((response) => {
                const hasError = response.error; // || !!response.httpError;
                return {
                  error: hasError,
                  errorReasons: response.invalidProperties,
                  // httpError: response.httpError,
                  message: response.message,
                  result: false,
                } as ActionResult<boolean>;
              })
            );

        case 'zentral':
          return this.returnApi
            .ReturnReturnItemImpediment({
              itemId: params.remissionProductId,
              data: {
                comment: 'Produkt nicht gefunden',
              },
            })
            .pipe(
              tap((_) =>
                this.reloadProducts({
                  remissionProcessId: params.remissionProcessId,
                })
              ),
              catchError(this.handleHttpError),
              map((response) => {
                const hasError = response.error; // || !!response.httpError;
                return {
                  error: hasError,
                  errorReasons: response.invalidProperties,
                  // httpError: response.httpError,
                  message: response.message,
                  result: false,
                } as ActionResult<boolean>;
              })
            );
      }
    }

    if (source === 'ueberlauf') {
      const returnSuggestionValues: ReturnSuggestionValues = {
        inStock: params.inStock,
        quantity: params.quantity,
        remainingQuantity: params.inStock - params.quantity <= 0 ? null : params.inStock - params.quantity,
        placementType: params.placementType,
        returnSuggestionId: params.remissionProductId,
      };

      const isPartialRemit = params.predefinedRemissionQuantity && params.predefinedRemissionQuantity > params.quantity;

      if (isPartialRemit) {
        returnSuggestionValues.impedimentComment = 'Restmenge';
      }

      return this.returnApi
        .ReturnAddReturnSuggestion({
          returnId: process.value.externalId,
          receiptId: params.shippingDocumentId,
          data: returnSuggestionValues,
        })
        .pipe(
          tap(
            (response) =>
              !response.error &&
              this.updateShippingDocumentProducts({
                response,
                remissionProcessId: params.remissionProcessId,
                placementType: params.placementType,
              })
          ),
          catchError(this.handleHttpError),
          map((r) => {
            const hasError = r.error; // || !!r.httpError;
            return {
              error: hasError,
              errorReasons: r.invalidProperties,
              // httpError: r.httpError,
              message: r.message,
              result: !hasError,
            } as ActionResult<boolean>;
          }),
          switchMap((result) => {
            const selectedFilters = this.restFilterService.getSelectedFilters(process.value.filter);

            const supplierId = process.value.filter && process.value.filter.target && process.value.filter.target.id;

            return this.updateCapacities({
              selectedFilters,
              supplierId,
            }).pipe(
              map((capacities) => {
                this.processStore.set({ ...process.value, capacities });

                return result;
              })
            );
          }),
          tap((_) =>
            this.reloadProducts({
              remissionProcessId: params.remissionProcessId,
            })
          )
        );
    }

    return this.returnApi
      .ReturnAddReturnItem({
        returnId: process.value.externalId,
        receiptId: params.shippingDocumentId,
        data: {
          returnItemId: params.remissionProductId,
          placementType: params.placementType,
          quantity: params.quantity,
          inStock: params.inStock,
        },
      })
      .pipe(
        tap(
          (response) =>
            !response.error &&
            this.updateShippingDocumentProducts({
              response,
              remissionProcessId: params.remissionProcessId,
              placementType: params.placementType,
            })
        ),
        catchError(this.handleHttpError),
        map((r) => {
          const hasError = r.error; // || !!r.httpError;
          return {
            error: hasError,
            errorReasons: r.invalidProperties,
            // httpError: r.httpError,
            message: r.message,
            result: !hasError,
          } as ActionResult<boolean>;
        }),
        tap((_) => this.restRemissionProcessService.removePriorityProducts(params.remissionProductId, process.value.filter.target.id || 1)),
        tap((_) =>
          this.reloadProducts({
            remissionProcessId: params.remissionProcessId,
          })
        )
      );
  }
  removeProductFromShippingDocument(params: {
    remissionProcessId: number;
    shippingDocumentId: number;
    remissionProductId: number;
    placementType?: string;
  }): Observable<boolean> {
    const process = this.processStore.get(params.remissionProcessId);

    return this.returnApi
      .ReturnRemoveReturnItem({
        returnId: process.value.externalId,
        receiptId: params.shippingDocumentId,
        receiptItemId: params.remissionProductId,
      })
      .pipe(
        switchMap((response) => {
          let newProcess: RemissionProcess;
          if (!response.error) {
            let shippingDocToUpdate = process.value.shippingDocuments.find((document) => document.id === params.shippingDocumentId);
            shippingDocToUpdate = {
              ...shippingDocToUpdate,
              products: shippingDocToUpdate.products.filter((product) => product.id !== params.remissionProductId),
            };
            newProcess = {
              ...process.value,
              shippingDocuments: [
                ...process.value.shippingDocuments.filter((document) => document.id !== params.shippingDocumentId),
                shippingDocToUpdate,
              ],
            };
          }

          this.reloadCapacitiesSub.next();
          this.reloadProducts({
            remissionProcessId: params.remissionProcessId,
          });

          if (process.value.filter.source === 'ueberlauf') {
            const selectedFilters = this.restFilterService.getSelectedFilters(newProcess.filter);

            const supplierId = process.value.filter && process.value.filter.target && process.value.filter.target.id;

            return this.updateCapacities({
              selectedFilters,
              supplierId,
            }).pipe(
              tap((capacities) => process.next({ ...newProcess, capacities })),
              map((_) => !response.error)
            );
          }

          return of(!response.error).pipe(tap((_) => process.next(newProcess)));
        })
      );
  }

  reloadProducts(params: { remissionProcessId: number }): void {
    this.reloadProductsSub.next();
  }

  completeShippingDocument(params: {
    remissionProcessId: number;
    shippingDocumentId: number;
    containerId: string;
  }): Observable<ActionResult<boolean>> {
    const process = this.processStore.get(params.remissionProcessId);

    if (process === undefined) {
      throw new Error('Prozess nicht verfügbar.');
    }

    return this.returnApi
      .ReturnFinalizeReceipt({
        returnId: process.value.externalId,
        receiptId: params.shippingDocumentId,
        data: {
          packageCode: params.containerId,
        },
      })
      .pipe(
        tap((response) => {
          if (!response.error) {
            const shippingDocuments = process.value.shippingDocuments;
            let shippingDocumentToComplete = shippingDocuments.find((document) => document.id === params.shippingDocumentId);
            shippingDocumentToComplete = {
              ...shippingDocumentToComplete,
              isCompleted: true,
            };

            process.next({
              ...process.value,
              shippingDocuments: [
                ...shippingDocuments.filter((document) => document.id !== params.shippingDocumentId),
                shippingDocumentToComplete,
              ],
            });
          }
        }),
        switchMap((shippingDocumentCompleteResponse) => {
          if (!shippingDocumentCompleteResponse.error && !!Object.keys(shippingDocumentCompleteResponse.invalidProperties).length) {
            return of({
              result: false,
              error: shippingDocumentCompleteResponse.error || !!Object.keys(shippingDocumentCompleteResponse.invalidProperties).length,
              message: shippingDocumentCompleteResponse.message,
              errorReasons: shippingDocumentCompleteResponse.invalidProperties,
            });
          }

          if (!!shippingDocumentCompleteResponse.error) {
            return of({
              result: true,
              error: shippingDocumentCompleteResponse.error || !!Object.keys(shippingDocumentCompleteResponse.invalidProperties).length,
              message: shippingDocumentCompleteResponse.message,
              errorReasons: shippingDocumentCompleteResponse.invalidProperties,
            });
          }

          return this.completeRemission({
            remissionProcessId: params.remissionProcessId,
          }).pipe(map((response) => ({ result: response })));
        })
      );
  }

  createShippingDocument(params: { remissionProcessId: number; receiptNumber?: string }): Observable<ShippingDocument> {
    const process = this.processStore.get(params.remissionProcessId);

    const receiptMapper = this.mappingService.get<Mapper<ReceiptDTO, ShippingDocument>>('ReceiptDTO', 'ShippingDocument');

    if (process === undefined) {
      throw new Error('Prozess nicht verfügbar.');
    }

    return this.stock$.pipe(
      flatMap((stock) =>
        this.returnApi.ReturnCreateReceipt({
          returnId: process.value.externalId,
          data: {
            receiptNumber: params.receiptNumber || null,
            stock: {
              id: stock.id,
            },
            supplier: { id: process.value.filter.target.id } as any,
            receiptType: 1, // ShippingNote = 1
          },
        })
      ),
      catchError(this.handleHttpError),
      mergeMap((response) => {
        if (response.error /*|| !!response.httpError*/) {
          return throwError(response);
        }
        return of(response);
      }),
      map((receipts) => receiptMapper.map(receipts.result)),
      tap((document) => {
        const newProcess = {
          ...process.value,
          shippingDocuments: process.value.shippingDocuments ? [...process.value.shippingDocuments, document] : [document],
        };
        process.next(newProcess);
      })
    );
  }

  getAllRemissions(params: {
    showOpen?: boolean;
    showOpenLastWeekOnly?: boolean;
    showCompleted?: boolean;
    showCompletedLastWeekOnly?: boolean;
    receiptNumber?: string;
    returnGroup?: number;
    take?: number;
  }): Observable<RemissionProcess[]> {
    // tslint:disable-next-line: no-shadowed-variable
    const { take } = params;
    const eagerLoading = 3;
    let startOpen: string;
    let stopOpen: string;

    let stopCompleted: string;
    let startCompleted: string;

    let input = {} as ReturnQueryTokenDTO['input'];

    const openFilter = {
      showCompleted: 'false',
    } as ReturnQueryTokenDTO['filter'];

    const completedFilter = {
      showCompleted: 'true',
    } as ReturnQueryTokenDTO['filter'];

    if (params.showOpen && params.showOpenLastWeekOnly) {
      startOpen = addDays(new Date(), -7).toISOString();
      stopOpen = new Date().toISOString();
    }

    if (params.showCompleted && params.showCompletedLastWeekOnly) {
      startCompleted = addDays(new Date(), -7).toISOString();
      stopCompleted = new Date().toISOString();
    }

    if (params.receiptNumber) {
      input = {
        receiptnumber: params.receiptNumber,
      } as ReturnQueryTokenDTO['input'];
    }

    if (params.returnGroup) {
      input = { ...input, returngroup: String(params.returnGroup) };
    }

    const queryTokenOpen = {
      eagerLoading,
      ...{ start: startOpen },
      ...{ stop: stopOpen },
      filter: openFilter,
      ...{ input },
      ...{ take },
    } as ReturnQueryTokenDTO;

    const queryTokenCompleted = {
      eagerLoading,
      ...{ start: startCompleted },
      ...{ stop: stopCompleted },
      filter: completedFilter,
      ...{ input },
      ...{ take },
    } as ReturnQueryTokenDTO;

    return this.stock$.pipe(
      flatMap((stock) => {
        const openReturns$: Observable<ReturnDTO[]> = this.returnApi
          .ReturnQueryReturns({
            stockId: stock.id,
            queryToken: queryTokenOpen,
          })
          .pipe(
            map((response) => response.result),
            map((returns) =>
              returns.filter((r) => !r.receipts.every((receipt) => (receipt.data && receipt.data.status === 4) || receipt.data.completed))
            )
          );
        const completedReturns$: Observable<ReturnDTO[]> = this.returnApi
          .ReturnQueryReturns({
            stockId: stock.id,
            queryToken: queryTokenCompleted,
          })
          .pipe(map((response) => response.result));

        if (!params.showCompleted && !!params.showOpen) {
          return openReturns$;
        }

        if (!params.showOpen && !!params.showCompleted) {
          return completedReturns$;
        }

        return forkJoin([openReturns$, completedReturns$]).pipe(
          map(([openReturns, completedReturns]: [ReturnDTO[], ReturnDTO[]]) => {
            const openReturnIds = openReturns.map((r) => r.id);
            return [...openReturns, ...completedReturns.filter((completedReturn) => !openReturnIds.includes(completedReturn.id))];
          })
        );
      }),
      withLatestFrom(this.stock$, this.targets$),
      mergeMap(([result, stock, targets]) => {
        if (!result.length) {
          return of([] as RemissionProcess[]);
        }

        return forkJoin(
          result.map((returnDto) => {
            const receiptMapper = this.mappingService.get<Mapper<ReceiptDTO, ShippingDocument>>('ReceiptDTO', 'ShippingDocument');

            const supplierMapper = this.mappingService.get<Mapper<SupplierDTO, RemissionSupplier>>('SupplierDTO', 'RemissionSupplier');

            const remission: RemissionProcess = {
              id: returnDto.id,
              returnGroup: Number(returnDto.returnGroup),
              externalId: returnDto.id,
              stockId: stock.id,
              startDate: String(returnDto.start),
              capacities: [],
              filter: {
                filter: {},
                source: 'zentral', // default
                skip: 0,
                take: 25,
                target: supplierMapper.map(targets.find((target) => target.id === returnDto.supplier.id)),
              },
              shippingDocuments: !!returnDto.receipts.length ? returnDto.receipts.map((receipt) => receiptMapper.map(receipt.data)) : [],
              completed: !!returnDto['completed'],
            };

            return of(remission as RemissionProcess).pipe(
              mergeMap((remi) => {
                const shippingDocumentsWithProductGroupNames$ = forkJoin(
                  // tslint:disable-next-line: no-shadowed-variable
                  remi.shippingDocuments.map((shippingDocument) => {
                    const productsWithGroupNames$ = this.restRemissionProcessService.addGroupnameToProducts(
                      shippingDocument.products,
                      stock.id
                    );

                    return productsWithGroupNames$.pipe(
                      map((products) => ({
                        ...shippingDocument,
                        products,
                      }))
                    );
                  })
                );

                return shippingDocumentsWithProductGroupNames$.pipe(
                  map((shippingDocuments) => ({
                    ...remi,
                    shippingDocuments,
                  }))
                );
              })
            );
          })
        );
      }),
      map((remissions: RemissionProcess[]) =>
        remissions.filter(
          (remission) =>
            !!remission.id && !!remission.shippingDocuments && !remission.shippingDocuments.every((document) => document.isDeleted)
        )
      ),
      tap((remissions: RemissionProcess[]) => remissions.forEach((remission) => this.processStore.set(remission)))
    );
  }

  getUncompletedRemissions(): Observable<RemissionProcess[]> {
    this.reloadRemissionSub.next();
    const receiptMapper = this.mappingService.get<Mapper<ReceiptDTO, ShippingDocument>>('ReceiptDTO', 'ShippingDocument');

    const baseProcess$: Observable<Partial<RemissionProcess>> = of({
      filter: {
        filter: {},
        skip: 0,
        take: 25,
        source: 'zentral',
        target: undefined,
      } as RemissionFilter,
      stockId: undefined,
    });

    const remissionWithoutShippingDocument$ = combineLatest([
      this.reloadRemissionSub.asObservable(),
      this.stock$,
      baseProcess$,
      this.targets$,
    ]).pipe(
      switchMap(([_, stock, baseProcess, targets]) =>
        this.returnApi.ReturnGetUncompletedReturns({ stockId: stock.id }).pipe(
          map((response) => response.result),
          map((returns) => {
            const processes = returns.map((r) => ({
              ...baseProcess,
              id: this.uidGenerator.generate({ type: 'number' }),
              externalId: r.id,
              filter: {
                ...baseProcess.filter,
                target: targets.find((supplier) => supplier.id === r.supplier.id),
              },
              stockId: stock.id,
              shippingDocuments: r.receipts.map((receipt) => ({
                id: receipt.id,
              })),
            }));

            return processes as RemissionProcess[];
          })
        )
      )
    );

    const remissionWithShippingDocument$ = remissionWithoutShippingDocument$.pipe(
      mergeMap((remissionsWithoutShippingDocument) => {
        if (!remissionsWithoutShippingDocument.length) {
          return of(remissionsWithoutShippingDocument);
        }

        const shippingDocuments = remissionsWithoutShippingDocument.map((remission) => {
          return forkJoin(
            remission.shippingDocuments.map((shippingDoc) =>
              this.returnApi
                .ReturnGetReturnReceipt({
                  returnId: remission.externalId,
                  receiptId: shippingDoc.id,
                  eagerLoading: 3,
                })
                .pipe(map((response) => response.result))
            )
          );
        });

        return forkJoin(shippingDocuments).pipe(
          map((response) => response.map((r) => receiptMapper.map(r[0]))),
          map((shippingDocs: ShippingDocument[]) => {
            let resultRemissions: RemissionProcess[] = [];

            remissionsWithoutShippingDocument.forEach((remission) => {
              // tslint:disable-next-line: no-shadowed-variable
              return remission.shippingDocuments.map((shippingDocument) => {
                const matchedShippingDocument = shippingDocs.find((doc) => doc.id === shippingDocument.id);
                const updatedProcessWithShippingDocument: RemissionProcess = {
                  ...remission,
                  shippingDocuments: [matchedShippingDocument],
                };

                resultRemissions = [...resultRemissions, updatedProcessWithShippingDocument];
              });
            });

            // Save Remissions in store
            resultRemissions.forEach((remission) => this.processStore.set(remission));

            return resultRemissions;
          })
        );
      })
    );

    return remissionWithShippingDocument$;
  }

  deleteRemission(params: { remissionProcessId: number; externalId?: number }): Observable<boolean> {
    const process = this.processStore.get(params.remissionProcessId);

    return this.returnApi.ReturnCancelReturn({ returnId: params.externalId || process.value.externalId }).pipe(
      map((response) => (response.error ? false : true)),
      tap((_) => this.reloadRemissionSub.next())
    );
  }

  deleteShippingDocument(params: {
    remissionProcessId: number;
    shippingDocumentId: number;
    externalId?: number;
  }): Observable<ActionResult<{ deleted: boolean; completedRemissionsExist: boolean }>> {
    const process = this.processStore.get(params.remissionProcessId);

    return this.returnApi
      .ReturnCancelReturnReceipt({
        returnId: params.externalId || process.value.externalId,
        receiptId: params.shippingDocumentId,
      })
      .pipe(
        map((response) => {
          const remainingShippingDocument = process.value.shippingDocuments.filter((document) => document.id !== params.shippingDocumentId);

          this.processStore.set({
            ...process.value,
            shippingDocuments: [...remainingShippingDocument],
          });

          return !response.error;
        }),
        switchMap((isCancelSuccessful) => {
          if (!isCancelSuccessful) {
            return of({
              result: {
                deleted: false,
                completedRemissionsExist: false,
              },
              error: true,
              message: 'Warenbegleitschein konnte nicht gelöscht werden',
            });
          }

          return this.deleteRemission({
            remissionProcessId: process.value.id,
          }).pipe(
            map((response) => ({
              result: {
                deleted: response,
                completedRemissionsExist: false,
              },
              error: !response,
            })),
            switchMap((result) =>
              this.getAllRemissions({
                returnGroup: process.value.returnGroup,
                showCompleted: true,
              }).pipe(
                map((response) => {
                  return {
                    ...result,
                    result: {
                      ...result.result,
                      completedRemissionsExist: !!response.length,
                    },
                  };
                })
              )
            )
          );
        })
      );
  }

  handleHttpError = (error: HttpErrorResponse) => {
    if (error.status === 0) {
      return NEVER;
    }

    return of({
      message: error?.message,
      ...(error?.error || {}),
      error: true,
    });
  };
}
