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

import { ELoanChargeType } from '../enums/loan-charge-type.enum';
import { ELoanTypes } from '../enums/loan-types.enum';
import { roundToXDigits } from '../helpers/numbers.helper';
import {
  ITermLoanSettingsOnly,
  TERM_LOAN_SETTINGS_FIELDS,
  TermLoanSettingsModel,
} from '../models/TermLoanSettings.model';
import {
  ETermLoanStatus,
  ITermLoan,
  ITermLoanView,
  TERM_LOAN_FIELDS,
  TermLoanModel,
} from '../models/TermLoan.model';
import {
  ITermCalculatedDaily,
  ITermLoanCalculated,
  ITermLoanCalculatedDoc,
  TermLoanCalculatedModel,
} from '../models/TermLoanCalculated.model';
import {
  ILoanTransactionDoc,
} from '../models/LoanTransaction.model';
import { LoanProduct } from '../models/LoanProducts.model';

import { EFinancialIndex, FinancialIndexesService } from './financial-indexes.service';
import { LoanChargesService } from './loan-charges.service';
import { LoanStatementStatusService } from './loan-statement-status.service';
import { LoanTransactionsService } from './loan-transactions.service';
import { UploadsService } from './uploads.service';

const newEmptySettings: ITermLoanSettingsOnly = {
  termMonths: 12,
  amortizationProfileMonths: 12,
  gracePeriodMonths: 1,
  principalDueDay: 7,
};

export class TermLoanService {

  constructor(
    private readonly financialIndexesService: FinancialIndexesService,
    private readonly loanChargesService: LoanChargesService,
    private readonly getLoanStatementStatusService: () => LoanStatementStatusService,
    private readonly getLoanTransactionsService: () => LoanTransactionsService,
    private readonly getUploadsService: () => UploadsService,
  ) {
  }

  private async createTermLoan(productId: string, actual: boolean) {
    const product = await this.loanChargesService.getLoanProductById(productId);
    const charges = await this.loanChargesService.getLoanChargeForProduct(productId);
    if (charges.length === 0) {
      return null;
    }
    const adminCharge = charges.find((charge) => charge.chargeType === ELoanChargeType.ADMIN_FEE);
    const interestCharge = charges.find((charge) => charge.chargeType === ELoanChargeType.INTEREST_FEE);
    if (!adminCharge || !interestCharge) {
      return null;
    }

    const primeRate = await this.financialIndexesService.getFinancialIndexValue(EFinancialIndex.PRIME_RATE);

    const newEmptyTerm: ITermLoan = {
      productId: new mongoose.Types.ObjectId(String(product._id)),
      initialBalance: product.commitment,
      adminRate: adminCharge.percent,
      interestRate: interestCharge.percent,
      loanStartDate: product.startDate,
      primeRate: primeRate,
      actual,
      isMismatched: false,
      calculationStatus: ETermLoanStatus.NEW,
      lastCalculated: null,
    };
    return TermLoanModel.findOneAndUpdate({ productId, actual }, newEmptyTerm, { upsert: true, new: true });
  }

  private async refreshChargeSettings(productId: string, actual: boolean) {
    const product = await this.loanChargesService.getLoanProductById(productId);
    const charges = await this.loanChargesService.getLoanChargeForProduct(productId);
    if (charges.length === 0) {
      return null;
    }
    const adminCharge = charges.find((charge) => charge.chargeType === ELoanChargeType.ADMIN_FEE);
    const interestCharge = charges.find((charge) => charge.chargeType === ELoanChargeType.INTEREST_FEE);
    if (!adminCharge || !interestCharge) {
      return null;
    }

    const newEmptyTerm: Partial<ITermLoan> = {
      productId: new mongoose.Types.ObjectId(String(product._id)),
      initialBalance: product.commitment,
      adminRate: adminCharge.percent,
      interestRate: interestCharge.percent,
      loanStartDate: product.startDate,
      actual,
    };
    return TermLoanModel.findOneAndUpdate({ productId, actual }, newEmptyTerm, { upsert: true, new: true });
  }

  private async createTermLoanSettings(termLoanId: string) {
    return TermLoanSettingsModel.findOneAndUpdate({ termLoanId }, { termLoanId, ...newEmptySettings }, {
      upsert: true,
      new: true,
    });
  }

  private async getCreateTermLoan(productId: string, actual: boolean) {
    if (!productId) {
      return null;
    }
    const foundTermLoan = await TermLoanModel.findOne({ productId, actual }).lean();
    const termLoan = foundTermLoan ? foundTermLoan : await this.createTermLoan(productId, actual);
    if (!termLoan) {
      return null;
    }

    const foundSettings = termLoan ? await TermLoanSettingsModel.findOne({ termLoanId: termLoan._id }).lean() : null;
    const settings = foundSettings ? foundSettings : await this.createTermLoanSettings(String(termLoan._id));

    const pureTermLoan = _.pick(termLoan as unknown as ITermLoan & { _id: mongoose.Types.ObjectId }, TERM_LOAN_FIELDS);

    const termLoanFull = {
      ...pureTermLoan,
      settings: _.pick(settings, TERM_LOAN_SETTINGS_FIELDS) as ITermLoanSettingsOnly,
    };
    return termLoanFull as ITermLoanView;
  }

  async getTermLoan(productId: string, actual: boolean): Promise<ITermLoanView & {
    calculated: ITermLoanCalculatedDoc[]
  }> {
    const termLoan = await this.getCreateTermLoan(productId, actual);
    if (!termLoan) {
      return null;
    }
    const calculatedTermLoan = await TermLoanCalculatedModel
      .find({ termLoanId: termLoan._id })
      .sort({ relevantStatement: 1, openingBalance: -1, monthlyPrincipal: -1 })
      .lean();
    const mappedCalculatedTermLoan = calculatedTermLoan.map((c) => {
      const startDate = termLoan.loanStartDate;
      const minStart = dayjs.max(dayjs(startDate), dayjs(c.relevantStatement).startOf('month'));
      const period = `${minStart.format('MM/DD/YYYY')} - ${dayjs(c.relevantStatement).format('MM/DD/YYYY')}`;
      return {
        ...c,
        period,
      } as unknown as (ITermLoanCalculatedDoc & { period: string });
    });
    return { ...termLoan, calculated: mappedCalculatedTermLoan };
  }

  async saveTermLoanSettings(productId: string, actual: boolean, settings: ITermLoanSettingsOnly): Promise<any> {
    await this.refreshChargeSettings(productId, actual);
    let termLoan = await TermLoanModel.findOne({ productId, actual }).lean();
    if (!termLoan || termLoan.calculationStatus === ETermLoanStatus.NEW) {
      termLoan = await this.createTermLoan(productId, actual);
    }
    const updatedSettings = await TermLoanSettingsModel.findOneAndUpdate({ termLoanId: termLoan._id }, settings, { new: true });
    return { productId, settings: _.pick(updatedSettings, TERM_LOAN_SETTINGS_FIELDS) };
  }

  async calculateAllTermLoans() {
    const products = await LoanProduct.find({ type: ELoanTypes.TERM }).lean();
    for (const product of products) {
      await this.calculateTermLoan(product._id.toString(), true);
    }
  }

  async setTermLoanExpected(productId: string, recalculate = false) {
    const termLoan = await this.getCreateTermLoan(productId, true);
    if (termLoan) {
      await this.updateTermLoanStatus(termLoan._id.toString(), ETermLoanStatus.EXPECTED);
      if (recalculate) {
        await this.calculateTermLoan(productId, true);
      }
    }
  }

  async updateTermLoanStatus(termLoanId: string, calculationStatus: ETermLoanStatus, lastCalculated: Date = null) {
    try {
      await TermLoanModel.findByIdAndUpdate(termLoanId, { calculationStatus });
      if (lastCalculated) {
        await TermLoanModel.findByIdAndUpdate(termLoanId, { lastCalculated });
      }
    } catch (e) {
      console.error(e);
    }
  }

  async calculateTermLoan(productId: string, actual: boolean): Promise<any> {
    const loanTransactionsService = this.getLoanTransactionsService();
    let termLoan = await this.getCreateTermLoan(productId, actual);
    if (!termLoan || termLoan.calculationStatus === ETermLoanStatus.NEW) {
      await this.createTermLoan(productId, actual);
      termLoan = await this.getCreateTermLoan(productId, actual);
    }
    if (!termLoan) {
      return;
    }

    await this.updateTermLoanStatus(termLoan._id.toString(), ETermLoanStatus.CALCULATING);

    const daysInYear = 360;
    const initialTransaction = await loanTransactionsService.getLastTransactionForDate(productId, termLoan.loanStartDate);

    const initialData = actual
      ? { date: termLoan.loanStartDate, amount: initialTransaction ? initialTransaction.balance : 0 }
      : { date: termLoan.loanStartDate, amount: termLoan.initialBalance };

    const initialBalance = actual ? initialData.amount : termLoan.initialBalance;

    const startDate = dayjs(initialData.date);
    await TermLoanModel.findByIdAndUpdate(termLoan._id, { loanStartDate: startDate, initialBalance: initialBalance });
    const endGraceDate = dayjs(startDate).add(termLoan.settings.gracePeriodMonths, 'month');
    const endTermMonths = dayjs(startDate).add(termLoan.settings.termMonths, 'month');
    const endAmortizationDate = dayjs(startDate).add(termLoan.settings.amortizationProfileMonths, 'month');

    const product = await this.loanChargesService.getLoanProductById(productId);
    const minPercent = product.minPercent ?? 0;
    const maxPercent = (product.maxPercent ?? 0) === 0 ? 1 : product.maxPercent;

    const getDynamicInterestRate = async (date: Date): Promise<number> => {
      const primeRate = await this.financialIndexesService.getFinancialIndexValue(EFinancialIndex.PRIME_RATE, date);
      const interestRate = new Decimal(primeRate).add(termLoan.interestRate).toDP(4).toNumber();
      return Math.min(Math.max(interestRate, minPercent), maxPercent);
    };

    let currentDate = startDate;

    const getMonthPayments = async (date: dayjs.Dayjs) => {
      const transactions = await loanTransactionsService.getPaymentsForPeriod(productId, date.toDate(), date.endOf('month').toDate());
      if (!initialTransaction) {
        return transactions;
      }
      return transactions.filter((transaction) => {
        return (dayjs(transaction.date).format('YYYY-MM-DD') !== dayjs(initialTransaction.date).format('YYYY-MM-DD'));
      });
    };

    const getMonthDisbursements = async (date: dayjs.Dayjs) => {
      const transactions = await loanTransactionsService.getLastDisbursementTransactionForDate(productId, date.toDate(), date.endOf('month').toDate());
      if (!initialTransaction) {
        return transactions;
      }
      return transactions.filter((transaction) => {
        return (dayjs(transaction.date).format('YYYY-MM-DD') !== dayjs(initialTransaction.date).format('YYYY-MM-DD'));
      });
    };

    const checkGracePeriodDate = (checkDate: dayjs.Dayjs) => {
      return checkDate < endGraceDate.endOf('month');
    };

    const interestRates = await this.financialIndexesService.getFinancialAllIndexes(EFinancialIndex.PRIME_RATE);
    const transactions = await loanTransactionsService.getLoanTransactionsForProduct(productId);

    const findLastIndexValue = (date: Date) => {
      const found = interestRates.find((i) => i.date.getTime() <= date.getTime());
      const primeRate = found ? found.value : 0;
      const interestRate = new Decimal(primeRate).add(termLoan.interestRate).toDP(4).toNumber();
      return Math.min(Math.max(interestRate, minPercent), maxPercent);
    };

    const findLastTransaction = (date: Date) => {
      return transactions.find((i) => i.date.getTime() <= date.getTime());
    };

    const getComplexEMI = async (termLoan: ITermLoanView) => {
      const calculateBalance = async (termLoan: ITermLoanView, EMI: number) => {
        console.log({ EMI });
        let balance = initialData.amount;
        let accumulatedMonthlyInterest = 0;
        let lastMonthInterest = 0;
        let EMICurrentDate = currentDate;
        let firstPeriod = true;
        const endAmortization = endAmortizationDate;
        let monthPayments: ILoanTransactionDoc[] = [];
        while (EMICurrentDate.startOf('day').isBefore(endAmortization.startOf('day')) || EMICurrentDate.startOf('day').isSame(endAmortization.startOf('day'))) {

          const lastTransaction = findLastTransaction(EMICurrentDate.subtract(1, 'day').toDate());
          if (actual && lastTransaction && dayjs(lastTransaction.date).format('YYYY-MM-DD') === EMICurrentDate.subtract(1, 'day').format('YYYY-MM-DD')) {
            balance = lastTransaction.balance;
          }

          const interestRate = findLastIndexValue(EMICurrentDate.toDate());
          const interestRateDaily = interestRate / daysInYear;
          const dailyInterest = EMICurrentDate.startOf('day').isAfter(endAmortizationDate.startOf('day')) ? 0 : new Decimal(balance).mul(interestRateDaily).toDP(2).toNumber();
          accumulatedMonthlyInterest = new Decimal(accumulatedMonthlyInterest).add(dailyInterest).toDP(2).toNumber();

          if (termLoan.actual) {
            if (
              (EMICurrentDate.format('YYYY-MM-DD') === EMICurrentDate.startOf('month').format('YYYY-MM-DD')) ||
              (EMICurrentDate.format('YYYY-MM-DD') === startDate.format('YYYY-MM-DD'))
            ) {
              monthPayments = await getMonthPayments(EMICurrentDate);
            }
          }

          if (
            (EMICurrentDate.format('YYYY-MM-DD') === EMICurrentDate.endOf('month').format('YYYY-MM-DD')) ||
            (EMICurrentDate.format('YYYY-MM-DD') === endAmortizationDate.format('YYYY-MM-DD'))
          ) {
            if (EMICurrentDate.format('YYYY-MM-DD') === endAmortizationDate.format('YYYY-MM-DD')) {
              lastMonthInterest = lastMonthInterest + accumulatedMonthlyInterest;
            } else {
              lastMonthInterest = accumulatedMonthlyInterest;
            }
            accumulatedMonthlyInterest = 0;
            firstPeriod = false;
          }

          if (monthPayments.length > 0) {
            const principal = monthPayments
              .filter((p) => dayjs(p.date).format('YYYY-MM-DD') === EMICurrentDate.format('YYYY-MM-DD'))
              .reduce((acc, p) => acc + Math.abs(p.amount), 0);
            if (principal > 0) {
              balance = new Decimal(balance).sub(principal).toDP(2).toNumber();
              lastMonthInterest = 0;
            }
          } else {
            if ((EMICurrentDate.format('YYYY-MM-DD') === EMICurrentDate.date(termLoan.settings.principalDueDay).format('YYYY-MM-DD'))) {
              const isGracePeriod = checkGracePeriodDate(EMICurrentDate);
              const principal = (isGracePeriod || firstPeriod) ? 0 : new Decimal(EMI).sub(lastMonthInterest).toDP(2).toNumber();
              balance = new Decimal(balance).sub(principal).toDP(2).toNumber();
              lastMonthInterest = 0;
            }
          }
          EMICurrentDate = EMICurrentDate.add(1, 'day');
        }
        const principal = new Decimal(EMI).sub(lastMonthInterest).toDP(2).toNumber();
        balance = new Decimal(balance).sub(principal).toDP(2).toNumber();
        return balance;
      };

      let lowerBound = 0;
      let upperBound = initialBalance;
      let optimalEMI = 0;
      const tolerance = 0.01;
      while (new Decimal(upperBound).sub(lowerBound).toDP(2).toNumber() > tolerance) {
        const mid = new Decimal(lowerBound).add(upperBound).div(2).toDP(2).toNumber();
        const currentBalance = await calculateBalance(termLoan, mid);
        if (currentBalance < 0) {
          upperBound = mid;
        } else {
          lowerBound = mid;
          optimalEMI = mid;
        }
      }
      return new Decimal(optimalEMI).add(tolerance).toDP(2).toNumber();
    };

    const getNextPaymentDate = (date: dayjs.Dayjs) => {
      let paymentDate = date.date(termLoan.settings.principalDueDay);
      if (paymentDate < date) {
        paymentDate = paymentDate.add(1, 'month').date(termLoan.settings.principalDueDay);
      }
      return paymentDate;
    };

    const complexEMI = await getComplexEMI(termLoan);
    console.log({ complexEMI });

    const getDaysInMonth = (date: dayjs.Dayjs) => {
      const maxStartDate = dayjs.max(startDate, date.startOf('month')).startOf('day');
      const maxEndDate = dayjs.min(endAmortizationDate, date.endOf('month')).startOf('day');
      return maxEndDate.diff(maxStartDate, 'day') + 1;
    };

    let calculatedDailyResults = [];
    const calculatedMonthlyResults: ITermLoanCalculated[] = [];

    let monthOpeningBalance = initialBalance;
    let openingBalance = initialBalance;
    let closingBalance = openingBalance;

    let monthInterest = 0;

    let lastMonthInterest = 0;
    let lastMonthPrincipal = 0;

    let dayAdminFee = 0;

    let monthAdminFee = 0;

    const charges = await this.loanChargesService.getLoanChargeForProduct(productId);
    const adminCharge = charges.find((charge) => charge.chargeType === ELoanChargeType.ADMIN_FEE);
    const minAdminFee = adminCharge ? adminCharge.minimumAmount : 0;

    let monthPayments: ILoanTransactionDoc[] = [];
    let monthDisbursements: ILoanTransactionDoc[] = [];
    let isLastPeriod = false;

    while (
      (currentDate.startOf('day').isBefore(endAmortizationDate.startOf('day')) || currentDate.startOf('day').isSame(endAmortizationDate.startOf('day'))) &&
      !isLastPeriod
      ) {
      if (
        (currentDate.format('YYYY-MM-DD') === currentDate.startOf('month').format('YYYY-MM-DD')) ||
        (currentDate.format('YYYY-MM-DD') === startDate.format('YYYY-MM-DD'))
      ) {
        if (actual) {
          monthPayments = await getMonthPayments(currentDate.startOf('day'));
          monthDisbursements = await getMonthDisbursements(currentDate.startOf('day'));
        }
        monthOpeningBalance = openingBalance;
      }

      const lastTransaction = await loanTransactionsService.getLastTransactionForDate(productId, currentDate.subtract(1, 'day').toDate());
      if (actual && lastTransaction && dayjs(lastTransaction.date).format('YYYY-MM-DD') === currentDate.subtract(1, 'day').format('YYYY-MM-DD')) {
        openingBalance = lastTransaction.balance;
      }
      if (currentDate.format('YYYY-MM-DD') === currentDate.date(termLoan.settings.principalDueDay).add(1, 'day').format('YYYY-MM-DD')) {
        openingBalance = closingBalance;
      }

      dayAdminFee = openingBalance * termLoan.adminRate / getDaysInMonth(currentDate);
      monthAdminFee = monthAdminFee + dayAdminFee;
      const interestRate = await getDynamicInterestRate(currentDate.toDate());
      const interestRateDaily = interestRate / daysInYear;
      const dailyInterest = new Decimal(openingBalance).mul(interestRateDaily).toDP(2).toNumber();

      const principalDueDay = getNextPaymentDate(currentDate);

      if (monthPayments.length > 0) {
        const foundPayment = monthPayments.find((payment) => dayjs(payment.date).format('YYYY-MM-DD') === currentDate.format('YYYY-MM-DD'));
        if (foundPayment) {
          closingBalance = foundPayment.balance;
        }
        const lastTransaction = await loanTransactionsService.getLastTransactionForDate(productId, currentDate.toDate());
        if (actual && lastTransaction && dayjs(lastTransaction.date).format('YYYY-MM-DD') === currentDate.format('YYYY-MM-DD')) {
          closingBalance = lastTransaction.balance;
        }
      } else {
        if (currentDate.format('YYYY-MM-DD') === principalDueDay.format('YYYY-MM-DD')) {
          const monthlyPayment = new Decimal(complexEMI).sub(lastMonthInterest).toDP(2).toNumber();
          const isLastPeriod = monthlyPayment > openingBalance;
          const isGracePeriod = checkGracePeriodDate(principalDueDay);
          const monthlyPrincipal = (isGracePeriod) ? 0 : (isLastPeriod ? openingBalance : monthlyPayment);
          closingBalance = roundToXDigits(openingBalance - monthlyPrincipal);
        }
      }

      monthInterest = roundToXDigits(monthInterest + dailyInterest);

      const dailyResult: ITermCalculatedDaily = {
        day: currentDate.format('YYYY-MM-DD'),
        interestRate: interestRate,
        interest: dailyInterest,
        paymentDate: currentDate.date() === termLoan.settings.principalDueDay,
      };
      calculatedDailyResults.push(dailyResult);
      if (
        (currentDate.format('YYYY-MM-DD') === currentDate.endOf('month').format('YYYY-MM-DD')) ||
        (currentDate.format('YYYY-MM-DD') === endAmortizationDate.format('YYYY-MM-DD')) ||
        (currentDate.format('YYYY-MM-DD') === endTermMonths.format('YYYY-MM-DD'))
      ) {
        if (monthPayments.length > 0 && calculatedMonthlyResults.length > 0) {
          calculatedMonthlyResults[calculatedMonthlyResults.length - 1].payments = monthPayments.map((p) => ({
            paymentId: new mongoose.Types.ObjectId(String(p._id)),
            amount: Math.abs(p.amount),
          }));
          calculatedMonthlyResults[calculatedMonthlyResults.length - 1].monthlyPrincipal = monthPayments.reduce((acc, p) => acc + Math.abs(p.amount), 0);
        }

        const actualMonthlyPayment = new Decimal(complexEMI).sub(monthInterest).toDP(2).toNumber();
        isLastPeriod = actualMonthlyPayment > monthOpeningBalance || (currentDate.format('YYYY-MM-DD') === endTermMonths.format('YYYY-MM-DD'));
        const isGracePeriod = checkGracePeriodDate(principalDueDay);
        const monthlyPrincipal = (isGracePeriod) ? 0 : (isLastPeriod ? monthOpeningBalance : actualMonthlyPayment);
        const totalPayment = monthPayments.length > 0 ? monthPayments.reduce((acc, p) => new Decimal(acc).add(Math.abs(p.amount)).toDP(2).toNumber(), 0) : lastMonthPrincipal;
        const totalDisbursements = monthDisbursements.length > 0 ? monthDisbursements.reduce((acc, p) => new Decimal(acc).add(p.amount).toDP(2).toNumber(), 0) : 0;
        const closingBalance = new Decimal(monthOpeningBalance).sub(totalPayment).add(totalDisbursements).toDP(2).toNumber();
        // openingBalance = closingBalance;
        const monthlyResult: ITermLoanCalculated = {
          termLoanId: termLoan._id,
          openingBalance: monthOpeningBalance,
          relevantStatement: currentDate.endOf('month').format('YYYY-MM-DD'),
          monthlyInterest: monthInterest,
          monthlyPrincipal: monthlyPrincipal,
          closingBalance: closingBalance,
          adminFee: Math.max(roundToXDigits(monthAdminFee), minAdminFee),
          interestRate: interestRate,
          interestDaily: monthOpeningBalance * interestRate / daysInYear,
          gracePeriod: isGracePeriod,
          amortization: true, // TODO calculate
          paymentDueDate: principalDueDay.date(termLoan.settings.principalDueDay).toDate(),
          payments: null,
          disbursements: Math.abs(totalDisbursements),
          dailyResults: calculatedDailyResults,
        };
        calculatedMonthlyResults.push(monthlyResult);
        lastMonthInterest = monthInterest;
        lastMonthPrincipal = monthlyPrincipal;
        monthInterest = 0;
        monthAdminFee = 0;
        calculatedDailyResults = [];
      }
      currentDate = currentDate.add(1, 'day');
    }
    const paymentDates = [...new Set(calculatedMonthlyResults.map((res) => dayjs(res.paymentDueDate).format('YYYY-MM-DD')))];
    const mergedResults = paymentDates.reduce((acc, paymentDate) => {
      const filteredDateResults = calculatedMonthlyResults.filter((res) => dayjs(res.paymentDueDate).format('YYYY-MM-DD') === paymentDate);
      if (filteredDateResults.length === 1) {
        return [...acc, ...filteredDateResults];
      }
      const totalMonthlyInterest = filteredDateResults.reduce((acc, res) => {
        return roundToXDigits(acc + res.monthlyInterest);
      }, 0);
      const mergedOpeningBalance = filteredDateResults[0].openingBalance;
      const totalMonthlyPrincipal = Math.min(filteredDateResults[0].closingBalance, roundToXDigits(complexEMI - totalMonthlyInterest));
      const mergedClosingBalance = filteredDateResults[0].closingBalance;
      const totalAdminFee = filteredDateResults.reduce((acc, res) => {
        return roundToXDigits(acc + res.adminFee);
      }, 0);
      const lastStatement: ITermLoanCalculated = {
        termLoanId: filteredDateResults[0].termLoanId,
        openingBalance: mergedOpeningBalance,
        relevantStatement: filteredDateResults[0].relevantStatement,
        monthlyInterest: totalMonthlyInterest,
        monthlyPrincipal: totalMonthlyPrincipal,
        closingBalance: mergedClosingBalance,
        adminFee: totalAdminFee,
        interestRate: filteredDateResults[0].interestRate,
        interestDaily: filteredDateResults[0].openingBalance * filteredDateResults[0].interestRate / daysInYear,
        gracePeriod: filteredDateResults[0].gracePeriod,
        amortization: true, // TODO calculate
        paymentDueDate: filteredDateResults[0].paymentDueDate,
        payments: null,
        disbursements: filteredDateResults[0].disbursements,
        dailyResults: filteredDateResults[0].dailyResults, // totalCalculatedDailyResults
      };
      const lastPaymentDate = dayjs(filteredDateResults[1].paymentDueDate).isBefore(dayjs(filteredDateResults[1].relevantStatement))
        ? getNextPaymentDate(dayjs(filteredDateResults[1].relevantStatement)).toDate()
        : filteredDateResults[1].paymentDueDate;
      const emptyStatement: ITermLoanCalculated = {
        termLoanId: filteredDateResults[1].termLoanId,
        openingBalance: mergedClosingBalance,
        relevantStatement: filteredDateResults[1].relevantStatement,
        monthlyInterest: 0,
        monthlyPrincipal: endTermMonths.format() !== endAmortizationDate.format() ? mergedClosingBalance : 0,
        closingBalance: 0,
        adminFee: 0,
        interestRate: 0,
        interestDaily: 0,
        gracePeriod: filteredDateResults[1].gracePeriod,
        amortization: true, // TODO calculate
        paymentDueDate: lastPaymentDate,
        payments: null,
        disbursements: filteredDateResults[1].disbursements,
        dailyResults: filteredDateResults[1].dailyResults,
      };
      return [...acc, lastStatement, emptyStatement];
    }, <ITermLoanCalculated[]>[]);
    if (mergedResults[mergedResults.length - 1].closingBalance !== 0) {
      // mergedResults[mergedResults.length - 1].monthlyPrincipal = mergedResults[mergedResults.length - 1].closingBalance;
      const lastUnfinishedResult = mergedResults[mergedResults.length - 1];
      const finishingResult = JSON.parse(JSON.stringify(lastUnfinishedResult));
      finishingResult.openingBalance = lastUnfinishedResult.closingBalance;
      finishingResult.monthlyInterest = 0;
      finishingResult.monthlyPrincipal = 0;
      finishingResult.adminFee = 0;
      finishingResult.interestRate = 0;
      finishingResult.interestDaily = 0;
      finishingResult.closingBalance = 0;
      finishingResult.dailyResults = [];
      mergedResults.push(finishingResult);
    }
    console.log('saving...');
    const filteredResults = mergedResults
      .map((res) => ({ ...res, monthlyPrincipal: res.monthlyPrincipal > 0 ? res.monthlyPrincipal : 0 }));
    const filteredRes = (await Promise.all(filteredResults.map(async (res) => {
      if (!actual) {
        return res;
      }
      const trDate = typeof res.paymentDueDate === 'string' ? new Date(res.paymentDueDate) : res.paymentDueDate;
      const lastTransaction = await loanTransactionsService.getLastTransactionForDate(product._id.toString(), trDate);
      if (lastTransaction && lastTransaction.balance >= 0) {
        return res;
      }
    }))).filter((res) => !!res);
    let balanceIsZero = false;
    const afterClosingBalance = filteredRes.reduce((acc, res) => {
      if (balanceIsZero) {
        return acc;
      }
      balanceIsZero = res.closingBalance === 0;
      return [...acc, res];
    }, []);
    await this.saveCalculatedTermLoan(afterClosingBalance, productId);
    await this.updateTermLoanStatus(termLoan._id.toString(), ETermLoanStatus.CALCULATED, new Date());
    return await this.getTermLoan(productId, actual);
  }

  async saveCalculatedTermLoan(calculatedTermLoans: ITermLoanCalculated[], productId: string) {
    if (calculatedTermLoans.length === 0) {
      return;
    }
    const termLoanId = calculatedTermLoans[0].termLoanId;
    await TermLoanCalculatedModel.deleteMany({ termLoanId, payments: null });
    await Promise.all(calculatedTermLoans.map(async (calculatedTermLoan) => {
      if (calculatedTermLoan.payments === null) {
        await TermLoanCalculatedModel.findOneAndDelete({
          termLoanId: calculatedTermLoan.termLoanId,
          relevantStatement: calculatedTermLoan.relevantStatement,
        });
        const newCalculatedTermLoan = new TermLoanCalculatedModel(calculatedTermLoan);
        await newCalculatedTermLoan.save();
      } else {
        const foundTermLoanCalculated = await TermLoanCalculatedModel.findOne({
          termLoanId: calculatedTermLoan.termLoanId,
          relevantStatement: calculatedTermLoan.relevantStatement,
        }).lean();
        if (!foundTermLoanCalculated) {
          const newCalculatedTermLoan = new TermLoanCalculatedModel(calculatedTermLoan);
          await newCalculatedTermLoan.save();
        } else {
          const loanStatementStatusService = this.getLoanStatementStatusService();
          const statementStatus = await loanStatementStatusService.getStatementStatus(productId, new Date(calculatedTermLoan.relevantStatement));
          if (!statementStatus.locked) {
            await TermLoanCalculatedModel.findByIdAndUpdate(foundTermLoanCalculated._id, calculatedTermLoan);
          }
        }
      }
    }));
  }

  async getTermLoanExcel(productId: string, actual: boolean, short: boolean) {
    const termLoan = await this.getTermLoan(productId, actual);
    const header = short
      ? ['PERIOD', 'OPENING BALANCE', 'PRINCIPAL', 'INTEREST', 'CLOSING BALANCE', 'ADMIN FEE']
      : ['DATE', 'OPENING BALANCE', 'INTEREST', 'DAILY INTEREST ACCRUED', 'PRINCIPAL', 'CLOSING BALANCE', 'STATEMENT DATE'];

    const getMonthlyData = (data: ITermLoanView & { calculated: ITermLoanCalculatedDoc[] }) => {
      return data.calculated.map((row) => {
        return [
          new Date(row.relevantStatement),
          row.openingBalance,
          row.monthlyPrincipal,
          row.monthlyInterest,
          row.closingBalance,
          row.adminFee,
        ];
      });
    };

    const getDailyData = (data: ITermLoanView & { calculated: ITermLoanCalculatedDoc[] }) => {
      let lastMonthClosingBalance = null;

      let lastPaymentClosingBalance = null;
      let lastPaymentMonthlyPrincipal = null;
      let lastPaymentDate = null;
      const calculatedResults = data.calculated.reduce((acc, row) => {
        let isPaymentDay = false;
        let openingBalance = lastMonthClosingBalance ?? row.openingBalance;
        let closingBalance = openingBalance;
        return [
          ...acc,
          ...row.dailyResults.map((daily, index) => {
            isPaymentDay = isPaymentDay || daily.paymentDate;
            openingBalance = closingBalance;
            if (dayjs(daily.day).format('YYYY-MM-DD') === dayjs(lastPaymentDate).format('YYYY-MM-DD')) {
              closingBalance = lastPaymentClosingBalance - lastPaymentMonthlyPrincipal;
            }
            const newRow = [
              new Date(daily.day),
              openingBalance,
              daily.interestRate,
              daily.interest,
              daily.paymentDate ? lastPaymentMonthlyPrincipal : null,
              closingBalance,
              row.relevantStatement,
            ];
            if (dayjs(daily.day).format('YYYY-MM-DD') === dayjs(lastPaymentDate).format('YYYY-MM-DD')) {
              openingBalance = closingBalance;
            }
            isPaymentDay = isPaymentDay || daily.paymentDate;
            if (daily.paymentDate) {
              lastPaymentClosingBalance = row.closingBalance;
              lastPaymentMonthlyPrincipal = row.monthlyPrincipal;
              lastPaymentDate = row.paymentDueDate;
            }
            if (index + 1 === row.dailyResults.length) {
              lastMonthClosingBalance = row.closingBalance;
            }
            return newRow;
          })];
      }, []);
      const lastRow = data.calculated[data.calculated.length - 1];
      const lastPayment = [
        new Date(lastRow.paymentDueDate),
        lastRow.openingBalance,
        null,
        null,
        lastRow.monthlyPrincipal,
        lastRow.closingBalance,
        lastRow.relevantStatement,
      ];
      return [...calculatedResults, lastPayment];
    };

    const product = await LoanProduct.findById(productId).lean();
    const preparedData = short ? getMonthlyData(termLoan) : getDailyData(termLoan);
    const dataToConvert = [{ [product.name]: [header, ...preparedData] }];
    const uploadsService = this.getUploadsService();
    return await uploadsService.convertDataToFile(dataToConvert);
  }

  async isBorrowerTermLoanActual(borrowerId: string) {
    const products = await this.loanChargesService.getLoanProducts(borrowerId);
    const termLoanProduct = products.find((product) => product.type === ELoanTypes.TERM);
    if (!termLoanProduct) {
      return true;
    }
    const termLoan = await TermLoanModel.findOne({ productId: termLoanProduct._id, actual: true });
    if (!termLoan) {
      return true;
    }
    return termLoan.calculationStatus === ETermLoanStatus.CALCULATED;
  }
}
