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

import { ISelectedMonth } from '../helpers/date.helper';

import {
  ILoanStatementStatus,
  LoanStatementStatusModel,
} from '../models/LoanStatementStatus.model';
import { IStatementPeriod } from './loan-statement.service';
import { LoanChargesService } from './loan-charges.service';
import { TermLoanService } from './term-loan.service';
import { EventMap } from '../microservice/event-map';
import { QueueMap } from '../microservice/queue-map';
import { MainQueueService } from './main-queue.service';

export type SelectablePeriod = 'LAST_MONTH' | 'CURRENT_MONTH' | 'ENTIRE_LOAN' | 'SELECTED_PERIOD';

export class LoanStatementStatusService {
  constructor(
    private readonly getLoanChargesService: () => LoanChargesService,
    private readonly termLoanService: TermLoanService,
    private readonly mainQueueService: MainQueueService,
  ) {
  }

  async initStatementStatuses() {
    const allStatuses = await LoanStatementStatusModel.find().lean();
    if (allStatuses.length === 0) {
      const loanChargesService = this.getLoanChargesService();
      const loanProducts = await loanChargesService.getAllLoanProducts();
      const endDate = dayjs().utcOffset(0).startOf('month');
      for (const product of loanProducts) {
        let currentDate = dayjs(product.startDate).utcOffset(0).startOf('month');
        while (currentDate.diff(endDate, 'month') <= 0) {
          const statementDate = this.getStatementDateForDate(currentDate.toDate());
          await this.getCreateStatementStatus(product._id.toString(), statementDate);
          currentDate = currentDate.add(1, 'month');
        }
      }
    }
  }

  getStatementDateFromStringPeriod(period: SelectablePeriod): ISelectedMonth {
    let startOf = null;
    switch (period) {
      case 'CURRENT_MONTH':
        startOf = dayjs().utcOffset(0).startOf('month');
        break;
      case 'LAST_MONTH':
        startOf = dayjs().utcOffset(0).startOf('month').subtract(1, 'second').startOf('month');
        break;
    }
    if (!startOf) {
      return startOf;
    }
    return { year: startOf.year(), month: startOf.month() + 1 };
  }

  getStatementDateForDate(date: Date): ISelectedMonth {
    const startOf = dayjs(date).utcOffset(0).startOf('month');
    return { year: startOf.year(), month: startOf.month() + 1 };
  }

  getStatementDateFromPeriod(period: IStatementPeriod): ISelectedMonth {
    return { year: period.start.year(), month: period.start.month() + 1 };
  }

  getDateFromStatementDate(statementDate: ISelectedMonth): Date {
    return dayjs().utcOffset(0).set('year', statementDate.year).set('month', statementDate.month - 1).startOf('month').toDate();
  }

  getPeriodsFromStatementDate(statementDate: ISelectedMonth): IStatementPeriod[] {
    if (statementDate.month < 1 || statementDate.month > 12) {
      throw new Error('Invalid month number. Month should be between 1 and 12.');
    }

    const startOfMonth = dayjs()
      .set('year', statementDate.year)
      .set('month', statementDate.month - 1)
      .utcOffset(0)
      .startOf('month');
    const endOfMonth = startOfMonth.endOf('month');

    return [{ start: startOfMonth, end: endOfMonth }];
  }

  async toggleStatementLock(productId: string, statementDate: ISelectedMonth) {
    const statementStatus = await this.getCreateStatementStatus(productId, statementDate);
    if (statementStatus) {
      statementStatus.locked = !statementStatus.locked;
      await statementStatus.save();
    }
  }

  async updateStatementStatus(productId: string, statementDate: ISelectedMonth, actual: boolean) {
    const statementStatus = await this.getCreateStatementStatus(productId, statementDate);
    if (statementStatus && !statementStatus.locked) {
      statementStatus.actual = actual;
      if (actual) {
        statementStatus.lastCalculated = new Date();
      }
      await statementStatus.save();
    }
  }

  async getAllStatementsStatus(statementDate: ISelectedMonth) {
    await this.initStatementStatuses();
    const loanChargesService = this.getLoanChargesService();
    const products = await loanChargesService.getAllLoanProducts();
    const results = await Promise.all(products.map(async (product) => {
      const statementsStatus = await LoanStatementStatusModel.findOne({ productId: product._id, ...statementDate }).lean();
      if (statementsStatus) {
        return { ...statementsStatus, product };
      }
      return null;
    }));
    return results.filter((res) => !!res);
  }

  async getStatementStatus(productId: string, date: Date) {
    const statementDate = this.getStatementDateForDate(date);
    return this.getCreateStatementStatus(productId, statementDate);
  }

  async getCreateStatementStatus(productId: string, statementDate: ISelectedMonth) {
    const statementStatus = await LoanStatementStatusModel.findOne({ productId, ...statementDate });
    if (statementStatus) {
      return statementStatus;
    } else {
      const loanStatementStatus: ILoanStatementStatus = {
        productId: new mongoose.Types.ObjectId(productId),
        year: statementDate.year,
        month: statementDate.month,
        actual: true,
        locked: false,
        lastCalculated: null,
      };
      const newStatementStatus = new LoanStatementStatusModel(loanStatementStatus);
      await newStatementStatus.save();
      return newStatementStatus;
    }
  }

  async validateDate(productId: string, date: Date) {
    try {
      const statementsStatus = await this.getStatementStatus(productId, date);
      if (statementsStatus && statementsStatus.locked) {
        return 'Statement is locked!';
      }
      return null;
    } catch (error) {
      console.error(error);
      return 'Internal Server Error';
    }
  }

  async getBorrowerStatement(borrowerId: string): Promise<{ [K in SelectablePeriod | 'TERM_LOAN']?: boolean }> {
    try {
      const lastStatementDate = this.getStatementDateFromStringPeriod('LAST_MONTH');
      const currentStatementDate = this.getStatementDateFromStringPeriod('CURRENT_MONTH');
      const lastStatus = await this.getBorrowerMonthStatus(borrowerId, lastStatementDate);
      const currentStatus = await this.getBorrowerMonthStatus(borrowerId, currentStatementDate);
      const termLoanStatus = await this.termLoanService.isBorrowerTermLoanActual(borrowerId);
      return { 'LAST_MONTH': lastStatus, 'CURRENT_MONTH': currentStatus, 'TERM_LOAN': termLoanStatus };
    } catch (e) {
      console.error(e);
      throw e;
    }
  }

  async getBorrowerMonthStatus(borrowerId: string, statementDate: ISelectedMonth) {
    const loanChargesService = this.getLoanChargesService();
    const products = await loanChargesService.getLoanProducts(borrowerId);
    const lastResults = await Promise.all(products.map(async (product) => {
      return await this.getCreateStatementStatus(product._id.toString(), statementDate);
    }));
    return lastResults.reduce((acc, res) => res.actual && acc, true);
  }

  async recalculateStatementQueued(productMap: Record<string, string>) {
    this.mainQueueService.sendTaskToQueue({
      data: { productMap },
      pattern: EventMap.recalc.STATEMENT_STATUS,
    }, QueueMap.main).catch(console.error);
  }
}
