import path from 'path';
import fs from 'fs';
import * as XLSX from 'xlsx';
import mongoose from 'mongoose';
import dayjs from 'dayjs';
import Decimal from 'decimal.js';

import {
  convertDataToFile,
  convertDataToFileWithStyleOld,
} from '../helpers/excel.helper';
import { IMulterFile } from '../interfaces/multer.interface';
import { ITEMS_PAGINATION } from '../db/collaterals.db';
import { IPaginatorOptions } from '../interfaces/collaterals.interface';
import { ECollaterals } from '../enums/collaterals.enum';
import { EValidators } from '../enums/validators.enums';
import { IReceivableItem } from '../models/ReceivableItem.model';
import { COLLATERALS } from '../helpers/column-desciptions.helper';
import { convertAnyToDate, excelDateToJSDate } from '../helpers/date.helper';
import { IUploadedDataView } from '../interfaces/upload-data.interface';
import { EUploadTypes } from '../enums/upload-types.enum';
import { BBCDateModel } from '../models/BBCDate.model';
import { AvailabilitySignsModel } from '../models/AvailabilitySigns.model';
import { ReceivableAvailabilityModel } from '../models/ReceivableAvailability.model';
import { BBCSheetModel } from '../models/BBCSheet.model';
import { InventoryManualEntryModel } from '../models/InventoryManualEntry.model';
import { ELogActionType, ELogType } from '../models/UserLog.model';
import { createMicroserviceTasks } from '../db/microservice-tasks.db';
import { EMicroserviceTask } from '../enums/microservice-task.enum';

import UploadedFile, { IUploadedFile, IUploadedFileDoc } from '../models/UploadedFile.model';
import UploadedSheet, {
  IUploadedSheet,
  IUploadedSheetDocWithFile,
  IUploadedSheetView,
} from '../models/UploadedSheet.model';
import UploadedData, { IUploadedData, IUploadedDataDoc } from '../models/UploadedData.model';
import { InventoryItem } from '../classes/inventory-item';
import { ReceivableItem } from '../classes/receivable-item';
import { PayableAccountItem } from '../classes/payable-account-item';
import { getSheetWithFile } from '../db/uploads.db';
import { getAllTimeOldestSKUDateNew } from '../db/inventories';
import { LoanTransactionItem } from '../classes/bank-transaction-item';
import EquipmentModel from '../models/Equipment.model';
import ReserveModel from '../models/Reserve.model';
import { IQuickbooksUploadItem } from '../classes/quickbook-item';
import { BankUploadedTransactionItem } from '../classes/bank-uploaded-transaction';
import { BankUploadedTransactionsService } from './bank-uploaded-transactions.service';
import { CollateralsService } from './collaterals.service';
import { LoanTransactionsService } from './loan-transactions.service';
import { QuickbooksService } from './quickbooks.service';
import { UserLogsService } from './user-logs.service';
import { LockService } from './lock.service';
import { BorrowersDB } from './borrowers.db';
import { UsersService } from './users.service';
import { SalesItem } from '../classes/sales-item';

interface IMappedColumns {
  [columnName: string]: number;
}

export interface IMapping {
  sheetId: string;
  startRow: number;
  endRow: number;
  bbcDate: Date;
  effectiveDate?: string;
  sheetType: ECollaterals;
  mappedColumns: IMappedColumns;
  extraOptions?: { [key: string]: string };
}

interface IMappedData {
  [key: string]: any;
}

export interface IExcelDataCellWithStyles {
  v: string | number | Date | boolean;
  t?: string;
  s?: object;
  z?: object;
}

const validators: { [key in EValidators]?: any } = {
  [EValidators.NOT_EMPTY]: (field, value): { error: string } | void => {
    if (!value || value.toString().trim() === '') {
      return { error: `value ${field} must not be empty!` };
    }
  },
  [EValidators.NUMBER_GT_NULL]: (field, value): { error: string } | void => {
    if (value === 0 || value < 0) {
      return { error: `value ${field} must be greater than 0!` };
    }
  },
  [EValidators.NUMBER_GTE_NULL]: (field, value): { error: string } | void => {
    if (value < 0) {
      return { error: `value ${field} must be greater or equal 0!` };
    }
  },
  [EValidators.IS_NUMBER]: (field, value): { error: string } | void => {
    if (typeof value === 'object' || isNaN(Number(value))) {
      return { error: `value ${field} must be number!` };
    }
  },
  [EValidators.IS_DATE]: (field, value): { error: string } | void => {
    if (!value) {
      return;
    }
    if (value.toString().trim() !== '' && Object.prototype.toString.call(convertAnyToDate(value)) !== '[object Date]') {
      return { error: `value ${field} must be date!` };
    }
  },
};

export interface IUploadOption {
  fileName: string;
  sheetName: string;
  userUploaded?: string;
  effectiveDate: string;
}

export class UploadsService {

  constructor(
    private readonly rootDir: string,
    private readonly bankUploadedTransactionsService: BankUploadedTransactionsService,
    private readonly getCollateralsService: () => CollateralsService,
    private readonly getLoanTransactionsService: () => LoanTransactionsService,
    private readonly getQuickbooksService: () => QuickbooksService,
    private readonly getLockService: () => LockService,
    private readonly userLogsService: UserLogsService,
    private readonly borrowersDB: BorrowersDB,
    private readonly usersService: UsersService,
  ) {
  }

  async createUploadFile(file: IMulterFile, borrowerId: string, userId: string, uploadType: EUploadTypes) {
    const borrowerIdToWrite = new mongoose.Types.ObjectId(borrowerId);
    const newFile: IUploadedFile = {
      borrowerId: borrowerIdToWrite,
      filename: file.filename,
      originalName: file.originalname,
      userId,
      type: uploadType,
    };
    return await UploadedFile.create(newFile);
  }

  async convertFileToData(uploadedFileId: string) {
    const uploadedFile = await UploadedFile.findById(uploadedFileId).lean();
    if (!uploadedFile) {
      return;
    }
    const absolutePath = path.join(this.rootDir, 'uploads', uploadedFile.filename);

    let excelData;
    fs.readFile(absolutePath, async (err, data) => {
      if (err) {
        console.error({ err });
        return;
      }
      const xlsxOptions = { cellDates: true, dateNF: 'yyyy/mm/dd;@' };
      const workbook = XLSX.read(data, xlsxOptions);
      excelData = Object.keys(workbook.Sheets).reduce((acc, ws) => {
        const worksheet = workbook.Sheets[ws];
        const arrayList = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: null });
        const trimmedArray = this.trimExcelArray(arrayList);
        return { ...acc, [ws]: trimmedArray.map((arr) => Object.assign({}, arr)) };
      }, {});
      await Promise.all(Object.entries(excelData).map(async ([sheetName, sheetData]: [sheetName: string, sheetData: object[]]) => {
        const newSheet: IUploadedSheet = {
          fileId: new mongoose.Types.ObjectId(uploadedFileId),
          sheetName: sheetName,
          total: sheetData.length,
        };
        const createdSheet = await UploadedSheet.create(newSheet);

        (<object[]>sheetData).map(async (data, order) => {
          const convertedData = Object.entries(data).reduce((acc, [key, value]) => {
            return {
              ...acc,
              [key]: typeof value === 'number' ? new Decimal(value).toDecimalPlaces(2).toNumber() : value,
            };
          }, {});
          const newData: IUploadedData = {
            sheetId: new mongoose.Types.ObjectId(String(createdSheet._id)),
            order,
            data: convertedData,
          };
          await UploadedData.create(newData);
        });
      }));

      fs.unlink(absolutePath, async (err) => {
        if (err) {
          console.error(err);
        }
      });

    });
  }

  async convertDataToFile(dataToConvert: { [sheetName: string]: any[] }[]) {
    return convertDataToFile(dataToConvert);
  }

  async convertDataToFileWithStyleOld(dataToConvert: { [sheetName: string]: IExcelDataCellWithStyles[][] }, options?: {
    width?: {
      [sheetName: string]: { [columnId: string]: number }
    },
    sheetOptions?: {
      [sheetName: string]: { showGridLines?: boolean };
    };
  }) {
    return convertDataToFileWithStyleOld(dataToConvert, options);
  }

  async deleteFile(uploadedFileId: string) {
    await UploadedFile.deleteOne({ _id: uploadedFileId });
  }

  async deleteSheet(uploadedSheetId: string) {
    const sheet = await UploadedSheet.findById(uploadedSheetId).lean();
    const file = await UploadedFile.findById(sheet.fileId).lean();
    const borrowerId = file.borrowerId ? file.borrowerId.toString() : null;
    await UploadedSheet.deleteOne({ _id: uploadedSheetId });
    await UploadedData.deleteMany({ sheetId: uploadedSheetId });
    const sheets = await UploadedSheet.find({ fileId: file._id });
    if (!sheets.length) {
      await UploadedFile.deleteOne({ _id: file._id });
    }
    return await this.getUploadedSheets(borrowerId, file.type);
  }

  async deleteAllSheets(uploadType: EUploadTypes) {
    const files = await UploadedFile.find({ type: uploadType });
    const fileIds = files.map((i) => i._id);
    const sheets = await UploadedSheet.find({ fileId: { $in: fileIds } });
    const sheetIds = sheets.map((i) => i._id);
    await UploadedSheet.deleteMany({ _id: { $in: sheetIds } });
    await UploadedData.deleteMany({ sheetId: { $in: sheetIds } });
    await UploadedFile.deleteMany({ _id: { $in: fileIds } });
  }

  trimExcelArray(data: any[]): unknown[] {
    let dataFiltered = data.filter((arr) => arr.some((elem) => elem !== null && elem !== 0 && elem.toString().trim() !== ''));
    if (!dataFiltered.length) {
      return [];
    }
    const dataRotated = dataFiltered[0].map((_, index) => dataFiltered.map(row => row[index]).reverse());
    let dataRotatedFiltered = dataRotated.filter((arr) => arr.some((elem) => elem !== null && elem !== 0 && elem.toString().trim() !== ''));
    return dataRotatedFiltered[0].map((_, index) => dataRotatedFiltered.map((row) => row[row.length - 1 - index]));
  }

  async getUploadedRawFiles(borrowerId: string, uploadType: EUploadTypes) {
    const findFilter = uploadType === EUploadTypes.BBC
      ? { borrowerId, type: uploadType }
      : { type: uploadType };
    const uploadedFiles = await UploadedFile.find(findFilter).lean();
    const files = await Promise.all(uploadedFiles.map(async (file) => {
      const sheets = await UploadedSheet.find({ fileId: file._id });
      if (!sheets.length) {
        return file;
      }
    }));
    return files.filter((file) => !!file);
  }

  async getUploadedSheets(borrowerId: string, uploadType: EUploadTypes) {
    const borrowerFilter = uploadType === EUploadTypes.BBC ? { borrowerId: new mongoose.Types.ObjectId(borrowerId) } : {};
    const files = await UploadedFile.aggregate<IUploadedFileDoc>([
      {
        $match: {
          ...borrowerFilter,
          'type': uploadType,
        },
      }, {
        $sort: {
          'createdAt': 1,
        },
      },
    ]);
    const fileIds = files.map((file) => file._id);
    const sheets = await UploadedSheet.aggregate<IUploadedSheetDocWithFile>([
      {
        $match: {
          'fileId': {
            $in: fileIds,
          },
        },
      }, {
        $lookup: {
          from: 'uploadedfiles',
          localField: 'fileId',
          foreignField: '_id',
          as: 'file',
        },
      }, {
        $unwind:
          {
            path: '$file',
          },
      }, {
        $sort: {
          'createdAt': 1,
        },
      },
    ]);

    return sheets.map((sheet) => {
      return <IUploadedSheetView>{
        _id: sheet._id,
        sheetName: sheet.sheetName,
        fileId: sheet.file._id,
        fileName: sheet.file.originalName,
        total: sheet.total,
      };
    });
  }

  async getUploadedData(sheetId: string, paginatorOptions?: IPaginatorOptions): Promise<IUploadedDataView> {
    const uploadedData = await UploadedData.aggregate<IUploadedDataDoc>([
      {
        $match: {
          'sheetId': new mongoose.Types.ObjectId(sheetId),
        },
      }, {
        $sort: {
          'order': 1,
        },
      },
      ...ITEMS_PAGINATION(paginatorOptions),
    ]);
    const totalItems = (await UploadedSheet.findById(sheetId)).total;
    return {
      paginatorOptions,
      totalItems,
      items: uploadedData.map((row) => row.data),
    };
  }

  private transformNumberIntoDate(mappedRow: IReceivableItem, collateralType: ECollaterals): IReceivableItem {
    const dateFields = COLLATERALS[collateralType].filter((field) => field.isDate).map((field) => field.db_title);
    Object.entries(mappedRow).forEach(([key, value]) => {
      if (dateFields.includes(key) && typeof value === 'number') {
        mappedRow[key] = excelDateToJSDate(value);
      }
    });
    return mappedRow;
  }

  private validateRow(mappedRow: unknown, collateralType: ECollaterals): any[] {
    try {
      const rowValidationErrors = [];
      Object.entries(mappedRow).forEach(([field, value]) => {
        const foundDesc = COLLATERALS[collateralType].find((columnDesc) => field === columnDesc.db_title);
        const fieldValidationErrors = [];
        if (!foundDesc) {
          return;
        }
        if (foundDesc.validators) {
          foundDesc.validators.forEach((validator) => {
            const validationError = validators[validator](field, value);
            if (!!validationError) {
              fieldValidationErrors.push(validationError);
            }
          });
        }
        if (fieldValidationErrors.length) {
          rowValidationErrors.push({ [field]: fieldValidationErrors });
        }
      });
      return rowValidationErrors;
    } catch (e) {
      console.error(e);
      return [{ row: e }];
    }
  }

  async mapData(mapData: IMapping, paginators: { [table: string]: IPaginatorOptions }) {
    const mappedData = await this.prepareData(mapData);
    const hasDateMismatch = mappedData.approved.items.some((item) => {
      if (dayjs(item.date).format('YYYY-MM-DD') !== mapData.effectiveDate) {
      }
      return dayjs(item.date).format('YYYY-MM-DD') !== mapData.effectiveDate;
    });

    const paginateItems = (items: any[], paginator: IPaginatorOptions) => {
      const startItem = paginator.pageIndex * paginator.pageSize;
      return items.slice(startItem, startItem + paginator.pageSize);
    };

    return Object.keys(mappedData).reduce((acc, key) => {
      return {
        ...acc,
        hasDateMismatch,
        [key]: {
          totalItems: mappedData[key].totalItems,
          items: paginateItems(mappedData[key].items, paginators[key]),
          paginatorOptions: paginators[key],
        },
      };
    }, {});
  }

  async convertData(mapData: IMapping, userId: string) {
    const { approved } = await this.prepareData(mapData);
    const fileData = await getSheetWithFile(mapData.sheetId);

    const borrowerId = fileData[0].file.borrowerId?.toString() ?? null;
    const bbcDate = dayjs.utc(mapData.bbcDate).startOf('day').toDate();

    if (borrowerId) {
      const lockService = this.getLockService();
      const isDateLocked = await lockService.isDateLocked(bbcDate, borrowerId);
      if (isDateLocked) {
        return { success: false, message: 'This period is signed and locked', data: null };
      }
    }

    const newCollateralData = {
      borrowerId,
      bbcDate,
      fileName: fileData[0].file.originalName,
      sheetName: fileData[0].sheetName,
      items: approved.items,
    };
    const uploadOption: IUploadOption = {
      fileName: fileData[0].file.originalName,
      sheetName: fileData[0].sheetName,
      effectiveDate: mapData.effectiveDate,
    };
    try {
      let taskId: string;
      switch (mapData.sheetType) {
        case ECollaterals.LOAN_TRANSACTIONS: {
          const loanTransactionsService = this.getLoanTransactionsService();
          await loanTransactionsService.uploadLoanTransactions(newCollateralData.items, uploadOption, userId);
          await this.userLogsService.createUserLog({
            userId,
            logType: ELogType.TRANSACTIONS_UPLOAD,
            action: ELogActionType.UPLOAD,
            details: { mapData, uploadOption },
          });
          break;
        }
        case ECollaterals.QUICKBOOKS: {
          const quickbooksService = this.getQuickbooksService();
          await quickbooksService.uploadAccounts(newCollateralData.items, true);
          break;
        }
        case ECollaterals.UPLOADED_BANK_TRANSACTIONS: {
          taskId = await this.bankUploadedTransactionsService.uploadTransactions(newCollateralData.items);
          break;
        }
        default: {
          const collateralsService = this.getCollateralsService();
          const bbcDateId = await collateralsService.saveCollateralItems(newCollateralData, ECollaterals[mapData.sheetType]);
          const user = await this.usersService.getUserById(userId);
          if (user) {
            const params = {
              borrowerId: new mongoose.Types.ObjectId(borrowerId),
              bbcDateId,
              userId,
            };
            await createMicroserviceTasks(EMicroserviceTask.POST_UPLOAD, params);
          }
          await this.userLogsService.createUserLog({
            userId,
            logType: ELogType.COLLATERAL_UPLOAD,
            action: ELogActionType.UPLOAD,
            details: { mapData, uploadOption, borrowerId, bbcDate },
          });
          break;
        }
      }
      await this.removeConvertedData(mapData.sheetId);
      return { success: true, message: 'Successfully uploaded', data: { taskId } };
    } catch (e) {
      console.error({ e });
      return { success: false, message: 'Error while uploading', data: null };
    }
  }

  async prepareData(mapData: IMapping) {
    const { startRow, endRow, bbcDate, sheetType, mappedColumns, sheetId } = mapData;
    const excelData = await this.getUploadedData(sheetId);

    const SLICED_DATA = excelData.items.slice(startRow - 1, endRow);

    const MAPPED_DATA = <IMappedData[]>(SLICED_DATA.reduce((acc: IMappedData[], row) => {
      const RES = Object.entries(mappedColumns).reduce((result, [key, value]) => {
        if (value !== null) {
          return { ...result, [key]: row[value] ?? null };
        }
        return result;
      }, {});
      return [...acc, RES];
    }, []));

    let skuDates = [];

    if (mapData.sheetType === ECollaterals.INVENTORY) {
      const uploadedSheet = await UploadedSheet.findById(mapData.sheetId).lean();
      const uploadedFile = await UploadedFile.findById(uploadedSheet.fileId).lean();
      skuDates = await getAllTimeOldestSKUDateNew(uploadedFile.borrowerId.toString(), mapData.bbcDate);
    }

    const approvedItems = [];
    const removedItems = [];
    try {
      await Promise.all(MAPPED_DATA.map(async (row, index) => {
        const extraErrors = [];
        const mappedCollateralTypes = {
          [ECollaterals.INVENTORY]: () => new InventoryItem(row, bbcDate, skuDates),
          [ECollaterals.RECEIVABLE]: () => new ReceivableItem(row, bbcDate),
          [ECollaterals.ACCOUNT_PAYABLE]: () => new PayableAccountItem(row, bbcDate),
          [ECollaterals.SALES]: () => new SalesItem(row, bbcDate, mapData.extraOptions),
          [ECollaterals.LOAN_TRANSACTIONS]: async () => {
            const newItem = new LoanTransactionItem(row, this.borrowersDB);
            const classErrors = await newItem.initialize();
            if (classErrors.length) {
              classErrors.forEach((error) => extraErrors.push(error));
            }
            const cleanItem = { ...newItem, transactionType: '' };
            cleanItem.transactionType = newItem.isStatement ? 'STATEMENT' : newItem.transactionType;
            delete cleanItem.isStatement;
            return cleanItem;
          },
          [ECollaterals.UPLOADED_BANK_TRANSACTIONS]: async () => new BankUploadedTransactionItem({
            ...row,
            order: index,
          }, mapData.extraOptions),
          [ECollaterals.QUICKBOOKS]: async () => {
            const newItem = new IQuickbooksUploadItem(row, mapData.extraOptions);
            const classErrors = await newItem.initialize();
            if (classErrors.length) {
              classErrors.forEach((error) => extraErrors.push(error));
            }
            return newItem;
          },
        };
        const item = await mappedCollateralTypes[ECollaterals[sheetType]]();
        const mappedRow = this.transformNumberIntoDate(item, ECollaterals[sheetType]);
        const validationErrors = this.validateRow(mappedRow, ECollaterals[sheetType]);
        const allErrors = [...extraErrors, ...validationErrors];
        if (allErrors.length) {
          const errors = allErrors.map((errorValues) => {
            return Object.values(errorValues).reduce((acc: any[], obj) => {
              const errorsV = (obj as any).map((v) => v.error);
              return [...acc, errorsV];
            }, []);
          });
          removedItems[index] = {
            ...row,
            checked: true,
            validationErrors: errors.join(', '),
          };
        } else {
          approvedItems[index] = mappedRow;
        }
      }));
      const approvedItemsOnly = approvedItems.filter(Boolean);
      const removedItemsOnly = removedItems.filter(Boolean);
      return {
        approved: {
          items: approvedItemsOnly,
          totalItems: approvedItemsOnly.length,
        },
        removed: {
          items: removedItemsOnly,
          totalItems: removedItemsOnly.length,
        },
      };
    } catch (e) {
      console.error(e);
      throw new Error('error while loading');
    }
  }

  async removeConvertedData(sheetId: string) {
    try {
      const uploadedFile = await UploadedSheet.findById(sheetId);
      const fileId = uploadedFile.fileId;
      await UploadedSheet.deleteOne({ _id: sheetId });
      await UploadedData.deleteMany({ sheetId: sheetId });
      const siblingsSheets = await UploadedSheet.find({ fileId });
      if (!siblingsSheets.length) {
        await UploadedFile.deleteOne({ _id: fileId });
      }
    } catch (e) {
      console.error({ e });
    }
  }

  async removeEntriesWithoutFiles() {
    try {
      const uploadedFiles = await UploadedFile.find({});
      await Promise.all(uploadedFiles.map(async (file) => {
        const sheets = await UploadedSheet.find({ fileId: file._id });
        if (!sheets.length) {
          await UploadedFile.findByIdAndDelete(file._id);
        }
      }));
    } catch (e) {
      console.error({ e });
    }
  }

  async cleanUploads() {
    try {
      fs.readdir(path.join(this.rootDir, 'uploads'), async (err, files) => {
        await Promise.all(files.map((file) => {
          fs.unlink(path.join(this.rootDir, 'uploads', file), (err) => {
            if (err) {
              console.error({ err });
            }
          });
        }));
      });
      await UploadedFile.deleteMany();
      await UploadedSheet.deleteMany();
      await UploadedData.deleteMany();
    } catch (e) {
      console.error({ e });
    }
  }

  async removeEmptyBBC(remove = false) {
    const bbcDates = await BBCDateModel.find().lean();
    for (const bbcDate of bbcDates) {
      const sheets = await BBCSheetModel.find({ bbcDateId: bbcDate._id });
      if (sheets.length === 0) {
        console.log(bbcDate._id.toString(), 'can be deleted');
        if (remove) {
          await BBCDateModel.findByIdAndDelete(bbcDate._id);
          await AvailabilitySignsModel.deleteMany({ bbcDateId: bbcDate._id });
          await EquipmentModel.deleteMany({ bbcDateId: bbcDate._id });
          await InventoryManualEntryModel.deleteMany({ bbcDateId: bbcDate._id.toString() });
          await ReceivableAvailabilityModel.deleteMany({ bbcDateId: bbcDate._id });
          await ReserveModel.deleteMany({ bbcDateId: bbcDate._id });
        }
      }
    }
  }

}
