import dayjs, { ManipulateType } from 'dayjs';
import _ from 'lodash';
import mongoose from 'mongoose';

import {
  IBorrowerItemLean,
  IBorrowerItemInstance,
  IComplianceFile,
  IEmailRecipient, IBorrowerItemInstanceLean,
} from '../models/ComplianceItem.model';
import { IGroupedEmailsUpdate } from '../interfaces/email-addresses.interface';
import { EItemProgressStatus } from '../enums/item-progress-status.enum';
import {
  BorrowerCompliance,
  IComplianceBorrowerWithBorrower,
  IComplianceBorrowerDocumentFull,
  IComplianceBorrowerWithPlaidToken,
} from '../models/BorrowerCompliance.model';
import { BorrowerModel } from '../models/Borrower.model';
import { IUserAccess } from '../interfaces/auth-user.interface';
import FinancialComplianceBorrower from '../models/FinancialComplianceBorrower.model';
import { FileManagerService } from './file-manager.service';
import { UploadsService } from './uploads.service';

type FrequencyType = { [key: string]: { value: number, unit: ManipulateType } };

const frequencies: FrequencyType = {
  ONCE: { value: 1, unit: 'day' },
  MONTHLY: { value: 1, unit: 'month' },
  ANNUAL: { value: 1, unit: 'year' },
  WEEKLY: { value: 1, unit: 'week' },
  QUARTERLY: { value: 3, unit: 'month' },
  TWICE_MONTHLY: { value: 15, unit: 'day' },
  END_OF_MONTH: { value: 15, unit: 'day' },
};

const dueDifference = (date) => {
  const currentDate = dayjs().startOf('day');
  const nextDate = dayjs(new Date(date)).startOf('day');
  return currentDate.diff(nextDate, 'day');
};

export interface IInstanceStatus {
  progress: {
    text: string;
    status: string;
  };
  score: {
    value: number;
    tooltip: string;
  };
}

export interface IBorrowerItemInstanceWithStatusesLean extends IBorrowerItemInstanceLean, IInstanceStatus {
  itemId: mongoose.Types.ObjectId;
  files: (IComplianceFile & { uploadedByUser: string | null; })[];
}

export interface IBorrowerItemWithStatusesLean extends Omit<IBorrowerItemLean, 'instances'> {
  instances: IBorrowerItemInstanceWithStatusesLean[];
}

export interface IComplianceBorrowerDocumentFullWithStatuses extends Omit<IComplianceBorrowerDocumentFull, 'items'> {
  items: IBorrowerItemWithStatusesLean[];
}

export class ComplianceBorrowersService {

  constructor(
    private readonly _fileManagerService: FileManagerService,
    private readonly _uploadsService: UploadsService,
  ) {
  }

  isComplianceBorrowerAllowed(userAccess: IUserAccess, requestedBorrowerId: string): boolean {
    const allowedBorrower = userAccess.complianceBorrowersAccess.includes(requestedBorrowerId);
    if (!allowedBorrower) {
      throw new Error('Non allowed compliance borrower');
    }
    return true;
  }

  calculateInstanceStatuses(instance: IBorrowerItemInstance, item: IBorrowerItemLean): IInstanceStatus {
    let progress: { text: string, status: string };
    let score: { value: number, tooltip: string } = null;
    if (instance.submittedDate) {
      progress = { text: 'submitted', status: 'SUBMITTED' };
    }
    const diff = dueDifference(instance.nextDate);
    if (diff !== 0) {
      const progressText = `${diff > 0 ? 'overdue' : 'due'} ${Math.abs(diff)} ${Math.abs(diff) > 1 ? 'days' : 'day'}`;
      progress = { text: progressText, status: diff > 0 ? EItemProgressStatus.OVERDUE : EItemProgressStatus.DUE };
    }
    if (diff > 0) {
      score = {
        value: item.item.importanceScore * item.item.multiplier * diff,
        tooltip: `${item.item.importanceScore} (importance) x ${item.item.multiplier} (multiplier) x ${diff} (days)`,
      };
    }
    if (instance.status === 'IN_REVIEW') {
      progress = { text: 'in review', status: EItemProgressStatus.IN_REVIEW };
    }
    if (instance.status === 'ACCEPTED') {
      progress = null;
    }
    return { progress, score };
  }

  async getFullComplianceBorrowerById(complianceBorrowerId: string): Promise<IComplianceBorrowerDocumentFullWithStatuses> {
    if (!complianceBorrowerId) {
      return null;
    }
    const ourBorrower = await BorrowerCompliance
      .findById(complianceBorrowerId)
      .populate('borrower plaidAccessToken')
      .populate({
        path: 'items',
        populate: { path: 'item', model: 'complianceItems' },
      })
      // .populate({
      //   path: 'items',
      //   populate: { path: 'instances.files.uploadedBy', model: MODEL_NAMES.users },
      // }) // TODO remove this when we have a better way to handle users
      .transform((borrower) => ({ ...borrower, plaidAccessToken: borrower?.plaidAccessToken ?? false }))
      .lean<IComplianceBorrowerDocumentFull>();
    if (!ourBorrower.borrower) {
      return Promise.resolve({ ...ourBorrower, items: [] });
    }
    const financeBorrower = await FinancialComplianceBorrower.findOne({ borrower: ourBorrower.borrower?._id });
    if (financeBorrower) {
      const currentDate = dayjs();
      const stopFundingDate = dayjs(financeBorrower.stopFundingDate);
      if (financeBorrower.balanceOutstanding > 0 && currentDate.diff(stopFundingDate) > 0) {
        ourBorrower.fundingStatus = 'NOT_PAID';
      }
    }
    if (!ourBorrower) {
      return Promise.resolve(null);
    }

    const items = { items: [] as IBorrowerItemLean[], ...ourBorrower }.items.map((item) => {
      return {
        ...item,
        instances: item.instances.reduce((acc, instance) => {
          try {
            const statuses = this.calculateInstanceStatuses(instance, item);
            const files = instance.files.map((file) => {
              const uploadedByUser = file.uploadedBy ? file.uploadedBy['firstName'] : null;
              return { ...file, uploadedByUser };
            });
            return [...acc, { ...instance, ...statuses, itemId: item._id, files }];
          } catch (e) {
            console.error({ e });
            return acc;
          }
        }, [] as IBorrowerItemInstanceWithStatusesLean[]),
      };
    });
    return Promise.resolve({ ...ourBorrower, items });
  }

  async getAllBorrowersShortened(userAccess: IUserAccess) {
    type BorrowerComplianceShort = Pick<IComplianceBorrowerWithBorrower, 'borrower' | 'fundingStatus' | 'items'>;

    const allComplianceBorrower = await BorrowerCompliance
      .find(userAccess.allBorrowers ? { isVisible: true } : {
        'borrower': { $in: userAccess.borrowersAccess },
        isVisible: true,
      })
      .populate('borrower')
      .populate({
        path: 'items',
        populate: { path: 'item', model: 'complianceItems' },
      })
      .collation({ locale: 'en' })
      .lean<BorrowerComplianceShort[]>();
    const borrowers = await Promise.all(allComplianceBorrower.map(async (borrower) => {
      if (!borrower.borrower) {
        return borrower;
      }
      if (!borrower.borrower.active) {
        return;
      }
      const financeBorrower = await FinancialComplianceBorrower.findOne({ borrower: borrower.borrower._id });
      if (financeBorrower) {
        const currentDate = dayjs();
        const stopFundingDate = dayjs(financeBorrower.stopFundingDate);
        if (financeBorrower.balanceOutstanding > 0 && currentDate.diff(stopFundingDate) > 0) {
          borrower.fundingStatus = 'NOT_PAID';
        }
      }

      const items = borrower.items.map((item) => {
        return {
          ...item,
          instances: item.instances.reduce((acc, instance) => {
            try {
              const statuses = this.calculateInstanceStatuses(instance, item);
              return [...acc, { ...instance, ...statuses }];
            } catch (e) {
              return acc;
            }
          }, [] as IBorrowerItemInstanceWithStatusesLean[]),
        };
      });

      return { ...borrower, items };
    }));
    return borrowers
      .filter((borrower) => !!borrower)
      .sort((a, b) => a.borrower.code > b.borrower.code ? 1 : -1);
  }

  async generateInstances(complianceBorrowerId) {
    const complianceBorrower = await this.getFullComplianceBorrowerById(complianceBorrowerId);

    const getLatestInstanceDate = (item: IBorrowerItemLean) => {
      if (!item.instances || !item.instances.length) {
        return null;
      }

      return item.instances
        .reduce((a, b) => (new Date(a.nextDate).getTime() > new Date(b.nextDate).getTime() ? a : b))
        .nextDate;
    };

    for (let [index, item] of complianceBorrower.items.entries()) {
      const { frequency, startDate } = item;
      if (frequency && startDate) {

        const currentDatePlusNotification = dayjs(new Date())
          .set('hour', 0)
          .set('minute', 0)
          .set('second', 0)
          .add(item.item.notificationDays, 'day')
          .toDate();

        const emptyInstance = {
          submittedDate: null,
          status: 'NEW',
          files: [],
        };
        let newInstance;
        let newInstances;
        let nextDate;
        switch (frequency) {
          case 'ONCE':
            newInstance = { nextDate: dayjs(item.startDate).toDate(), ...emptyInstance };
            if (!item.instances.length && currentDatePlusNotification >= newInstance.nextDate) {
              await BorrowerCompliance.findByIdAndUpdate(complianceBorrowerId, { $push: { ['items.' + index + '.instances']: newInstance } });
            }
            break;
          case 'END_OF_MONTH':
            newInstances = [];
            do {
              const latestDate = newInstances.length ? newInstances[newInstances.length - 1].nextDate : getLatestInstanceDate(item);
              let nextDateJs = !latestDate
                ? dayjs(item.startDate)
                : dayjs(latestDate)
                  .add(1, 'day');
              nextDate = nextDateJs
                .endOf('month')
                .set('hour', 0)
                .set('minute', 0)
                .set('second', 0).toDate();
              const newInstance = { nextDate, ...emptyInstance };
              if (currentDatePlusNotification >= nextDate) {
                newInstances.push(newInstance);
              }
            } while (currentDatePlusNotification >= nextDate);
            if (newInstances.length) {
              await BorrowerCompliance.findByIdAndUpdate(complianceBorrowerId, { $push: { ['items.' + index + '.instances']: newInstances } });
            }
            break;
          default:
            const { value, unit } = frequencies[frequency];
            newInstances = [];
            do {
              const latestDate = newInstances.length ? newInstances[newInstances.length - 1].nextDate : getLatestInstanceDate(item);
              nextDate = !latestDate
                ? item.startDate
                : dayjs(latestDate)
                  .add(value, unit)
                  .set('hour', 0)
                  .set('minute', 0)
                  .set('second', 0).toDate();
              const newInstance = { nextDate, ...emptyInstance };
              if (currentDatePlusNotification >= nextDate) {
                newInstances.push(newInstance);
              }
            } while (currentDatePlusNotification >= nextDate);
            if (newInstances.length) {
              await BorrowerCompliance.findByIdAndUpdate(complianceBorrowerId, { $push: { ['items.' + index + '.instances']: newInstances } });
            }
            break;
        }
      }
    }
    return await this.getFullComplianceBorrowerById(complianceBorrowerId);
  }

  async calculateAndUpdateFundingStatus(complianceBorrowerId: string) {
    const complianceBorrower = await this.getFullComplianceBorrowerById(complianceBorrowerId);
    const fundingStatus = this.calculateFundingStatus(complianceBorrower);
    await this.updateFundingStatus(complianceBorrowerId, fundingStatus);
  }

  calculateFundingStatus(complianceBorrower: IComplianceBorrowerDocumentFull): string {
    let hasErrors = false;
    let hasWarnings = false;
    complianceBorrower.items.forEach((item) => {
      item.instances.forEach((instance) => {
        if (instance.status !== 'ACCEPTED') {
          const { progress } = this.calculateInstanceStatuses(instance, item);
          switch (progress?.status) {
            case EItemProgressStatus.IN_REVIEW:
              hasWarnings = true;
              break;
            case EItemProgressStatus.OVERDUE:
              hasErrors = true;
              break;
          }
        }
      });
    });
    let fundingStatus = 'COMPLIANT';
    if (hasWarnings) {
      fundingStatus = 'PENDING';
    }
    if (hasErrors) {
      fundingStatus = 'OVERDUE';
    }
    if (complianceBorrower.fundingStatus === 'NOT_PAID') {
      fundingStatus = 'NOT_PAID';
    }
    return fundingStatus;
  }

  async updateComplianceBorrower(complianceBorrower: IComplianceBorrowerDocumentFull) {
    try {
      const { _id } = complianceBorrower;
      const fundingStatus = this.calculateFundingStatus(complianceBorrower);
      return await BorrowerCompliance.findByIdAndUpdate(_id, { ...complianceBorrower, fundingStatus });
    } catch (e) {
      console.error(e);
      return null;
    }
  }

  async updateFundingStatus(complianceBorrowerId: string, fundingStatus: string) {
    try {
      return await BorrowerCompliance
        .findByIdAndUpdate(complianceBorrowerId, { fundingStatus });
    } catch (e) {
      console.error(e);
      return null;
    }
  }

  async updateLastEmailSent(complianceBorrowerId: string, lastEmailSentAt: Date) {
    try {
      return await BorrowerCompliance
        .findByIdAndUpdate(complianceBorrowerId, { lastEmailSentAt });
    } catch (e) {
      console.error(e);
      return null;
    }
  }

  async getAllBorrowersIds(onlyActive = true) {
    try {
      const mainFilter = { isVisible: true };
      const filter = onlyActive ? { 'borrower.active': true, ...mainFilter } : { ...mainFilter };
      const complianceBorrowers = await BorrowerCompliance.aggregate([
        {
          $lookup: {
            from: 'borrowers',
            localField: 'borrower',
            foreignField: '_id',
            as: 'borrower',
          },
        }, {
          $unwind: {
            path: '$borrower',
          },
        }, {
          $match: filter,
        },
      ]);
      return complianceBorrowers.map((complianceBorrower) => complianceBorrower._id.toString());
    } catch (e) {
      console.error(e);
      return null;
    }
  }

  async getBorrowerListByItemId(complianceItemId: string) {
    return BorrowerCompliance
      .find({
        'items': {
          $elemMatch: {
            item: complianceItemId,
          },
        },
      })
      .populate('borrower')
      .lean<IComplianceBorrowerWithBorrower[]>()
      .exec();
  }

  async deleteInstanceById(complianceBorrowerId: string, complianceInstanceId: string) {
    await BorrowerCompliance
      .findOneAndUpdate(
        { _id: complianceBorrowerId },
        { '$pull': { 'items.$[].instances': { _id: complianceInstanceId } } },
      );
  }

  async getBorrowerByInstanceId(complianceInstanceId: string) {
    return BorrowerCompliance
      .findOne({
        'items.instances': {
          $elemMatch: {
            _id: complianceInstanceId,
          },
        },
      });
  }

  async markInstanceFilesAsOld(complianceInstanceId: string) {
    const items = await this._fileManagerService.findItemByInstanceId(complianceInstanceId);
    if (items.length > 0) {
      const itemId = items.pop().items._id;
      return BorrowerCompliance
        .findOneAndUpdate(
          {
            'items.instances': {
              $elemMatch: {
                _id: complianceInstanceId,
              },
            },
          },
          { $set: { 'items.$[item].instances.$[instance].files.$[].isFileNew': false } },
          {
            arrayFilters: [
              { 'item._id': itemId },
              { 'instance._id': new mongoose.Types.ObjectId(complianceInstanceId) },
            ],
          },
        );
    }
  }

  async getBorrowerWithPlaidToken() {
    return BorrowerCompliance
      .find({ plaidAccessToken: { $exists: true } })
      .select('+plaidAccessToken')
      .lean<IComplianceBorrowerWithPlaidToken[]>()
      .exec();
  }

  async getBorrowerEmails(userAccess: IUserAccess) {
    const allComplianceBorrower = await BorrowerCompliance
      .find(userAccess.allBorrowers ? {} : { 'borrower': { $in: userAccess.borrowersAccess } })
      .populate('borrower items.item')
      .collation({ locale: 'en' })
      .sort({ 'borrower.name': 1 })
      .lean<IComplianceBorrowerWithBorrower[]>();
    return allComplianceBorrower.map((b) => {
      const items = b.items.map((item) => {
        return {
          value: item.item?._id.toString() ?? null,
          title: item.item?.name ?? null,
        };
      });
      const uniqEmails = _.uniq((b.items.reduce((acc, item) => [...acc, ...item.emailAddresses], [])).map((email) => email.email));
      const groupedEmails = b.items.reduce((acc, item) => {
        if (item.emailAddresses.length) {
          acc[item.item?.id ?? null] = item.emailAddresses;
        }
        return acc;
      }, {});
      if (!b.borrower) {
        return;
      }
      return {
        borrower: { _id: b._id.toString(), name: b.borrower?.name ?? '', borrowerId: b.borrower?._id.toString() ?? '' },
        email: b.email,
        mainEmails: b.mainEmails ?? [],
        financialEmails: b.financialEmails ?? [],
        isEmailingActive: b.isEmailingActive,
        isDailyTransactionsEmailingActive: b.isDailyTransactionsEmailingActive,
        uniqEmails,
        groupedEmails,
        items,
      };
    });
  }

  async getOneBorrowerEmails(userAccess, borrowerId) {
    const borrower = await BorrowerCompliance
      .findOne({ _id: borrowerId })
      .populate('borrower items.item')
      .collation({ locale: 'en' })
      .sort({ 'borrower.name': 1 })
      .lean<IComplianceBorrowerWithBorrower>();
    const items = borrower.items.map((item) => {
      return {
        value: item.item?._id.toString() ?? null,
        title: item.item?.name ?? null,
      };
    });
    const uniqEmails = _.uniq((borrower.items.reduce((acc, item) => [...acc, ...item.emailAddresses], [])).map((email) => email.email));
    const groupedEmails = borrower.items.reduce((acc, item) => {
      if (item.emailAddresses.length) {
        acc[item.item?._id.toString() ?? null] = item.emailAddresses;
      }
      return acc;
    }, {});
    if (!borrower.borrower) {
      return;
    }
    return {
      borrower: {
        _id: borrower._id.toString(),
        name: borrower.borrower?.name ?? '',
        borrowerId: borrower.borrower?._id.toString() ?? '',
      },
      email: borrower.email,
      mainEmails: borrower.mainEmails ?? [],
      financialEmails: borrower.financialEmails ?? [],
      isEmailingActive: borrower.isEmailingActive,
      isDailyTransactionsEmailingActive: borrower.isDailyTransactionsEmailingActive,
      uniqEmails,
      groupedEmails,
      items,
    };
  }

  async getBorrowerEmailsGrouped(borrowerId: string): Promise<{
    mainEmails: IEmailRecipient[],
    financialEmails: IEmailRecipient[]
  }> {
    const borrower = await BorrowerCompliance.findOne({ borrower: borrowerId });
    if (borrower) {
      return {
        mainEmails: borrower.mainEmails,
        financialEmails: borrower.financialEmails,
      };
    }
  }

  async updateBorrowerEmailAddresses(emailUpdate: IGroupedEmailsUpdate) {
    const filter = { _id: emailUpdate._id };
    const update = {
      $set: {
        isEmailingActive: emailUpdate.isEmailingActive,
        isDailyTransactionsEmailingActive: emailUpdate.isDailyTransactionsEmailingActive,
        mainEmails: emailUpdate.mainEmails,
        financialEmails: emailUpdate.financialEmails,
        'items.$[].emailAddresses': [],
      },
    };
    await BorrowerCompliance.updateOne(filter, update);
    await Promise.all(Object.entries(emailUpdate.groupedEmails).map(async ([id, emails]) => {
      const itemUpdate = {
        $set: {
          'items.$[item].emailAddresses': emails,
        },
      };
      const arrayFilter = {
        arrayFilters: [
          { 'item.item': id },
        ],
      };
      await BorrowerCompliance.updateOne(filter, itemUpdate, arrayFilter);
    }));
    return BorrowerCompliance.findOne(filter);
  }

  async toggleComplianceBorrowerVisibility(borrowerId: string, isVisible: boolean) {
    const foundBorrower = await BorrowerCompliance.findOne({ borrower: borrowerId });
    if (!!foundBorrower) {
      await BorrowerCompliance.findByIdAndUpdate(foundBorrower._id, { isVisible });
    }
  }

  async createComplianceBorrower(borrowerId: string, isVisible = true) {
    const foundBorrower = await BorrowerCompliance.findOne({ borrower: borrowerId });
    if (!!foundBorrower) {
      await BorrowerCompliance.findByIdAndUpdate(foundBorrower._id, { isVisible });
    } else {
      const newComplianceBorrower = new BorrowerCompliance({ borrower: borrowerId, isVisible });
      await newComplianceBorrower.save();
    }
  }

  async removeIncorrectBorrowers() {
    const allComplianceBorrowers = await BorrowerCompliance.find({});
    let n = 0;
    for (const borrower of allComplianceBorrowers) {
      if ((borrower['isDeleted'] === true) || borrower['isDeleted'] === undefined) {
        try {
          await BorrowerCompliance.findByIdAndDelete(borrower._id);
          n += 1;
        } catch (e) {
          console.error(e);
        }
      }
      await BorrowerCompliance.findByIdAndUpdate(borrower._id, { isVisible: true });
    }

    const allBorrowers = await BorrowerModel.find({});
    for (const borrower of allBorrowers) {
      const complianceBorrower = await BorrowerCompliance.findOne({ borrower: borrower._id });
      if (!complianceBorrower) {
        await this.createComplianceBorrower(borrower._id.toString(), false);
      }
    }
  }

  async getBorrowerListReport(user: IUserAccess) {
    const userFilter = user.allBorrowers
      ? { isVisible: true }
      : { 'borrower': { $in: user.borrowersAccess }, isVisible: true };
    const borrowers = await BorrowerCompliance
      .find(userFilter)
      .populate('borrower')
      .populate('items.item')
      .lean<IComplianceBorrowerWithBorrower[]>()
    ;
    const finalList = [
      ['borrower', 'due date', 'item', 'status'],
    ];
    borrowers
      .sort((a, b) => a.borrower.name.localeCompare(b.borrower.name))
      .forEach((borrower) => {
        borrower.items
          .filter((borrowerItem) => !!borrowerItem.item)
          .sort((a, b) => a.item.name.localeCompare(b.item.name))
          .forEach((item) => {
            item.instances.forEach((instance) => {
              if (instance.status !== 'ACCEPTED') {
                finalList.push([
                  borrower.borrower.name.trim(),
                  instance.nextDate.toString(),
                  item.item.name.trim(),
                  instance.status,
                ]);
              }
            });
          });
      });
    return await this._uploadsService.convertDataToFile([{ list: finalList }]);
  }

}
