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

import { convertSelectedMonthToDate, getStartOfMonthsBetweenDates, ISelectedMonth } from '../helpers/date.helper';
import {
  ETotalType,
  IYieldParams,
  IYieldViewData,
  YIELD_TOTALS_MAP,
  YieldData,
  yieldParamsValidationSchema,
} from '../models/Yield.model';
import { ILoanChargeDoc, LoanCharge } from '../models/LoanCharges.model';
import { ILoanProductDoc, LoanProduct } from '../models/LoanProducts.model';
import { createMicroserviceTasks } from '../db/microservice-tasks.db';
import { EMicroserviceTask } from '../enums/microservice-task.enum';

export class YieldService {

  private async getCalculatedYieldData(charge: ILoanChargeDoc, selectedMonth: ISelectedMonth) {
    const oldData = await YieldData.findOne({
      chargeId: charge._id,
      productId: charge.productId,
      year: selectedMonth.year,
      month: selectedMonth.month,
    }).lean();
    if (oldData) {
      return oldData.valuePercent;
    }
    return null;
  }

  async getCalculatedYieldTotals(productId: string, selectedMonth: ISelectedMonth) {
    return YieldData.find({
      chargeId: null,
      productId: productId,
      year: selectedMonth.year,
      month: selectedMonth.month,
    }).lean();
  }

  async getCalculatedYieldTotalsForPeriod(productIds: string[], periodStart: ISelectedMonth, periodEnd: ISelectedMonth) {
    return YieldData.find({
      productId: { $in: productIds.map((productId) => new mongoose.Types.ObjectId(productId)) },
      $or: [
        {
          year: periodStart.year,
          month: { $gte: periodStart.month },
        },
        {
          year: periodEnd.year,
          month: { $lte: periodEnd.month },
        },
        {
          year: { $gt: periodStart.year, $lt: periodEnd.year },
        },
      ],
    }).sort({
      year: 1,
      month: 1,
    }).lean();
  }

  async getYieldData(params: IYieldParams): Promise<{ data: IYieldViewData[], dateColumns: string[] }> {
    const validationRes = yieldParamsValidationSchema.validate(params);
    if (validationRes.error) {
      console.error(validationRes.error);
      return null;
    }

    if (params.ignoreOldData) {
      return await this.getEmptyResult(params);
    }

    const startDate = convertSelectedMonthToDate(params.periodStart);
    const endDate = convertSelectedMonthToDate(params.periodEnd);
    const charges = await LoanCharge.find({ includeInYield: true }).lean();

    const productIDs = charges.reduce((acc, charge) => [...acc, charge.productId.toString()], <string[]>[]);

    const products = await LoanProduct
      .find({ _id: { $in: Object.values(productIDs).map((id) => new mongoose.Types.ObjectId(id)) } })
      .populate('borrowerId')
      .lean();

    const productMap = products
      .reduce((acc, product) => {
        return {
          ...acc,
          [product._id.toString()]: {
            product,
            charges: charges.filter((charge) => charge.productId.toString() == product._id.toString()),
          },
        };
      }, <{ [productId: string]: { product: ILoanProductDoc, charges: ILoanChargeDoc[] } }>{});

    const allDates = getStartOfMonthsBetweenDates(startDate, endDate);

    const allResults: IYieldViewData[] = [];

    let hasAllData = true;

    await Promise.all(Object.keys(productMap).map(async (productId) => {

      await Promise.all(productMap[productId].charges.map(async (charge) => {

        const monthlyData = {
          charge,
          product: productMap[charge.productId.toString()].product,
          order: 0,
          title: productMap[charge.productId.toString()].product.name,
        };

        await Promise.all(Object.entries(allDates).map(async ([dateView, date]) => {
          if (!hasAllData) {
            return;
          }
          const yieldDataValuePercent = await this.getCalculatedYieldData(charge, date);
          if (!yieldDataValuePercent) {
            console.error(`Yield data not found for product ${productMap[productId].product.name} for charge ${charge.name} and date ${dateView}`);
            hasAllData = false;
          } else {
            monthlyData[`valuePercent-${dateView}`] = yieldDataValuePercent;
          }
        }));

        allResults.push({ ...monthlyData });

      }));

    }));

    if (!hasAllData) {
      // return await this.getEmptyResult(params); // TODO return it
    }

    await Promise.all(products.map(async (product) => {

      const productTotalList = [
        ETotalType.GROSS,
        ETotalType.BROKER,
        ETotalType.NET,
        ETotalType.AVERAGE_BALANCE,
        ETotalType.AVERAGE_LIFE_BALANCE,
        ETotalType.APR_GROSS,
        ETotalType.APR_LIFE_GROSS,
      ];

      const productTotal = productTotalList.reduce((acc, p) => {
        return {
          ...acc,
          [p]: {
            charge: null,
            product,
            order: YIELD_TOTALS_MAP[p].order,
            title: `${product.name} (${YIELD_TOTALS_MAP[p].title})`,
            class: YIELD_TOTALS_MAP[p].class,
          },
        };
      }, {});

      await Promise.all(Object.entries(allDates).map(async ([dateView, dateValue]) => {
        const productTotals = await this.getCalculatedYieldTotals(product._id.toString(), dateValue);
        Object.keys(productTotal).forEach((totalType) => {
          const foundType = productTotals.find((total) => total.totalType === totalType);
          if (foundType) {
            productTotal[totalType][`value-${dateView}`] = foundType.value;
            productTotal[totalType][`valuePercent-${dateView}`] = foundType.valuePercent;
          }
        });
      }));
      Object.values(productTotal).forEach((values: IYieldViewData) => {
        allResults.push(values);
      });
    }));

    const sortedData = _.orderBy(allResults, ['product.borrowerId.name', 'product.order', 'order', 'charge.name'], ['asc', 'asc', 'asc', 'asc']);
    return { data: sortedData, dateColumns: Object.keys(allDates) };
  }

  async createYieldTask(params: IYieldParams) {
    await createMicroserviceTasks(EMicroserviceTask.YIELD, params);
  }

  private async getEmptyResult(params: IYieldParams) {
    await this.createYieldTask(params);
    return { data: [], dateColumns: [] };
  }
}
