import mongoose from 'mongoose';
import dayjs from 'dayjs';
import Decimal from 'decimal.js';
import Joi from 'joi';
import { RedisClientType } from 'redis';

import { createFilteredObject } from '../helpers/common.helper';
import { IPaginatorOptions } from '../interfaces/collaterals.interface';
import { ELoanChargeType } from '../enums/loan-charge-type.enum';
import { ELoanTypes } from '../enums/loan-types.enum';
import {
  ILoanStatementTransactionDoc,
  ILoanStatementTransactionWithId,
  LoanStatementTransactionModel,
} from '../models/LoanStatementTransaction.model';
import { ITEMS_PAGINATION } from '../db/collaterals.db';
import {
  ELoanTransactionTypes,
  ILoanTransaction,
  LoanTransaction,
} from '../models/LoanTransaction.model';
import { LoanProduct } from '../models/LoanProducts.model';
import { BorrowerModel } from '../models/Borrower.model';
import {
  ICombinedPayment,
  ILoanPaymentDoc,
  ILoanPaymentWithId,
  LOAN_PAYMENT_FIELDS,
  LoanPaymentModel,
} from '../models/LoanPayment.model';

import { BorrowersDB } from './borrowers.db';
import { LoanStatementBalanceService } from './loan-statement-balance.service';
import { LoanProductsService } from './loan-products.service';
import { LoanChargesService } from './loan-charges.service';
import { FinancialComplianceService } from './financial-compliance.service';
import { LoanStatementService } from './loan-statement.service';
import { LoanStatementStatusService } from './loan-statement-status.service';
import { LoanTransactionsService } from './loan-transactions.service';
import { TermLoanService } from './term-loan.service';
import { LockService } from './lock.service';
import { UsersService } from './users.service';
import { isProductActive } from '../helpers/date.helper';

export const paymentOrder = [
  ELoanChargeType.RECOVERABLE,
  ELoanChargeType.INTEREST_FEE,
  ELoanChargeType.ADMIN_FEE,
  ELoanChargeType.UNUSED_LINE_FEE,
  ELoanChargeType.WIRE_FEE,
  ELoanChargeType.OTHER,
  ELoanChargeType.ANNUAL_LINE_FEE,
];

const CALCULATION_VAR = 'BULK_PAYMENT';

export class LoanPaymentsService {
  constructor(
    private readonly redisClient: RedisClientType,
    private readonly borrowersDB: BorrowersDB,
    private readonly financialComplianceService: FinancialComplianceService,
    private readonly loanChargesService: LoanChargesService,
    private readonly loanProductsService: LoanProductsService,
    private readonly loanStatementBalanceService: LoanStatementBalanceService,
    private readonly getLoanStatementService: () => LoanStatementService,
    private readonly loanStatementStatusService: LoanStatementStatusService,
    private readonly loanTransactionsService: LoanTransactionsService,
    private readonly lockService: LockService,
    private readonly termLoanService: TermLoanService,
    private readonly usersService: UsersService,
  ) {
  }

  async getLoanPayment(paymentId: string) {
    return LoanPaymentModel.findById(paymentId).lean();
  }

  async getLoanPayments(filter: { productIds: string[], start: Date, end: Date }, paginatorOptions: IPaginatorOptions) {
    return LoanPaymentModel.aggregate<ILoanPaymentDoc>([
      {
        $match: {
          $and: [
            { 'productId': { $in: filter.productIds.map((id) => new mongoose.Types.ObjectId(id)) } },
            { 'date': { $gte: filter.start } },
            { 'date': { $lte: dayjs(filter.end).utcOffset(0).endOf('day').toDate() } },
          ],
        },
      }, {
        $sort: {
          'date': 1,
        },
      },
      ...ITEMS_PAGINATION(paginatorOptions),
    ]);
  }

  async getTotalLoanPaymentsForProduct(filter: { productIds: string[], start: Date, end: Date }) {
    return LoanPaymentModel.countDocuments(
      {
        $and: [
          { 'productId': { $in: filter.productIds } },
          { 'date': { $gte: filter.start } },
          { 'date': { $lte: filter.end } },
        ],
      },
    );
  }

  async saveLoanPayment(payment: Partial<ILoanPaymentWithId>, userId: string) {
    if (!payment._id) {
      delete payment._id;
      const newPayment = new LoanPaymentModel(payment);
      await newPayment.save();
      await this.calculatePaymentProportions(newPayment as Partial<ILoanPaymentWithId>, newPayment.productId.toString(), userId);
    } else {
      await LoanPaymentModel.findByIdAndUpdate(payment._id, payment);
      await this.calculatePaymentProportions(payment, payment.productId.toString(), userId);
    }
  }

  async createLoanPayment(payment: Partial<ILoanPaymentWithId>, userId: string) {
    const purePayment = createFilteredObject<ILoanPaymentWithId>(payment, LOAN_PAYMENT_FIELDS);
    await this.saveLoanPayment(purePayment, userId);
  }

  async updateLoanPayment(payment: Partial<ILoanPaymentWithId>, userId: string) {
    const foundPayment = await LoanPaymentModel.findById(payment._id).lean();
    if (!foundPayment) {
      return;
    }
    await this.loanStatementBalanceService.cleanDecreasePayment(foundPayment as unknown as ILoanPaymentWithId, userId);
    const purePayment = createFilteredObject<ILoanPaymentWithId>(payment, LOAN_PAYMENT_FIELDS);
    await this.saveLoanPayment(purePayment, userId);
  }

  async recalculateAfterPayment(productId: string, userId: string) {
    console.log('started recalculating product', productId);
    await this.recalculateProduct(productId, userId);
    await this.loanTransactionsService.recalculateProduct(productId);
  }

  async deleteLoanPayment(paymentId: string, userId: string) {
    const foundPayment = await LoanPaymentModel.findById(paymentId).lean();
    if (!foundPayment) {
      return;
    }
    await this.loanStatementBalanceService.cleanDecreasePayment(foundPayment as unknown as ILoanPaymentWithId, userId);
    await LoanPaymentModel.findByIdAndDelete(paymentId);
    const statementDate = this.loanStatementStatusService.getStatementDateForDate(new Date(foundPayment.date));
    await this.loanStatementStatusService.updateStatementStatus(foundPayment.productId.toString(), statementDate, false);
    await this.termLoanService.setTermLoanExpected(foundPayment.productId.toString());
  }

  private async calculatePaymentProportions(payment: Partial<ILoanPaymentWithId>, productId: string, userId: string) {
    const product = await LoanProduct.findById(productId).lean();
    const settlementCode = payment.settlementCode;
    const borrower = await BorrowerModel.findById(product.borrowerId).lean();

    const loanCharges = await this.loanChargesService.getLoanChargeForProduct(productId);
    const chargesIds = loanCharges.map((charge) => charge._id);

    const productPaymentOrder = !payment.paymentOrder || payment.paymentOrder.length === 0
      ? paymentOrder
      : payment.paymentOrder.map((p) => loanCharges.find((charge) => charge._id.toString() === p).chargeType);

    const paymentOrderQuery = productPaymentOrder.map((charge, index) => {
      return {
        case: {
          $eq: ['$charge.chargeType', charge],
        },
        then: index,
      };
    });

    const unpaidStatements = await LoanStatementTransactionModel.aggregate([
      {
        $match: {
          'chargeId': { '$in': chargesIds },
          'date': { $lte: dayjs(new Date(payment.date)).utcOffset(0).startOf('month').subtract(1, 'second').toDate() },
          $expr: { $lt: ['$amountPaid', '$amount'] },
        },
      }, {
        $lookup:
          {
            from: 'loan_charges',
            localField: 'chargeId',
            foreignField: '_id',
            as: 'charge',
          },
      }, {
        $unwind:
          {
            path: '$charge',
          },
      }, {
        $addFields:
          {
            customSortValue: {
              $switch: {
                branches: paymentOrderQuery,
                default: paymentOrder.length,
              },
            },
          },
      }, {
        $sort:
          {
            customSortValue: 1,
            date: 1,
            order: 1,
          },
      },
    ]);
    let availableAmount = payment.amount;
    const paidArray = [];
    let firstPaidStatement: any = null;
    for (const statement of unpaidStatements) {
      const foundStatement = await LoanStatementTransactionModel.findById(statement._id);
      if (!foundStatement) {
        return;
      }
      const prefPaidAmount = new Decimal(statement.amount).sub(foundStatement.amountPaid).toNumber();
      const amountPaid = prefPaidAmount > availableAmount ? availableAmount : prefPaidAmount;
      availableAmount = new Decimal(availableAmount).sub(amountPaid).toNumber();
      paidArray.push({ statementId: statement._id, amount: amountPaid });
      await LoanStatementTransactionModel.findByIdAndUpdate(statement._id, {
        amountPaid: new Decimal(foundStatement.amountPaid).add(amountPaid).toNumber(),
        settlementCode,
      });
      if (!firstPaidStatement ||
        (statement.date.getTime() < firstPaidStatement.date.getTime()) ||
        (firstPaidStatement.date.getTime() === statement.date.getTime() && firstPaidStatement.order > statement.order)
      ) {
        firstPaidStatement = statement;
      }
      if (availableAmount === 0) {
        break;
      }
    }
    const loanTransactions = [];

    if (availableAmount > 0) {
      const transaction: Partial<ILoanTransaction> = {
        amount: -availableAmount,
        date: payment.date,
        description: `${borrower.code} - STATEMENT PAID`,
        effectiveDate: null,
        loanPaymentId: payment._id,
        order: 0,
        productId: new mongoose.Types.ObjectId(productId),
        reference: `${borrower.code} - STATEMENT PAID`,
        transactionType: ELoanTransactionTypes.COLLECTION,
        userId,
      };
      transaction.effectiveDate = await this.loanTransactionsService.calculateEffectiveDate(transaction);
      const newTransaction = await this.loanTransactionsService.createLoanTransaction(transaction);
      loanTransactions.push({ transactionId: newTransaction._id, amount: newTransaction.amount });
    }
    await LoanPaymentModel.findByIdAndUpdate(payment._id, { paid: paidArray, loanTransactions }, { new: true });
    if (firstPaidStatement) {
      await this.loanStatementBalanceService.calculateProductBalance(productId, firstPaidStatement.date);
    }
    await this.updateComplianceBorrowerAmountReceived(borrower._id.toString());
    const statementDate = this.loanStatementStatusService.getStatementDateForDate(new Date(payment.date));
    await this.loanStatementStatusService.updateStatementStatus(productId, statementDate, false);
    await this.termLoanService.setTermLoanExpected(productId, true);
  }

  async updateComplianceBorrowerAmountReceived(borrowerId: string) {
    const allProducts = await this.loanChargesService.getLoanProducts(borrowerId);
    const productIds = allProducts.map((p) => p._id.toString());
    const startDate = dayjs().startOf('month').toDate();
    const endDate = dayjs().endOf('month').toDate();
    const payments = await this.getPaymentsForPeriod(productIds, startDate, endDate);
    const totalPayments = payments.reduce((acc, p) => new Decimal(acc).add(p.amount ?? 0).toNumber(), 0);
    const complianceBorrower = await this.financialComplianceService.getFinancialComplianceBorrower(borrowerId);
    if (!complianceBorrower) {
      return;
    }
    await this.financialComplianceService.updateBorrower(complianceBorrower._id.toString(), {
      ...complianceBorrower,
      amountReceived: totalPayments,
    });
  }

  async recalculateProduct(productId: string, userId: string) {
    const loanCharges = await this.loanChargesService.getLoanChargeForProduct(productId);
    const product = await this.loanChargesService.getLoanProductById(productId);
    const chargesIds = loanCharges.map((charge) => charge._id);
    await LoanStatementTransactionModel.updateMany({ 'chargeId': { $in: chargesIds } }, { amountPaid: 0 });

    const firstStatements = await LoanStatementTransactionModel.aggregate<ILoanStatementTransactionDoc>([
      {
        $match: {
          'chargeId': { $in: chargesIds },
        },
      }, {
        $sort: {
          'date': 1,
          'order': 1,
        },
      }, {
        $limit: 1,
      },
    ]);

    if (firstStatements.length > 0) {
      const loanStatementService = this.getLoanStatementService();
      await loanStatementService.updateStatementTransaction(firstStatements[0] as Partial<ILoanStatementTransactionWithId>);
    }

    const payments = await LoanPaymentModel.aggregate<ILoanPaymentDoc>([
      {
        $match: { 'productId': new mongoose.Types.ObjectId(productId) },
      },
      {
        $sort: {
          'date': 1,
          'createdAt': 1,
        },
      },
    ]);
    for (const [index, payment] of payments.entries()) {
      try {
        console.log(`updating payment (for ${product.name}) ${index + 1}/${payments.length}`, dayjs(payment.date).format('DD-MM-YYYY'), payment.amount);
        await LoanTransaction.deleteMany({
          _id: { $in: payment.loanTransactions.map(t => t.transactionId) }
        });
        await LoanPaymentModel.findByIdAndUpdate(payment._id, { paid: [], loanTransactions: [] });
        await this.updateLoanPayment(payment as Partial<ILoanPaymentWithId>, userId);
      } catch (e) {
        console.error({ productId, payments });
        console.error(e);
      } finally {
        console.log(`finished updating payment (for ${product.name}) ${index + 1}/${payments.length}`);
      }
    }
  }

  async getPaymentsForPeriod(productIds: string[], startDate: Date, endDate: Date) {
    return LoanPaymentModel.aggregate<ILoanPaymentDoc>([
      {
        $match: {
          $and: [
            { 'productId': { $in: productIds.map((id) => new mongoose.Types.ObjectId(id)) } },
            { 'date': { $gte: startDate } },
            { 'date': { $lte: endDate } },
          ],
        },
      }, {
        $sort: {
          'date': 1,
          'createdAt': 1,
        },
      },
    ]);
  }

  async proceedCombinedPayment(combinedPayment: ICombinedPayment, userId: string) {
    try {
      if (combinedPayment.addToRevolver) {
        const isLockedRes = await Promise.all([...combinedPayment.products, ...combinedPayment.linkedProducts].map(async (productGroup) => {
          const product = await LoanProduct.findById(productGroup.productId).lean();
          if (product) {
            return await this.lockService.isDateLocked(new Date(combinedPayment.date), product.borrowerId.toString());
          }
          return false;
        }));
        if (isLockedRes.some((isLocked) => !!isLocked)) {
          return { success: false, message: 'Period is signed and locked' };
        }
      }

      const loanStatementService = this.getLoanStatementService();
      const data = await loanStatementService.getBorrowerComplianceData(combinedPayment.borrowerId, new Date());
      let totalSumToCover = combinedPayment.payment;
      const products = await this.loanChargesService.getLoanProducts(combinedPayment.borrowerId);
      const revolverProduct = products.find((product) => product.type === ELoanTypes.REVOLVER && isProductActive(product.startDate, product.deactivationDate, combinedPayment.date));

      for (const p of combinedPayment.products) {
        await this.loanProductsService.savePaymentOrder(p.productId, p.paymentOrder);
        const desiredProductSum = data.productTotals[p.productId];
        const product = await LoanProduct.findById(p.productId).lean();
        const sumForProduct = desiredProductSum > totalSumToCover ? totalSumToCover : desiredProductSum;
        if (sumForProduct > 0) {
          const newPayment: Partial<ILoanPaymentWithId> = {
            _id: null,
            productId: new mongoose.Types.ObjectId(p.productId),
            amount: sumForProduct,
            date: new Date(combinedPayment.date),
            settlementCode: p.settlementCode,
            paymentOrder: p.paymentOrder,
          };
          await this.createLoanPayment(newPayment, userId);

          if (combinedPayment.addToRevolver && revolverProduct) {
            const newTransactions: Partial<ILoanTransaction> = {
              amount: sumForProduct,
              bankId: null,
              date: combinedPayment.date,
              description: `${product.type} STATEMENT ADDED TO LOAN`,
              effectiveDate: null,
              productId: revolverProduct._id,
              reference: `${product.type} STATEMENT ADDED TO LOAN`,
              transactionType: ELoanTransactionTypes.DISBURSEMENT,
              userId,
            };
            newTransactions.effectiveDate = await this.loanTransactionsService.calculateEffectiveDate(newTransactions);
            await this.loanTransactionsService.createLoanTransaction(newTransactions);
          }
        }
        totalSumToCover = new Decimal(totalSumToCover).sub(sumForProduct).toNumber();
      }

      if (totalSumToCover > 0 && revolverProduct && !combinedPayment.addToRevolver) {
        const newTransactions: Partial<ILoanTransaction> = {
          amount: -totalSumToCover,
          bankId: null,
          date: combinedPayment.date,
          description: `OVERPAYMENT OF STATEMENT`,
          effectiveDate: null,
          productId: revolverProduct._id,
          reference: `OVERPAYMENT OF STATEMENT`,
          transactionType: ELoanTransactionTypes.COLLECTION,
          userId,
        };
        newTransactions.effectiveDate = await this.loanTransactionsService.calculateEffectiveDate(newTransactions);
        await this.loanTransactionsService.createLoanTransaction(newTransactions);
      }

      await Promise.all(combinedPayment.linkedProducts.map(async (p) => {
        const newTransactions: Partial<ILoanTransaction> = {
          amount: -p.payableSum,
          bankId: null,
          date: combinedPayment.date,
          description: `EXTRA PAYMENT`,
          effectiveDate: null,
          productId: new mongoose.Types.ObjectId(p.productId),
          reference: `EXTRA PAYMENT REF`,
          transactionType: ELoanTransactionTypes.COLLECTION,
          userId,
        };
        newTransactions.effectiveDate = await this.loanTransactionsService.calculateEffectiveDate(newTransactions);
        await this.loanTransactionsService.createLoanTransaction(newTransactions);
      }));

      return { success: true, message: null };
    } catch (e) {
      console.error(e);
    }
  }

  async getExpectedPayments(userId: string) {
    const user = await this.usersService.getUserById(userId);
    if (!user) {
      return [];
    }
    const borrowers = await this.borrowersDB.getAllowedBorrowers(user);
    const isCalculated = await this.redisClient.get(CALCULATION_VAR);
    const borrowersData = await Promise.all(borrowers
      .filter((borrower) => borrower.active)
      .map(async (borrower) => {
        const loanStatementService = this.getLoanStatementService();
        const dueAmounts = await loanStatementService.getBorrowerProductTotals(borrower._id.toString());
        return {
          borrowerId: borrower._id.toString(),
          borrowerTitle: borrower.name,
          totalDue: dueAmounts.total,
          productTotals: dueAmounts.productTotals,
          revolverBalance: dueAmounts.revolverBalance,
          termBalance: dueAmounts.termBalance,
        };
      }),
    );
    return { borrowers: borrowersData, isCalculated: !!isCalculated };
  }

  async payExpectedPayments(userId: string, borrowers: { [borrowerId: string]: boolean }, selectedDate: Date) {

    const arrayOfStringsSchema = Joi.array().items(Joi.string()).required();
    const borrowerIds = Object.keys(borrowers);
    const { error } = arrayOfStringsSchema.validate(borrowerIds, { abortEarly: false });
    if (error) {
      return { message: 'Incorrect list of borrowers IDs' };
    }

    await this.redisClient.set(CALCULATION_VAR, 1);
    const user = await this.usersService.getUserById(userId);
    if (!user) {
      return { message: 'Cannot find user with this id' };
    }
    const allowedBorrowers = await this.borrowersDB.getAllowedBorrowers(user);
    const paymentPromises = allowedBorrowers.map(async (borrower) => {
      if (borrowerIds.includes(borrower._id.toString())) {
        try {
          const borrowerProducts = await this.loanChargesService.getLoanProducts(borrower._id.toString());
          const revolverProduct = borrowerProducts.find((product) => product.type === ELoanTypes.REVOLVER && isProductActive(product.startDate, product.deactivationDate, selectedDate));
          const loanStatementService = this.getLoanStatementService();
          const dueAmounts = await loanStatementService.getBorrowerProductTotals(borrower._id.toString());
          if (dueAmounts.total === 0) {
            console.log('no payment for this borrower');
            return;
          }
          const products = await Promise.all(
            Object.entries(dueAmounts.productTotals).map(async ([productId, amount]) => {
              const product = await LoanProduct.findById(productId);
              if (product) {
                return {
                  productId: productId,
                  payableSum: amount,
                  settlementCode: borrowers[borrower._id.toString()] ? (revolverProduct?.code ?? product.code) : product.settlementCode,
                  paymentOrder: null,
                };
              }
            }),
          );

          const combinedPayment: ICombinedPayment = {
            addToRevolver: borrowers[borrower._id.toString()],
            borrowerId: borrower._id.toString(),
            date: selectedDate,
            linkedProducts: [],
            payment: dueAmounts.total,
            products: products.filter((product) => !!product),
          };

          await this.proceedCombinedPayment(combinedPayment, userId);
          console.log(`Success on payment ${borrower.title}`);
        } catch (err) {
          console.error(`Error on payment ${borrower.title}:`, err);
        }
      }
    });

    const processPayments = async (paymentPromises: Promise<void>[]) => {
      try {
        await Promise.allSettled(paymentPromises);
        console.log('All payments processed.');
        await this.redisClient.del(CALCULATION_VAR);
      } catch (err) {
        console.error('Error during payment processing:', err);
      }
    };

    processPayments(paymentPromises).catch(console.error);

    return { message: 'Payments are being processed in the background.' };
  }
}
