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

import { ILoanProductDoc, LoanProduct } from '../models/LoanProducts.model';
import { ELedgerReportType, ledgerHeadersMap } from './loan-statement.db';
import mongoose from 'mongoose';
import { BorrowerModel } from '../models/Borrower.model';
import {
  ELoanTransactionTypes,
  ILoanTransactionDoc,
  LoanTransaction,
} from '../models/LoanTransaction.model';
import { ELoanTypes } from '../enums/loan-types.enum';
import {
  ILoanStatementTransactionDocWithCharge,
  LoanStatementTransactionModel,
} from '../models/LoanStatementTransaction.model';
import { convertDateToFormat } from '../helpers/date.helper';
import dayjs from 'dayjs';
import { getLoanChargeForProduct } from './loan-charges.db';
import { getLoanProductBalance } from './loan-products.db';
import { getPaymentForTransaction } from './loan-payments.db';
import { enrichWithBrokers } from './brokers.db';
import { TermLoanModel } from '../models/TermLoan.model';
import { TermLoanCalculatedModel } from '../models/TermLoanCalculated.model';
import { MODEL_NAMES } from '../models/_models';

export enum EChargeType {
  INTEREST_FEE = 'INTEREST',
  ADMIN_FEE = 'ADMIN FEE',
  UNUSED_LINE_FEE = 'UNUSED LINE FEE',
  ANNUAL_LINE_FEE = 'ANNUAL LINE FEE',
  WIRE_FEE = 'WIRE FEE',
  RECOVERABLE = 'RECOVERABLE',
  OTHER = 'OTHER',
  DISBURSEMENT = 'DISBURSEMENT',
  PRINCIPAL = 'PRINCIPAL',
  COLLECTION = 'COLLECTION',
  ADJUSTMENT = 'ADJUSTMENT',
  EXPECTED_PRINCIPAL = 'EXPECTED PRINCIPAL ACCRUED',
  EXPECTED_PRINCIPAL_PAID = 'EXPECTED PRINCIPAL PAID',
}

export interface ILedgerReportRow {
  borrowerCode: string;
  date: string;
  type: string;
  chargeCode: string;
  PLCode: string;
  productCode: string;
  productName: string;
  amount: number;
  statementAmount: number;
  title: string;
  balance: number;
  floatedBalance: number;
  info: string;
  paymentDate: string;
}

export interface ILedgerDataRow {
  typeOrder: number;
  order: number;
  date: Date;
  type: EChargeType;
  amount: number;
  statementAmount: number;
  title: string;
  chargeCode: string;
  PLCode: string;
  productCode: string;
  productName: string;
  balance: number;
  floatedBalance: number;
  createdAt: Date;
  info: string;
  paymentDate: Date;
  isBrokerFee?: boolean;
}

export type LedgerTransactions = {
  transactions: ILoanTransactionDoc[];
  product: ILoanProductDoc;
  productId: mongoose.Types.ObjectId;
}

export type ReportLedgerParams = {
  productIds: string[],
  startDate: Date,
  endDate: Date,
  reportType: ELedgerReportType,
}

export type ReportLedgerOptions = {
  showFloatedBalance: boolean,
  showBalances: boolean,
}

const defaultReportLedgerOptions: ReportLedgerOptions = {
  showBalances: true,
  showFloatedBalance: true,
};

export const getLedger = async (params: ReportLedgerParams, options = defaultReportLedgerOptions): Promise<{
  [productId: string]: ILedgerReportRow  []
}> => {
  const { productIds, reportType } = params;
  const fullTransactions = await getLedgerData(params, options);
  const products = await LoanProduct
    .find({ _id: { $in: productIds.map((id) => new mongoose.Types.ObjectId(id)) } })
    .sort({ code: 1 })
    .lean();

  const header = ledgerHeadersMap[reportType];
  const headerKeys = Object.keys(header);

  const sumKeys = ['amount', 'statementAmount'];
  const groupKeys = headerKeys.filter((key) => !sumKeys.includes(key));

  return products.reduce((acc, product) => {
    const transactions = fullTransactions[product._id.toString()];
    const finalTransactions = reportType === ELedgerReportType.SHORT
      ? mergeLedgerData(transactions, groupKeys, sumKeys)
      : transactions;
    const sheetName = `${product.code} - ${product.name}`;
    return {
      ...acc,
      [sheetName]: _.map(finalTransactions, (obj) => _.pick(obj, headerKeys)) as ILedgerReportRow[],
    };
  }, {});
};

const getLedgerData = async (params: ReportLedgerParams, options = defaultReportLedgerOptions): Promise<{
  [productId: string]: ILedgerReportRow[]
}> => {
  const { productIds, startDate, endDate } = params;

  const addBrokers = true;
  const products = await LoanProduct.find({ _id: { $in: productIds.map((id) => new mongoose.Types.ObjectId(id)) } }).lean();
  const borrowers = await BorrowerModel.find({ _id: { $in: products.map((product) => product.borrowerId) } }).lean();
  const borrowerMap = borrowers.reduce((acc, borrower) => (
    { ...acc, [borrower._id.toString()]: borrower.code }
  ), {});
  const borrowerCodesMap = products.reduce((acc, product): { [productId: string]: string } => ({
    ...acc,
    [product._id.toString()]: borrowerMap[product.borrowerId.toString()],
  }), {});

  const groupedTransactions = await LoanTransaction.aggregate<LedgerTransactions>([
    {
      $match: {
        $and: [
          { 'date': { $gte: startDate } },
          { 'date': { $lte: dayjs(endDate).endOf('day').toDate() } },
          {
            productId: {
              $in: products.map((product) => product._id),
            },
          },
        ],
      },
    }, {
      $lookup: {
        from: MODEL_NAMES.loanProducts,
        localField: 'productId',
        foreignField: '_id',
        as: 'product',
      },
    }, {
      $unwind: {
        path: '$product',
        preserveNullAndEmptyArrays: true,
      },
    }, {
      $sort: {
        'date': 1,
        'order': 1,
      },
    },
    {
      $group: {
        _id: '$productId',
        transactions: { $push: '$$ROOT' },
        product: { $first: '$product' },
      },
    },
    {
      $project: {
        _id: 0,
        productId: '$_id',
        product: 1,
        transactions: 1,
      },
    },
  ]);

  const convertTypes = (transactionType: ELoanTransactionTypes, product: ILoanProductDoc): EChargeType => {
    if (product.type === ELoanTypes.TERM && transactionType === ELoanTransactionTypes.COLLECTION) {
      return EChargeType.PRINCIPAL;
    }
    return EChargeType[transactionType];
  };

  const groupedProductsWithTransactions = products.map((p) => {
    const productTransactions = groupedTransactions.find((group) => group.productId.toString() === p._id.toString());
    return {
      product: p,
      productId: p._id,
      transactions: productTransactions ? productTransactions.transactions : [],
    };
  });

  const mappedGroups: {
    [productId: string]: { transactions: ILedgerDataRow[], product: ILoanProductDoc }
  } = groupedProductsWithTransactions.reduce((acc, group) => {
    return {
      ...acc,
      [group.productId.toString()]: {
        product: group.product,
        transactions: group.transactions.map((transaction): ILedgerDataRow => {
          return {
            typeOrder: 0,
            order: transaction.order,
            date: transaction.date,
            type: convertTypes(transaction.transactionType, group.product),
            amount: transaction.amount,
            statementAmount: null,
            title: convertTypes(transaction.transactionType, group.product),
            balance: transaction.balance,
            floatedBalance: transaction.floatedBalance,
            chargeCode: group.product.code,
            PLCode: null,
            productCode: group.product.code,
            productName: group.product.name,
            createdAt: transaction['createdAt'],
            info: transaction.reference,
            paymentDate: null,
          };
        }),
      },
    };
  }, {});

  const allData = {};
  await Promise.all(Object.entries(mappedGroups).map(async ([productId, group]) => {
    const loanCharges = await getLoanChargeForProduct(productId);
    const chargesIds = loanCharges.map((charge) => charge._id);
    const statementTransactions = await LoanStatementTransactionModel.aggregate<ILoanStatementTransactionDocWithCharge>([
      {
        $match: {
          $and: [
            { 'date': { $gte: startDate } },
            { 'date': { $lte: dayjs(endDate).endOf('day').toDate() } },
            { 'chargeId': { '$in': chargesIds } },
          ],
        },
      }, {
        $lookup: {
          from: MODEL_NAMES.loanCharges,
          localField: 'chargeId',
          foreignField: '_id',
          as: 'charge',
        },
      }, {
        $unwind: {
          path: '$charge',
        },
      }, {
        $sort: {
          'date': 1,
          'createdAt': 1,
          'order': 1,
        },
      },
    ]);

    const statementTransactionsWithBalance = options.showBalances
      ? await Promise.all(statementTransactions.map(async (statement) => {
        const balances = await getLoanProductBalance(productId, statement.date);
        return {
          ...statement,
          balance: balances.balance,
          floatedBalance: balances.floatedBalance,
        };
      }))
      : statementTransactions.slice();

    const isParticipant = group.product.masterLoanProductId != null;
    const groupedMappedStatementTransactions = await Promise.all(statementTransactionsWithBalance.map(async (statement) => {

      const transactions = [];

      if (statement.amountPaid > 0) {
        const payments = await getPaymentForTransaction(productId, statement._id.toString());
        for (const payment of payments) {
          transactions.push({
            typeOrder: 1,
            order: statement.order,
            date: statement.date,
            type: statement.charge.chargeType,
            amount: null,
            statementAmount: isParticipant ? statement.amountPaid : -statement.amountPaid,
            title: `${statement.charge.name} PAID`,
            chargeCode: statement.charge.code,
            PLCode: statement.charge.PLCode,
            productCode: group.product.code,
            productName: group.product.name,
            balance: statement.balance,
            floatedBalance: statement.floatedBalance,
            createdAt: statement['createdAt'],
            info: statement.memo,
            paymentDate: convertDateToFormat(payment.date),
          });
        }
      }
      return [...transactions, {
        typeOrder: 1,
        order: statement.order,
        date: statement.date,
        type: statement.charge.chargeType,
        amount: null,
        statementAmount: isParticipant ? -statement.amount : statement.amount,
        title: `${statement.charge.name} ACCRUED`,
        chargeCode: statement.charge.code,
        PLCode: statement.charge.PLCode,
        productCode: group.product.code,
        productName: group.product.name,
        balance: statement.balance,
        floatedBalance: statement.floatedBalance,
        createdAt: statement['createdAt'],
        info: statement.memo,
        paymentDate: null,
      }];
    }));
    const mappedStatementTransactions: ILedgerDataRow[] = groupedMappedStatementTransactions
      .reduce((acc, group) => [...acc, ...group], []);
    const statementTransactionsWithBrokers = addBrokers
      ? await enrichWithBrokers(mappedStatementTransactions, group.product)
      : mappedStatementTransactions;
    const sortedTransactions = [...statementTransactionsWithBrokers, ...group.transactions]
      .sort((a, b) => {
        if (a.date.valueOf() !== b.date.valueOf()) {
          return b.date.valueOf() - a.date.valueOf();
        }
        if (a.typeOrder !== b.typeOrder) {
          return b.typeOrder - a.typeOrder;
        }
        return b.createdAt.valueOf() - a.createdAt.valueOf();
      });

    const fullTransactions = sortedTransactions
      .map((row): ILedgerReportRow => ({
        borrowerCode: borrowerCodesMap[productId],
        date: convertDateToFormat(row.date, 'MM/DD/YYYY'),
        type: row.isBrokerFee ? `${EChargeType[row.type]} BROKER` : EChargeType[row.type],
        chargeCode: row.chargeCode,
        PLCode: row.PLCode,
        productCode: row.productCode,
        productName: row.productName,
        amount: row.amount,
        statementAmount: row.statementAmount,
        title: row.title,
        balance: row.balance,
        floatedBalance: row.floatedBalance,
        info: row.info,
        paymentDate: row.paymentDate ? convertDateToFormat(row.paymentDate) : '',
      }));

    const termLoan = await TermLoanModel.findOne({ productId, actual: true });
    if (termLoan) {
      const calculatedTermLoan = await TermLoanCalculatedModel.findOne({
        termLoanId: termLoan._id.toString(),
        relevantStatement: convertDateToFormat(endDate),
      });
      if (calculatedTermLoan) {
        const expectedPayment = <ILedgerReportRow>{
          borrowerCode: borrowerCodesMap[productId],
          date: convertDateToFormat(endDate, 'MM/DD/YYYY'),
          type: EChargeType.EXPECTED_PRINCIPAL,
          chargeCode: null,
          PLCode: null,
          productCode: group.product.code,
          amount: isParticipant ? -calculatedTermLoan.monthlyPrincipal : calculatedTermLoan.monthlyPrincipal,
          statementAmount: null,
          title: EChargeType.EXPECTED_PRINCIPAL,
          balance: null,
          floatedBalance: null,
          info: EChargeType.EXPECTED_PRINCIPAL,
          paymentDate: null,
        };
        fullTransactions.unshift(expectedPayment);
        if (calculatedTermLoan.payments && calculatedTermLoan.payments.length > 0) {
          for (const payment of calculatedTermLoan.payments) {
            const paymentDoc = await LoanTransaction.findById(payment.paymentId).lean();
            if (paymentDoc) {
              const expectedPayment = <ILedgerReportRow>{
                borrowerCode: borrowerCodesMap[productId],
                date: convertDateToFormat(endDate, 'MM/DD/YYYY'),
                type: EChargeType.EXPECTED_PRINCIPAL_PAID,
                chargeCode: null,
                PLCode: null,
                productCode: group.product.code,
                amount: isParticipant ? -payment.amount : payment.amount,
                statementAmount: null,
                title: EChargeType.EXPECTED_PRINCIPAL_PAID,
                balance: null,
                floatedBalance: null,
                info: EChargeType.EXPECTED_PRINCIPAL_PAID,
                paymentDate: convertDateToFormat(paymentDoc.date, 'MM/DD/YYYY'),
              };
              fullTransactions.unshift(expectedPayment);
            }
          }
        }
      }
    }
    allData[productId] = fullTransactions;
  }));
  return allData;
};

const mergeLedgerData = (transactions: ILedgerReportRow[], groupKeys: string[], sumKeys: string[]): ILedgerReportRow[] => {
  const groupedObjects = _.groupBy(
    transactions,
    (obj) => groupKeys.reduce((acc, key) => `${acc}${obj[key]}`, ''),
  );

  const mergedObjects = _.mapValues(groupedObjects, group =>
    _.reduce(group, (result: any, obj) => {
      sumKeys.forEach((key) => {
        result[key] = new Decimal(result[key] || 0).plus(obj[key] || 0).toNumber();
      });
      groupKeys.forEach((key) => {
        result[key] = obj[key];
      });
      return result;
    }, {}),
  );

  return _.values(mergedObjects);
};
