import _ from 'lodash';
import mongoose from 'mongoose';
import dayjs from 'dayjs';

import { IStatementPeriod, LoanStatementTransactionModel } from '../models/LoanStatementTransaction.model';
import { LoanTransaction } from '../models/LoanTransaction.model';
import { getLoanProductById, getLoanProducts } from './loan-products.db';
import { LoanProduct } from '../models/LoanProducts.model';
import { BorrowerModel } from '../models/Borrower.model';
import { getLastTransactionForDate } from './loan-transactions.db';
import { getLedger } from './reports.db';
import Decimal from 'decimal.js';

export enum ELedgerReportType {
  SHORT,
  DETAILED,
}

export const ledgerHeadersMap = {
  [ELedgerReportType.SHORT]: {
    borrowerCode: 'borrower code',
    type: 'type',
    chargeCode: 'BS code',
    PLCode: 'P&L code',
    productCode: 'product code',
    productName: 'product name',
    info: 'memo/reference',
    title: 'title',
    amount: 'amount',
    statementAmount: 'Statement amount',
  },
  [ELedgerReportType.DETAILED]: {
    borrowerCode: 'borrower code',
    date: 'date',
    type: 'type',
    chargeCode: 'BS code',
    PLCode: 'P&L code',
    productCode: 'product code',
    productName: 'product name',
    info: 'memo/reference',
    amount: 'amount',
    statementAmount: 'Statement amount',
    title: 'title',
    balance: 'balance',
    floatedBalance: 'floated balance',
    paymentDate: 'payment date',
  },
};

export const getTotalTransactionAmountForCharge = async (chargeId: string, start: Date, end: Date) => {
  const results = await LoanStatementTransactionModel.aggregate([
    {
      '$match': {
        $and: [
          { 'date': { $gte: start } },
          { 'date': { $lte: end } },
          { 'chargeId': new mongoose.Types.ObjectId(chargeId) },
        ],
      },
    }, {
      '$group': {
        '_id': '$chargeId',
        'totalAmount': { '$sum': '$amount' },
      },
    }, {
      '$project': {
        '_id': 0,
        'totalAmount': { $round: ['$totalAmount', 2] },
      },
    },
  ]);
  if (results.length === 0) {
    return 0;
  } else {
    return results[0].totalAmount;
  }
};

export const getTotalForDate = async (productId: string, date: Date): Promise<number> => {
  const totalAmount = await LoanTransaction.aggregate<{ _id: Date, totalAmount: number }>([
    {
      $match: {
        'productId': new mongoose.Types.ObjectId(productId),
        'effectiveDate': date,
        $expr: { $ne: ['$date', '$effectiveDate'] },
      },
    }, {
      $group: {
        '_id': '$effectiveDate',
        'totalAmount': { $sum: '$amount' },
      },
    },
  ]);
  if (totalAmount.length === 0) {
    return 0;
  }
  return totalAmount[0].totalAmount;
};

export const getStatementBorrowersTransactions = async (start: string, end: string, selectedPeriod: string, selectedBorrowerIds: string[], ledgerType: ELedgerReportType) => {

  const header = ledgerHeadersMap[ledgerType];

  if (!selectedBorrowerIds) {
    return [{ transactions: [header] }];
  }

  const periodFull = selectedPeriod === 'ENTIRE_LOAN'
    ? [{
      period: {
        selectedPeriod: selectedPeriod,
        start: dayjs(start).utc().toDate().toISOString(),
        end: dayjs(end).utc().toDate().toISOString(),
      },
    }]
    : await getBorrowerWithPeriods(selectedBorrowerIds[0], {
      selectedStart: new Date(start),
      selectedEnd: new Date(end),
      selectedPeriod,
    });

  const allProductIds = [];
  await Promise.all(selectedBorrowerIds.map(async (borrowerId) => {
    const borrowerProducts = await getLoanProducts(borrowerId);
    allProductIds.push(borrowerProducts.map((product) => product._id.toString()));
  }));

  const showBalances = true;

  const products = await LoanProduct.find({ borrowerId: { $in: selectedBorrowerIds.map((id) => new mongoose.Types.ObjectId(id)) } }).lean();
  const productIds = products.map((product) => product._id.toString());
  const ledgerData = await getLedger({
      productIds,
      startDate: new Date(dayjs(periodFull[0].period.start).utc().toDate().toISOString()),
      endDate: new Date(dayjs(periodFull[0].period.end).utc().toDate().toISOString()),
      reportType: ledgerType,
    }, { showBalances: showBalances, showFloatedBalance: showBalances },
  );
  const transactions = Object.values(ledgerData).reduce((acc, ledgerTransaction) => [...acc, ...ledgerTransaction], []);
  const transactionsWithoutBalance = showBalances
    ? transactions.map((tr) => {
      return {
        ...tr,
        balance: tr.balance ? new Decimal(tr.balance).toDP(2).toNumber() : tr.balance,
        floatedBalance: tr.floatedBalance ? new Decimal(tr.floatedBalance).toDP(2).toNumber() : tr.floatedBalance,
      }
    })
    : _.map(transactions, (obj) => _.omit(obj, ['balance', 'floatedBalance']));
  return [{ transactions: [header, ...transactionsWithoutBalance] }];
};

export const getBorrowerWithPeriods = async (borrowerId: string, period: {
  selectedPeriod: string,
  selectedStart: Date,
  selectedEnd: Date
}, splitPeriods = false) => {
  const borrower = await BorrowerModel.findById(borrowerId).lean();
  let start: dayjs.Dayjs;
  let end: dayjs.Dayjs;
  switch (period.selectedPeriod) {
    case 'SELECTED_PERIOD': {
      start = dayjs(new Date(period.selectedStart)).startOf('day');
      end = dayjs(new Date(period.selectedEnd)).endOf('day');
      break;
    }
    case 'ENTIRE_LOAN': {
      const products = await LoanProduct.find({ borrowerId: borrower._id.toString() }).lean();
      const productPeriods = await Promise.all(products.map(async (product) => {
        const periods = await getPeriods(period.selectedPeriod, product._id.toString());
        const periodStart = periods[0].start.startOf('day');
        const periodEnd = periods[periods.length - 1].end.endOf('day');
        return { periodStart, periodEnd };
      }));
      const starts = productPeriods.map((p) => p.periodStart);
      const ends = productPeriods.map((p) => p.periodEnd);
      start = dayjs.min(starts);
      end = dayjs.max(ends);
      break;
    }
    default: {
      const periods = await getPeriods(period.selectedPeriod, null);
      start = periods[0].start.startOf('day');
      end = periods[periods.length - 1].end.endOf('day');
      break;
    }
  }

  if (splitPeriods) {
    const result = [];
    let currentMonth = start.startOf('month');
    while (currentMonth.isBefore(end) || currentMonth.isSame(end, 'month')) {
      const startOfMonth = currentMonth.startOf('month');
      const endOfMonth = currentMonth.endOf('month');
      result.push({
        _id: borrower._id.toString(),
        code: borrower.code,
        name: borrower.name,
        period: {
          selectedPeriod: period.selectedPeriod,
          start: startOfMonth.utc().toDate().toISOString(),
          end: endOfMonth.utc().toDate().toISOString(),
        },
      });
      currentMonth = currentMonth.add(1, 'month');
    }
    return result;
  } else {
    return [{
      _id: borrower._id.toString(),
      code: borrower.code,
      name: borrower.name,
      period: {
        selectedPeriod: period.selectedPeriod,
        start: start.utc().toDate().toISOString(),
        end: end.utc().toDate().toISOString(),
      },
    }];
  }
};

export const getPeriods = async (selectedPeriod: string, productId: string): Promise<IStatementPeriod[]> => {
  let start: dayjs.Dayjs;
  let end: dayjs.Dayjs;
  switch (selectedPeriod) {
    case 'CURRENT_MONTH': {
      start = dayjs().utcOffset(0).startOf('month');
      end = dayjs().utcOffset(0).endOf('month');
      return [{ start, end }];
    }
    case 'LAST_MONTH': {
      start = dayjs().subtract(1, 'month').utcOffset(0).startOf('month');
      end = dayjs().subtract(1, 'month').utcOffset(0).endOf('month');
      return [{ start, end }];
    }
    case 'ENTIRE_LOAN': {
      const product = await getLoanProductById(productId);
      const lastTransaction = await getLastTransactionForDate(productId);
      start = dayjs(product.startDate).tz('UTC');
      let lastDate = dayjs(new Date()).endOf('month').toDate();
      if (lastTransaction) {
        if (dayjs(lastTransaction.date).tz('UTC').toDate() > dayjs(new Date()).endOf('month').toDate()) {
          lastDate = lastTransaction.date;
        }
      }
      end = dayjs(lastDate);
      const finish = dayjs(lastDate);
      const periods = [];
      do {
        end = start.endOf('month');
        periods.push({ start, end });
        start = end.add(1, 'day').startOf('month').utc();
      } while (finish.toDate() > end.toDate());
      return periods;
    }
  }
  return [];
};
