import dayjs from 'dayjs';
import Decimal from 'decimal.js';

import { IEmail } from '../interfaces/email.interface';
import { BorrowerModel } from '../models/Borrower.model';
import { createMicroserviceTasks } from '../db/microservice-tasks.db';
import { EMicroserviceTask } from '../enums/microservice-task.enum';
import { BorrowerCompliance, IComplianceBorrowerDoc } from '../models/BorrowerCompliance.model';

import FinancialComplianceBorrower, {
  IFinancialComplianceBorrowerDocument,
  IFinancialComplianceBorrowerDocumentWithEmailingStatus,
  IFinancialComplianceBorrowerDocumentWithMails,
  IFinancialComplianceBorrowerSettings,
} from '../models/FinancialComplianceBorrower.model';
import FinancialCompliance, {
  IFinancialComplianceSettings,
  IFinancialComplianceSettingsDocument,
} from '../models/FinancialCompliance.model';
import { EmailWithFinancial, NodemailerService } from './nodemailer.service';
import { GlobalsRepository } from '../repositories/globals.repository';
import { GlobalsService } from './globals.service';
import { LoanStatementService } from './loan-statement.service';
import { OrganizationsService } from './organizations.service';

export class FinancialComplianceService {

  constructor(
    private readonly globalsRepository: GlobalsRepository,
    private readonly globalsService: GlobalsService,
    private readonly getLoanStatementService: () => LoanStatementService,
    private readonly getNodemailerService: () => NodemailerService,
    private readonly organizationsService: OrganizationsService,
  ) {
  }

  async getFinancialComplianceSettings(): Promise<IFinancialComplianceSettingsDocument> {
    return await FinancialCompliance.findOne({}) ?? await new FinancialCompliance({}).save();
  }

  async updateSettings(newSettings: IFinancialComplianceSettings) {
    try {
      return await FinancialCompliance.findOneAndUpdate({}, { ...newSettings }, { new: true });
    } catch (e) {
      console.error({ e });
      return null;
    }
  }

  async refreshFinancial(): Promise<unknown> {
    try {
      const newSettings = {
        invoiceSentDate: null,
        dueDate: dayjs().date(7).startOf('day').toDate(),
        stopFundingDate: dayjs().date(10).startOf('day').toDate(),
      };
      await this.updateSettings(newSettings);

      const newBorrowerSettings: IFinancialComplianceBorrowerSettings = {
        invoiceSentDate: null,
        dueDate: null,
        stopFundingDate: null,
        lastReminderSentAt: null,
        accountClearedDate: null,
        amountOwed: 0,
        addToLoan: null,
        willPayIn: null,
        transferDone: null,
        amountReceived: 0,
        balanceOutstanding: 0,
        reminderCounter: 0,
      };
      const borrowers = await this.getAllFinancialComplianceBorrowers();
      await Promise.all(borrowers.map(async (borrower) => {
        await this.updateBorrower(String(borrower._id), newBorrowerSettings);
      }));
      return { message: 'refreshed' };
    } catch (e) {
      console.error(e);
      return { message: 'error', e };
    }
  }

  async refillFinancial(): Promise<null> {
    const borrowers = await FinancialComplianceBorrower.find().lean();
    for (const borrower of borrowers) {
      const loanStatementService = this.getLoanStatementService();
      const complianceData = await loanStatementService.getBorrowerComplianceData(borrower.borrower.toString(), new Date());
      if (complianceData.total > 0) {
        const foundBorrower = await FinancialComplianceBorrower.findById(borrower._id).lean();
        if (foundBorrower) {
          await FinancialComplianceBorrower.findByIdAndUpdate(borrower._id, {
            amountOwed: complianceData.total,
            dueDate: complianceData.dueDate,
            amountReceived: complianceData.totalPaid,
            balanceOutstanding: new Decimal(complianceData.total).sub(complianceData.totalPaid).toNumber(),
          });
        }
      }
    }
    return null;
  }

  async sendFinancialReminders(ids: string = null) {
    const reminderBorrowers = await this.sendExternalReminder(ids, true);
    setTimeout(async () => {
      await this.sendInternalReminders(reminderBorrowers.remindedBorrowers, reminderBorrowers.stopFundingBorrowers);
    }, 60_000);
  }

  async sendExternalReminder(ids: string = null, ignoreRules: boolean): Promise<any> {
    const financialSettings: IFinancialComplianceSettingsDocument = await FinancialCompliance.findOne({});
    const financeBorrowers = ids
      ? await FinancialComplianceBorrower.find({ _id: { $in: ids } }).populate('borrower') as unknown as IFinancialComplianceBorrowerDocumentWithMails[]
      : await FinancialComplianceBorrower.find({}).populate('borrower') as unknown as IFinancialComplianceBorrowerDocumentWithMails[];
    const stopFundingBorrowers = [];
    const remindedBorrowers = [];
    await Promise.all(financeBorrowers.map(async (b) => {
      const nodemailerService = this.getNodemailerService();
      const fullBorrower = b.toObject();
      const organization = await this.organizationsService.getOrganizationForBorrower(fullBorrower.borrower._id.toString());
      fullBorrower.invoiceSentDate = fullBorrower.invoiceSentDate ?? financialSettings.invoiceSentDate;
      fullBorrower.stopFundingDate = fullBorrower.stopFundingDate ?? financialSettings.stopFundingDate;
      fullBorrower.dueDate = fullBorrower.dueDate ?? financialSettings.dueDate;
      const foundBorrower: IComplianceBorrowerDoc = await BorrowerCompliance.findOne({ borrower: fullBorrower.borrower._id });
      if (!foundBorrower.isEmailingActive) {
        return;
      }
      fullBorrower.mainEmails = foundBorrower.mainEmails ?? [];
      fullBorrower.financialEmails = foundBorrower.financialEmails ?? [];
      const currentDate = dayjs();
      const stopFundingDate = dayjs(fullBorrower.stopFundingDate);
      const invoiceSentDate = dayjs(fullBorrower.invoiceSentDate);
      const isTodayStopFundingDate = currentDate.isSame(stopFundingDate, 'day');

      if (fullBorrower.balanceOutstanding > 0 && isTodayStopFundingDate && !b.transferDone) {
        const email: IEmail = {
          text: null,
          subject: `${organization.name.toUpperCase()}: ${b.borrower.code} UNPAID Statement ${new Intl.NumberFormat('us-US').format(fullBorrower.balanceOutstanding)} – ${dayjs().subtract(1, 'month').format('MMMM')}`,
          addresses: fullBorrower.mainEmails.map((email) => email.email),
          customData: fullBorrower,
        };
        await nodemailerService.sendFinancialExternal(email, organization._id.toString(), true);
        stopFundingBorrowers.push(fullBorrower);
      }

      if (!isTodayStopFundingDate && (ignoreRules || (currentDate.diff(invoiceSentDate, 'day') >= 3 && currentDate.diff(stopFundingDate, 'day') < 0))) {
        const email: IEmail = {
          text: null,
          subject: `${organization.name.toUpperCase()}: ${b.borrower.code} Statement ${new Intl.NumberFormat('us-US').format(fullBorrower.balanceOutstanding)} – ${dayjs().subtract(1, 'month').format('MMMM')}`,
          addresses: [
            ...fullBorrower.financialEmails.map((email) => email.email),
            ...fullBorrower.mainEmails.map((email) => email.email),
          ],
          customData: fullBorrower,
        };
        await nodemailerService.sendFinancialExternal(email, organization._id.toString());
      }

      if (
        (currentDate.diff(invoiceSentDate, 'day') > 0 && currentDate.diff(stopFundingDate, 'day') < 0)
        || fullBorrower.addToLoan
      ) {
        remindedBorrowers.push(fullBorrower);
      }
    }));

    return { remindedBorrowers, stopFundingBorrowers };
  }

  async sendInternalReminders(remindedBorrowers, stopFundingBorrowers) {
    try {
      const financialSettings: IFinancialComplianceSettingsDocument = await FinancialCompliance.findOne({});

      const remindedBorrowersUpdated = await Promise.all(remindedBorrowers.map(async (b) => {
        const updatedBorrower = await FinancialComplianceBorrower.findOne({ _id: b._id });
        b.lastReminderSentAt = updatedBorrower.lastReminderSentAt;
        b.reminderCounter = updatedBorrower.reminderCounter;
        return b;
      }));

      const stopFundingBorrowersUpdated = await Promise.all(stopFundingBorrowers.map(async (b) => {
        const updatedBorrower = await FinancialComplianceBorrower.findOne({ _id: b._id });
        b.lastReminderSentAt = updatedBorrower.lastReminderSentAt;
        b.reminderCounter = updatedBorrower.reminderCounter;
        return b;
      }));

      const nodemailerService = this.getNodemailerService();

      if (remindedBorrowersUpdated.length) {
        const financialEmails = financialSettings.financialEmails
          .reduce((acc, email) => email.isActive ? [...acc, email.email] : acc, []);
        const email: EmailWithFinancial = {
          text: null,
          subject: 'UNPAID INTEREST STATEMENTS',
          addresses: financialEmails,
          borrowers: remindedBorrowersUpdated,
        };
        await nodemailerService.sendFinancialInternal(email);
      }

      if (stopFundingBorrowersUpdated.length) {
        const managementAddresses = financialSettings.managementEmails
          .reduce((acc, email) => email.isActive ? [...acc, email.email] : acc, []);
        const email: EmailWithFinancial = {
          text: null,
          subject: 'UNPAID INTEREST STATEMENTS – POTENTIAL STOP FUNDING',
          addresses: managementAddresses,
          borrowers: stopFundingBorrowersUpdated,
        };
        await nodemailerService.sendFinancialInternal(email, true);
      }
    } catch (e) {
      console.error({ e });
    }
  }

  async createEmptyBorrower(borrowerId: string): Promise<void> {
    const foundBorrower = await FinancialComplianceBorrower.findOne({ borrower: borrowerId });
    if (!foundBorrower) {
      await new FinancialComplianceBorrower({ borrower: borrowerId }).save();
    }
  }

  async removeBorrower(borrowerId: string): Promise<void> {
    await FinancialComplianceBorrower.findByIdAndDelete(borrowerId);
  }

  async getAllFinancialComplianceBorrowers(): Promise<IFinancialComplianceBorrowerDocumentWithEmailingStatus[]> {
    const borrowers: IFinancialComplianceBorrowerDocument[] = await FinancialComplianceBorrower
      .find({})
      .populate('borrower')
      .lean();
    const borrowersWithEmailingStatus: IFinancialComplianceBorrowerDocumentWithEmailingStatus[] = await Promise.all(borrowers.map(async (borrower) => {
      if (!borrower.borrower.active) {
        return;
      }
      const foundBorrower: IComplianceBorrowerDoc = await BorrowerCompliance.findOne({ borrower: borrower.borrower._id });
      return {
        ...borrower,
        isEmailingActive: foundBorrower?.isEmailingActive ?? true,
        isDailyTransactionsEmailingActive: foundBorrower?.isDailyTransactionsEmailingActive ?? true,
      } as IFinancialComplianceBorrowerDocumentWithEmailingStatus;
    }));
    return borrowersWithEmailingStatus
      .filter((borrower) => !!borrower)
      .sort((a, b) => a.borrower.code > b.borrower.code ? 1 : -1);
  }

  async updateBorrower(id: string, updatedBorrower: IFinancialComplianceBorrowerSettings): Promise<IFinancialComplianceBorrowerDocument> {
    try {
      updatedBorrower.amountOwed = new Decimal(updatedBorrower.amountOwed).toDP().toNumber();
      updatedBorrower.amountReceived = new Decimal(updatedBorrower.amountReceived).toDP().toNumber();
      updatedBorrower.balanceOutstanding = Math.max(0, new Decimal(updatedBorrower.amountOwed).sub(updatedBorrower.amountReceived).toNumber());
      if ((updatedBorrower.amountOwed === updatedBorrower.amountReceived) && (updatedBorrower.amountOwed > 0) && !updatedBorrower.accountClearedDate) {
        updatedBorrower.accountClearedDate = new Date();
      }
      return await FinancialComplianceBorrower.findByIdAndUpdate(id, { ...updatedBorrower }, { new: true });
    } catch (e) {
      console.error({ e });
      return null;
    }
  }

  async updateLastReminder(financialBorrowerId: string) {
    try {
      const borrower = await FinancialComplianceBorrower.findById(financialBorrowerId);
      await FinancialComplianceBorrower.findByIdAndUpdate(financialBorrowerId, {
        lastReminderSentAt: new Date(),
        reminderCounter: borrower.reminderCounter + 1,
      });
    } catch (e) {
      console.error({ e });
    }
  }

  async sendPlaidReminders() {
    const settings = await this.globalsService.getGlobalSetting('plaidReminded');
    if (settings && settings.plaidReminded) {
      return null;
    }
    const complianceBorrowers = await BorrowerCompliance.find().select('+plaidAccessToken').lean();
    const borrowersWithoutTokes = complianceBorrowers.filter((c) => !c.plaidAccessToken);
    const notifiedBorrowers: string[] = [];
    await Promise.all(borrowersWithoutTokes.map(async (complianceBorrower) => {
      const borrower = await BorrowerModel.findById(complianceBorrower.borrower);
      if (complianceBorrower.isEmailingActive && borrower.active) {
        const mainEmails = complianceBorrower.mainEmails.map((mainEmail) => mainEmail.email.trim());
        const financialEmails = complianceBorrower.financialEmails.map((financialEmail) => financialEmail.email.trim());
        const allEmails = Array.from(new Set([...mainEmails, ...financialEmails]));
        const nodemailerService = this.getNodemailerService();
        await nodemailerService.sendPlaidReminder({
          addresses: allEmails,
          subject: 'Loan Requirement: online read-only bank access',
          text: '',
        });
        notifiedBorrowers.push(borrower.name);
      }
    }));
    await this.globalsRepository.update({ plaidReminded: true });
    await createMicroserviceTasks(EMicroserviceTask.ENABLE_PLAID_NOTIFICATION, {}, dayjs(new Date()).add(1, 'day').toDate());
    return notifiedBorrowers.sort();
  }

  async getFinancialComplianceBorrower(borrowerId: string) {
    return FinancialComplianceBorrower.findOne({ borrower: borrowerId }).lean();
  }
}
