import mongoose from 'mongoose';
import dayjs from 'dayjs';
import Decimal from 'decimal.js';

import { ILoanTransactionDoc, LoanTransaction } from '../models/LoanTransaction.model';
import { defaultDateFormat, convertDateToFormat, isProductActive } from '../helpers/date.helper';
import { formatNumbers } from '../helpers/numbers.helper';
import { BorrowerCompliance } from '../models/BorrowerCompliance.model';
import { EComplianceItemStatus } from '../models/ComplianceItem.model';
import { getBBCSheetsByTypeGroupedByBBC } from '../db/bbcSheets.db';
import { ECollaterals } from '../enums/collaterals.enum';
import { ELoanTypes } from '../enums/loan-types.enum';
import { getReceivableItemsCustomerAndValue } from '../db/receivables.db';
import {
  getBBCDatesByBorrower,
  getBBCDatesForPeriod,
  getLatestSignedBBCDateDoc,
} from '../db/bbcDates.db';

import {
  BorrowerSummary,
  EChartNames,
  ETableNames,
  IBorrowerSummary,
  IBorrowerSummaryDoc,
} from '../models/BorrowerSummary.model';
import { getInventoryItemsSKUAndValue } from '../db/inventories';
import { AvailabilityService } from './availability.service';
import { BorrowerService } from './borrowers.service';
import { InventoryService } from './inventory.service';
import { LoanChargesService } from './loan-charges.service';
import { LoanTransactionsService } from './loan-transactions.service';
import { ReportsService } from './reports.service';
import { BorrowersDB } from './borrowers.db';

export class BorrowerSummaryService {

  constructor(
    private readonly availabilityService: AvailabilityService,
    private readonly borrowersDB: BorrowersDB,
    private readonly borrowerService: BorrowerService,
    private readonly inventoryService: InventoryService,
    private readonly loanChargesService: LoanChargesService,
    private readonly loanTransactionsService: LoanTransactionsService,
    private readonly reportsService: ReportsService,
  ) {
  }

  async getBorrowerSummary(borrowerId: string): Promise<IBorrowerSummaryDoc> {
    return BorrowerSummary.findOne({ borrowerId });
  }

  private async getHighLevelTable(borrowerId: string, useSignedBBC: boolean) {
    const extraInfo = await this.borrowerService.getExtraInfo(borrowerId);
    const lastBBCDate = useSignedBBC ? extraInfo.lastSignedBBCDate : extraInfo.lastBBCDate;
    const loanBalances = {
      [ELoanTypes.REVOLVER]: 0,
      [ELoanTypes.TERM]: 0,
    };
    const products = await this.loanChargesService.getLoanProducts(borrowerId);
    await Promise.all(products.map(async (p) => {
      const lastTransaction = await this.loanTransactionsService.getLastTransactionForDate(p._id.toString(), dayjs(lastBBCDate).utcOffset(0).endOf('day').toDate());
      if (lastTransaction) {
        loanBalances[ELoanTypes[p.type]] = new Decimal(loanBalances[ELoanTypes[p.type]]).add(lastTransaction.balance).toNumber();
      }
    }));

    if (!useSignedBBC) {
      return {
        title: 'Loan summary - Current',
        data: [
          ['Loan Balance - REVOLVER', formatNumbers(loanBalances[ELoanTypes.REVOLVER])],
          ['Loan Balance - TERM', formatNumbers(loanBalances[ELoanTypes.TERM])],
        ],
      };
    }

    const allProductsData = await this.reportsService.generateBankPortfolioSummary([borrowerId], lastBBCDate, {
      useSignedBBC,
      useStoredBalance: false,
    });
    const collateralRevolver = allProductsData.reduce((acc, product) => {
      return new Decimal(acc).add(product.totalCollateral).toNumber();
    }, 0);

    const collateralTerm = allProductsData.reduce((acc, product) => {
      return new Decimal(acc).add(product.equipmentCollateral).toNumber();
    }, 0);

    return {
      title: `Summary of Last Approved BBC - ${dayjs(lastBBCDate).format(defaultDateFormat)}`,
      data: [
        ['Loan Balance - REVOLVER', formatNumbers(loanBalances[ELoanTypes.REVOLVER])],
        ['Loan Balance - TERM', formatNumbers(loanBalances[ELoanTypes.TERM])],
        ['Collateral - REVOLVER', formatNumbers(collateralRevolver)],
        ['Collateral - TERM', formatNumbers(collateralTerm)],
        ['Available to borrow', formatNumbers(new Decimal(collateralRevolver).sub(loanBalances[ELoanTypes.REVOLVER]).toNumber())],
      ],
    };
  }

  private async getComplianceItemsTable(borrowerId: string) {
    const complianceBorrower = await BorrowerCompliance
      .findOne({ borrower: borrowerId })
      .populate('items.item')
      .lean();
    if (!complianceBorrower) {
      return [];
    }
    const outstandingItems = [];
    const newItems = [];
    complianceBorrower.items.map((item) => {
      item.instances.forEach((i) => {
        if (i.status === EComplianceItemStatus.NEW) {
          newItems.push([
            `${item.item.name} (${dayjs(i.nextDate).format('MMM.D, YYYY')})`,
            dayjs(i.nextDate).format(defaultDateFormat),
          ]);
        } else if (i.status === EComplianceItemStatus.REJECTED) {
          outstandingItems.push([
            `${item.item.name} (${dayjs(i.nextDate).format('MMM.D, YYYY')})`,
            dayjs().diff(i.nextDate, 'day'),
          ]);
        }
      });
    });
    return [...outstandingItems, ...newItems];
  }

  async getTop5SKUsData(borrowerId: string) {
    const lastBBC = await getLatestSignedBBCDateDoc(borrowerId);
    if (lastBBC) {
      const bbcGroups = await getBBCSheetsByTypeGroupedByBBC([lastBBC._id.toString()], ECollaterals.INVENTORY);
      const sheetIds = [];
      bbcGroups.forEach((bbcGroup) => {
        bbcGroup.sheets.forEach((sheet) => {
          sheetIds.push(sheet._id.toString());
        });
      });
      return await getInventoryItemsSKUAndValue(sheetIds);
    }
    return [];
  }

  private async getTop5SKUs(borrowerId: string) {
    const lastBBC = await getLatestSignedBBCDateDoc(borrowerId);
    if (!lastBBC) {
      return [['no signed BBC', '']];
    }
    const SKUs = await this.getTop5SKUsData(borrowerId);
    if (SKUs.length === 0) {
      return [['MANUALLY ENTERED', '']];
    }
    return SKUs.slice(0, 5).map((SKU) => [SKU._id, formatNumbers(SKU.totalAmount)]);
  }

  private async getInventoryTurn(borrowerId: string) {
    const startPeriod = dayjs().utcOffset(0).startOf('day').subtract(150, 'day').toDate().toISOString();
    const endPeriod = dayjs().utcOffset(0).endOf('day').toDate().toISOString();
    const allSkuValues = await this.inventoryService.getInventoryTurn(borrowerId, startPeriod, endPeriod);
    const values = allSkuValues.tableByUnit.length > 0
      ? allSkuValues.tableByUnit.sort((a, b) => a.current < b.current ? 1 : -1).slice(0, 5)
      : [];
    return values.map((value) => [value.sku, formatNumbers(value.current)]);
  }

  async getTop5CustomersConcentrationData(borrowerId: string) {
    const bbc = await getBBCDatesByBorrower(borrowerId, -1);
    if (bbc.length > 0) {
      const lastBBC = bbc[0];
      const bbcGroups = await getBBCSheetsByTypeGroupedByBBC([lastBBC._id.toString()], ECollaterals.RECEIVABLE);
      const sheetIds = [];
      bbcGroups.forEach((bbcGroup) => {
        bbcGroup.sheets.forEach((sheet) => {
          sheetIds.push(sheet._id.toString());
        });
      });
      return await getReceivableItemsCustomerAndValue(sheetIds);
    }
    return [];
  }

  private async getTop5CustomersConcentration(borrowerId: string) {
    const bbc = await getBBCDatesByBorrower(borrowerId, -1);
    if (!bbc) {
      return [['no BBC', '']];
    }
    const customers = await this.getTop5CustomersConcentrationData(borrowerId);
    if (customers.length === 0) {
      return [['MANUALLY ENTERED', '']];
    }
    const totalAmount = customers.reduce((acc, customer) => new Decimal(acc).add(customer.totalAmount).toNumber(), 0);
    return customers
      .slice(0, 5)
      .map((customer) => {
        return [customer._id, `${formatNumbers(new Decimal(customer.totalAmount).div(totalAmount).mul(100).toDP(2).toNumber())}%`];
      });
  }

  private async getAgedCustomers(borrowerId: string) {
    const lastSignedBBC = await getLatestSignedBBCDateDoc(borrowerId);
    if (lastSignedBBC) {
      const { customerItems } = await this.availabilityService.getAvailabilityItemsWithFilter(lastSignedBBC._id.toString(), 'crossAgeDeduction');
      return customerItems
        .sort((a, b) => a.customerAmount < b.customerAmount ? 1 : -1)
        .map((c) => [c.customerTitle, formatNumbers(c.customerAmount)]);
    }
    return [['no BBC', '']];
  }

  private async getLast5TransactionsTable(borrowerId: string) {
    const products = await this.loanChargesService.getLoanProducts(borrowerId);
    const startDate = dayjs().utcOffset(0).subtract(5, 'day').startOf('day').toDate();
    const transactions = await LoanTransaction.aggregate<ILoanTransactionDoc>([
      {
        '$match': {
          'productId': { '$in': products.map((p) => p._id) },
          'date': { $gte: startDate },
        },
      }, {
        '$sort': {
          'date': -1,
        },
      }, {
        '$limit': 5,
      },
    ]);
    return transactions.map((t) => {
      return [
        dayjs(t.date).format(defaultDateFormat),
        t.reference,
        formatNumbers(t.amount),
      ];
    }, []);
  }

  private async getChartData(borrowerId: string) {
    const products = await this.loanChargesService.getLoanProducts(borrowerId);
    const revolverProduct = products.find((product) => product.type === ELoanTypes.REVOLVER && isProductActive(product.startDate, product.deactivationDate, new Date()));
    const chartData = [];
    if (revolverProduct) {
      const lastSignedBBC = await getLatestSignedBBCDateDoc(borrowerId);
      if (lastSignedBBC) {
        const periodEnd = lastSignedBBC.bbcDate;
        const periodStart = dayjs(lastSignedBBC.bbcDate).utcOffset(0).subtract(6, 'month').toDate();
        const BBCs = await getBBCDatesForPeriod(borrowerId, periodStart, periodEnd);
        await Promise.all(BBCs.map(async (bbc) => {
          if (bbc) {
            const lastTransaction = await this.loanTransactionsService.getLastTransactionForDate(revolverProduct._id.toString(), dayjs(bbc.bbcDate).utcOffset(0).endOf('day').toDate());
            const allProductsData = await this.reportsService.generateBankPortfolioSummary([borrowerId], bbc.bbcDate, {
              useSignedBBC: false,
              useStoredBalance: false,
            });
            const revolverData = allProductsData.find((p) => p.productType === ELoanTypes.REVOLVER);
            chartData.push({
              bbc: convertDateToFormat(bbc.bbcDate),
              revolverCollateralTotal: revolverData ? revolverData.totalCollateral : 0,
              revolverLoanBalance: lastTransaction ? lastTransaction.balance : 0,
            });
          }
        }));
      }
    }

    return {
      data: chartData.sort((a, b) => new Date(a.bbc).valueOf() - new Date(b.bbc).valueOf()),
      series: [
        {
          type: 'bar',
          xKey: 'bbc',
          yKey: 'revolverCollateralTotal',
          yName: 'Revolver Collateral Total',
        },
        {
          type: 'bar',
          xKey: 'bbc',
          yKey: 'revolverLoanBalance',
          yName: 'Revolver Loan Balance',
        },
      ],
    };
  }

  async updateBorrowerSummary(borrowerId: string): Promise<void> {
    const newSummary: IBorrowerSummary = {
      borrowerId: new mongoose.Types.ObjectId(borrowerId),
      lastCalculatedAt: new Date(),
      tableData: {
        [ETableNames.HIGH_LEVEL]: await this.getHighLevelTable(borrowerId, false),
        [ETableNames.HIGH_LEVEL_APPROVED]: await this.getHighLevelTable(borrowerId, true),
        [ETableNames.COMPLIANCE_ITEMS]: {
          title: 'Compliance Items',
          data: await this.getComplianceItemsTable(borrowerId),
        },
        [ETableNames.TOP_5_SKUS]: {
          title: 'Top 5 SKUs',
          data: await this.getTop5SKUs(borrowerId),
        },
        [ETableNames.LAST_5_DAYS_TRANSACTIONS]: {
          title: 'Last 5 days transactions',
          data: await this.getLast5TransactionsTable(borrowerId),
        },
        [ETableNames.INVENTORY_TURN]: {
          title: 'Inventory turn (last 90 days)',
          data: await this.getInventoryTurn(borrowerId),
        },
        [ETableNames.TOP_5_CUSTOMERS_CONCENTRATION]: {
          title: 'Top 5 Customers Concentration',
          data: await this.getTop5CustomersConcentration(borrowerId),
        },
        [ETableNames.AGED_OUT_CUSTOMERS]: {
          title: 'Aged out customers',
          data: await this.getAgedCustomers(borrowerId),
        },
      },
      chartData: {
        [EChartNames.LAST_6_MONTH]: await this.getChartData(borrowerId),
      },
    };
    await BorrowerSummary.findOneAndUpdate({ borrowerId }, newSummary, { upsert: true });
    console.log(`calculation finished - ${borrowerId}`);
  }

  async recalculateAllActive() {
    const borrowers = await this.borrowersDB.getActiveBorrowers();
    let i = 1;
    for (const borrower of borrowers) {
      try {
        await this.updateBorrowerSummary(borrower._id.toString());
      } catch (e) {
        console.error(e);
      }
      i = i + 1;
    }
  }
}
