import mongoose from 'mongoose';

import {
  BorrowerSettingsModel,
  IBorrowerSettings,
  IBorrowerSettingsDocPopulated,
} from '../models/BorrowerSettings.model';
import { BorrowerModel, IBorrowerDoc } from '../models/Borrower.model';
import {
  BorrowerDataModel,
  BorrowerDataOptionModel,
  IBorrowerDataDocWithName,
} from '../models/BorrowerData.model';
import { BorrowerDataContactModel } from '../models/BorrowerDataContact.model';
import { BorrowerDataGuaranteeModel } from '../models/BorrowerDataGuarantee.model';
import { BorrowerDataLocationModel } from '../models/BorrowerDataLocation.model';
import { BorrowerDataBankModel } from '../models/BorrowerDataBank.model';
import { BorrowerDataInsuranceModel } from '../models/BorrowerDataInsurance.model';
import { BorrowerDataReceivableModel } from '../models/BorrowerDataReceivable.model';
import { BorrowerDataInventoryModel } from '../models/BorrowerDataInventory.model';
import { BorrowerDataTermModel } from '../models/BorrowerDataTerm.model';
import { BorrowerDataBootCollateralModel } from '../models/BorrowerDataBootCollateral.model';
import { CollateralsService } from './collaterals.service';
import { LoanStatementStatusService } from './loan-statement-status.service';
import { LoanTransactionsService } from './loan-transactions.service';
import { SignsService } from './signs.service';
import { IUser, IUserAccess } from '../interfaces/auth-user.interface';
import { ECollaterals } from '../enums/collaterals.enum';

export const borrowerDataModels: {
  [modelName: string]: mongoose.Model<any>
} = {
  receivables: BorrowerDataReceivableModel,
  inventories: BorrowerDataInventoryModel,
  terms: BorrowerDataTermModel,
  bootCollaterals: BorrowerDataBootCollateralModel,
  contacts: BorrowerDataContactModel,
  guarantees: BorrowerDataGuaranteeModel,
  locations: BorrowerDataLocationModel,
  banks: BorrowerDataBankModel,
  insurance: BorrowerDataInsuranceModel,
};

export class BorrowerService {

  constructor(
    private readonly getCollateralsService: () => CollateralsService,
    private readonly getLoanStatementStatusService: () => LoanStatementStatusService,
    private readonly getLoanTransactionsService: () => LoanTransactionsService,
    private readonly getSignsService: () => SignsService,
  ) {
  }


  isBorrowerAllowed(userAccess: IUserAccess, requestedBorrowerId: string): boolean {
    if (!userAccess) {
      return false;
    }
    if (userAccess.allBorrowers) {
      return true;
    }
    return userAccess.borrowersAccess?.includes(requestedBorrowerId);
  }

  checkBorrower(user: IUser, requestedBorrowerId: string): boolean {
    const allowedBorrower = this.isBorrowerAllowed(user, requestedBorrowerId);
    if (!allowedBorrower) {
      throw new Error('Non allowed borrower');
    }
    return true;
  }

  async getBorrowerById(borrowerId: string) {
    return BorrowerModel.findById(borrowerId).lean();
  }

  async isNameCodeAllowed(borrower: IBorrowerDoc) {
    const existingUserName = await BorrowerModel.findOne({ name: borrower.name });
    const existingUserCode = await BorrowerModel.findOne({ code: borrower.code });
    if (existingUserName && existingUserName.id !== borrower._id) {
      return { message: 'There already exists a borrower with this name' };
    }
    if (existingUserCode && existingUserCode.id !== borrower._id) {
      return { message: 'There already exists a borrower with this code' };
    }
    return null;
  }

  private async getDefaultSettings(borrowerId: string) {
    const options = await BorrowerDataOptionModel.find().lean();
    const dataOptions = options.reduce((acc, option) => {
      return [
        ...acc,
        { dataOption: option._id, dataValue: option.type === 'boolean' ? false : '' },
      ];
    }, []);
    const defaultSettings: IBorrowerSettings = {
      borrower: new mongoose.Types.ObjectId(borrowerId),
      queries: {
        [ECollaterals.INVENTORY]: {},
        [ECollaterals.RECEIVABLE]: {},
        [ECollaterals.OTHER]: {},
        [ECollaterals.CASH]: {},
        [ECollaterals.SALES]: {},
      },
      dataTypes: {
        upload: [],
      },
      data: {
        options: dataOptions,
        receivables: [],
        inventories: [],
        contacts: [],
        guarantees: [],
        banks: [],
        insurance: [],
        locations: [],
        terms: [],
        bootCollaterals: [],
      },
      dataAvailability: {
        receivables: false,
        inventories: false,
      },
    };
    return defaultSettings;
  }

  private async addOptionNames(unmappedSettings: IBorrowerSettingsDocPopulated) {
    const borrowerDataOptions = await BorrowerDataOptionModel.find().lean();
    const borrowerDataOptionsMap = new Map();
    borrowerDataOptions.forEach((option) => borrowerDataOptionsMap.set(option._id.toString(), option.name));
    unmappedSettings.data.options = unmappedSettings.data.options.map((item) => {
      const dataName: string = borrowerDataOptionsMap.get(item.dataOption.toString());
      return { ...item, dataName } as unknown as IBorrowerDataDocWithName;
    });
    return unmappedSettings;
  }

  async getBorrowerOption(borrowerId: string, optionName: string): Promise<IBorrowerDataDocWithName> {
    const borrowerSettings = await this.getBorrowerSettings(borrowerId);
    if (!borrowerSettings) {
      return null;
    }
    const foundOption = borrowerSettings.data.options.find((option) => option.dataName === optionName);
    if (!foundOption) {
      return null;
    }
    return foundOption;
  }

  async getBorrowerSettings(borrowerId: string) {
    const borrowerSettings = await BorrowerSettingsModel
      .findOne({ borrower: borrowerId })
      .populate('data.options')
      .populate('data.receivables')
      .populate('data.inventories')
      .populate('data.terms')
      .populate('data.bootCollaterals')
      .populate('data.contacts')
      .populate('data.guarantees')
      .populate('data.locations')
      .populate('data.banks')
      .populate('data.insurance')
      .lean() as unknown as IBorrowerSettingsDocPopulated;

    const options = await BorrowerDataOptionModel.find({ isActive: true }).lean();
    const optionsMap = new Map(options.map((opt) => [opt._id.toString(), opt]));
    if (borrowerSettings?.data?.options) {
      borrowerSettings.data.options = borrowerSettings.data.options
        .filter((item) => optionsMap.has(item.dataOption.toString()))
        .sort((a, b) => {
          const foundA = optionsMap.get(a.dataOption.toString());
          const foundB = optionsMap.get(b.dataOption.toString());

          if (!foundA || !foundB) return 0;
          return foundA.order - foundB.order;
        });
    }
    const unmappedSettings = borrowerSettings ? borrowerSettings : await this.getDefaultSettings(borrowerId);
    return await this.addOptionNames(unmappedSettings as IBorrowerSettingsDocPopulated);
  }

  async getBorrowerByCode(code: string): Promise<IBorrowerDoc> {
    return BorrowerModel.findOne({ code });
  }

  async getExtraInfo(borrowerId: string) {
    const loanTransactionsService = this.getLoanTransactionsService();
    const collateralsService = this.getCollateralsService();
    const signsService = this.getSignsService();
    const extraInfo = await loanTransactionsService.getLoanBalanceForBorrowerId(borrowerId);
    const { lastBBCDate, lastBBCId } = await collateralsService.getLatestBBCDate(borrowerId);
    const lastSignedBBCDate = await signsService.getLatestSignedBBCDate(borrowerId);
    return {
      extraInfo,
      lastBBCDate,
      lastBBCId,
      lastSignedBBCDate: lastSignedBBCDate?.bbcDate || null,
      lastSignedBBCId: lastSignedBBCDate?._id.toString() || null,
    };
  }

  async updateBorrowerData(borrowerSettings: IBorrowerSettingsDocPopulated) {
    const results = await Promise.all(Object.entries(borrowerDataModels).map(async ([modelName, model]) => {
      const newIds: mongoose.Types.ObjectId[] = await Promise.all(borrowerSettings.data[modelName].map(async (contact: any) => {
        if (!contact._id) {
          delete contact._id;
          const newContact = new model(contact);
          const savedContact = await newContact.save();
          return savedContact._id;
        }
        await model.findByIdAndUpdate(contact._id, contact);
        return new mongoose.Types.ObjectId(contact._id);
      }));
      return { [modelName]: newIds };
    }));

    return results.reduce((acc, modelCollection) => {
      const collections = Object.entries(modelCollection).reduce((innerAcc, [modelName, ids]) => {
        return { ...innerAcc, [modelName]: ids };
      }, {});
      return { ...acc, ...collections };
    }, {});
  }

  async updateBorrowerSettings(borrowerId: string, borrowerSettings: IBorrowerSettingsDocPopulated) {
    const newOptions = [];
    await Promise.all(borrowerSettings.data.options.map(async (option) => {
      if (option._id) {
        await BorrowerDataModel.findByIdAndUpdate(option._id, { dataValue: option.dataValue });
      } else {
        const newOption = await BorrowerDataModel.create(option);
        await newOption.save();
        newOptions.push(newOption._id);
      }
    }));
    const oldSettings = await BorrowerSettingsModel.findOne({ borrower: borrowerId }).lean();
    const updatedData = await this.updateBorrowerData(borrowerSettings);
    await Promise.all(Object.keys(borrowerDataModels).map(async (modelName) => {
        borrowerSettings.data[modelName] = updatedData[modelName];
        if (oldSettings) {
          const oldIds: string[] = oldSettings.data[modelName].map((id) => id.toString());
          const updatedIds = updatedData[modelName].map((id) => id.toString());
          const idsToRemoved = oldIds.reduce((acc, oldId) => {
            if (updatedIds.find((id) => id === oldId)) {
              return acc;
            }
            return [...acc, oldId];
          }, []);
          await Promise.all(idsToRemoved.map(async (id) => {
            await borrowerDataModels[modelName].findByIdAndDelete(id);
          }));
        }
      }),
    );
    const foundSettings = await BorrowerSettingsModel.findOne({ borrower: borrowerId });
    if (foundSettings) {
      await BorrowerSettingsModel.findOneAndUpdate(
        { borrower: borrowerId },
        { borrower: borrowerId, ...borrowerSettings },
        { upsert: true },
      );
    } else {
      const newSettings = await BorrowerSettingsModel.create(
        { borrower: borrowerId, ...borrowerSettings, data: { ...borrowerSettings.data, options: newOptions } },
      );
      await newSettings.save();
    }
  }

  async getBorrowerCodesMap() {
    const borrowers = await BorrowerModel.find().lean();
    const codesMap = new Map<string, string>();
    borrowers.forEach((borrower) => {
      codesMap.set(borrower.code.toLowerCase(), borrower._id.toString());
    });
    return codesMap;
  }

  async getBorrowerStatementDetails(borrowers: IBorrowerDoc[]) {
    const allBorrowersWithExtraInfo = await Promise.all(borrowers.map(async (borrower) => {
      const borrowerId = borrower._id.toString();
      const loanStatementStatusService = this.getLoanStatementStatusService();
      const statementStatuses = await loanStatementStatusService.getBorrowerStatement(borrowerId);
      return { [borrowerId]: statementStatuses };
    }));
    return allBorrowersWithExtraInfo.reduce((acc, borrower) => {
      const borrowerRes = Object.entries(borrower).reduce((innerAcc, [borrowerId, values]) => ({
        ...innerAcc,
        [borrowerId]: values,
      }), {});
      return { ...acc, ...borrowerRes };
    }, {});
  }

}
