import { IUploadedBankTransaction } from '../models/UploadedBankTransaction.model';
import { fieldsToUnset, getUUID, removeFields } from '../helpers/main.helper';
import {
  CashAllocationReference,
  ICashAllocationReference,
  ICashAllocationReferenceDoc,
  ICashAllocationReferencePlain,
} from '../models/CashAllocationReference.model';
import {
  CashAllocationProduct,
  ICashAllocationProduct,
  ICashAllocationProductStringified,
} from '../models/CashAllocationProduct.model';

export class CashAllocationService {
  async createCashAllocation(transactions: IUploadedBankTransaction[]) {
    const newReferencesSet = new Set(transactions.map((transaction: IUploadedBankTransaction) => transaction.reference.toLowerCase()));
    const newReferences = Array.from(newReferencesSet);
    const foundCashAllocation = await this.getCashAllocationsByReferences(newReferences);
    const excludedReferences = foundCashAllocation.map((allocation) => allocation.reference.toLowerCase());
    const createdReferences = newReferences.filter((r) => !excludedReferences.includes(r));
    const createdTransactions = createdReferences.reduce((acc, cur) => {
      const newTransactions = transactions.find((tr) => tr.reference.toLowerCase() === cur);
      return [...acc, newTransactions];
    }, <IUploadedBankTransaction[]>[]);
    const order = await this.getNextOrder();
    await Promise.all(createdTransactions.map(async (transactions) => {
      if (excludedReferences.includes(transactions.reference.toLowerCase())) {
        return;
      }
      if (!newReferencesSet.has(transactions.reference.toLowerCase())) {
        return;
      }
      const cashAllocation: ICashAllocationReference = {
        uiId: getUUID(),
        order,
        reference: transactions.reference.trim(),
        cashAllocationProductId: null,
      };
      const newCashAllocationReference = new CashAllocationReference(cashAllocation);
      await newCashAllocationReference.save();
      newReferencesSet.delete(transactions.reference.toLowerCase());
    }));
  }

  async getNextOrder() {
    const lastOrder = await CashAllocationReference.findOne().sort({ order: -1 }).select('order').lean();
    return lastOrder ? lastOrder.order + 1 : 0;
  }

  async getCashAllocationsByReferences(references: string[]) {
    return CashAllocationReference
      .find({ reference: { $in: references.map(ref => new RegExp(`^${ref}$`, 'i')) } })
      .lean();
  }

  async getAllCashAllocations() {
    const cashAllocationReferences = await CashAllocationReference
      .find()
      .sort({ order: 1 });
    const cashAllocationsProducts = await CashAllocationProduct
      .find()
      .sort({ order: 1 });
    return {
      references: cashAllocationReferences.map((i) => removeFields<ICashAllocationReferenceDoc>(i, fieldsToUnset)),
      products: cashAllocationsProducts.map((i) => removeFields<ICashAllocationProduct>(i, fieldsToUnset)),
    };
  }

  async updateAllCashAllocation(products: ICashAllocationProductStringified[], references: ICashAllocationReferencePlain[]) {
    const existingCashAllocations = await this.getAllCashAllocations();

    const existingCashAllocationProductIds = existingCashAllocations.products.map((i) => i._id.toString());
    const cashAllocationProductsToDelete = existingCashAllocationProductIds.filter((id) => !products.some((i) => i._id === id));
    await CashAllocationProduct.deleteMany({ _id: { $in: cashAllocationProductsToDelete } });

    const newProductsMap = new Map();

    await Promise.all(products.map(async (product, order) => {
      const { _id, ...update } = product;
      if (_id.includes('new_')) {
        const newCashAllocationProduct = new CashAllocationProduct({ ...update, order });
        const createdCashAllocationProduct = await newCashAllocationProduct.save();
        newProductsMap.set(_id, createdCashAllocationProduct._id.toString());
      } else {
        await CashAllocationProduct.findByIdAndUpdate(_id, { ...update, order });
      }
    }));

    const existingCashAllocationReferenceIds = existingCashAllocations.references.map((i) => i._id.toString());
    const cashAllocationReferencesToDelete = existingCashAllocationReferenceIds.filter((id) => !references.some((i) => i._id === id));
    await CashAllocationReference.deleteMany({ _id: { $in: cashAllocationReferencesToDelete } });

    await Promise.all(references.map(async (reference, order) => {
      if (reference.cashAllocationProductId.includes('new_')) {
        reference.cashAllocationProductId = newProductsMap.get(reference.cashAllocationProductId);
      }
      const { _id, ...update } = reference;
      if (_id.includes('new_')) {
        const newCashAllocationReference = new CashAllocationReference({ ...update, order });
        await newCashAllocationReference.save();
      } else {
        await CashAllocationReference.findByIdAndUpdate(_id, { ...update, order });
      }
    }));
  }
}
