import mongoose from 'mongoose';
import dayjs from 'dayjs';

import { InventoryItemModel, IInventoryItem } from '../models/InventoryItem.model';
import { IUploadedItems } from '../interfaces/collaterals.interface';
import { ECollaterals } from '../enums/collaterals.enum';
import {
  getBBCSheetsByType,
  getBBCSheetsByTypeGroupedByBBC,
  getUniqueDates,
  getUniqueDatesForPeriod,
} from '../db/bbcSheets.db';
import { getBBCDatesByBorrower, getBBCDatesForPeriod } from '../db/bbcDates.db';
import { getBBCSheetsForBorrowerAndType } from '../db/collaterals.db';
import { convertToPercentage } from '../helpers/numbers.helper';
import { getUnitCostDifference } from '../queries/inventory/unit-cost-difference';
import { getInventoryTurn } from '../queries/inventory/turn';
import { getInconsistentData } from '../queries/inventory/inconsistent-data';
import { getInvoiceMovement } from '../queries/inventory/movement';
import { getExtension } from '../queries/inventory/extension';
import { CustomerGroupModel } from '../models/CustomerGroup.model';
import { getInventoryDifference } from '../queries/inventory/invoice-difference';

import {
  aggregateAllValuesForBbc,
  aggregateBbcExposures,
  aggregateBbcExposuresGrouped,
  aggregateSKUDateAgingPlot,
  getAllTimeOldestSKUDateNew,
  getLastDigitFromAllValues,
} from '../db/inventories';
import { getBBCAPByAPPeriod } from '../db/payable-accounts';
import { getAPCustomerGroups, getMappedGroups } from '../db/groups';
import { CollateralsService } from './collaterals.service';

export class InventoryService {

  constructor(
    private readonly collateralsService: CollateralsService,
  ) {
  }

  async updateInventorySKU({ bbcDate, borrowerId, items }: IUploadedItems<IInventoryItem>) {
    const skuDates = await getAllTimeOldestSKUDateNew(borrowerId, bbcDate);
    return items.map((i) => {
      const previousSKUDate = skuDates.find((d) => i.sku.toString().trim() === d._id);
      if (!previousSKUDate) {
        return { ...i, skuDate: bbcDate };
      }
      const isPreviousDateGreaterThanSubmittedDate = new Date(previousSKUDate.oldestDate).getTime() > new Date(bbcDate).getTime();
      if (isPreviousDateGreaterThanSubmittedDate) {
        return { ...i, skuDate: bbcDate };
      }
      return { ...i, skuDate: previousSKUDate.oldestDate };
    });
  }

  async getBenford(borrowerId: string) {
    const sheetIds = await getUniqueDates(borrowerId, 3, ECollaterals.INVENTORY);
    const inventory = await getLastDigitFromAllValues(sheetIds);
    return this.collateralsService.getCollateralsBenford(inventory);
  }

  async getInventoryAgingPlot(borrowerId: string, rangeStart, rangeEnd) {
    const latestFiveNew = await getUniqueDatesForPeriod(borrowerId, ECollaterals.INVENTORY, new Date(rangeStart), new Date(rangeEnd));
    const aggregationPromises = latestFiveNew.map((bbcGroup) =>
      aggregateSKUDateAgingPlot(bbcGroup.ids),
    );

    const aggregations = await Promise.all(aggregationPromises);
    return [
      { name: '0-30', data: aggregations.map((i) => i['0-30']) },
      { name: '30-60', data: aggregations.map((i) => i['30-60']) },
      { name: '60-90', data: aggregations.map((i) => i['60-90']) },
      { name: '90+', data: aggregations.map((i) => i['90+']) },
    ];
  }

  async getAllSkuValues(borrowerId: string, startDate: string, endDate: string) {
    const bbcDates = await getBBCDatesForPeriod(borrowerId, new Date(startDate), new Date(endDate));
    const sheetGroups = await getBBCSheetsByTypeGroupedByBBC(bbcDates.map((bbcDate) => bbcDate._id.toString()), ECollaterals.INVENTORY);

    const aggregationPromises = sheetGroups.map((sheetGroup) => aggregateAllValuesForBbc(sheetGroup.sheets.map((sheet) => new mongoose.Types.ObjectId(String(sheet._id)))));
    const aggregations = await Promise.all(aggregationPromises);
    const chartData = [];

    function toEntries<T>(a: T[]) {
      return a.map((value, index) => [index, value] as const);
    }

    const allSheets = await getBBCSheetsForBorrowerAndType(borrowerId, ECollaterals.INVENTORY);
    const allSheetsIds = allSheets.map((sheet) => sheet._id);

    const uniqSkus = [...new Set(aggregations.reduce((acc: string[], skuGroup) => {
      return [...acc, ...skuGroup.map((skuObject) => skuObject.sku)];
    }, []))];
    const skusWithDates = uniqSkus.reduce((acc, sku) => ({ ...acc, [sku]: null }), {});
    await Promise.all(uniqSkus.map(async (sku) => {
      const firstSeen = (await this.getFirstSeenNew(allSheetsIds.map((id) => new mongoose.Types.ObjectId(String(id))), sku)).pop();
      skusWithDates[sku] = firstSeen.bbcDate;
    }));

    for (const [bbcIndex, item] of toEntries(aggregations)) {
      chartData.push({
        name: dayjs(bbcDates[bbcIndex]?.bbcDate).format('YYYY/MM/DD'),
        data: [],
      });

      for (const [itemIndex, skuAndValue] of toEntries(item)) {
        chartData[bbcIndex].data.push([
          itemIndex + 1,
          skuAndValue.value,
          null,
          {
            sku: skuAndValue.sku,
            skuDescription: skuAndValue.skuDescription,
            category: skuAndValue.category,
            firstSeen: dayjs(skusWithDates[skuAndValue.sku]).format('YYYY/MM/DD'),
          },
        ]);
      }
    }
    return chartData;
  }

  async getFirstSeenNew(sheetIds: mongoose.Types.ObjectId[], sku: string) {
    return InventoryItemModel.aggregate<{ bbcDate: Date }>([
      {
        $match: {
          'bbcSheetId': { $in: sheetIds },
          'sku': sku,
        },
      },
      {
        $lookup: {
          from: 'bbcsheets',
          localField: 'bbcSheetId',
          foreignField: '_id',
          as: 'bbcSheet',
        },
      },
      {
        $unwind: {
          path: '$bbcSheet',
        },
      },
      {
        $lookup: {
          from: 'bbcdates',
          localField: 'bbcSheet.bbcDateId',
          foreignField: '_id',
          as: 'bbc',
        },
      },
      {
        $unwind: {
          path: '$bbc',
        },
      },
      {
        $sort: {
          'bbc.bbcDate': 1,
        },
      },
      {
        $limit: 1,
      },
      {
        $project: {
          '_id': 0,
          'bbcDate': '$bbc.bbcDate',
        },
      },
    ]);
  }

  async getInconsistentData(bbcDateId: string) {
    return getInconsistentData({ bbcDateId: new mongoose.Types.ObjectId(bbcDateId) });
  }

  async getUnitCostDifference(borrowerId: string) {
    return getUnitCostDifference({ borrowerId: new mongoose.Types.ObjectId(borrowerId) });
  }

  async getExposures(borrowerId: string, useCustomerGroup: boolean) {
    const latestThree = await getUniqueDates(borrowerId, 3, ECollaterals.INVENTORY);
    const aggregationPromises = latestThree.map((bbcDate) =>
      useCustomerGroup
        ? aggregateBbcExposuresGrouped(bbcDate.ids)
        : aggregateBbcExposures(bbcDate.ids),
    );

    const aggregations = await Promise.all(aggregationPromises);

    const allCategoryNames = [];
    const formattedSeriesData = [
      { name: latestThree[0]?.bbcDate, data: [] },
      { name: latestThree[1]?.bbcDate, data: [] },
      { name: latestThree[2]?.bbcDate, data: [] },
    ];
    aggregations.forEach((setOfBbcExposures) => {
      setOfBbcExposures.topItemsForBbc.map((topItem) => {
        if (!allCategoryNames.includes(topItem.name)) {
          allCategoryNames.push(topItem.name);
        }
      });
    });
    aggregations.forEach((setOfBbcExposures, i) => {
      allCategoryNames.map((categoryName) => {
        const foundValue = setOfBbcExposures.topItemsForBbc.find((topItem) => topItem.name === categoryName);
        const convertedValue = foundValue ? convertToPercentage(foundValue.totalItemValue, setOfBbcExposures.inventoryTotal) : 0;
        formattedSeriesData[i].data.push(convertedValue);
      });
    });
    return { allCategoryNames, formattedSeriesData };
  }

  async getInventoryTurn(borrowerId: string, startDate: string, endDate: string) {
    return getInventoryTurn({
      borrowerId: new mongoose.Types.ObjectId(borrowerId),
      startDate: new Date(startDate),
      endDate: new Date(endDate),
    });
  }

  async getInvoiceMovement(borrowerId: string, bbcDateId: string) {
    return getInvoiceMovement({
      borrowerId: new mongoose.Types.ObjectId(borrowerId),
      bbcDateId: new mongoose.Types.ObjectId(bbcDateId),
    });
  }

  async getContras(borrowerId: string, startDate: string, endDate: string) {
    const bbcDates = await getBBCDatesByBorrower(borrowerId);
    const bbcIds = bbcDates.map((bbc) => bbc._id.toString());
    const bbcSheets = await getBBCSheetsByType(bbcIds, ECollaterals.ACCOUNT_PAYABLE);
    const aps = await getBBCAPByAPPeriod(bbcSheets.map((bbcSheet) => new mongoose.Types.ObjectId(String(bbcSheet._id))), startDate, endDate);
    const customerAPGroups = await getAPCustomerGroups(borrowerId);
    const contras = [];
    await Promise.all(aps.map(async (ap) => {
      const contra = {
        invoiceDate: ap.apDate,
        customer: ap.customerName,
        invoiceNumber: ap.poNumber,
        amount: ap.amount,
        newAPName: ap.customerName,
        contra: 0,
      };
      const customerAPGroup = customerAPGroups.find((group) => group.items === ap.customerName);
      if (customerAPGroup) {
        const mappedGroup = await getMappedGroups({ group2: customerAPGroup._id.toString() });
        if (mappedGroup && mappedGroup.length) {
          const customerGroup = await CustomerGroupModel.findById(mappedGroup[0].group1);
          if (customerGroup) {
            contra.newAPName = customerAPGroup.groupName;
            contra.contra = 1;
          }
        }
      }
      contras.push(contra);
    }));
    return contras.sort((a, b) => new Date(a.invoiceDate).valueOf() - new Date(b.invoiceDate).valueOf());
  }

  async getExtension(borrowerId: string, startDate: string, endDate: string) {
    return getExtension({
      borrowerId: new mongoose.Types.ObjectId(borrowerId),
      startDate: new Date(startDate),
      endDate: new Date(endDate),
    });
  }

  async compareInventories(bbcDateId: string) {
    return await getInventoryDifference({ bbcDateId: new mongoose.Types.ObjectId(bbcDateId) });
  }
}
