import { Injectable } from '@angular/core';
import { ItemDTO, ListResponseArgsOfItemDTO, SearchService } from '@swagger/cat';
import {
  RemiService,
  StockService,
  SupplierService,
  ReturnService,
  RemiQueryTokenDTO,
  QueryTokenDTO,
  ReturnItemDTO,
  StockDTO,
  ReceiptDTO,
  ReturnDTO,
  ReturnQueryTokenDTO,
  BatchResponseArgsOfReturnItemDTOAndReturnItemDTO,
} from '@swagger/remi';
import { memorize } from '@utils/common';
import { Observable, of, throwError } from 'rxjs';
import { catchError, map, shareReplay, switchMap } from 'rxjs/operators';
import { RemissionListItem } from './defs';
import { fromItemDto, mapFromReturnItemDTO, mapFromReturnSuggestionDTO } from './mappings';
import { Logger } from '@core/logger';
import { DateAdapter } from '@ui/common';
import { RemissionPlacementType } from '@isa/remission';

@Injectable()
export class DomainRemissionService {
  constructor(
    private readonly _logger: Logger,
    private readonly _remiService: RemiService,
    private readonly _stockService: StockService,
    private readonly _supplierService: SupplierService,
    private readonly _returnService: ReturnService,
    private readonly _search: SearchService
  ) {}

  @memorize()
  getCurrentStock() {
    return this._stockService.StockCurrentStock().pipe(
      map((res) => res.result),
      catchError((err: Error) => {
        this._logger.error('Fehler beim Laden des aktuellen Lagers', err);
        return throwError(err);
      }),
      shareReplay()
    );
  }

  @memorize()
  getSuppliers() {
    return this.getCurrentStock().pipe(
      switchMap((stock) =>
        this._supplierService.SupplierGetSuppliers({
          stockId: stock.id,
        })
      ),
      map((res) => res.result),
      catchError((err: Error) => {
        this._logger.error('Fehler beim Laden der Lieferanten', err);
        return throwError(err);
      }),
      shareReplay()
    );
  }

  getSources() {
    return of(['Pflichtremission', 'Abteilungsremission']);
  }

  getQuerySettings(arg: { source: string; supplierId: number }) {
    return this.getCurrentStock().pipe(
      switchMap((stock) =>
        this._remiService
          .RemiRemissionQuerySettings({
            remiType: arg.source,
            supplierId: arg.supplierId,
            stockId: stock.id,
          })
          .pipe(map((res) => res.result))
      ),
      catchError((err: Error) => {
        this._logger.error('Fehler beim Laden des Filters', err);
        return throwError(err);
      })
    );
  }

  @memorize()
  getProductGroups() {
    return this.getCurrentStock().pipe(
      switchMap((stock) =>
        this._remiService.RemiProductgroups({ stockId: stock.id }).pipe(
          map((res) => res.result),
          catchError((err: Error) => {
            this._logger.error('Fehler beim Laden der Produktgruppen', err);
            return throwError(err);
          })
        )
      ),
      shareReplay()
    );
  }

  @memorize()
  getReturnReasons() {
    return this.getCurrentStock().pipe(
      switchMap((stock) =>
        this._returnService
          .ReturnGetReturnReasons({
            stockId: stock.id,
          })
          .pipe(
            map((res) => res.result),
            catchError((err: Error) => {
              this._logger.error('Fehler beim Laden der Remigünde', err);
              return throwError(err);
            })
          )
      ),
      shareReplay()
    );
  }

  getItems(arg: {
    source: string;
    supplierId: number;
    queryToken: QueryTokenDTO;
  }): Observable<{ hits: number; result: RemissionListItem[] }> {
    let result$: Observable<{ hits: number; result: RemissionListItem[] }>;

    if (arg.source === 'Pflichtremission') {
      result$ = this.getCurrentStock().pipe(
        switchMap((stock) =>
          this.getItemsForPflichtremission({
            queryToken: {
              stockId: stock.id,
              supplierId: arg.supplierId,
              ...arg.queryToken,
            },
          })
        )
      );
    } else if (arg.source === 'Abteilungsremission') {
      if (!arg.queryToken.filter.abteilungen) {
        result$ = of({ hits: 0, result: [] });
      } else {
        result$ = this.getCurrentStock().pipe(
          switchMap((stock) =>
            this.getItemsForAbteilungsremission({
              queryToken: {
                stockId: stock.id,
                supplierId: arg.supplierId,
                ...arg.queryToken,
              },
            })
          )
        );
      }
    } else {
      this._logger.error('Unbekannte Quelle', arg.source);
      return throwError(new Error(`Unknown source: ${arg.source}`));
    }

    return result$.pipe(
      switchMap((res) =>
        res?.hits
          ? this.getStockInformation(res.result).pipe(
              map((result) => ({
                hits: res.hits,
                result,
              }))
            )
          : of(res)
      )
    );
  }

  getItemsForPflichtremission(arg: { queryToken: RemiQueryTokenDTO }): Observable<{ hits: number; result: RemissionListItem[] }> {
    return this._remiService
      .RemiPflichtremissionsartikel({
        queryToken: arg.queryToken,
      })
      .pipe(
        map((res) => ({
          hits: res.hits,
          result: res.result.map(mapFromReturnItemDTO),
        }))
      );
  }

  getItemsForAbteilungsremission(arg: { queryToken: RemiQueryTokenDTO }): Observable<{ hits: number; result: RemissionListItem[] }> {
    return this._remiService
      .RemiUeberlauf({
        queryToken: arg.queryToken,
      })
      .pipe(
        map((res) => ({
          hits: res.hits,
          result: res.result.map(mapFromReturnSuggestionDTO),
        }))
      );
  }

  getStockInformation(items: RemissionListItem[], recalculate: boolean = false) {
    return this.getCurrentStock().pipe(
      switchMap((stock) =>
        this._stockService
          .StockInStock({
            stockId: stock.id,
            articleIds: items
              .filter((item) => !!item.dto.product.catalogProductNumber)
              .map((item) => +item.dto.product.catalogProductNumber),
          })
          .pipe(
            map((res) => {
              const o = items.map((item) => {
                const stockInfo = res?.result?.find((stockInfo) => stockInfo.itemId === +item.dto.product.catalogProductNumber);

                if (!stockInfo) {
                  const defaultStockData = {
                    inStock: 0,
                    remainingQuantity: 0,
                    remissionQuantity: item.remissionQuantity || 0,
                  };

                  return { ...item, ...defaultStockData };
                }

                const availableStock = stockInfo.inStock - stockInfo.removedFromStock;
                const inStock = availableStock < 0 ? 0 : availableStock;

                let { remainingQuantity, remissionQuantity } = item;

                if (!remissionQuantity || recalculate) {
                  remissionQuantity = inStock - (remainingQuantity || 0);
                  if (remissionQuantity < 0) {
                    remissionQuantity = 0;
                  }
                }
                if (!remainingQuantity || recalculate) {
                  remainingQuantity = inStock - (remissionQuantity || 0);
                  if (remainingQuantity < 0) {
                    remainingQuantity = 0;
                  }
                }

                return { ...item, remainingQuantity, remissionQuantity, inStock };
              });

              return o;
            })
          )
      )
    );
  }

  getRequiredCapacities(params: { departments?: string[]; supplierId: number }) {
    return this.getCurrentStock().pipe(
      switchMap((stock) =>
        this._remiService
          .RemiGetRequiredCapacities({
            stockId: stock?.id,
            payload: {
              departments: params?.departments || [],
              supplierId: params?.supplierId,
            },
          })
          .pipe(map((res) => res.result))
      )
    );
  }

  searchItemToRemit(ean: string): Observable<ReturnItemDTO> {
    return this.getCurrentStock().pipe(
      switchMap((stock) =>
        this._search
          .SearchSearch({
            stockId: null,
            queryToken: {
              stockId: stock.id,
              input: { qs: ean },
              doNotTrack: true,
            },
          })
          .pipe(
            catchError((err) => of({ hits: 0, result: [] })),
            map((res) => [res, stock] as [ListResponseArgsOfItemDTO, StockDTO])
          )
      ),
      map(([res, stock]) => {
        if (res.hits === 0) {
          return undefined;
        }

        const item = res.result[0] as ItemDTO;

        return fromItemDto(item, stock);
      })
    );
  }

  canAddReturnItem(item: ReturnItemDTO): Observable<BatchResponseArgsOfReturnItemDTOAndReturnItemDTO> {
    return this._remiService.RemiCanAddReturnItem({
      data: [item],
    });
  }

  async createReturn(supplierId: number, returnGroup?: string): Promise<ReturnDTO> {
    const response = await this._returnService
      .ReturnCreateReturn({
        data: {
          supplier: { id: supplierId },
          returnGroup: returnGroup ?? String(Date.now()),
        },
      })
      .toPromise();

    return response.result;
  }

  completeReturn(returnId: number) {
    return this._returnService.ReturnFinalizeReturn({ returnId }).toPromise();
  }

  async completeRemission(returnId: number): Promise<ReturnDTO[]> {
    const returnDto = await this.getReturn(returnId).toPromise();
    const response = await this._returnService
      .ReturnFinalizeReturnGroup({
        returnGroup: returnDto.returnGroup,
      })
      .toPromise();

    return response.result;
  }

  async deleteRemission(returnId: number): Promise<void> {
    await this._returnService
      .ReturnCancelReturn({
        returnId,
      })
      .toPromise();
  }

  getReturns(params: { start?: Date; returncompleted: boolean }): Observable<ReturnDTO[]> {
    const queryToken: ReturnQueryTokenDTO = {
      start: params.start?.toISOString(),
      filter: {
        returncompleted: params.returncompleted ? 'true' : 'false',
      },
      eagerLoading: 3,
      input: {},
    };

    Object.keys(queryToken).forEach((key) => {
      if (!queryToken[key]) {
        delete queryToken[key];
      }
    });

    return this.getCurrentStock().pipe(
      switchMap((stock) => this._returnService.ReturnQueryReturns({ stockId: stock.id, queryToken })),
      map((res) => res.result)
    );
  }

  getReturn(returnId: number): Observable<ReturnDTO> {
    return this._returnService.ReturnGetReturn({ returnId, eagerLoading: 3 }).pipe(map((res) => res.result));
  }

  async deleteReturn(returnId: number) {
    const returnDto = await this.getReturn(returnId).toPromise();
    for (const receipt of returnDto?.receipts) {
      await this.deleteReceipt(returnDto.id, receipt.id);
    }
    await this.deleteRemission(returnDto.id);
  }

  addReturnItem({
    returnId,
    receiptId,
    returnItemId,
    quantity,
    placementType,
    inStock,
  }: {
    returnId: number;
    receiptId: number;
    returnItemId: number;
    quantity?: number;
    placementType?: RemissionPlacementType;
    inStock: number;
  }) {
    return this._returnService
      .ReturnAddReturnItem({ returnId, receiptId, data: { returnItemId, quantity, placementType, inStock } })
      .pipe(map((r) => r.result));
  }

  addReturnSuggestion({
    returnId,
    receiptId,
    returnSuggestionId,
    quantity,
    placementType,
    inStock,
    impedimentComment,
    remainingQuantity,
  }: {
    returnId: number;
    receiptId: number;
    returnSuggestionId: number;
    quantity?: number;
    placementType?: RemissionPlacementType;
    inStock: number;
    impedimentComment: string;
    remainingQuantity: number;
  }) {
    return this._returnService
      .ReturnAddReturnSuggestion({
        returnId,
        receiptId,
        data: { returnSuggestionId, quantity, placementType, inStock, impedimentComment, remainingQuantity },
      })
      .pipe(map((r) => r.result));
  }

  removeReturnItemFromList({ itemId }: { itemId: number }) {
    return this._returnService.ReturnDeleteReturnItem({ itemId });
  }

  removeReturnItemFromReceipt({ returnId, receiptId, receiptItemId }: { returnId: number; receiptId: number; receiptItemId: number }) {
    return this._returnService.ReturnRemoveReturnItem({ returnId, receiptItemId, receiptId });
  }

  returnImpediment(itemId: number) {
    return this._returnService
      .ReturnReturnItemImpediment({ itemId, data: { comment: 'Produkt nicht gefunden' } })
      .pipe(map((r) => r.result));
  }

  returnSuggestion(itemId: number) {
    return this._returnService
      .ReturnReturnSuggestionImpediment({ itemId, data: { comment: 'Produkt nicht gefunden' } })
      .pipe(map((r) => r.result));
  }

  /**
   * Create a new receipt for the given return/remission
   * @param returnId Return ID
   * @param receiptNumber Receipt number
   * @returns ReturnDTO - ShippingDocument
   */
  async createReceipt(returnDTO: ReturnDTO, receiptNumber?: string): Promise<ReceiptDTO> {
    const stock = await this._getStock();

    const response = await this._returnService
      .ReturnCreateReceipt({
        returnId: returnDTO.id,
        data: {
          receiptNumber: receiptNumber ?? null,
          stock: {
            id: stock.id,
          },
          supplier: { id: returnDTO.supplier.id },
          receiptType: 1, // ShippingNote = 1
        },
      })
      .toPromise();

    const receipt: ReceiptDTO = response.result;

    return receipt;
  }

  async completeReceipt(returnId: number, receiptId: number, packageCode: string): Promise<ReceiptDTO> {
    const res = await this._returnService
      .ReturnFinalizeReceipt({
        returnId,
        receiptId,
        data: {
          packageCode,
        },
      })
      .toPromise();

    const result = res.result;

    return result;
  }

  async deleteReceipt(returnId: number, receiptId: number): Promise<void> {
    await this._returnService
      .ReturnCancelReturnReceipt({
        returnId,
        receiptId,
      })
      .toPromise();
  }

  addProductToRemit(item: ReturnItemDTO, reason: string, quantity: number) {
    return this._remiService.RemiCreateReturnItem({
      data: [
        {
          assortment: item.assortment,
          product: item.product,
          returnReason: reason,
          predefinedReturnQuantity: quantity,
          stock: item.stock,
          retailPrice: item.retailPrice,
          source: 'manually-added',
        },
      ],
    });
  }

  private _getStock() {
    return this.getCurrentStock().toPromise();
  }
}
