import { Injectable } from '@angular/core';
import {
  Observable,
  of,
  BehaviorSubject,
  Subject,
  merge,
  combineLatest,
} from 'rxjs';
import { delay, distinctUntilChanged, map } from 'rxjs/operators';
import { createRandomStringId, createRandomId } from './util';
import {
  product,
  removeSourcePriceAndProductFromProduct,
} from './data/product';
import { RemissionService } from '../services/remission.service';
import { RemissionProcess } from '../models/remission-process';
import { RemissionProduct } from '../models/remission-product';
import {
  remissionProducts,
  createRemissionProduct,
} from './data/remission-products';
import { remissionProcessTemplateMock } from './data/remission-process-template';
import { ShippingDocument } from '../models/shipping-document';
import { shippingDocument } from './data/shipping-document';
import { RemissionFilter } from '../models/remission-filter';
import { Printer } from '../models/printer';
import { printers } from './data/remission-printers';
import { Product } from '../models/product';
import { RemissionSourceType } from '../types/remission-source.type';
import { Filter } from '../models/filter';
import { filterUeberlauf, filter } from './data/remission-filter';
import { RemissionPlacementType } from '../types/remission-placement-types';
import { remissionPlacementTypes } from './data/remission-placement-types';
import { remissionReasons } from './data/remission-reasons';
import { RemissionSupplier } from '../models/remission-supplier';
import { remissionTargets } from './data/remission-targets';
import { remissionSources } from './data/remission-sources';
import { ActionResult, CapacityType } from '../models';

@Injectable({ providedIn: 'root' })
export class MockRemissionService extends RemissionService {
  private delayInMs = 100;
  private remissionSubjectStore = new Map<
    number,
    BehaviorSubject<RemissionProcess>
  >();
  private remissionSubjectIdRef = new Map<number, number>();

  private reloadProductsSubject = new Subject();
  private productSubject = new BehaviorSubject<RemissionProduct[]>(
    remissionProducts
  );

  private getRemissionFromStore = (
    remissionProcessId: number
  ): BehaviorSubject<RemissionProcess> => {
    const remission =
      this.remissionSubjectStore.get(remissionProcessId) ||
      this.remissionSubjectStore.get(
        this.remissionSubjectIdRef.get(remissionProcessId)
      );

    return remission;
  };

  private saveRemissionInStore = (
    remissionProcessId: number,
    remissionSubject: BehaviorSubject<RemissionProcess>
  ) => {
    this.remissionSubjectStore.set(remissionProcessId, remissionSubject);
  };

  createProcess(source?: RemissionProcess): Observable<RemissionProcess> {
    const process = { ...remissionProcessTemplateMock, ...source };
    const processSubject = new BehaviorSubject<RemissionProcess>(process);

    this.saveRemissionInStore(process.id, processSubject);

    return processSubject.pipe(distinctUntilChanged(), delay(this.delayInMs));
  }

  getRemissionProducts(params: {
    remissionProcessId: number;
  }): Observable<{
    skip?: number;
    take?: number;
    hits?: number;
    items: RemissionProduct[];
    completed: boolean;
  }> {
    const processSubject = this.getRemissionFromStore(
      params.remissionProcessId
    );

    if (!processSubject) {
      throw new Error('Prozess nicht gefunden');
    }

    const process$ = merge(
      processSubject,
      this.reloadProductsSubject.pipe(map((_) => processSubject.value))
    );

    const products$ = this.productSubject.asObservable();

    return combineLatest(process$, products$).pipe(
      map(([process, products]) => {
        // tslint:disable-next-line: no-shadowed-variable
        const filter = process.filter;

        const result: {
          skip?: number;
          take?: number;
          hits?: number;
          items: RemissionProduct[];
          completed: boolean;
        } = {
          skip: filter.skip,
          take: filter.take,
          hits: 0,
          items: [...products],
          completed: false,
        };

        const productGroupFilter = filter.filter['421228ce'] || [];

        if (productGroupFilter.length > 0) {
          result.items = result.items.filter((product) =>
            productGroupFilter.some((s) => product.productGroup === s)
          );
        }

        const actionFilter = filter.filter['ce9ee0be'] || [];

        result.hits = result.items.length;

        result.items = result.items.slice(
          result.skip,
          result.skip + result.take
        );

        result.completed = result.skip + result.take >= result.hits;

        return result;
      })
    );
  }

  startRemission(params: {
    remissionProcessId: number;
    config?: {
      createShippingDocument?: boolean;
    };
  }): Observable<RemissionProcess> {
    const processSubject = this.remissionSubjectStore.get(
      params.remissionProcessId
    );

    if (!processSubject) {
      throw new Error('Prozess nicht gefunden');
    }

    const process = processSubject.value;
    const id = createRandomId();
    const doc: ShippingDocument = shippingDocument;

    this.remissionSubjectIdRef.set(id, process.id);
    processSubject.next({
      ...process,
      id,
      shippingDocuments: [doc],
    });

    return of(processSubject.value).pipe(delay(this.delayInMs));
  }

  continueProcess(source: RemissionProcess): Observable<RemissionProcess> {
    let processSubject = this.getRemissionFromStore(source.id);

    if (!processSubject) {
      processSubject = new BehaviorSubject<RemissionProcess>(source);
    }

    this.saveRemissionInStore(processSubject.value.id, processSubject);

    return processSubject.pipe(distinctUntilChanged(), delay(this.delayInMs));
  }

  continueProcessFromScan(containerId: string): Observable<RemissionProcess> {
    return of();
  }

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

    if (!!processSubject) {
      const process = processSubject.value;
      const nextProcess = {
        ...process,
        filter: { ...process.filter, ...changes },
      };

      processSubject.next(nextProcess);
      return of(nextProcess.filter).pipe(delay(this.delayInMs));
    } else {
      throw new Error('Prozess nicht gefunden');
    }
  }

  completeRemission(params: {
    remissionProcessId: number;
  }): Observable<boolean> {
    let processSubject = this.remissionSubjectStore.get(
      params.remissionProcessId
    );

    if (!processSubject) {
      const refId = this.remissionSubjectIdRef.get(params.remissionProcessId);
      if (!!refId) {
        processSubject = this.remissionSubjectStore.get(refId);
      }

      processSubject.next(processSubject.value);
      processSubject.complete();

      return of(false).pipe(delay(this.delayInMs));
    } else {
      return of(true).pipe(delay(this.delayInMs));
    }
  }

  getRemission(params: {
    remissionProcessId: number;
  }): Observable<RemissionProcess> {
    const remission = this.getRemissionFromStore(params.remissionProcessId);

    if (!remission) {
      throw new Error('Prozess nicht gefunden');
    }

    return remission.pipe(distinctUntilChanged(), delay(this.delayInMs));
  }

  isPrintingRequired({
    remissionProcessId,
  }: {
    remissionProcessId: number;
  }): Observable<boolean> {
    const remission = this.getRemissionFromStore(remissionProcessId);

    if (!remission) {
      return of(false).pipe(delay(this.delayInMs));
    }

    switch (remission.value.filter.target.name) {
      case 'Libri':
        return of(true).pipe(delay(this.delayInMs));

      case 'Blank':
        return of(false).pipe(delay(this.delayInMs));
    }
  }

  getPrinters(): Observable<Printer[]> {
    return of(printers).pipe(delay(this.delayInMs));
  }

  printRemissionList(params: {
    remissionProcessId: number;
    printerKey: string;
  }): Observable<boolean> {
    const remission = this.getRemissionFromStore(params.remissionProcessId);

    if (!remission) {
      return of(false).pipe(delay(this.delayInMs));
    }

    return of(true).pipe(delay(this.delayInMs));
  }

  printShippingDocument(params: {
    remissionProcessId: number;
    shippingDocumentId?: number;
    printerKey: string;
  }): Observable<boolean> {
    const remission = this.getRemissionFromStore(params.remissionProcessId);

    if (!remission || !params.printerKey) {
      return of(false).pipe(delay(this.delayInMs));
    }
    return of(true).pipe(delay(this.delayInMs));
  }

  searchProduct({ ean }: { ean: string }): Observable<Product> {
    const searchResult = product;

    return of(searchResult).pipe(delay(this.delayInMs));
  }

  getFilters(params: { remissionProcessId: number }): Observable<Filter[]> {
    const remission = this.getRemissionFromStore(params.remissionProcessId);

    const filterForGivenSourceType =
      remission.value.filter.source === 'zentral' ? filter : filterUeberlauf;
    return of(filterForGivenSourceType).pipe(delay(this.delayInMs));
  }

  getPlacementTypes(): Observable<RemissionPlacementType[]> {
    return of(remissionPlacementTypes).pipe(delay(this.delayInMs));
  }

  getRemissionReasons(): Observable<string[]> {
    return of(remissionReasons).pipe(delay(this.delayInMs));
  }

  getRemissionTargets(): Observable<RemissionSupplier[]> {
    return of(remissionTargets).pipe(delay(this.delayInMs));
  }
  getRemissionSources(): Observable<RemissionSourceType[]> {
    return of(remissionSources).pipe(delay(this.delayInMs));
  }

  getShippingDocuments(params: {
    remissionProcessId: number;
    shippingDocumentId: number;
  }): Observable<ShippingDocument> {
    const remissionSub = this.getRemissionFromStore(params.remissionProcessId);

    return remissionSub.pipe(
      map((remission) =>
        remission.shippingDocuments.find(
          (shippingDoc) => shippingDoc.id === params.shippingDocumentId
        )
      ),
      distinctUntilChanged(),
      delay(this.delayInMs)
    );
  }

  deleteProductFromRemissionList(input: {
    remissionProcessId: number;
    remissionProductId: number;
  }): Observable<ActionResult<boolean>> {
    return of({
      error: false,
      result: true,
    });
  }

  addProductToRemit(input: {
    product: Product;
    remissionReason: string;
    remissionQuantity: number;
  }): Observable<RemissionProduct> {
    const productToAdd = removeSourcePriceAndProductFromProduct({ ...product });

    const newProduct = createRemissionProduct({
      remissionReason: input.remissionReason,
      remissionQuantity: input.remissionQuantity,
      ...productToAdd,
    });

    const newProductSubject = [...this.productSubject.value, newProduct];

    this.productSubject.next(newProductSubject);

    return of(newProduct).pipe(delay(this.delayInMs));
  }

  addProductToShippingDocument(params: {
    remissionProcessId: number;
    remissionProductId: number;
    shippingDocumentId: number;
    placementType?: RemissionPlacementType;
    quantity?: number;
    inStock?: number;
  }): Observable<ActionResult<boolean>> {
    const processSubject = this.getRemissionFromStore(
      params.remissionProcessId
    );
    const process = processSubject.value;
    const products = this.productSubject.value;

    if (!processSubject) {
      throw new Error('Prozess nicht gefunden');
    }

    let productToAdd = products.find(
      (product) => product.id === params.remissionProductId
    );
    let newProducts = products.filter(
      (product) => product.id !== params.remissionProductId
    );

    if (!!params.placementType || !!params.quantity) {
      const productToUpdateInProductList = {
        ...productToAdd,
        remissionQuantity: productToAdd.remissionQuantity - params.quantity,
      };
      newProducts =
        productToAdd.remissionQuantity - params.quantity < 1
          ? newProducts
          : [...newProducts, productToUpdateInProductList];

      productToAdd = {
        ...productToAdd,
        remissionQuantity: Number(params.quantity),
      };
    }

    const shippingDocumentToUpdate = process.shippingDocuments.find(
      (document) => document.id === params.shippingDocumentId
    );
    const updatedShippingDocument = {
      ...shippingDocumentToUpdate,
      products: [...shippingDocumentToUpdate.products, productToAdd],
    };

    const updatedProcess = {
      ...process,
      shippingDocuments: [
        ...process.shippingDocuments.filter(
          (document) => document.id !== params.shippingDocumentId
        ),
        updatedShippingDocument,
      ],
    };

    processSubject.next(updatedProcess);
    this.productSubject.next(newProducts);

    return of<ActionResult<boolean>>({
      error: false,
      result: true,
    }).pipe(delay(this.delayInMs));
  }

  removeProductFromShippingDocument(params: {
    remissionProcessId: number;
    shippingDocumentId: number;
    remissionProductId: number;
    placementType?: string;
  }): Observable<boolean> {
    const processSubject = this.getRemissionFromStore(
      params.remissionProcessId
    );
    const process = processSubject.value;
    const products = this.productSubject.value;

    if (!processSubject) {
      throw new Error('Prozess nicht gefunden');
    }

    const shippingDocumentToUpdate = process.shippingDocuments.find(
      (document) => document.id === params.shippingDocumentId
    );
    const updatedShippingDocument = {
      ...shippingDocumentToUpdate,
      products: shippingDocumentToUpdate.products.filter(
        (product) => product.id !== params.remissionProductId
      ),
    };
    const productToRemove = shippingDocumentToUpdate.products.find(
      (product) => product.id === params.remissionProductId
    );
    const productInProductList = products.find(
      (product) => product.id === params.remissionProductId
    );
    const updatedProductInProductList = !!productInProductList
      ? {
          ...productInProductList,
          remissionQuantity:
            Number(productInProductList.remissionQuantity) +
            Number(productToRemove.remissionQuantity),
        }
      : productToRemove;

    const updatedProcess = {
      ...process,
      shippingDocuments: [
        ...process.shippingDocuments.filter(
          (document) => document.id !== params.shippingDocumentId
        ),
        updatedShippingDocument,
      ],
    };

    const updatedProducts = [
      ...this.productSubject.value.filter(
        (product) => product.id !== updatedProductInProductList.id
      ),
      updatedProductInProductList,
    ];

    processSubject.next(updatedProcess);
    this.productSubject.next(updatedProducts);

    return of(true).pipe(delay(this.delayInMs));
  }

  reloadProducts(): void {
    this.reloadProductsSubject.next();
  }

  createShippingDocument(params: {
    remissionProcessId: number;
  }): Observable<ShippingDocument> {
    const processSubject = this.getRemissionFromStore(
      params.remissionProcessId
    );

    if (!processSubject) {
      throw new Error('Prozess nicht gefunden');
    }

    const process = processSubject.value;
    const newShippingDocument: ShippingDocument = {
      ...shippingDocument,
      id: createRandomId(),
      shippingDocumentNumber: createRandomStringId(),
    };
    const updatedShippingDocuments = [
      ...process.shippingDocuments,
      newShippingDocument,
    ];
    const updatedProcess = {
      ...process,
      shippingDocuments: updatedShippingDocuments,
    };

    processSubject.next(updatedProcess);

    return of(newShippingDocument).pipe(delay(this.delayInMs));
  }

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

    if (!processSubject) {
      throw new Error('Prozess nicht gefunden');
    }

    const process = processSubject.value;
    const shippingContainerToUpdate = process.shippingDocuments.find(
      (document) => document.id === params.shippingDocumentId
    );
    const updatedShippingContainer = {
      ...shippingContainerToUpdate,
      isCompleted: true,
    };
    const updatedProcess = {
      ...process,
      shippingDocuments: [
        ...process.shippingDocuments.filter(
          (document) => document.id !== params.shippingDocumentId
        ),
        updatedShippingContainer,
      ],
    };

    processSubject.next(updatedProcess);

    return of({
      error: false,
      message: '',
    }).pipe(delay(this.delayInMs));
  }

  completeRemissions(params: {
    remissionProcessId: number;
  }): Observable<boolean> {
    return;
  }

  getAllRemissions(params: {
    showOpen?: boolean;
    showOpenLastWeekOnly?: boolean;
    showCompleted?: boolean;
    showCompletedLastWeekOnly?: boolean;
    receiptNumber?: string;
    returnGroup?: number;
    take?: number;
  }): Observable<RemissionProcess[]> {
    return;
  }

  getUncompletedRemissions(): Observable<RemissionProcess[]> {
    return of([]);
  }

  deleteRemission(): Observable<boolean> {
    return of(true);
  }

  deleteShippingDocument(): Observable<
    ActionResult<{ deleted: boolean; completedRemissionsExist: boolean }>
  > {
    return of({
      result: {
        deleted: true,
        completedRemissionsExist: false,
      },
    });
  }

  getAllShippingDocuments(): Observable<ShippingDocument[]> {
    return;
  }

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