import mongoose from 'mongoose';
import dayjs from 'dayjs';
import minMax from 'dayjs/plugin/minMax';
import Decimal from 'decimal.js';

import { getDays } from '../helpers/date.helper';
import { getTotalForDate } from './loan-statement.db';
import { IPostponedTransactionWithTransaction, PostponedTransaction } from '../models/PostponedTransactions.model';
import { ILoanProductView, LoanProduct } from '../models/LoanProducts.model';
import { ILoanChargeView } from '../models/LoanCharges.model';
import { ILoanTransactionDoc, LoanTransaction } from '../models/LoanTransaction.model';
import { IStatementPeriod } from '../models/LoanStatementTransaction.model';
import { ELoanTypes } from '../enums/loan-types.enum';
import { MODEL_NAMES } from '../models/_models';

dayjs.extend(minMax);

export const fieldsToUnset = ['order', '__v', 'createdAt', 'updatedAt', 'borrowerId'];

export const getLoanProductById = async (productId: string) => {
  return LoanProduct.findById(productId).lean();
};

export const getLoanProductBalance = async (productId: string, asOfDate = new Date()) => {

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

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


  const transactions = [...collectionsTransactions, ...normalTransactions].sort((a, b) => {
    if (a.date.valueOf() === b.date.valueOf()) {
      return a.effectiveDate.valueOf() - b.effectiveDate.valueOf();
    }
    return a.date.valueOf() - b.date.valueOf();
  });
  const collectionsTransaction = collectionsTransactions.length === 0 ? null : collectionsTransactions[0];
  const normalTransaction = normalTransactions.length === 0 ? null : normalTransactions[0];
  const balance = simpleTransactions.length === 0 ? 0 : simpleTransactions[0].balance;
  let floatedBalance = 0;

  if (normalTransaction && collectionsTransaction) {
    const totalCollections = await getTotalForDate(productId, collectionsTransaction.effectiveDate);
    if (normalTransaction.date.valueOf() === collectionsTransaction.date.valueOf()) {
      floatedBalance = new Decimal(normalTransaction.floatedBalance).add(totalCollections).toNumber();
    }
    if (normalTransaction.date.valueOf() > collectionsTransaction.date.valueOf()) {
      floatedBalance = normalTransaction.floatedBalance;
    }
    if (normalTransaction.date.valueOf() < collectionsTransaction.date.valueOf()) {
      floatedBalance = collectionsTransaction.balance;
    }
  }
  if (normalTransaction && !collectionsTransaction) {
    floatedBalance = normalTransaction.floatedBalance;
  }
  const totalPostponed = await getPostponedTransactions(asOfDate, productId);
  if (transactions.length === 0) {
    return { balance: 0, floatedBalance: totalPostponed };
  }
  return {
    balance: balance,
    floatedBalance: floatedBalance,
  };
};

export const getPostponedTransactions = async (date: Date, productId: string, removePostponed = false) => {
  const currentDay = dayjs(new Date(date)).format('YYYY-MM-DD');
  const postponedTransactions = await PostponedTransaction.aggregate<IPostponedTransactionWithTransaction>([
    {
      $match: {
        'productId': new mongoose.Types.ObjectId(productId),
      },
    }, {
      $lookup: {
        from: MODEL_NAMES.loanTransactions,
        localField: 'transactionId',
        foreignField: '_id',
        as: 'transaction',
      },
    }, {
      $unwind: {
        path: '$transaction',
      },
    },
  ]);
  const currentDate = dayjs(`${currentDay}T00:00:00.000Z`);
  const mappedTransactions = postponedTransactions.reduce((acc, t) => {
    const transactionDate = dayjs(`${t.postponedToDate}T00:00:00.000Z`);
    if (transactionDate <= currentDate) {
      return [...acc, t];
    }
    return acc;
  }, (<IPostponedTransactionWithTransaction[]>[]));
  let totalAmount = 0;
  await Promise.all(mappedTransactions.map(async (t) => {
    totalAmount = new Decimal(totalAmount).add(t.transaction.amount).toNumber();
    if (removePostponed) {
      await PostponedTransaction.findByIdAndDelete(t._id);
    }
  }));
  return totalAmount;
};

export const getAverageBalance = async (productId: string, period: IStatementPeriod, charge?: ILoanChargeView) => {
  const useFloating = await useFloatingBalance(productId);
  if (charge) {
    period.start = dayjs.max(period.start, dayjs(charge.applyFrom));
  }
  const days = getDays(period);
  if (days.length === 0) {
    return 0;
  }
  const balances = await Promise.all(days.map(async (day) => {
    return await getLoanProductBalance(productId, new Date(day));
  }));
  const totalBalance = balances.reduce((acc, balance) => new Decimal(acc).add(useFloating ? balance.floatedBalance : balance.balance).toNumber(), 0);
  return new Decimal(totalBalance).div(days.length).toNumber();
};

export const getAverageActualBalance = async (productId: string, period: IStatementPeriod, charge?: ILoanChargeView) => {
  if (charge) {
    period.start = dayjs.max(period.start, dayjs(charge.applyFrom));
  }
  const days = getDays({ ...period, end: dayjs(period.end).add(1, 'day') });
  if (days.length === 0) {
    return 0;
  }
  const balances = await Promise.all(days.map(async (day, i) => {
    return await getLoanProductBalance(productId, dayjs(day).add(1, 'day').toDate());
  }));
  const totalBalance = balances.reduce((acc, balance) => new Decimal(acc).add(balance.balance).toNumber(), 0);
  return new Decimal(totalBalance).div(days.length).toNumber();
};


export const useFloatingBalance = async (productId: string) => {
  const product = await getLoanProductById(productId);
  return product.type === ELoanTypes.REVOLVER;
};

export const getLoanProducts = async (borrowerId: string) => {
  return LoanProduct.aggregate<ILoanProductView>([
    {
      $match: {
        'borrowerId': new mongoose.Types.ObjectId(borrowerId),
      },
    }, {
      $sort: {
        'order': 1,
      },
    }, {
      $unset: fieldsToUnset,
    }, {
      $addFields: {
        valid: true,
      },
    },
  ]);
}
