import mongoose from 'mongoose';
import dayjs from 'dayjs';

import { IPaginatorOptions } from '../interfaces/collaterals.interface';
import { ELoanChargeType } from '../enums/loan-charge-type.enum';
import { MODEL_NAMES } from '../models/_models';
import {
  fieldsToUnset,
  getLoanProductBalance as getLoanProductBalanceDB,
  getLoanProductById as getLoanProductDB,
  getLoanProducts,
} from '../db/loan-products.db';
import { EChargeCalculationBasis, EChargeFrequencies, ELoanTypes } from '../enums/loan-types.enum';
import { getLoanChargeForProduct } from '../db/loan-charges.db';
import {
  ILoanProductDoc,
  ILoanProductView,
  ILoanProductWithId,
  LoanProduct,
} from '../models/LoanProducts.model';
import {
  chargeViewValidationSchema,
  ILoanChargeView,
  ILoanChargeWithId,
  LoanCharge,
} from '../models/LoanCharges.model';
import {
  ILoanStatementTransactionDoc,
  ILoanStatementTransactionDocWithCharge,
  LoanStatementTransactionModel,
} from '../models/LoanStatementTransaction.model';
import { ITEMS_PAGINATION } from '../db/collaterals.db';
import { BorrowersDB } from './borrowers.db';

const updateChargesForProduct = false;

export class LoanChargesService {

  constructor(
    private readonly borrowersDB: BorrowersDB,
  ) {
  }

  private validateCharge = (charge: ILoanChargeView) => {
    const chargePLCode = charge.PLCode ? charge.PLCode.trim() : '';
    const chargeBorrowerCode = charge.code.trim().slice(3, 6);
    return chargePLCode.length > 0 && chargeBorrowerCode.length > 0;
  };

  async getProductByChargeId(chargeId: string) {
    const charge = await LoanCharge.findById(chargeId).lean();
    if (!charge) {
      return null;
    }
    return LoanProduct.findById(charge.productId).lean();
  }

  async getBorrowersLoanProducts(borrowerId: string) {
    return LoanProduct.find({ borrowerId }).lean();
  }

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

  async getLoanProducts(borrowerId: string) {
    return getLoanProducts(borrowerId);
  }

  async getLoanProductsByIds(productIds: string[]) {
    const ids = productIds.map((productId) => new mongoose.Types.ObjectId(productId));
    return LoanProduct.aggregate<ILoanProductView>([
      {
        $match: {
          '_id': { $in: ids },
        },
      }, {
        $sort: {
          'order': 1,
        },
      }, {
        $unset: fieldsToUnset,
      }, {
        $addFields: {
          valid: true,
        },
      },
    ]);
  }

  async getLoanProductBalance(productId: string, forDate = new Date()) {
    return getLoanProductBalanceDB(productId, forDate);
  }

  async getParticipationBalance(code: string, end: Date){
    const masterProduct = await LoanProduct.findOne({ code }).lean();
    if (!masterProduct) {
      return 0;
    }
    const participationProduct = await LoanProduct.findOne({ masterLoanProductId: masterProduct._id }).lean();
    if (!participationProduct) {
      return 0;
    }
    const { balance } = await this.getLoanProductBalance(participationProduct._id.toString(), end);
    return balance;
  };

  async getLoanProductById(productId: string) {
    return getLoanProductDB(productId);
  }

  async getLoanProductIdByCode(code: string): Promise<string> {
    const foundProduct = await LoanProduct.findOne({ code }).lean();
    if (foundProduct) {
      return foundProduct._id.toString();
    }
    return null;
  }

  async getLoanChargeIdByCode(code: string): Promise<string> {
    const foundCharge = await LoanCharge.findOne({ code }).lean();
    if (foundCharge) {
      return foundCharge._id.toString();
    }
    return null;
  }

  async saveLoanProducts(borrowerId: string, loanProducts: ILoanProductView[]): Promise<void> {
    const mappedProducts: ILoanProductWithId[] = loanProducts.map((product, order) => {
      return { ...product, borrowerId: new mongoose.Types.ObjectId(borrowerId), order };
    });
    const allProducts = await this.getLoanProducts(borrowerId);
    const productIdsToRemove = allProducts.map((product) => product._id.toString());
    await Promise.all(mappedProducts.map(async (product) => {
      if (!product._id) {
        const newProduct = new LoanProduct(product);
        const savedProduct = await newProduct.save();
        if (product.type === ELoanTypes.TERM) {
          const chargeTypes = [
            {
              chargeType: ELoanChargeType.INTEREST_FEE,
              calculationBasis: EChargeCalculationBasis.DAILY_BALANCE,
              frequency: EChargeFrequencies.DAILY,
            },
            {
              chargeType: ELoanChargeType.ADMIN_FEE,
              calculationBasis: EChargeCalculationBasis.AVERAGE_MONTHLY_BALANCE,
              frequency: EChargeFrequencies.MONTHLY,
            },
          ];
          const charges = chargeTypes.map((charge) => {
            const newCharge: ILoanChargeView = {
              PLCode: 'XXXXXX',
              _id: null,
              active: true,
              applyFrom: product.startDate,
              calculationBasis: charge.calculationBasis,
              chargeType: charge.chargeType,
              code: 'XXXXXX',
              frequency: charge.frequency,
              includeInYield: true,
              minimumAmount: 0,
              name: charge.chargeType,
              percent: 0,
              productId: new mongoose.Types.ObjectId(String(savedProduct._id)),
            };
            return newCharge;
          });
          await this.saveLoanCharges(borrowerId, charges, true);
        }
      } else {
        await LoanProduct.findByIdAndUpdate(product._id, product);
        if (updateChargesForProduct) {
          await this.updatedChargeCodes(product._id.toString());
        }
        const index = productIdsToRemove.indexOf(product._id.toString());
        if (index > -1) {
          productIdsToRemove.splice(index, 1);
        }
      }
    }));
    await Promise.all(productIdsToRemove.map(async (id) => {
      await LoanProduct.findByIdAndDelete(id);
      const foundLinkedCharges = await LoanCharge.find({ productId: new mongoose.Types.ObjectId(id) });
      await Promise.all(foundLinkedCharges.map(async (foundLinked) => {
        await LoanCharge.findByIdAndDelete(foundLinked._id);
      }));
    }));
  }

  async getLoanCharges(borrowerId: string): Promise<ILoanChargeView[]> {
    return LoanCharge.aggregate<ILoanChargeView>([
      {
        $match: {
          'borrowerId': new mongoose.Types.ObjectId(borrowerId),
          'deletedAt': { $exists: false },
        },
      }, {
        $sort: {
          'productId': 1,
          'order': 1,
        },
      }, {
        $unset: fieldsToUnset,
      }, {
        $addFields: {
          valid: true,
        },
      },
    ]);
  }

  async getLoanChargeForProduct(productId: string): Promise<ILoanChargeView[]> {
    return getLoanChargeForProduct(productId);
  }

  validateLoanCharge(loanCharge: ILoanChargeView) {
    const validationResult = chargeViewValidationSchema.validate(loanCharge);
    if (validationResult.error) {
      console.error('Validation error for loan charge:', validationResult.error.details);
    }
    return !!validationResult.error;
  }

  async saveLoanCharges(borrowerId: string, loanCharges: ILoanChargeView[], quickCreate = false): Promise<void> {
    const mappedCharges: ILoanChargeWithId[] = loanCharges.map((charge, order) => {
      return { ...charge, borrowerId: new mongoose.Types.ObjectId(borrowerId), order };
    });
    const allCharges = await this.getLoanCharges(borrowerId);
    const chargeIdsToRemove = allCharges.map((charge) => charge._id.toString());
    await Promise.all(mappedCharges.map(async (charge) => {
      if (!quickCreate) {
        if (!this.validateCharge(charge)) {
          if (charge._id) {
            const index = chargeIdsToRemove.indexOf(charge._id.toString());
            if (index > -1) {
              chargeIdsToRemove.splice(index, 1);
            }
          }
          return;
        }
      }
      if (!charge._id || charge._id.toString().includes('new_')) {
        const { _id, ...rest } = charge;
        const newCharge = new LoanCharge(rest);
        await newCharge.save();
      } else {
        await LoanCharge.findByIdAndUpdate(charge._id, charge);
        const index = chargeIdsToRemove.indexOf(charge._id.toString());
        if (index > -1) {
          chargeIdsToRemove.splice(index, 1);
        }
      }
    }));
    if (!quickCreate) {
      await Promise.all(chargeIdsToRemove.map(async (id) => {
        await LoanCharge.findByIdAndUpdate(id, { deletedAt: new Date() });
      }));
    }
  }

  private async getChargeIds(productIds: string[], chargeType: ELoanChargeType = null) {
    const loanChargesGrouped = await Promise.all(productIds.map(async (productId) => {
      return await this.getLoanChargeForProduct(productId);
    }));
    const loanCharges = loanChargesGrouped.reduce((acc, group) => [...acc, ...group], []);
    const filteredCharges = chargeType ? loanCharges.filter((charge) => charge.chargeType === chargeType) : loanCharges;
    return filteredCharges.map((charge) => charge._id);
  }

  async getLoanStatementsForProduct(filter: {
    productIds: string[],
    start: Date,
    end: Date
  }, paginatorOptions: IPaginatorOptions = null, chargeType: ELoanChargeType = null) {
    const chargesIds = await this.getChargeIds(filter.productIds, chargeType);
    return LoanStatementTransactionModel.aggregate<ILoanStatementTransactionDoc>([
      {
        $match: {
          $and: [
            { 'date': { $gte: filter.start } },
            { 'date': { $lte: dayjs(filter.end).endOf('day').toDate() } },
            { 'chargeId': { '$in': chargesIds } },
          ],
        },
      }, {
        $sort: {
          'date': 1,
          'order': 1,
        },
      },
      ...ITEMS_PAGINATION(paginatorOptions),
    ]);
  }

  async getLoanStatementsForIdsAndCharge(filter: {
    productIds: string[],
    transactionIds: string[],
    start: Date,
    end: Date
  }, paginatorOptions: IPaginatorOptions = null, chargeType: ELoanChargeType = null) {
    const chargesIds = await this.getChargeIds(filter.productIds, chargeType);
    return LoanStatementTransactionModel.aggregate<ILoanStatementTransactionDoc>([
      {
        $match: {
          $and: [
            { '_id': { '$in': filter.transactionIds } },
            { 'chargeId': { '$in': chargesIds } },
          ],
        },
      }, {
        $sort: {
          'date': 1,
          'order': 1,
        },
      },
      ...ITEMS_PAGINATION(paginatorOptions),
    ]);
  }

  async getLoanStatementsForForProductWithCharges(filter: {
    productIds: string[],
    start: Date,
    end: Date
  }, paginatorOptions?: IPaginatorOptions) {
    const loanChargesGrouped = await Promise.all(filter.productIds.map(async (productId) => {
      return await this.getLoanChargeForProduct(productId);
    }));
    const loanCharges = loanChargesGrouped.reduce((acc, group) => [...acc, ...group], []);
    const chargesIds = loanCharges.map((charge) => charge._id);
    return LoanStatementTransactionModel.aggregate<ILoanStatementTransactionDocWithCharge>([
      {
        $match: {
          $and: [
            { 'date': { $gte: filter.start } },
            { 'date': { $lte: filter.end } },
            { 'chargeId': { '$in': chargesIds } },
          ],
        },
      }, {
        $sort: {
          'date': 1,
          'order': 1,
        },
      }, {
        $lookup: {
          from: 'loan_charges',
          localField: 'chargeId',
          foreignField: '_id',
          as: 'charge',
        },
      }, {
        $unwind: {
          path: '$charge',
        },
      },
      ...ITEMS_PAGINATION(paginatorOptions),
    ]);
  }

  async getTotalLoanStatementsForProduct(filter: { productIds: string[], start: Date, end: Date }) {
    const loanChargesGrouped = await Promise.all(filter.productIds.map(async (productId) => {
      return await this.getLoanChargeForProduct(productId);
    }));
    const loanCharges = loanChargesGrouped.reduce((acc, group) => [...acc, ...group], []);
    const chargesIds = loanCharges.map((charge) => charge._id);
    return LoanStatementTransactionModel.countDocuments(
      {
        $and: [
          { 'chargeId': { $in: chargesIds } },
          { 'date': { $gte: filter.start } },
          { 'date': { $lte: filter.end } },
        ],
      },
    );
  }

  async updatedChargeCodes(productId: string) {
    const product = await LoanProduct.findById(productId).lean();
    const borrowerProductCode = product.code.trim().slice(3, 6);
    const charges = await this.getLoanChargeForProduct(productId);
    await Promise.all(charges.map(async (charge) => {
      const newCode = charge.code.trim().slice(0, 3) + borrowerProductCode;
      await LoanCharge.findByIdAndUpdate(charge._id, { code: newCode });
    }));
  }

}
