import Decimal from 'decimal.js';

import {
  IProductBrokerView,
  ProductBrokerModel,
} from '../models/ProductBroker.model';
import { ILoanBrokerEmail, ILoanBrokerView, LoanBrokerModel } from '../models/LoanBroker.model';
import { getProductBrokers } from '../db/brokers.db';

import { getAllLoanBrokers, getAllProductBrokers, getBorrowerBrokers } from '../db/brokers.db';
import { LoanChargesService } from './loan-charges.service';
import { getUUID } from '../helpers/main.helper';

type BrokerView = Omit<IProductBrokerView, '_id'> & { _id: string };
type BrokerTotalView = Omit<IProductBrokerView, 'productId'> & { productId: string };

const defaultBrokerList: BrokerView[] = [
  {
    _id: `new_${getUUID()}`,
    active: true,
    BSCode: '',
    PLCode: '',
    productId: null,
    loanBrokerId: null,
    adminShare: 0,
    interestShare: 0,
    otherShare: 0,
    createdAt: null,
    updatedAt: null,
  },
];

type ShareType = 'adminShare' | 'interestShare' | 'otherShare';

type ShareValue = {
  [_ in ShareType]: number;
}

type ProductTotal = {
  [product_id: string]: ShareValue;
};

export class BrokersService {

  constructor(
    private readonly loanChargesService: LoanChargesService,
  ) {
  }

  async normalizeObjects() {
    try {
      await LoanBrokerModel.updateMany({}, { emails: [] });
    } catch (e) {
      console.error(e);
    }
  }

  async getAllProductBrokers() {
    return getAllProductBrokers();
  }

  async getBorrowerBrokers(borrowerId: string) {
    const allBorrowerBrokers = await getBorrowerBrokers(borrowerId);
    if (!allBorrowerBrokers.length) {
      return defaultBrokerList;
    }
    return allBorrowerBrokers;
  }

  async getProductBrokers(productId: string) {
    return await getProductBrokers(productId);
  }

  async getTotalShares(borrowerId: string, brokers: IProductBrokerView[], replaceNames = false) {
    const products = await this.loanChargesService.getLoanProducts(borrowerId);
    const productShares = products.reduce((acc, product) => {
      const sharesTypes: ShareType[] = ['adminShare', 'interestShare', 'otherShare'];
      const shares = sharesTypes.reduce((acc, share) => ({ ...acc, [share]: 0 }), <ShareValue>{});
      return { ...acc, [product._id.toString()]: shares };
    }, <ProductTotal>{});

    const updateShares = (broker: IProductBrokerView, productId: string, shareFraction = 1) => {
      const sharedKeys = Object.keys(productShares[productId]);
      sharedKeys.forEach((sharedKey) => {
        const productShare = new Decimal(broker[sharedKey]).mul(shareFraction);
        return productShares[productId][sharedKey] = new Decimal(productShares[productId][sharedKey]).plus(productShare).toNumber();
      });
    };

    brokers.forEach((broker) => {
      updateShares(broker, broker.productId.toString());
    });
    if (replaceNames) {
      return Object.keys(productShares).reduce((acc, productId) => {
        const product = products.find((products) => products._id.toString() === productId);
        return { ...acc, [product.name]: productShares[productId] };
      }, <ProductTotal>{});
    }
    return productShares;
  }

  async getTotals(borrowerId: string) {
    const brokers = await ProductBrokerModel.find({ borrowerId }) as IProductBrokerView[];
    const shortTotals = await this.getTotalShares(borrowerId, brokers, true);
    return Object.entries(shortTotals).map(([product, shares]) => {
      return <BrokerTotalView>{
        active: true,
        _id: null,
        BSCode: null,
        PLCode: null,
        productId: product,
        adminShare: shares.adminShare,
        interestShare: shares.interestShare,
        otherShare: shares.otherShare,
      };
    });
  }

  async checkTotalShares(borrowerId: string, brokers: IProductBrokerView[]) {
    const productShares = await this.getTotalShares(borrowerId, brokers);
    return !Object.values(productShares).some(values => values.adminShare > 1 || values.interestShare > 1 || values.otherShare > 1);
  }

  async saveProductBrokers(borrowerId: string, brokers: IProductBrokerView[]) {
    const existingBrokers = await getBorrowerBrokers(borrowerId);

    const existingBrokerIds = existingBrokers.map((broker) => broker._id.toString());
    const brokersToDelete = existingBrokerIds.filter((id) => !brokers.some((broker) => String(broker._id) === id));
    await ProductBrokerModel.deleteMany({ _id: { $in: brokersToDelete } });

    const brokersToSave = brokers.map((broker) => {
      if (broker._id.toString().startsWith('new_')) {
        let { _id, ...brokerWithoutId } = broker;
        return { ...brokerWithoutId, borrowerId: borrowerId };
      }
      return { ...broker, borrowerId: borrowerId };
    });

    const getFilters = (broker: Partial<IProductBrokerView>) => {
      return broker.hasOwnProperty('_id') ? { _id: broker._id } : { createdAt: Math.random() };
    };

    try {
      return ProductBrokerModel.bulkWrite(
        brokersToSave.map((broker, order) => ({
          updateOne: {
            filter: getFilters(broker),
            update: { $set: { ...broker, order } },
            upsert: true,
          },
        })),
      );
    } catch (e) {
      console.error(e);
    }
  }

  async getAllLoanBrokers() {
    return getAllLoanBrokers();
  }

  async saveLoanBrokers(brokers: ILoanBrokerView[]) {
    const existingBrokers = await getAllLoanBrokers();

    const existingBrokerIds = existingBrokers.map((broker) => broker._id.toString());
    const brokersToDelete = existingBrokerIds.filter((id) => !brokers.some((broker) => String(broker._id) === id));
    await LoanBrokerModel.deleteMany({ _id: { $in: brokersToDelete } });

    const brokersToSave = brokers.map((broker) => {
      if (broker._id.toString().startsWith('new_')) {
        let { _id, ...brokerWithoutId } = broker;
        return brokerWithoutId;
      }
      return broker;
    });

    await Promise.all(brokersToSave.map(async (brokerToSave, order) => {
      await this.saveLoanBroker(brokerToSave, order);
    }));
  }

  async saveLoanBroker(broker: Partial<ILoanBrokerView>, order: number) {
    const getFilters = (broker: Partial<IProductBrokerView>) => {
      return broker.hasOwnProperty('_id') ? { _id: broker._id } : { createdAt: Math.random() };
    };
    const { emails, ...update } = broker;
    const updatedBroker = await LoanBrokerModel.findOneAndUpdate(
      getFilters(broker),
      { ...update, order },
      { upsert: true, new: true },
    );
    await this.saveLoanBrokerEmails(updatedBroker._id.toString(), emails);
  }

  async saveLoanBrokerEmails(loanBrokerId: string, emails: ILoanBrokerEmail[]) {
    const emailsWithoutId = emails.map((email) => {
      const { _id, ...rest } = email;
      return _id.toString().includes('new_') ? rest : email;
    });
    await LoanBrokerModel.findByIdAndUpdate(loanBrokerId, { emails: emailsWithoutId });
  }
}
