import { Injectable } from '@angular/core';
import { ItemDTO } from '@generated/swagger/cat-search-api';
import {
  AvailabilityDTO,
  BranchDTO,
  OLAAvailabilityDTO,
  StoreCheckoutBranchService,
  StoreCheckoutSupplierService,
  SupplierDTO,
} from '@generated/swagger/checkout-api';
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
import {
  AvailabilityRequestDTO,
  AvailabilityService,
  AvailabilityDTO as SwaggerAvailabilityDTO,
  AvailabilityType,
} from '@generated/swagger/availability-api';
import { AvailabilityDTO as CatAvailabilityDTO } from '@generated/swagger/cat-search-api';
import { map, shareReplay, switchMap, withLatestFrom, mergeMap, timeout, first } from 'rxjs/operators';
import { isArray, memorize } from '@utils/common';
import { LogisticianDTO, LogisticianService } from '@generated/swagger/oms-api';
import {
  ResponseArgsOfIEnumerableOfStockInfoDTO,
  StockDTO,
  StockInfoDTO,
  StockService,
} from '@generated/swagger/inventory-api';
import { PriceDTO } from '@generated/swagger/availability-api';
import { AvailabilityByBranchDTO, ItemData, Ssc } from './defs';
import { Availability } from './defs/availability';
import { isEmpty } from 'lodash';

@Injectable()
export class DomainAvailabilityService {
  // Ticket #3378 Keep Result List Items and Details Page SSC in sync
  sscs$ = new BehaviorSubject<Array<Ssc>>([]);
  sscsObs$ = this.sscs$.asObservable();

  constructor(
    private _availabilityService: AvailabilityService,
    private _logisticanService: LogisticianService,
    private _stockService: StockService,
    private _supplierService: StoreCheckoutSupplierService,
    private _branchService: StoreCheckoutBranchService,
  ) {}

  @memorize({ ttl: 10000 })
  memorizedAvailabilityShippingAvailability(request: Array<AvailabilityRequestDTO>) {
    return this._availabilityService.AvailabilityShippingAvailability(request).pipe(shareReplay(1));
  }

  @memorize()
  getSuppliers(): Observable<SupplierDTO[]> {
    return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe(
      map((response) => response.result),
      shareReplay(1),
    );
  }

  @memorize()
  getTakeAwaySupplier(): Observable<SupplierDTO> {
    return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe(
      map(({ result }) => result?.find((supplier) => supplier?.supplierNumber === 'F')),
      shareReplay(1),
    );
  }

  @memorize()
  getBranches(): Observable<BranchDTO[]> {
    return this._branchService.StoreCheckoutBranchGetBranches({}).pipe(
      map((response) => response.result),
      shareReplay(1),
    );
  }

  @memorize()
  getStockByBranch(branchId: number): Observable<StockDTO> {
    return this._stockService.StockGetStocksByBranch({ branchId }).pipe(
      map((response) => response.result),
      map((result) => result?.find((_) => true)),
      shareReplay(1),
    );
  }

  @memorize()
  getDefaultStock(): Observable<StockDTO> {
    return this._stockService.StockCurrentStock().pipe(
      map((response) => response.result),
      shareReplay(1),
    );
  }

  @memorize()
  getDefaultBranch(): Observable<BranchDTO> {
    return this._stockService.StockCurrentBranch().pipe(
      map((response) => ({
        id: response.result.id,
        name: response.result.name,
        address: response.result.address,
        branchType: response.result.branchType,
        branchNumber: response.result.branchNumber,
        changed: response.result.changed,
        created: response.result.created,
        isDefault: response.result.isDefault,
        isOnline: response.result.isOnline,
        key: response.result.key,
        label: response.result.label,
        pId: response.result.pId,
        shortName: response.result.shortName,
        status: response.result.status,
        version: response.result.version,
      })),
      shareReplay(1),
    );
  }

  @memorize({})
  getLogisticians(): Observable<LogisticianDTO> {
    return this._logisticanService.LogisticianGetLogisticians({}).pipe(
      map((response) => response.result?.find((l) => l.logisticianNumber === '2470')),
      shareReplay(1),
    );
  }

  getTakeAwayAvailabilityByBranches({
    branchIds,
    itemId,
    price,
    quantity,
  }: {
    branchIds: number[];
    itemId: number;
    price: PriceDTO;
    quantity: number;
  }): Observable<AvailabilityByBranchDTO[]> {
    return this._stockService.StockStockRequest({ stockRequest: { branchIds, itemId } }).pipe(
      map((response) => response.result),
      withLatestFrom(this.getTakeAwaySupplier()),
      map(([result, supplier]) => {
        const availabilities: AvailabilityByBranchDTO[] = result.map((stockInfo) => {
          return {
            availableQuantity: stockInfo.availableQuantity,
            availabilityType: quantity <= stockInfo.inStock ? 1024 : 1, // 1024 (=Available)
            inStock: stockInfo.inStock,
            supplierSSC: quantity <= stockInfo.inStock ? '999' : '',
            supplierSSCText: quantity <= stockInfo.inStock ? 'Filialentnahme' : '',
            price,
            supplier: { id: supplier?.id },
            branchId: stockInfo.branchId,
          };
        });
        return availabilities;
      }),
      shareReplay(1),
    );
  }

  @memorize({ ttl: 10000 })
  getTakeAwayAvailability({
    item,
    quantity,
    branch,
  }: {
    item: ItemData;
    quantity: number;
    branch?: BranchDTO;
  }): Observable<AvailabilityDTO> {
    const request = branch ? this.getStockByBranch(branch.id) : this.getDefaultStock();
    return request.pipe(
      switchMap((s) =>
        combineLatest([
          this._stockService.StockInStock({ articleIds: [item.itemId], stockId: s.id }),
          this.getTakeAwaySupplier(),
          this.getDefaultBranch(),
        ]),
      ),
      map(([response, supplier, defaultBranch]) => {
        const price = item?.price;
        return this._mapToTakeAwayAvailability({
          response,
          supplier,
          branchId: branch?.id ?? defaultBranch?.id,
          quantity,
          price,
        });
      }),
      shareReplay(1),
    );
  }

  @memorize({ ttl: 10000 })
  getTakeAwayAvailabilityByBranch({
    branch,
    itemId,
    price,
    quantity,
  }: {
    branch: BranchDTO;
    itemId: number;
    price: PriceDTO;
    quantity: number;
  }): Observable<AvailabilityDTO> {
    return combineLatest([
      this._stockService.StockStockRequest({ stockRequest: { branchIds: [branch.id], itemId } }),
      this.getTakeAwaySupplier(),
    ]).pipe(
      map(([response, supplier]) => {
        return this._mapToTakeAwayAvailability({ response, supplier, branchId: branch.id, quantity, price });
      }),
      shareReplay(1),
    );
  }

  getTakeAwayAvailabilityByEan({
    eans,
    price,
    quantity,
    branchId,
  }: {
    eans: string[];
    price: PriceDTO;
    quantity: number;
    branchId?: number;
  }): Observable<AvailabilityDTO> {
    const request = branchId ? this.getStockByBranch(branchId) : this.getDefaultStock();
    return request.pipe(
      switchMap((s) => this._stockService.StockInStockByEAN({ eans, stockId: s.id })),
      withLatestFrom(this.getTakeAwaySupplier(), this.getDefaultBranch()),
      map(([response, supplier, defaultBranch]) => {
        return this._mapToTakeAwayAvailability({
          response,
          supplier,
          branchId: branchId ?? defaultBranch.id,
          quantity,
          price,
        });
      }),
      shareReplay(1),
    );
  }

  getTakeAwayAvailabilitiesByEans({ eans }: { eans: string[] }): Observable<StockInfoDTO[]> {
    const eansFiltered = Array.from(new Set(eans));
    return this.getDefaultStock().pipe(
      switchMap((s) => this._stockService.StockInStockByEAN({ eans: eansFiltered, stockId: s.id })),
      withLatestFrom(this.getTakeAwaySupplier(), this.getDefaultBranch()),
      map((response) => response[0].result),
      shareReplay(1),
    );
  }

  @memorize({ ttl: 10000 })
  getPickUpAvailability({
    item,
    branch,
    quantity,
  }: {
    item: ItemData;
    quantity: number;
    branch: BranchDTO;
  }): Observable<Availability<AvailabilityDTO, SwaggerAvailabilityDTO>> {
    return this._availabilityService
      .AvailabilityStoreAvailability([
        {
          qty: quantity,
          ean: item?.ean,
          itemId: item?.itemId ? String(item?.itemId) : null,
          shopId: branch?.id,
          price: item?.price,
        },
      ])
      .pipe(
        map((r) => this._mapToPickUpAvailability(r.result)?.find((_) => true)),
        shareReplay(1),
      );
  }

  @memorize({ ttl: 10000 })
  getDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
    return this.memorizedAvailabilityShippingAvailability([
      {
        ean: item?.ean,
        itemId: item?.itemId ? String(item?.itemId) : null,
        price: item?.price,
        qty: quantity,
      },
    ]).pipe(
      timeout(5000),
      map((r) => this._mapToShippingAvailability(r.result)?.find((_) => true)),
      shareReplay(1),
    );
  }

  @memorize({ ttl: 10000 })
  getDigDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
    return this.memorizedAvailabilityShippingAvailability([
      {
        qty: quantity,
        ean: item?.ean,
        itemId: item?.itemId ? String(item?.itemId) : null,
        price: item?.price,
      },
    ]).pipe(
      timeout(5000),
      map((r) => {
        const availabilities = r.result;
        const preferred = availabilities?.find((f) => f.preferred === 1);

        return {
          availabilityType: preferred?.status,
          ssc: preferred?.ssc,
          sscText: preferred?.sscText,
          supplier: { id: preferred?.supplierId },
          isPrebooked: preferred?.isPrebooked,
          estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
          estimatedDelivery: preferred?.estimatedDelivery,
          price: preferred?.price,
          logistician: { id: preferred?.logisticianId },
          supplierProductNumber: preferred?.supplierProductNumber,
          supplierInfo: preferred?.requestStatusCode,
          lastRequest: preferred?.requested,
          priceMaintained: preferred?.priceMaintained,
        };
      }),
      shareReplay(1),
    );
  }

  @memorize({ ttl: 10000 })
  getB2bDeliveryAvailability({
    item,
    quantity,
    branch,
  }: {
    item: ItemData;
    quantity: number;
    branch?: BranchDTO;
  }): Observable<AvailabilityDTO> {
    const logistician$ = this.getLogisticians();

    const currentBranch$ = this.getDefaultBranch();

    return currentBranch$.pipe(
      timeout(5000),
      mergeMap((defaultBranch) =>
        this.getPickUpAvailability({ item, quantity, branch: branch ?? defaultBranch }).pipe(
          mergeMap((availability) =>
            logistician$.pipe(
              map((logistician) => ({
                ...(availability?.length > 0 ? availability[0] : []),
                logistician: { id: logistician.id },
              })),
            ),
          ),
          shareReplay(1),
        ),
      ),
    );
  }

  @memorize({ ttl: 10000 })
  getDownloadAvailability({ item }: { item: ItemData }): Observable<AvailabilityDTO> {
    return this.memorizedAvailabilityShippingAvailability([
      {
        ean: item?.ean,
        itemId: item?.itemId ? String(item?.itemId) : null,
        price: item?.price,
        qty: 1,
      },
    ]).pipe(
      map((r) => {
        const availabilities = r.result;
        const preferred = availabilities?.find((f) => f.preferred === 1);

        return {
          availabilityType: preferred?.status,
          ssc: preferred?.ssc,
          sscText: preferred?.sscText,
          supplier: { id: preferred?.supplierId },
          isPrebooked: preferred?.isPrebooked,
          estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
          price: preferred?.price,
          supplierProductNumber: preferred?.supplierProductNumber,
          logistician: { id: preferred?.logisticianId },
          supplierInfo: preferred?.requestStatusCode,
          lastRequest: preferred?.requested,
          priceMaintained: preferred?.priceMaintained,
        };
      }),
      shareReplay(1),
    );
  }

  @memorize({ ttl: 10000 })
  getTakeAwayAvailabilities(items: { id: number; price: PriceDTO }[], branchId: number) {
    return this._stockService.StockGetStocksByBranch({ branchId }).pipe(
      map((req) => req.result?.find((_) => true)?.id),
      switchMap((stockId) =>
        stockId
          ? this._stockService.StockInStock({ articleIds: items.map((i) => i.id), stockId })
          : of({ result: [] } as ResponseArgsOfIEnumerableOfStockInfoDTO),
      ),
      timeout(20000),
      withLatestFrom(this.getTakeAwaySupplier()),
      map(([response, supplier]) => {
        return response.result?.map((stockInfo) =>
          this._mapToTakeAwayAvailabilities({
            stockInfo,
            supplier,
            quantity: 1,
            price: items?.find((i) => i.id === stockInfo.itemId)?.price,
          }),
        );
      }),
      shareReplay(1),
    );
  }

  @memorize({ ttl: 10000 })
  getPickUpAvailabilities(payload: AvailabilityRequestDTO[], preferred?: boolean) {
    return this._availabilityService.AvailabilityStoreAvailability(payload).pipe(
      timeout(20000),
      map((response) => (preferred ? this._mapToPickUpAvailability(response.result) : response.result)),
    );
  }

  @memorize({ ttl: 10000 })
  getDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
    return this.memorizedAvailabilityShippingAvailability(payload).pipe(
      timeout(20000),
      map((response) => this._mapToShippingAvailability(response.result)),
    );
  }

  @memorize({ ttl: 10000 })
  getDigDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
    return this.memorizedAvailabilityShippingAvailability(payload).pipe(
      timeout(20000),
      map((response) => this._mapToShippingAvailability(response.result)),
    );
  }

  @memorize({ ttl: 10000 })
  getB2bDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
    const logistician$ = this.getLogisticians();

    return this.getPickUpAvailabilities(payload, true).pipe(
      timeout(20000),
      switchMap((availability) =>
        logistician$.pipe(
          map((logistician) => ({ availability: [...availability], logistician: { id: logistician.id } })),
        ),
      ),
      shareReplay(1),
    );
  }

  getPriceForAvailability(
    purchasingOption: string,
    catalogAvailability: CatAvailabilityDTO | AvailabilityDTO,
    availability: AvailabilityDTO,
  ): PriceDTO {
    switch (purchasingOption) {
      case 'take-away':
        return availability?.price || catalogAvailability?.price;
      case 'delivery':
      case 'dig-delivery':
        if (catalogAvailability?.price?.value?.value < availability?.price?.value?.value) {
          return catalogAvailability?.price;
        }
        return availability?.price || catalogAvailability?.price;
    }
    return availability?.price;
  }

  isAvailable({ availability }: { availability: AvailabilityDTO }) {
    if (availability?.supplier?.id === 16 && availability?.inStock == 0) {
      return false;
    }
    return [2, 32, 256, 1024, 2048, 4096].some((code) => availability?.availabilityType === code);
  }

  private _mapToTakeAwayAvailability({
    response,
    supplier,
    branchId,
    quantity,
    price,
  }: {
    response: ResponseArgsOfIEnumerableOfStockInfoDTO;
    supplier: SupplierDTO;
    branchId: number;
    quantity: number;
    price: PriceDTO;
  }): AvailabilityDTO {
    const stockInfo = response.result?.find((si) => si.branchId === branchId);
    const inStock = stockInfo?.inStock ?? 0;
    const availability: AvailabilityDTO = {
      availabilityType: quantity <= inStock ? 1024 : 1, // 1024 (=Available)
      inStock: inStock,
      supplierSSC: quantity <= inStock ? '999' : '',
      supplierSSCText: quantity <= inStock ? 'Filialentnahme' : '',
      price: stockInfo?.retailPrice ?? price, // #4553 Es soll nun immer der retailPrice aus der InStock Abfrage verwendet werden, egal ob "price" empty ist oder nicht
      supplier: { id: supplier?.id },
      // TODO: Change after API Update
      // LH: 2021-03-09 preis Property hat nun ein Fallback auf retailPrice
      // retailPrice: (stockInfo as any)?.retailPrice,
    };
    return availability;
  }

  private _mapToTakeAwayAvailabilities({
    stockInfo,
    quantity,
    price,
    supplier,
  }: {
    stockInfo: StockInfoDTO;
    quantity: number;
    price: PriceDTO;
    supplier: SupplierDTO;
  }) {
    const inStock = stockInfo?.inStock ?? 0;

    const availability = {
      itemId: stockInfo.itemId,
      availabilityType: quantity <= inStock ? (1024 as AvailabilityType) : (1 as AvailabilityType), // 1024 (=Available)
      inStock: inStock,
      supplierSSC: quantity <= inStock ? '999' : '',
      supplierSSCText: quantity <= inStock ? 'Filialentnahme' : '',
      price,
      supplier: { id: supplier?.id },
    };
    return availability;
  }

  private _mapToPickUpAvailability(
    availabilities: SwaggerAvailabilityDTO[],
  ): Availability<AvailabilityDTO, SwaggerAvailabilityDTO>[] {
    if (isArray(availabilities)) {
      const preferred = availabilities.filter((f) => f.preferred === 1);
      const totalAvailable = availabilities.reduce((sum, av) => sum + (av?.qty || 0), 0);

      return preferred.map((p) => {
        return [
          {
            orderDeadline: p?.orderDeadline,
            availabilityType: p?.status,
            ssc: p?.ssc,
            sscText: p?.sscText,
            supplier: { id: p?.supplierId },
            isPrebooked: p?.isPrebooked,
            estimatedShippingDate: p?.requestStatusCode === '32' ? p?.altAt : p?.at,
            price: p?.price,
            inStock: totalAvailable,
            supplierProductNumber: p?.supplierProductNumber,
            supplierInfo: p?.requestStatusCode,
            lastRequest: p?.requested,
            itemId: p.itemId,
            priceMaintained: p.priceMaintained,
          },
          p,
        ];
      });
    }
  }

  private _mapToShippingAvailability(availabilities: SwaggerAvailabilityDTO[]): AvailabilityDTO[] {
    const preferred = availabilities.filter((f) => f.preferred === 1);
    return preferred.map((p) => {
      return {
        availabilityType: p?.status,
        ssc: p?.ssc,
        sscText: p?.sscText,
        isPrebooked: p?.isPrebooked,
        estimatedShippingDate: p?.requestStatusCode === '32' ? p?.altAt : p?.at,
        estimatedDelivery: p?.estimatedDelivery,
        price: p?.price,
        supplierProductNumber: p?.supplierProductNumber,
        supplierInfo: p?.requestStatusCode,
        lastRequest: p?.requested,
        itemId: p.itemId,
        priceMaintained: p.priceMaintained,
      };
    });
  }

  getInStockByEan(params: { eans: string[]; branchId?: number }): Observable<Record<string, StockInfoDTO>> {
    let branchId$ = of(params.branchId);

    if (!params.branchId) {
      branchId$ = this.getDefaultBranch().pipe(
        first(),
        map((b) => b.id),
      );
    }

    const stock$ = branchId$.pipe(
      mergeMap((branchId) =>
        this._stockService.StockGetStocksByBranch({ branchId }).pipe(map((response) => response.result?.[0])),
      ),
    );

    return stock$.pipe(
      mergeMap((stock) =>
        this._stockService.StockInStockByEAN({ eans: params.eans, stockId: stock.id }).pipe(
          map((response) => {
            const result = response.result ?? [];

            for (const stockInfo of result) {
              stockInfo.ean = stockInfo.ean;
            }

            return result.reduce<Record<string, StockInfoDTO>>((acc, stockInfo) => {
              acc[stockInfo.ean] = stockInfo;
              return acc;
            }, {});
          }),
        ),
      ),
    );
  }

  getInStock({ itemIds, branchId }: { itemIds: number[]; branchId: number }): Observable<StockInfoDTO[]> {
    return this.getStockByBranch(branchId).pipe(
      mergeMap((stock) =>
        this._stockService
          .StockInStock({ articleIds: itemIds, stockId: stock.id })
          .pipe(map((response) => response.result)),
      ),
    );
  }
}
