import mongoose from 'mongoose';
import _ from 'lodash';

import { ECollaterals } from '../enums/collaterals.enum';
import { IPaginatorOptions, IUploadedItems } from '../interfaces/collaterals.interface';
import { BBCDateModel, IBBCDateDoc, ICreateBBCDate } from '../models/BBCDate.model';
import { BBCSheetModel, IBBCSheetDoc, IBBCSheetWithBBCDate } from '../models/BBCSheet.model';
import { IInventoryItem } from '../models/InventoryItem.model';
import { IReceivableItem } from '../models/ReceivableItem.model';
import {
  IAccountPayableItem,
} from '../models/AccountPayableItem.model';
import { createBBCDate, getBBCDates, getBBCDatesByType } from '../db/bbcDates.db';
import { createBBCSheet, getBBCSheetsByType } from '../db/bbcSheets.db';
import {
  collateralMap,
  COLLATERALS_LOOKUP,
  createQuery,
  getCollateralListBySheetId,
  ITEMS_PAGINATION,
} from '../db/collaterals.db';
import { getDigitsFrequencies } from '../helpers/numbers.helper';
import { getEnumKeyByEnumValue } from '../helpers/enums.helper';

import { GroupsService } from './groups.service';
import { UploadsService } from './uploads.service';

export class CollateralsService {

  constructor(
    private readonly _groupsService: GroupsService,
    private readonly _uploadsService: UploadsService,
  ) {
  }

  async findCollateralsByType(bbcDateId: string, collateralType: ECollaterals) {
    const sheets = await this.findCollateralSheets(bbcDateId, collateralType);
    return (await Promise.all(sheets.map(async (sheet) => {
      return await this.getCollateralsBySheet(sheet);
    })))
      .reduce((acc, group) => [...acc, ...group], []);
  }

  async findCollaterals(collateralsId: string, group: boolean, paginatorOptions?: IPaginatorOptions) {
    const sheet = await this.findCollateralSheetsById(collateralsId);
    if (group) {
      return await this.getCollateralsBySheet(sheet, paginatorOptions);
    }
    return await getCollateralListBySheetId(sheet, paginatorOptions);
  }

  async countCollateralItems(bbcSheetId: string) {
    const sheet = await this.findCollateralSheetsById(bbcSheetId);
    return await this.getCollateralItemTotal(sheet);
  }

  async findCollateralSheets(bbcDateId: string, collateralType: ECollaterals) {
    return BBCSheetModel.find({ bbcDateId, dataType: collateralType });
  }

  async findCollateralSheetsById(collateralsId: string): Promise<IBBCSheetDoc> {
    return BBCSheetModel.findById(collateralsId);
  }

  async findCollateralSheetByIdWithBBCDate(collateralsId: string) {
    const bbcSheet: IBBCSheetWithBBCDate[] = await BBCSheetModel.aggregate([
      {
        $match: {
          '_id': new mongoose.Types.ObjectId(collateralsId),
        },
      }, {
        $lookup: {
          'from': 'bbcdates',
          'localField': 'bbcDateId',
          'foreignField': '_id',
          'as': 'bbcDate',
        },
      }, {
        $unwind: {
          'path': '$bbcDate',
        },
      },
    ]);
    return bbcSheet.length ? bbcSheet[0] : null;
  }

  async findAllCollaterals(borrowerId: string, collateralTypes: ECollaterals[]) {
    return getBBCDatesByType(borrowerId, collateralTypes);
  }

  async saveCollateralItems<T extends IInventoryItem | IReceivableItem | IAccountPayableItem>(uploadedItems: IUploadedItems<T>, collateralType: ECollaterals, shift = 0) {
    const bbcDates = await getBBCDates(uploadedItems.borrowerId, uploadedItems.bbcDate);
    const bbcSheets = await getBBCSheetsByType(bbcDates.map((bbcDate) => String(bbcDate._id)), collateralType);
    const lastBBCDate = bbcDates.pop();
    const lastBBCSheet = bbcSheets.find((bbcSheet) => (bbcSheet.fileName === uploadedItems.fileName) && (bbcSheet.sheetName === uploadedItems.sheetName));

    let savedBBCDate;
    if (!lastBBCDate) {
      const newBBCDate = new BBCDateModel({
        borrowerId: uploadedItems.borrowerId,
        bbcDate: uploadedItems.bbcDate,
      });
      savedBBCDate = await newBBCDate.save();
    } else {
      savedBBCDate = lastBBCDate;
    }

    let savedBBCSheet;
    if (!lastBBCSheet) {
      const newBBCSheet = new BBCSheetModel({
        bbcDateId: savedBBCDate._id,
        dataType: collateralType,
        fileName: uploadedItems.fileName,
        sheetName: uploadedItems.sheetName,
      });
      savedBBCSheet = await newBBCSheet.save();
    } else {
      savedBBCSheet = bbcSheets.pop();
    }

    const orderedItems = uploadedItems.items.map((item, index) => ({
      ...item,
      order: index + shift,
    }));
    await Promise.all(orderedItems.map(async (item) => {
      const newCollateralItem = new collateralMap[collateralType]({
        bbcSheetId: savedBBCSheet._id,
        order: item.order,
        ...item,
      });
      await newCollateralItem.save();
    }));
    return savedBBCDate._id;
  }

  async getCollateralsBySheet(sheet: IBBCSheetDoc, paginatorOptions?: IPaginatorOptions) {

    const groupFields = {
      bbcDate: 'bbc.bbcDate',
      borrowerId: 'bbc.borrowerId',
      fileName: 'bbcSheet.fileName',
      sheetName: 'bbcSheet.sheetName',
    };

    const { groupQueries, itemQueries, enumKey } = createQuery(groupFields, sheet.dataType);
    const groupingQuery = { _id: '$bbcSheetId', ...groupQueries, items: { $push: itemQueries } };

    const collateralSheets = await collateralMap[enumKey].aggregate([
      {
        $match: {
          'bbcSheetId': sheet._id,
        },
      },
      ...COLLATERALS_LOOKUP,
      {
        $sort: {
          order: 1,
        },
      },
      ...ITEMS_PAGINATION(paginatorOptions),
      {
        $group: groupingQuery,
      },
    ]);
    if (collateralSheets.length === 0) {
      const bbcDates = await BBCDateModel.aggregate<IBBCDateDoc>([
        {
          $match: {
            '_id': sheet.bbcDateId,
          },
        },
      ]);
      if (bbcDates.length === 0) {
        return [];
      }
      return [{
        _id: sheet._id,
        bbcDate: bbcDates[0].bbcDate,
        borrowerId: bbcDates[0].borrowerId,
        fileName: sheet.fileName,
        sheetName: sheet.sheetName,
        items: [],
      }];
    }
    return collateralSheets;
  }

  async getCollateralItemTotal(sheet: IBBCSheetDoc) {
    const enumKey = getEnumKeyByEnumValue(ECollaterals, sheet.dataType);
    return collateralMap[enumKey].countDocuments(
      {
        'bbcSheetId': sheet._id,
      },
    );
  }

  getCollateralsBenford(collaterals: any[]) {
    if (collaterals.length === 0) {
      return [];
    }
    const eachInventoryBenfordResult = [];

    collaterals[0]?.digitArrays.forEach((lastDigitsArray) => {
      const digitsArrayWithoutZeros = lastDigitsArray.filter((digit) => digit !== '0');
      const results = [];
      const firstDigitFrequencies = getDigitsFrequencies(lastDigitsArray);

      for (let n = 1; n <= 9; n++) {
        const dataFrequency = firstDigitFrequencies[n];
        const dataFrequencyPercent = (dataFrequency / digitsArrayWithoutZeros.length) * 100;
        const dataFrequencyTotal = isNaN(dataFrequencyPercent) ? 0 : Math.round(dataFrequencyPercent * 100) / 100;
        results.push(dataFrequencyTotal);
      }
      eachInventoryBenfordResult.push(results);
    });
    return {
      _id: collaterals[0]._id,
      bbcDates: collaterals[0].bbcDates,
      confirmed: eachInventoryBenfordResult,
    };
  }

  async countCollateralDocsByBBC(bbcIds: string[], collateralType: ECollaterals) {
    const bbcSheets = await getBBCSheetsByType(bbcIds, collateralType);
    const collateralModel = collateralMap[collateralType];
    if (!collateralModel) {
      console.error(`Could not find a model for ${collateralType}`);
      return 0;
    }
    return collateralModel.countDocuments(
      {
        'bbcSheetId': { $in: bbcSheets.map((bbcSheet) => bbcSheet._id) },
      },
    );

  }

  excludeNoDataColumns(data: any[], columns: string[]) {
    return data.map((dataRow) => _.pick(dataRow, columns));
  }

  async addCustomerGroups(data: any[], borrowerId: string) {
    const groupName = 'customers';
    const groups = await this._groupsService.getBorrowerGroups(borrowerId, groupName);
    return data.map((dataRow) => {
      const foundGroup = groups[groupName].find((group) => group.items.includes(dataRow.customerTitle));
      return {
        ...dataRow,
        customerGroup: foundGroup ? foundGroup.groupName : dataRow.customerTitle,
      };
    });
  }

  async getLatestBBC(borrowerId: string, bbcDate = new Date()) {
    const bbcDateDocs = await BBCDateModel.aggregate<IBBCDateDoc>([
      {
        $match: {
          'borrowerId': new mongoose.Types.ObjectId(borrowerId),
          'bbcDate': { $lte: bbcDate },
        },
      }, {
        $sort: {
          'bbcDate': -1,
        },
      }, {
        $limit: 1,
      },
    ]);
    if (bbcDateDocs.length === 0) {
      return null;
    }
    return bbcDateDocs[0];
  }

  async gebNextBBC(borrowerId: string, bbcDate = new Date()) {
    return BBCDateModel.aggregate<IBBCDateDoc>([
      {
        $match: {
          'borrowerId': new mongoose.Types.ObjectId(borrowerId),
          'bbcDate': { $gte: bbcDate },
        },
      }, {
        $sort: {
          'bbcDate': -1,
        },
      },
    ]);
  }

  async getLatestBBCDate(borrowerId: string) {
    const latestBBC = await this.getLatestBBC(borrowerId);
    if (!latestBBC) {
      return { lastBBCDate: null, lastBBCId: null };
    }
    return { lastBBCDate: latestBBC.bbcDate, lastBBCId: latestBBC._id.toString() };
  }

  async createBBCDate(bbcDate: ICreateBBCDate) {
    if (bbcDate.inventory || bbcDate.receivables) {
      const existingBbcDates = await getBBCDates(bbcDate.borrowerId, bbcDate.date);
      let existingBbcDate = existingBbcDates.length > 0 ? existingBbcDates[0] : null;
      if (!existingBbcDate) {
        existingBbcDate = await createBBCDate(bbcDate.borrowerId, bbcDate.date);
      }
      if (bbcDate.inventory) {
        await createBBCSheet(existingBbcDate._id.toString(), 'INVENTORY');
      }
      if (bbcDate.receivables) {
        await createBBCSheet(existingBbcDate._id.toString(), 'RECEIVABLE');
      }
    }
  }

  async deleteCollateralById(collateralId: string) {
    const results = await BBCSheetModel.findByIdAndDelete(collateralId);
    setTimeout(async () => await this._uploadsService.removeEmptyBBC(true), 500);
    return results;
  }
}
