import { Injectable } from '@angular/core';
import { StockInfoDTO } from '@generated/swagger/inventory-api';
import { groupBy, isEqual, uniqWith } from 'lodash';
import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
import { buffer, debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { DomainAvailabilityService } from './availability.service';

export type ItemBranch = { itemId: number; branchId: number };
export type InStock = ItemBranch & { inStock: number; fetching: boolean };

@Injectable({ providedIn: 'root' })
export class DomainInStockService {
  private _inStockQueue = new Subject<ItemBranch>();
  private _inStockMap = new BehaviorSubject<Record<string, number>>({});
  private _inStockFetchingMap = new BehaviorSubject<Record<string, boolean>>({});

  constructor(private _availability: DomainAvailabilityService) {
    const queueBufferTrigger = this._createQueueBufferTrigger();

    this._inStockQueue.pipe(buffer(queueBufferTrigger)).subscribe(this._handleStockDataToFetch);
  }

  private _createQueueBufferTrigger() {
    return this._inStockQueue.pipe(debounceTime(1000));
  }

  private _handleStockDataToFetch = (itemBranchData: ItemBranch[]) => {
    const unique = uniqWith(itemBranchData, isEqual);
    if (unique?.length > 0) {
      this._fetchStockData(unique);
    }
  };

  getKey({ itemId, branchId }: ItemBranch) {
    return `${itemId}_${branchId}`;
  }

  getInStock$({ itemId, branchId }: ItemBranch): Observable<InStock> {
    return new Observable<InStock>((obs) => {
      if (!(itemId && branchId)) {
        obs.error(new Error(`ItemId: ${itemId} or BranchId: ${branchId} missing`));
        return;
      }

      const key = this.getKey({ itemId, branchId });
      this._addToInStockQueue({ itemId, branchId });

      let _previousValue: InStock;

      const sub = combineLatest([this._inStockMap, this._inStockFetchingMap])
        .pipe(distinctUntilChanged(isEqual))
        .subscribe(([inStockMap, inStockFetchingMap]) => {
          const inStock: InStock = {
            itemId,
            branchId,
            inStock: inStockMap[key],
            fetching: inStockFetchingMap[key] ?? false,
          };

          if (!isEqual(inStock, _previousValue)) {
            obs.next(inStock);
          }

          _previousValue = inStock;
        });
      return () => {
        sub.unsubscribe();
      };
    });
  }

  private _addToInStockQueue({ itemId, branchId }: ItemBranch): void {
    this._inStockQueue.next({ itemId, branchId });
    this._setInStockFetching({ itemId, branchId }, true);
  }

  private _setInStockFetching({ itemId, branchId }: ItemBranch, value: boolean) {
    const key = this.getKey({ itemId, branchId });
    const current = this._inStockFetchingMap.getValue();
    this._inStockFetchingMap.next({ ...current, [key]: value });
  }

  private _setInStock({ itemId, branchId }: ItemBranch, inStock: number) {
    const key = this.getKey({ itemId, branchId });
    const current = this._inStockMap.getValue();
    this._inStockMap.next({ ...current, [key]: inStock });
  }

  private _fetchStockData(itemBranchData: ItemBranch[]) {
    const grouped = groupBy(itemBranchData, 'branchId');
    Object.keys(grouped).forEach((key) => {
      const branchId = Number(key);
      const itemIds = itemBranchData
        .filter((itemBranch) => itemBranch.branchId === branchId)
        .map((item) => item.itemId);
      this._availability
        .getInStock({ itemIds, branchId })
        .subscribe(
          this._fetchStockDataResponse({ itemIds, branchId }),
          this._fetchStockDataError({ itemIds, branchId }),
        );
    });
  }

  private _fetchStockDataResponse =
    ({ itemIds, branchId }: { itemIds: number[]; branchId: number }) =>
    (stockInfos: StockInfoDTO[]) => {
      itemIds.forEach((itemId) => {
        const stockInfo = stockInfos.find(
          (stockInfo) => stockInfo.itemId === itemId && stockInfo.branchId === branchId,
        );
        let inStock = 0;

        if (stockInfo?.inStock) {
          inStock = stockInfo.inStock;
        }

        this._setInStockFetching({ itemId, branchId }, false);
        this._setInStock({ itemId, branchId }, inStock);
      });
    };

  private _fetchStockDataError =
    ({ itemIds, branchId }: { itemIds: number[]; branchId: number }) =>
    (error: Error) => {
      itemIds.forEach((itemId) => {
        this._setInStockFetching({ itemId, branchId }, false);
        this._setInStock({ itemId, branchId }, 0);
      });
      console.error('DomainInStockService._fetchStockData()', error);
    };
}
