import mongoose, { Document } from 'mongoose';
import _ from 'lodash';

import { IResult } from '../interfaces/result.interface';
import {
  addAvailabilitySign,
  findAvailabilitySignsByBBCDateId,
  getAvailabilitySigns,
  updateAvailabilitySigns,
} from '../db/availability.db';
import {
  AvailabilitySignsModel,
  IAvailabilitySignItem,
  IAvailabilitySignView,
  REQUIRED_SIGNS,
} from '../models/AvailabilitySigns.model';
import { BBCDateModel, IBBCDateDoc } from '../models/BBCDate.model';
import { MODEL_NAMES } from '../models/_models';
import { getOlderBBCDates } from '../db/bbcDates.db';
import { ELogActionType, ELogType } from '../models/UserLog.model';
import { createLog, ICreateLogParams } from '../db/user-logs.db';
import { IBorrowerDoc } from '../models/Borrower.model';
import { IPaginatorOptions } from '../interfaces/collaterals.interface';
import { ITEMS_PAGINATION } from '../db/collaterals.db';
import { UsersService } from './users.service';
import { AvailabilitySignedData } from '../models/AvilabilitySignedData.model';
import { LoanTransactionsService } from './loan-transactions.service';
import { LockService } from './lock.service';
import { LoanStatementService } from './loan-statement.service';
import { IUser } from '../interfaces/auth-user.interface';

export interface IBBCDateWithBorrower extends Document {
  borrowerId: IBorrowerDoc;
  bbcDate: Date;
}

export enum ESignStatus {
  STARTED = 'STARTED',
  APPROVED = 'APPROVED',
  PENDING = 'PENDING',
}

export class SignsService {

  constructor(
    private readonly lockService: LockService,
    private readonly loanTransactionsService: LoanTransactionsService,
    private readonly loanStatementService: LoanStatementService,
    private readonly usersService: UsersService,
  ) {
  }

  async getAvailabilitiesSigns(bbcDateIds: string[]): Promise<IAvailabilitySignView[]> {
    const availabilitySigns = await getAvailabilitySigns(bbcDateIds);
    const users = await this.usersService.getUserList();
    const usersMap = users.reduce((acc, user) => {
      return { ...acc, [user.id]: user };
    }, <Record<string, IUser>>{});
    return availabilitySigns.map((availabilitySign) => {
      return {
        ...availabilitySign,
        signs: availabilitySign.signs.map((sign) => ({
          ...sign,
          userSigned: usersMap[sign.userSignedId],
          userRevoked: usersMap[sign.userRevokedId],
        })),
      };
    }) as unknown as IAvailabilitySignView[];
  }

  async getAvailabilitySigns(bbcDateIds: string[]): Promise<IAvailabilitySignView[]> {
    const availabilitiesSigns = await this.getAvailabilitiesSigns(bbcDateIds);
    const availabilitiesSignsFull = bbcDateIds.map((bbcDateId) => {
      const foundAvailabilitySigns = availabilitiesSigns.find((signs) => signs.bbcDateId.toString() === bbcDateId);
      if (foundAvailabilitySigns) {
        return foundAvailabilitySigns;
      }
      return <IAvailabilitySignView>{
        _id: null,
        bbcDateId: new mongoose.Types.ObjectId(bbcDateId),
        requiredSigns: REQUIRED_SIGNS,
        signs: [],
      };
    });
    return availabilitiesSignsFull.map((availabilitySigns) => {
      const signs = availabilitySigns.signs.reduce((acc, userSign) => {
        if (!userSign.userSigned) {
          return acc;
        }
        userSign.userSigned = (_.pick(userSign.userSigned, ['id', 'firstName', 'lastName'])) as IUser;
        if (userSign.userRevoked) {
          userSign.userRevoked = (_.pick(userSign.userRevoked, ['id', 'firstName', 'lastName'])) as IUser;
        }
        return [...acc, _.pick(userSign, ['userSigned', 'signedAt', 'userRevoked', 'revokedAt'])];
      }, []);
      return { ...availabilitySigns, signs };
    });
  }

  async revokeAvailability(bbcDateId: string, userId: string, userIdRevoked: string): Promise<void> {
    const availability = await findAvailabilitySignsByBBCDateId(bbcDateId);
    const updatedSigns = availability.signs.map((sign) => {
      if (sign.userSignedId.toString() === userId && !sign.revokedAt) {
        sign.userRevokedId = userIdRevoked;
        sign.revokedAt = new Date();
      }
      return sign;
    });
    await updateAvailabilitySigns(bbcDateId, updatedSigns);
  }

  async signAvailability(bbcDateId: string, user: IUser): Promise<IResult> {
    const newSign = <IAvailabilitySignItem>{ userSignedId: user.id, signedAt: new Date() };
    return await addAvailabilitySign(bbcDateId, newSign);
  }

  async isBBCLocked(bbcDateId: string): Promise<boolean> {
    const bbcDoc = await BBCDateModel.findById(bbcDateId).lean();
    if (!bbcDoc) {
      return false;
    }
    return await this.lockService.isDateLocked(bbcDoc.bbcDate, bbcDoc.borrowerId.toString());
  }

  async getLatestSignedBBCDate(borrowerId: string, date?: Date): Promise<IBBCDateDoc> {
    const bbcDocs = await getOlderBBCDates(borrowerId, date || new Date());
    const activeSignsAfter = await AvailabilitySignsModel.aggregate([
      {
        $match: {
          'bbcDateId': { $in: bbcDocs.map((bbc) => bbc._id) },
        },
      }, {
        $unwind: {
          path: '$signs',
          preserveNullAndEmptyArrays: true,
        },
      }, {
        $match: {
          'signs.revokedAt': { $exists: false },
        },
      }, {
        $group: {
          '_id': '$_id',
          'requiredSigns': { $first: '$requiredSigns' },
          'bbcDateId': { $first: '$bbcDateId' },
          'signs': { $push: '$signs' },
        },
      }, {
        $match: {
          $expr: {
            $gte: [
              { '$size': '$signs' }, '$requiredSigns',
            ],
          },
        },
      }, {
        $lookup: {
          from: MODEL_NAMES.BBCDates,
          localField: 'bbcDateId',
          foreignField: '_id',
          as: 'bbc',
        },
      }, {
        $unwind: {
          path: '$bbc',
          preserveNullAndEmptyArrays: true,
        },
      }, {
        $sort: {
          'bbc.bbcDate': -1,
        },
      },
    ]);
    if (!activeSignsAfter.length) {
      return null;
    }
    return activeSignsAfter[0].bbc;
  }

  async getSavedData(bbcDateId: string) {
    const availabilitySignedData = await AvailabilitySignedData.findOne({ bbcDateId }).lean();
    if (!availabilitySignedData) {
      return {};
    }
    return availabilitySignedData.data;
  }

  async keepBalance(bbcDateId: string) {
    const loanBalances = await this.loanTransactionsService.getLoanBalanceForBBCDateId(bbcDateId);
    const accruedStatement = await this.loanStatementService.calculateAccruedStatementForBBC(bbcDateId);
    console.log({ bbcDateId });
    await AvailabilitySignedData.findOneAndUpdate({ bbcDateId }, {
      data: {
        loanBalances,
        accruedStatement,
      },
    }, { upsert: true });
  }

  async clearAvailabilityStoredData(bbcDateId: string) {
    await AvailabilitySignedData.findOneAndDelete({ bbcDateId });
  }

  async logSignStatus(bbcDateId: string, userId: string, isSigned: boolean) {
    const bbcDate = await BBCDateModel.findById(bbcDateId)
      .populate('borrowerId')
      .lean() as unknown as IBBCDateWithBorrower;
    if (!bbcDate) {
      return;
    }
    const newLog: ICreateLogParams = {
      userId,
      action: isSigned ? ELogActionType.CREATE : ELogActionType.DELETE,
      details: { date: bbcDate.bbcDate, borrower: bbcDate.borrowerId.name }, // , borrower: bbcDate.borrower.name
      logType: isSigned ? ELogType.SIGN_BBC : ELogType.REVOKE_BBC,
    };
    await createLog(newLog);
  }

  async BBCDatesShort(borrowerId: string, paginatorOptions: IPaginatorOptions) {
    const BBCDates = await BBCDateModel.aggregate<{ _id: mongoose.Types.ObjectId, bbcDate: Date, signStatus: string }>([
      {
        '$match': {
          'borrowerId': new mongoose.Types.ObjectId(borrowerId),
        },
      }, {
        '$sort': {
          'bbcDate': -1,
        },
      }, {
        '$project': {
          '_id': 1,
          'bbcDate': 1,
          'signStatus': 1,
        },
      },
      ...ITEMS_PAGINATION(paginatorOptions),
    ]);

    const availabilitiesSigns = await this.getAvailabilitiesSigns(BBCDates.map((bbcDate) => bbcDate._id.toString()));
    await Promise.all(BBCDates.slice().map(async (bbcDate) => {
      const foundAvailabilitySigns = availabilitiesSigns.find((aSigns) => aSigns.bbcDateId.toString() === bbcDate._id.toString());
      if (!foundAvailabilitySigns) {
        bbcDate.signStatus = ESignStatus.STARTED;
      }
      if (foundAvailabilitySigns) {
        const activeSigns = foundAvailabilitySigns.signs.filter((sign) => !sign.userRevoked).length;
        if (activeSigns === 0) {
          bbcDate.signStatus = ESignStatus.STARTED;
        } else if (activeSigns === foundAvailabilitySigns.requiredSigns) {
          bbcDate.signStatus = ESignStatus.APPROVED;
        } else if (activeSigns < foundAvailabilitySigns.requiredSigns) {
          bbcDate.signStatus = ESignStatus.PENDING;
        }
      }
    }));
    return BBCDates;
  }
}
