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

import { ILoanPaymentWithId, LoanPaymentModel } from '../models/LoanPayment.model';
import {
  LoanStatementTransactionModel,
  ILoanStatementTransactionDoc,
} from '../models/LoanStatementTransaction.model';
import { ELoanChargeType } from '../enums/loan-charge-type.enum';

import { LoanChargesService } from './loan-charges.service';
import { LoanStatementService } from './loan-statement.service';
import { LoanTransactionsService } from './loan-transactions.service';

export class LoanStatementBalanceService {

  constructor(
    private readonly loanChargesService: LoanChargesService,
    private readonly getLoanStatementService: () => LoanStatementService,
    private readonly loanTransactionsService: LoanTransactionsService,
  ) {
  }

  async cleanDecreasePayment(payment: ILoanPaymentWithId, userId: string) {
    if (!!payment.paid) {
      let firstPaidStatement: any = null;
      await Promise.all(payment.paid.map(async (paid) => {
        const foundPayment = await LoanStatementTransactionModel.findById(paid.statementId);
        if (!foundPayment) {
          return;
        }
        if (!firstPaidStatement ||
          (foundPayment.date.getTime() < firstPaidStatement.date.getTime()) ||
          (firstPaidStatement.date.getTime() === foundPayment.date.getTime() && firstPaidStatement.order > foundPayment.order)
        ) {
          firstPaidStatement = foundPayment;
        }
        await LoanStatementTransactionModel.findByIdAndUpdate(paid.statementId, {
          amountPaid: new Decimal(foundPayment.amountPaid).sub(paid.amount).toNumber(),
          settlementCode: null,
        });
      }));
      if (firstPaidStatement) {
        await this.calculateProductBalance(payment.productId.toString(), firstPaidStatement.date);
      }
    }
    if (!!payment.loanTransactions) {
      await Promise.all(payment.loanTransactions.map(async (t) => {
        await this.loanTransactionsService.deleteLoanTransaction(t.transactionId.toString(), false, userId);
      }));
    }
    await LoanPaymentModel.findByIdAndUpdate(payment._id, { paid: [] });
  }

  async calculateProductBalance(productId: string, startDate: Date) {
    const loanStatementService = this.getLoanStatementService();
    const statements = await loanStatementService.getTransactionsFromDate(productId, startDate);
    for (const statement of statements) {
      await this.updateBalance(statement);
    }
  }

  async updateBalance(loanStatementTransaction: ILoanStatementTransactionDoc) {
    const loanStatementService = this.getLoanStatementService();
    const previousTransaction = await loanStatementService.getPreviousTransaction(loanStatementTransaction);
    const previousBalance = !previousTransaction ? 0 : previousTransaction.balance;
    const newBalance = new Decimal(previousBalance).add(loanStatementTransaction.amount).sub(loanStatementTransaction.amountPaid).toNumber();
    await LoanStatementTransactionModel.findByIdAndUpdate(loanStatementTransaction._id, { balance: newBalance });
  }

  async getLastStatementTransactionForDate(productId: string, startDate: Date) {
    const charges = await this.loanChargesService.getLoanChargeForProduct(productId);
    const lastStatements = await LoanStatementTransactionModel.aggregate<ILoanStatementTransactionDoc>([
      {
        $match: {
          'chargeId': { $in: charges.map((charge) => charge._id) },
          'date': { $lt: startDate },
        },
      }, {
        $sort: {
          'date': -1,
          'order': -1,
        },
      }, {
        $limit: 1,
      },
    ]);
    if (lastStatements.length === 0) {
      return null;
    }
    return lastStatements[0];
  }

  async getNextAdminTransactionForDate(productId: string, date: Date) {
    const charges = await this.loanChargesService.getLoanChargeForProduct(productId);
    const adminCharge = charges.find((c) => c.chargeType === ELoanChargeType.ADMIN_FEE);
    if (adminCharge) {
      const nextAdminFee = await LoanStatementTransactionModel
        .findOne({
          chargeId: adminCharge._id,
          date: { $gte: dayjs(date).startOf('month').toDate() },
        })
        .sort({ date: 1 })
        .lean();
      if (nextAdminFee) {
        const totalDays = dayjs(date).daysInMonth();
        const day = dayjs(date).date();
        return new Decimal(day).div(totalDays).mul(nextAdminFee.amount).toDP(2).toNumber();
      }
    }
    return 0;
  }
}
