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

import {
  ILoanProductDoc,
  ILoanProductView,
  ILoanProductViewWithBalances,
  LoanProduct,
} from '../models/LoanProducts.model';
import { LoanCharge } from '../models/LoanCharges.model';
import { paymentOrder } from './loan-payments.service';
import { LoanChargesService } from './loan-charges.service';
import { BorrowersDB } from './borrowers.db';
import { MODEL_NAMES } from '../models/_models';
import { ILoanTransactionDoc, LoanTransaction } from '../models/LoanTransaction.model';
import { getTotalForDate } from '../db/loan-statement.db';
import { getPostponedTransactions } from '../db/loan-products.db';

export class LoanProductsService {

  constructor(
    private readonly loanChargesService: LoanChargesService,
    private readonly borrowersDB: BorrowersDB,
  ) {
  }

  async getLinkedLoanProducts(codes: string[]): Promise<{ [code: string]: string }> {
    const masterProducts = await LoanProduct.find({ code: { $in: codes } });
    const masterProductIds = masterProducts.map((product) => product._id);
    const loanProducts = await LoanProduct
      .find({ masterLoanProductId: { $in: masterProductIds } })
      .sort({ code: 1 })
      .lean();
    return loanProducts.reduce((acc, loanProduct) => ({ ...acc, [loanProduct.code]: loanProduct._id.toString() }), {});
  }

  async getProductsWithBalances(products: ILoanProductView[], showFloatedBalance: boolean) {
    await Promise.all(products.map(async (product, index) => {
      const balances = await this.loanChargesService.getLoanProductBalance(product._id.toString());
      (<ILoanProductViewWithBalances>products[index]).balance = balances.balance;
      (<ILoanProductViewWithBalances>products[index]).floatedBalance = showFloatedBalance ? balances.floatedBalance : 0;
    }));
    return products;
  }

  async getProductPaymentOrder(productId: string) {
    const product = await LoanProduct.findById(productId).lean();
    const charges = await LoanCharge.find({ productId }).lean();
    if (product.isDefaultPaymentOrder) {
      const chargeOrders = charges.map((charge) => {
        const foundIndex = paymentOrder.findIndex((order) => order === charge.chargeType);
        return { chargeId: charge._id.toString(), order: foundIndex };
      });
      const sortedChargeOrders = chargeOrders.sort((a, b) => a.order - b.order);
      return sortedChargeOrders.map((item) => item.chargeId);
    }
    const sortedCharges = charges.sort((a, b) => {
      return (a.paymentOrder ?? 0) - (b.paymentOrder ?? 0);
    });
    return sortedCharges.map((charge) => charge._id.toString());
  }

  async savePaymentOrder(productId: string, paymentOrder: string[]) {
    if (paymentOrder === null || paymentOrder.length === 0) {
      await LoanProduct.findByIdAndUpdate(productId, { isDefaultPaymentOrder: true });
    } else {
      await LoanProduct.findByIdAndUpdate(productId, { isDefaultPaymentOrder: false });
      await Promise.all(paymentOrder.map(async (chargeId, index) => {
        await LoanCharge.findByIdAndUpdate(chargeId, { paymentOrder: index });
      }));
    }
  }

  async getAllLoanProducts(borrowerIds?: string[]) {
    if (!borrowerIds) {
      const borrowers = await this.borrowersDB.getActiveBorrowers();
      borrowerIds = borrowers.map((borrower) => borrower._id.toString());
    }
    return LoanProduct.aggregate<ILoanProductDoc>([
      { $match: { borrowerId: { $in: borrowerIds.map((borrowerId) => new mongoose.Types.ObjectId(borrowerId)) } } },
      {
        $lookup: {
          from: MODEL_NAMES.borrowers,
          localField: 'borrowerId',
          foreignField: '_id',
          as: 'borrower',
        },
      },
      { $unwind: '$borrower' },
      { $sort: { 'borrower.name': 1 } },
      { $project: { borrower: 0 } },
    ]);
  };

  async getLoanProductBalance(productId: string, forDate = new Date()) {
    const simpleTransactions = await LoanTransaction.aggregate<ILoanTransactionDoc>([
      {
        $match: {
          productId: new mongoose.Types.ObjectId(productId),
          date: { $lte: dayjs(forDate).startOf('day').subtract(1, 'seconds').toDate() },
        },
      },
      { $sort: { date: -1, order: -1, createdAt: -1 } },
      { $limit: 1 },
    ]);

    const collectionsTransactions = await LoanTransaction.aggregate<ILoanTransactionDoc>([
      {
        $match: {
          productId: new mongoose.Types.ObjectId(productId),
          effectiveDate: { $lte: forDate },
          $expr: { $ne: ['$date', '$effectiveDate'] },
        },
      },
      { $sort: { effectiveDate: -1, order: -1, createdAt: -1 } },
      { $limit: 1 },
    ]);

    const normalTransactions = await LoanTransaction.aggregate<ILoanTransactionDoc>([
      {
        $match: {
          productId: new mongoose.Types.ObjectId(productId),
          effectiveDate: { $lte: forDate },
          $expr: { $eq: ['$date', '$effectiveDate'] },
        },
      },
      { $sort: { effectiveDate: -1, order: -1, createdAt: -1 } },
      { $limit: 1 },
    ]);

    return this.calculateBalanceFromTransactions(
      simpleTransactions.length > 0 ? simpleTransactions[0] : null,
      normalTransactions.length > 0 ? normalTransactions[0] : null,
      collectionsTransactions.length > 0 ? collectionsTransactions[0] : null,
      productId,
      forDate,
    );
  };

  async calculateBalanceFromTransactions(
    simpleTransaction: ILoanTransactionDoc | null,
    normalTransaction: ILoanTransactionDoc | null,
    collectionsTransaction: ILoanTransactionDoc | null,
    productId: string,
    forDate: Date,
  ): Promise<{ balance: number; floatedBalance: number }> {
    const balance = simpleTransaction?.balance || 0;
    let floatedBalance = 0;

    if (normalTransaction && collectionsTransaction) {
      const totalCollections = await getTotalForDate(productId, collectionsTransaction.effectiveDate);
      if (dayjs(normalTransaction.date).isSame(collectionsTransaction.date)) {
        floatedBalance = new Decimal(normalTransaction.floatedBalance).add(totalCollections).toNumber();
      } else if (dayjs(normalTransaction.date).isAfter(collectionsTransaction.date)) {
        floatedBalance = normalTransaction.floatedBalance;
      } else {
        floatedBalance = collectionsTransaction.balance;
      }
    } else if (normalTransaction) {
      floatedBalance = normalTransaction.floatedBalance;
    } else if (!normalTransaction && !collectionsTransaction) {
      const totalPostponed = await getPostponedTransactions(forDate, productId);
      return { balance: 0, floatedBalance: totalPostponed };
    }

    return { balance, floatedBalance };
  };

  async getLoanProductBalances(
    productIds: string[],
    forDate: Date,
  ): Promise<Record<string, { balance: number, floatedBalance: number }>> {
    const results: Record<string, { balance: number, floatedBalance: number }> = {};
    await Promise.all(
      productIds.map(async (productId) => {
        results[productId] = await this.getLoanProductBalance(productId, forDate);
      }),
    );
    return results;
  };

}
