import { AppointmentsAdapterMongoose } from "./Appointments.Adapter.Mongoose";
import {
    ApplyAppointmentDiscountParams,
    APPOINTMENT_NOTIFICATION_TYPE,
    AppointmentAdminByClientView,
    AppointmentAdminByDateView,
    AppointmentEvent,
    AppointmentEventByClient,
    AppointmentNotificationIntent,
    ChangeEmployeeToOwnerParams,
    CountAppointmentsByMonthParams,
    CreateAppointmentParams,
    DeleteAppointmentDiscountParams,
    DeleteAppointmentParams,
    DeleteAppointmentsByCompanyParams,
    FindAppointmentsByUserParams,
    FindAppointmentsByCollaboratorParams,
    FindAppointmentsByUserPaginatedParams,
    FindAppointmentsByCollaboratorPaginatedParams,
    PaginatedAppointmentEventByClientResult,
    FindAppointmentSchedulesParams,
    FindAppointmentsParams,
    GetAppointmentEventParams,
    GetAvailableDatesParams,
    GetAvailableDatesResult,
    HeatMapConfig,
    IAppointment,
    IAppointmentsManager,
    PaginateAppointmentsParams,
    PaginateAppointmentsResults,
    SendAppointmentNotificationParams,
    SendWapSystemNotificationParams,
    SetPaymentStatusParams,
    UpdateAppointmentParams,
    MoveAppointmentParams, ChangeServiceParams,
} from "./Appointments.Interface";
import UsersManager from "../Users/Users";
import ClientsManager from "../Clients/Clients";
import CompaniesManager from "../Companies/Companies";
import EmployeesList from "../Employees/Employee";
import ServicesManager from "../Services/Service";
import { HeatMapItem, IHeatMap } from "../../Models/HeatMap/HeatMap.Interface";
import HeatMapList from "../HeatMap/HeatMap";
import dayjs from "dayjs";
import "dayjs/locale/es";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
import weekday from "dayjs/plugin/weekday";

import { IClientDocument } from "../../Models/Clients/Clients.Adapter.Mongoose";
import { isNull } from "../../helpers/IsNull";
import { joinStrings } from "../../helpers/String";
import getAvatar from "../../helpers/getAvatar";
import ServiceList from "../Services/Service";
import { EmployeeRoles } from "../../Models/Employees/Employees.Interface";
import { NoPermissionMessage } from "../../helpers/NoPermissionMessage";
import SchedulesEnabledList from "../../Models/SchedulesEnabled/SchedulesEnabled";
import SchedulesList from "../../Models/Schedules/Schedules";
import MetricsList from "../../Models/Metrics/Metrics";
import { TextObjectFilterResult } from "../TextObjectFilter.model";
import { NotificationsManager } from "../Notifications/Notifications";
import SystemNoficationsList from "../SystemNotifications/SystemNotification";
import PlanSubscriptionsList from "../PlanSubscriptions/PlanSubscriptons";
import { PlanFeatures } from "../Plans/Plans.interface";
import { formatCurrency } from "../../helpers/formatCurrency";
import RepeatsList from "../Repeats/Repeats";
import { IUserDocument } from "../Users/Users.Adapter.Mongoose";
import UserList from "../Users/Users";
import { DiscountType } from "../Discounts/Discounts.Interface";
import Discounts from "../Discounts/Discounts";
import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check";
import Templates from "../Templates/Templates";
import { CollaboratorSchedulesView } from "../Schedules/Schedules.Interface";
import ClientAccount from "../ClientAccounts/ClientAccount";
import SchedulesDisabledList from "../SchedulesDisabled/SchedulesDisabled";
import DiscountsEmail from "../DiscountsEmail/DiscountsEmail";
import SchedulesOverridesList from "../SchedulesOverrides/SchedulesOverrides";
import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface";

dayjs.locale("es");
dayjs.extend(isSameOrAfter);
dayjs.extend(isSameOrBefore);
dayjs.extend(weekday);

class AppointmentManager implements IAppointmentsManager {
    Appointments: AppointmentsAdapterMongoose;

    constructor() {
        this.Appointments = new AppointmentsAdapterMongoose();
    }

    public async setPaymentStatus(data: SetPaymentStatusParams): Promise<void> {
        const sessionUser = await UsersManager.users.findOne({
            _id: data.sessionUser,
        });

        if (!sessionUser) {
            throw new Error("No se ha encontrado el usuario o no tiene permisos para realizar esta acción");
        }

        const appointment = await this.Appointments.findOne({
            _id: data.appointmentId,
        });

        if (!appointment) {
            throw new Error("El turno no existe");
        }

        const companyCheck = await CompaniesManager.companies.findOne({
            _id: String(appointment.companyId),
        });

        if (!companyCheck) {
            throw new Error("La compañia no existe");
        }

        if (!(await EmployeesList.checkPermission(companyCheck, data.sessionUser, EmployeeRoles.ADMIN))) {
            throw new Error(NoPermissionMessage());
        }

        appointment.payment = data.status;
        await appointment.save();
    }

    public async countAppointmentsByMonth(data: CountAppointmentsByMonthParams): Promise<number> {
        const startMonth = dayjs(new Date(data.year, data.month, 1)).startOf("month");
        const endMonth = dayjs(new Date(data.year, data.month, 1)).endOf("month");

        const appointments = await this.Appointments.findByDateRange(
            startMonth.toDate(),
            endMonth.toDate(),
            data.companyId
        );

        return appointments.length;
    }

    public async changeEmployeeToOwner(data: ChangeEmployeeToOwnerParams): Promise<void> {
        const org = await CompaniesManager.companies.findOne({
            _id: data.companyId,
        });

        if (!org) {
            throw new Error("La compañia no existe");
        }

        const appointment = await this.Appointments.AppointmentList.findOne({
            _id: data.appointmentId,
        });

        if (!appointment) {
            throw new Error("El turno no existe");
        }

        const employee = await EmployeesList.employees.findOne({
            _id: String(appointment.employeeId),
        });

        if (!employee) {
            throw new Error("El empleado no existe");
        }

        const userEmployee = await UsersManager.users.findOne({
            _id: String(employee.userId),
        });

        if (!userEmployee) {
            throw new Error("El empleado no tiene usuario asociado");
        }

        const employeeOwner = await EmployeesList.employees.findOne({
            companyId: String(org._id),
            userId: String(org.ownerId),
        });

        if (!employeeOwner) {
            throw new Error("El dueño de la compañia no tiene colaborador asociado");
        }

        const appClient = await ClientsManager.clients.findOne({
            _id: String(appointment.clientId),
        });

        if (!appClient) {
            throw new Error("El cliente no existe");
        }

        const appService = await ServicesManager.services.findOne({
            _id: String(appointment.serviceId),
        });

        if (!appService) {
            throw new Error("El servicio no existe");
        }

        appointment.employeeId = String(employeeOwner.id);
        appointment.comments = String(
            appointment.comments + " Turno transferido de " + UsersManager.getUserFullName(userEmployee)
        ).trimStart();
        await appointment.save();

        //Si el turno es para el dia actual o posterior, enviar notificacion al dueño de la compañia
        if (dayjs(appointment.dateDay).isSameOrAfter(dayjs().startOf("day"))) {
            await SystemNoficationsList.createNotification({
                userId: String(org.ownerId),
                subject: "Turno transferido",
                message: `El turno del dia ${dayjs(appointment.dateDay).format(
                    "DD/MM/YYYY"
                )} de ${UsersManager.getUserFullName(userEmployee)} del servicio ${
                    appService.name
                }  para el cliente ${ClientsManager.getClientFullName(
                    appClient
                )} ha sido transferido al dueño de la compañia.`,
            });
        }

        //Actualizar el heatmap.
        const defaultFraction = Number(process.env.DEFAULT_COMPANY_SERVICE_FRACTION);

        const serviceCheck = await ServicesManager.services.findOne({
            _id: String(appointment.serviceId),
            companyId: String(appointment.companyId),
        });

        if (!serviceCheck) {
            throw new Error("El servicio no existe o no pertenece a la compañia.");
        }

        const availabilityConfig: HeatMapConfig = {
            companyId: String(appointment.companyId),
            serviceId: String(appointment.serviceId),
            employeeId: String(appointment.employeeId),
            heatMapDate: dayjs(appointment.start).startOf("day").toDate(),
            start: dayjs(appointment.start).toDate(),
            fraction: defaultFraction,
            length: isNull<number>(appointment.length, 1),
            serviceLimit: serviceCheck.limit,
            serviceLength: serviceCheck.length,
        };

        await this.updateHeatMap(availabilityConfig);
    }

    public async deleteAppointmentsByCompany(data: DeleteAppointmentsByCompanyParams): Promise<void> {
        await this.Appointments.AppointmentList.deleteMany({
            companyId: data.companyId,
        });
    }

    private getAppointmentTime(appointmentTime: number | undefined): number {
        if (appointmentTime) {
            return Math.abs(appointmentTime);
        }
        return parseInt(`${process.env.APPOINTMEENT_TIME}`) || 24;
    }

    private validateAppointmentTime(aTime: number | undefined, start: string | Date): boolean {
        let appointmentTime = this.getAppointmentTime(aTime);

        const checkDate = dayjs(start).clone().add(-appointmentTime, "hours");
        const nowDate = dayjs().clone();

        if (nowDate.isBefore(checkDate)) {
            return true;
        }

        return false;
    }

    private getCancellationTime(cancellationTime: number | undefined): number {
        if (cancellationTime) {
            return Math.abs(cancellationTime);
        }
        return parseInt(`${process.env.CANCELLATION_TIME}`) || 24;
    }

    private validateCancellationTime(aTime: number | undefined, start: string | Date): boolean {
        let cancellationTime = this.getCancellationTime(aTime);

        const checkDate = dayjs(start).clone().add(-cancellationTime, "hours");
        const nowDate = dayjs().clone();

        if (nowDate.isBefore(checkDate)) {
            return true;
        }

        return false;
    }

    public async createAppointment(data: CreateAppointmentParams): Promise<IAppointment> {
        const { validation = true, notification = true } = data;
        const sessionUser = await UsersManager.users.findOne({
            _id: data.sessionUser,
        });

        if (!sessionUser) {
            throw new Error("No se ha encontrado el usuario o no tiene permisos para realizar esta acción");
        }

        const companyCheck = await CompaniesManager.companies.findOne({
            _id: data.companyId,
        });

        if (!companyCheck) {
            throw new Error("La compañia no existe");
        }

        const canAdd = await MetricsList.canAddAppointment(companyCheck.ownerId);

        if (!canAdd) {
            throw new Error("Ha alcanzado el limite de turnos permitidos de acuerdo a su plan.");
        }

        const serviceCheck = await ServicesManager.services.findOne({
            _id: data.serviceId,
            companyId: data.companyId,
        });

        if (!serviceCheck) {
            throw new Error("El servicio no existe o no pertenece a la compañia.");
        }

        let servicePrice = serviceCheck.price;

        if (!data.price) {
            data.price = servicePrice;
        }

        if (!data.price) {
            throw new Error("Error al determinar el valor  del turno.");
        }

        data.length = isNull<number>(data.length, 1);

        if (!data.length) {
            throw new Error("Error al determinar la duración  del turno.");
        }

        const employeeCheck = await EmployeesList.employees.findOne({
            companyId: data.companyId,
            _id: data.employeeId,
        });

        if (!employeeCheck) {
            throw new Error("El empleado no existe o no pertenece a la compañia.");
        }

        if (!this.validateAppointmentTime(companyCheck.appointmentTime, data.start) && validation) {
            throw new Error(
                `No es posible reservar el turno con menos de ${this.getAppointmentTime(
                    companyCheck.appointmentTime
                )} horas de anticipación.`
            );
        }

        let checkClient = {} as IClientDocument | null;

        if (data.userId) {
            checkClient = await ClientsManager.clients.findOne({
                userId: data.userId,
                companyId: data.companyId,
            });

            if (!checkClient) {
                //Si el cliente no existe hay que crearlo.
                checkClient = await ClientsManager.createClientByUser({
                    userId: data.userId,
                    companyId: data.companyId,
                    sessionUser: data.sessionUser,
                });

                if (!checkClient) {
                    throw new Error("No se ha podido crear el cliente");
                }
            }

            if (!checkClient.status) {
                throw new Error("El cliente no se encuentra activo.");
            }
        } else if (data.clientId) {
            checkClient = await ClientsManager.clients.findOne({
                _id: data.clientId,
            });

            if (!checkClient) {
                throw new Error("El cliente no existe.");
            }
        }

        if (!checkClient) {
            throw new Error("Ha ocurrido un error al identificar el cliente.");
        }

        if (checkClient.banned) {
            throw new Error(`La reserva no se pudo completar. Por favor comuniquese con ${companyCheck.name}`);
        }

        const checkAppointment = await this.Appointments.findOne({
            companyId: data.companyId,
            serviceId: data.serviceId,
            employeeId: data.employeeId,
            clientId: String(checkClient.id),
            start: dayjs(data.start).toDate(),
        });

        if (checkAppointment) {
            throw new Error("Ya existe una reserva para la fecha y horario seleccionados");
        }

        const defaultFraction = Number(process.env.DEFAULT_COMPANY_SERVICE_FRACTION);

        const availabilityConfig: HeatMapConfig = {
            companyId: data.companyId,
            serviceId: data.serviceId,
            employeeId: data.employeeId,
            heatMapDate: dayjs(data.start).startOf("day").toDate(),
            start: dayjs(data.start).toDate(),
            fraction: defaultFraction,
            length: isNull<number>(data.length, 1),
            serviceLimit: serviceCheck.limit,
            serviceLength: serviceCheck.length,
        };

        if (validation) {
            const checkAvailable = await this.checkAvailability(availabilityConfig, data);

            if (checkAvailable === false) {
                throw new Error("No hay disponibilidad para el horario seleccionado.");
            }

            const checkSchedules = await this.checkSchedules(availabilityConfig);

            if (checkSchedules === false) {
                throw new Error("No hay disponibilidad para el dia o el horario seleccionado.");
            }

            const checkDisabled = await this.checkSchedulesDisabled(availabilityConfig);

            if (!checkDisabled) {
                throw new Error("El profesional no se encuentra disponible en el horario seleccionado.");
            }
        }

        data.comments = isNull<string>(data.comments, "");

        try {
            const start = dayjs(data.start).clone();
            const end = dayjs(data.start)
                .clone()
                .add(serviceCheck.length * isNull<number>(data.length, 1), "minutes");

            const startHour = start.clone().hour() + start.clone().minute() / 60;
            const endHour = end.clone().hour() + end.clone().minute() / 60;

            const newAppointment = await this.Appointments.create({
                companyId: data.companyId,
                serviceId: data.serviceId,
                employeeId: data.employeeId,
                clientId: String(checkClient._id),
                userId: checkClient.userId ? String(checkClient.userId) : undefined,
                start: dayjs(data.start).toDate(),
                startHour: startHour,
                endHour: endHour,
                dateDay: dayjs(data.start).clone().startOf("day").toDate(),
                price: data.price,
                length: data.length,
                comments: data.comments,
                sessionUser: data.sessionUser,
            });

            if (data.repeatId) {
                const repeat = await RepeatsList.repeats.findOne({
                    _id: data.repeatId,
                });

                if (repeat && repeat.discountId) {
                    const discount = await Discounts.discounts.discountsList.findOne({
                        _id: repeat.discountId,
                    });

                    if (discount) {
                        await this.applyDiscount({
                            appointmentId: String(newAppointment.id),
                            discountId: String(discount._id),
                            companyId: String(newAppointment.companyId),
                            sessionUser: String(data.sessionUser),
                        });
                    }
                }
            }

            await MetricsList.addAppointment({
                userId: companyCheck.ownerId,
                quantity: 1,
            });

            await this.updateHeatMap(availabilityConfig);

            await this.tryApplyDiscounts(newAppointment);

            if (notification && checkClient.userId) {
                //Envio la notificacion al cliente.
                await NotificationsManager.sendSystemNotification({
                userId: String(checkClient.userId),
                subject: "Turno reservado",
                message: `Se ha reservado un turno para ${serviceCheck.name} el día ${dayjs(
                        data.start
                    ).format("DD/MM/YYYY")} a las ${dayjs(data.start).format("HH:mm")}`,
                type: NotificationType.APPOINTMENT,
                code: String((newAppointment as any)._id)
            });

                //Intento enviar notificacion al cliente por Email
                try {
                    if (newAppointment.id) {
                        await this.sendEmailNotification({
                            appointmentId: String(newAppointment.id),
                            type: APPOINTMENT_NOTIFICATION_TYPE.CREATION,
                            sessionUser: String(companyCheck.ownerId),
                        });
                    }
                } catch (err) {
                    console.log(
                        `Error al enviar la notificacion por Email para  ${companyCheck.name}(${companyCheck._id}):`,
                        err
                    );
                }

                //Intento enviar notificacion al cliente por Whatsapp
                try {
                    if (newAppointment.id) {
                        await this.sendWapNotification({
                            appointmentId: String(newAppointment.id),
                            type: APPOINTMENT_NOTIFICATION_TYPE.CREATION,
                            sessionUser: String(companyCheck.ownerId),
                        });
                    }
                } catch (err) {
                    console.log(
                        `Error al enviar la notificacion por Whatsapp para  ${companyCheck.name}(${companyCheck._id}):`,
                        err
                    );
                }
            }

            //Envio la notificacion al profesional.
            await NotificationsManager.sendSystemNotification({
                userId: String(employeeCheck.userId),
                subject: "Turno reservado",
                message: `${ClientsManager.getClientFullName(checkClient)} ha reservado un turno para ${
                    serviceCheck.name
                }  el día ${dayjs(data.start).format("DD/MM/YYYY")} a las ${dayjs(data.start).format(
                    "HH:mm"
                )}`,
                type: NotificationType.APPOINTMENT,
                code: String((newAppointment as any)._id)
            });

            return newAppointment;
        } catch (err) {
            console.log(err);
            throw new Error("Ha ocurrido un error al crear el turno.");
        }
    }

    private async tryApplyDiscounts(appointment: IAppointment) {
        //Intenta aplicar un descuento al turno si esta especificado en DiscountsEmail
        // y la fecha coincide con el rango especificado.

        try {
            //Obtener el mail del cliente.
            const client = await ClientsManager.clients.findOne({
                _id: String(appointment.clientId),
            });

            if (!client) {
                return;
            }

            let email = client.email;

            const userClient = await UsersManager.users.findOne({
                _id: String(client.userId),
            });

            if (userClient) {
                email = userClient.email;
            }

            //Obtener los descuentos del cliente.
            const existDiscount = await DiscountsEmail.discountsEmail.findOne({
                email: email,
                companyId: String(appointment.companyId),
                searchDate: appointment.start,
            });

            if (!existDiscount) {
                return;
            }

            //Ahora aplicamos el descuento
            await this.applyDiscount({
                appointmentId: String(appointment.id),
                discountId: String(existDiscount.discountId),
                companyId: String(appointment.companyId),
                sessionUser: String(appointment.userId),
            });
        } catch (err) {
            console.log(err);
        }
    }

    public async paginate(filters: PaginateAppointmentsParams): Promise<PaginateAppointmentsResults> {
        return await this.Appointments.paginate(filters);
    }


    public async moveAppointmentForce(data: MoveAppointmentParams): Promise<void> {
        await validateSessionUser({ sessionUser: data.sessionUser || "" });

        const appointment: any = await this.Appointments.findOne({
            _id: data.id,
        });

        if (!appointment) {
            throw new Error("El turno no existe");
        }

        await validatePermissionsByCompany({ companyId: String(appointment.companyId), sessionUser: data.sessionUser || "" });

        const serviceCheck = await ServicesManager.services.findOne({
            _id: String(appointment.serviceId),
            companyId: String(appointment.companyId),
        });

        if (!serviceCheck) {
            throw new Error("El servicio no existe o no pertenece a la compañia.");
        }

        // Get HeatMapConfig of the original date before changing it
        const originalConfig = this.getHeatMapConfig(appointment, serviceCheck.length);

        // Update the appointment's start times
        const newStart = dayjs(data.newStart).clone();
        appointment.start = newStart.toDate();
        appointment.dateDay = newStart.clone().startOf("day").toDate();
        appointment.startHour = newStart.clone().hour() + newStart.clone().minute() / 60;
        
        const end = newStart.clone().add(serviceCheck.length * (appointment.length ? appointment.length : 1), "minutes");
        appointment.endHour = end.clone().hour() + end.clone().minute() / 60;
        
        // Save appointment (directly, bypassing notifications & checks)
        await appointment.save();

        // Compute the new HeatMapConfig
        const newConfig = this.getHeatMapConfig(appointment, serviceCheck.length);

        // Update both heatmaps
        await this.updateHeatMap(originalConfig);
        await this.updateHeatMap(newConfig);
    }

    public async changeServiceForce(data: ChangeServiceParams): Promise<void> {
        await validateSessionUser({ sessionUser: data.sessionUser || "" });

        const appointment: any = await this.Appointments.findOne({
            _id: data.id,
        });

        if (!appointment) {
            throw new Error("El turno no existe");
        }

        await validatePermissionsByCompany({ companyId: String(appointment.companyId), sessionUser: data.sessionUser || "" });

        const oldServiceCheck = await ServicesManager.services.findOne({
            _id: String(appointment.serviceId),
            companyId: String(appointment.companyId),
        });

        if (!oldServiceCheck) {
            throw new Error("El servicio original no existe o no pertenece a la compañia.");
        }

        const newServiceCheck = await ServicesManager.services.findOne({
            _id: data.newServiceId,
            companyId: String(appointment.companyId),
        });

        if (!newServiceCheck) {
            throw new Error("El nuevo servicio no existe o no pertenece a la compañia.");
        }

        const newEmployeeCheck = await EmployeesList.employees.findOne({
            _id: data.newEmployeeId,
            companyId: String(appointment.companyId),
        });

        if (!newEmployeeCheck) {
            throw new Error("El nuevo profesional no existe o no pertenece a la compañia.");
        }

        // Get HeatMapConfig of the original appointment before changing it
        const originalConfig = this.getHeatMapConfig(appointment, oldServiceCheck.length);

        // Update appointment details
        appointment.serviceId = data.newServiceId;
        appointment.employeeId = data.newEmployeeId;
        appointment.price = newServiceCheck.price;

        // Recalculate endHour based on new service length
        const startHour = appointment.startHour;
        const newEndHour = startHour + (newServiceCheck.length * (appointment.length || 1)) / 60;
        appointment.endHour = newEndHour;

        // Save appointment
        await appointment.save();

        // If there is a discount, reapply it to update the finalPrice, else just save.
        if (appointment.discountId) {
            await this.applyDiscount({
                appointmentId: String(appointment._id),
                discountId: String(appointment.discountId),
                companyId: String(appointment.companyId),
                sessionUser: data.sessionUser
            });
        }

        // Compute the new HeatMapConfig
        const newConfig = this.getHeatMapConfig(appointment, newServiceCheck.length);

        // Update both heatmaps
        await this.updateHeatMap(originalConfig);
        await this.updateHeatMap(newConfig);
    }

    public buildHeatMapEmpty(config: HeatMapConfig): IHeatMap {
        const heatMapData: Array<HeatMapItem> = [];
        const startDate = dayjs(config.heatMapDate).clone().startOf("day");
        const endDate = dayjs(config.heatMapDate).clone().endOf("day");
        let currentDate = startDate.clone();

        while (currentDate.isBefore(endDate)) {
            heatMapData.push({
                keyDate: currentDate.toDate(),
                count: 0,
            });

            currentDate = currentDate.clone().add(config.fraction, "minutes");
        }

        return {
            companyId: config.companyId,
            employeeId: config.employeeId,
            serviceId: config.serviceId,
            heatMap: heatMapData,
            heatMapDate: startDate.clone().toDate(),
        };
    }

    public async getHeatMapToCheckAvailability(config: HeatMapConfig): Promise<IHeatMap> {
        //TODO: debe devolver un heatmap unificado entre todos los servicios del empleado.
        //Podria crear un heatmap cada 15 minutos. Siendo esta la menor unidad de reserva.
        //Y luego rellenarlo segun cada servicio sobre el que tenga reservas hechas el empleado.

        const globalHeatMap = this.buildHeatMapEmpty({
            companyId: config.companyId,
            employeeId: config.employeeId,
            serviceId: "", //no importa el servicio. es global.
            heatMapDate: config.heatMapDate,
            start: config.start,
            fraction: config.fraction, //minima fracción de tiempo
            length: 1,
            serviceLimit: -1, //no lo usa
            serviceLength: 15,
        });

        //console.table(globalHeatMap.heatMap);

        //Busco todos los heatmaps para todos los servicios.
        let heatMaps = await HeatMapList.find({
            companyId: config.companyId,
            employeeId: config.employeeId,
            heatMapDate: config.heatMapDate,
        });

        if (!globalHeatMap.heatMap) {
            return globalHeatMap;
        }

        for (let i = 0; i < globalHeatMap.heatMap.length; i++) {
            const currentHeatMap = globalHeatMap.heatMap[i];

            for (const serviceItem of heatMaps) {
                if (!serviceItem.heatMap) {
                    continue;
                }
                const serviceHeatMap = serviceItem.heatMap[i];
                currentHeatMap.count += serviceHeatMap.count;
            }
            const repeatsCount = await RepeatsList.countRepeats({
                companyId: config.companyId,
                employeeId: config.employeeId,
                countDate: currentHeatMap.keyDate,
                heatMapFraction: config.fraction,
            });
            currentHeatMap.count += repeatsCount;
        }

        //console.table(globalHeatMap.heatMap);

        return globalHeatMap;
    }
    public async getHeatMap(config: HeatMapConfig): Promise<IHeatMap> {
        const appointments = await this.Appointments.find({
            companyId: config.companyId,
            employeeId: config.employeeId,
            serviceId: config.serviceId,
            dateDay: config.heatMapDate,
        });

        let heatMap = this.buildHeatMapEmpty(config);

        if (!heatMap.heatMap) {
            throw new Error("El mapa de calor no existe");
        }

        for (const appointment of appointments) {
            const endDateTime = dayjs(appointment.start)
                .clone()
                .add(config.serviceLength * isNull<number>(appointment.length, 1), "minutes");
            const startIndex = this.getHeatMapPosition(heatMap, dayjs(appointment.start).clone().toDate());

            for (let i = startIndex; i < heatMap.heatMap.length; i++) {
                const heatMapItem = heatMap.heatMap[i];

                const currentDate = dayjs(heatMap.heatMap[i].keyDate).clone();

                if (currentDate.isSameOrAfter(endDateTime)) {
                    break;
                }

                heatMapItem.count += 1;
            }
        }

        return heatMap;
    }

    public async updateHeatMap(config: HeatMapConfig): Promise<void> {
        const heatMap = await this.getHeatMap(config);

        if (!heatMap.heatMap) {
            throw new Error("El mapa de calor no existe");
        }

        await HeatMapList.deleteHeatMapByDate({
            companyId: config.companyId,
            employeeId: config.employeeId,
            serviceId: config.serviceId,
            heatMapDate: dayjs(config.start).startOf("day").toDate(),
        });

        await HeatMapList.createHeatMap({
            companyId: config.companyId,
            employeeId: config.employeeId,
            serviceId: config.serviceId,
            heatmap: heatMap,
            heatMapDate: dayjs(config.start).startOf("day").toDate(),
        });
    }

    public async checkSchedulesDisabled(data: HeatMapConfig): Promise<boolean> {
        const check = await SchedulesDisabledList.exists({
            employeeId: data.employeeId,
            companyId: data.companyId,
            dateDay: dayjs(data.start).toDate(),
        });

        if (check) {
            return false;
        }

        return true;
    }

    public async checkSchedules(data: HeatMapConfig): Promise<boolean> {
        const start = dayjs(data.start);
        const end = start.clone().add(data.serviceLength * isNull<number>(data.length, 1), "minutes");
        const weekDay = start.weekday() + 1;
        const checkFrom = start.format("HH:mm");
        const checkTo = end.format("HH:mm");

        //chequear si hay un horario excepcional.
        const hasOverride = await SchedulesOverridesList.findOne({
            employeeId: data.employeeId,
            companyId: data.companyId,
            date: dayjs(data.start).startOf("day").toDate(),
        });

        if (!hasOverride) {
            //chequear que la fecha este habilitada.
            const check = await SchedulesEnabledList.exists({
                employeeId: data.employeeId,
                companyId: data.companyId,
                dateDay: dayjs(data.start).startOf("day").toDate(),
            });

            if (!check) {
                return false;
            }
        }

        let checkColaborator = false;

        if (hasOverride) {
            checkColaborator = await SchedulesOverridesList.isAvailable({
                employeeId: data.employeeId,
                companyId: data.companyId,
                dateDay: dayjs(data.start).startOf("day").toDate(),
                from: checkFrom,
                to: checkTo,
            });
        } else {
            //chequear que el colaborador este disponible.
            checkColaborator = await SchedulesList.isAvailable({
                employeeId: data.employeeId,
                companyId: data.companyId,
                weekDay: weekDay,
                from: checkFrom,
                to: checkTo,
            });
        }

        if (!checkColaborator) {
            return false;
        }

        return true;
    }

    public async checkAvailability(config: HeatMapConfig, data: CreateAppointmentParams): Promise<boolean> {
        const heatMap = await this.getHeatMapToCheckAvailability(config);

        if (!heatMap.heatMap) {
            return false;
        }

        const endDateTime = dayjs(data.start)
            .clone()
            .add(config.serviceLength * isNull<number>(data.length, 1), "minutes");
        const startIndex = this.getHeatMapPosition(heatMap, dayjs(data.start).clone().toDate());

        if (startIndex < 0) {
            return false;
        }

        for (let i = startIndex; i < heatMap.heatMap.length; i++) {
            const heatMapItem = heatMap.heatMap[i];

            const currentDate = dayjs(heatMap.heatMap[i].keyDate).clone();

            if (currentDate.isSame(endDateTime)) {
                break;
            }

            /* console.log(
                "heatmapCount:",
                heatMapItem.count,
                "serviceLimit:",
                config.serviceLimit,
                "check:",
                heatMapItem.count >= config.serviceLimit
            ); */

            if (heatMapItem.count >= config.serviceLimit) {
                return false;
            }
        }

        return true;
    }

    private getHeatMapPosition(heatMap: IHeatMap, date: Date): number {
        if (!heatMap.heatMap) {
            return -1;
        }
        for (let i = 0; i < heatMap.heatMap.length; i++) {
            const currentDate = dayjs(heatMap.heatMap[i].keyDate).clone();
            if (currentDate.isSame(dayjs(date).clone())) {
                return i;
            }
        }

        return -1;
    }

    public async findAppointmentsAdminByDate(
        data: FindAppointmentsParams
    ): Promise<AppointmentAdminByDateView> {
        const returnData: AppointmentEvent[] = [];

        const filterData: FindAppointmentsParams = {};

        if (data.dateDay) {
            filterData.dateDay = dayjs(data.dateDay).clone().startOf("day").toDate();
        }

        if (data.companyId) {
            filterData.companyId = data.companyId;
        }

        if (data.employeeId) {
            filterData.employeeId = data.employeeId;
        }

        const appointments = await this.Appointments.find(filterData);

        for (const appointment of appointments) {
            const client = await ClientsManager.clients.findOne({
                _id: String(appointment.clientId),
            });

            if (!client) {
                continue;
            }

            const userClient = await UsersManager.users.findOne({
                _id: client.userId ? String(client.userId) : undefined,
            });

            const employee = await EmployeesList.employees.findOne({
                _id: String(appointment.employeeId),
            });

            if (!employee) {
                continue;
            }

            const clientName = joinStrings([client.firstName, client.lastName], " ");
            const clientUserName = userClient
                ? joinStrings([userClient.firstName, userClient.lastName], clientName)
                : joinStrings([client.firstName, client.lastName], " ");
            const clientAvatar = userClient
                ? getAvatar(client.userId, userClient.avatar, clientUserName)
                : clientUserName;

            const discountData = await Discounts.getDiscountData(
                appointment.discountId,
                appointment.companyId
            );

            returnData.push({
                appointmentId: isNull<string>(appointment.id, ""),
                clientId: String(appointment.clientId),
                serviceId: String(appointment.serviceId),
                employeeId: String(appointment.employeeId),
                startTime: appointment.startHour,
                endTime: appointment.endHour,
                title: isNull<string>(clientName, clientUserName),
                avatar: clientAvatar,
                color: isNull<string>(employee.calendarColor, "#ff9900"),
                repeatId: "",
                discountId: isNull<string>(appointment.discountId, ""),
                discountName: isNull<string>(discountData.name, ""),
                discountCode: isNull<string>(discountData.code, ""),
                discountValue: isNull<number>(discountData.value, 0),
                discountType: isNull<DiscountType>(discountData.type, DiscountType.PERCENTAGE),
                payment: appointment.payment,
            });
        }

        const searchWeekDay = dayjs(data.dateDay).clone().weekday() + 1;

        //Agregar las repeticiones.
        const repeticiones = await RepeatsList.repeats.find({
            companyId: data.companyId,
            weekDay: searchWeekDay,
        });

        for (const repeticion of repeticiones) {
            const repClient = await ClientsManager.clients.findOne({
                _id: String(repeticion.clientId),
            });

            if (!repClient) {
                continue;
            }

            let userRepClient: IUserDocument | null = null;

            if (repClient.userId) {
                userRepClient = await UsersManager.users.findOne({
                    _id: String(repClient.userId),
                });

                if (!userRepClient) {
                    continue;
                }
            }

            const repEmployee = await EmployeesList.employees.findOne({
                _id: String(repeticion.employeeId),
            });

            if (!repEmployee) {
                continue;
            }

            const repService = await ServicesManager.services.findOne({
                _id: String(repeticion.serviceId),
            });

            if (!repService) {
                continue;
            }

            const searchHour = Math.floor(repeticion.startHour);
            const searchMinute = (repeticion.startHour - searchHour) * 60;
            const searchDJS = dayjs(data.dateDay)
                .clone()
                .startOf("day")
                .hour(searchHour)
                .minute(searchMinute);
            const searchDate = searchDJS.clone().toDate();

            if (searchDJS.isSameOrBefore(dayjs())) {
                continue;
            }

            //Solo hago push si la repeticion no tiene el turno creado.
            const appointmentCreated = await this.Appointments.findOne({
                companyId: data.companyId,
                clientId: String(repeticion.clientId),
                serviceId: String(repeticion.serviceId),
                employeeId: String(repeticion.employeeId),
                start: searchDate,
            });

            if (appointmentCreated) {
                continue;
            }

            //Tengo que chequear que la fecha no este en disabledDates.
            const checkDisabledDate = dayjs(data.dateDay)
                .clone()
                .startOf("day")
                .hour(searchHour)
                .minute(searchMinute);

            if (repeticion.disabledDates) {
                if (RepeatsList.existDisabledDate(checkDisabledDate.toDate(), repeticion.disabledDates)) {
                    continue;
                }
            }

            const discountData = await Discounts.getDiscountData(repeticion.discountId, repeticion.companyId);

            returnData.push({
                appointmentId: "",
                clientId: String(repeticion.clientId),
                serviceId: String(repeticion.serviceId),
                employeeId: String(repeticion.employeeId),
                startTime: repeticion.startHour,
                endTime: repeticion.startHour + repService.length / 60,
                title: ClientsManager.getClientFullName(repClient),
                avatar: getAvatar(
                    String(repClient.userId),
                    userRepClient ? userRepClient.avatar : "",
                    ClientsManager.getClientFullName(repClient)
                ),
                color: isNull<string>(repEmployee.calendarColor, "#ff9900"),
                repeatId: String(repeticion.id),
                payment: false,
                discountId: isNull<string>(undefined, ""),
                discountName: isNull<string>(discountData.name, ""),
                discountCode: isNull<string>(discountData.code, ""),
                discountValue: isNull<number>(discountData.value, 0),
                discountType: isNull<DiscountType>(discountData.type, DiscountType.PERCENTAGE),
            });
        }

        //get min startTime hour from returnData
        let minStartTime = 24;
        for (const event of returnData) {
            if (event.startTime < minStartTime) {
                minStartTime = event.startTime;
            }
        }

        return {
            events: returnData,
            from: Math.floor(minStartTime),
            to: 24,
        };
    }

    public async getAppointmentEvent(data: GetAppointmentEventParams): Promise<AppointmentEventByClient> {
        const sessionUser = UserList.users.findOne({
            _id: data.sessionUser,
        });

        if (!sessionUser) {
            throw new Error("El usuario no existe");
        }

        const appointment = await this.Appointments.AppointmentList.findOne({
            _id: data.appointmentId,
        });

        if (!appointment) {
            throw new Error("El turno no existe");
        }

        const client = await ClientsManager.clients.findOne({
            _id: String(appointment.clientId),
        });

        if (!client) {
            throw new Error("El cliente no existe");
        }

        const userClient = await UsersManager.users.findOne({
            _id: client.userId ? String(client.userId) : undefined,
        });

        const employee = await EmployeesList.employees.findOne({
            _id: String(appointment.employeeId),
        });

        if (!employee) {
            throw new Error("El colaborador no existe");
        }

        const userEmployee = await UsersManager.users.findOne({
            _id: String(employee.userId),
        });

        if (!userEmployee) {
            throw new Error("No se ha encontrado el usuario del colaborador");
        }

        const service = await ServiceList.services.findOne({
            _id: String(appointment.serviceId),
        });

        if (!service) {
            throw new Error("El servicio no existe");
        }

        const clientName = joinStrings([client.firstName, client.lastName], " ");
        const clientUserName = userClient
            ? joinStrings([userClient.firstName, userClient.lastName], clientName)
            : joinStrings([client.firstName, client.lastName], " ");
        const clientAvatar = userClient
            ? getAvatar(client.userId, userClient.avatar, clientUserName)
            : clientUserName;

        const employeeFirstName = isNull<string>(userEmployee.firstName, "");
        const employeeLastName = isNull<string>(userEmployee.lastName, "");
        const employeeFullName = joinStrings([employeeFirstName, employeeLastName], " ");

        const employeeAvatar = userEmployee
            ? getAvatar(employee.userId, userEmployee.avatar, employeeFullName)
            : clientUserName;

        const discountData = await Discounts.getDiscountData(appointment.discountId, appointment.companyId);

        let ctaBalance = 0;
        try {
            ctaBalance = await ClientAccount.getAvailableAmount({
                clientId: appointment.clientId,
                sessionUser: data.sessionUser,
            });
        } catch (e) {
            // Ignore error if user does not have payments plan
        }

        return {
            id: appointment.id,
            companyId: String(appointment.companyId),
            clientId: appointment.clientId,
            clientName: clientUserName,
            serviceId: appointment.serviceId,
            serviceName: service.name,
            serviceDescription: service.description,
            collaboratorId: employee.id,
            collaboratorName: employeeFullName,
            collaboratorAvatar: employeeAvatar,
            appointmentDate: dayjs(appointment.start).toISOString(),
            startTime: appointment.startHour,
            endTime: appointment.endHour,
            title: isNull<string>(clientName, clientUserName),
            avatar: clientAvatar,
            color: isNull<string>(employee.calendarColor, "#ff9900"),
            comments: appointment.comments,
            price: appointment.price,
            length: appointment.length,
            payment: appointment.payment,
            discountId: isNull<string>(appointment.discountId, ""),
            discountName: isNull<string>(discountData.name, ""),
            discountCode: isNull<string>(discountData.code, ""),
            discountValue: isNull<number>(discountData.value, 0),
            discountType: isNull<DiscountType>(discountData.type, DiscountType.PERCENTAGE),
            clientCtaCteBalance: ctaBalance,
        };
    }

    public async findAppointmentsAdminByClient(
        data: FindAppointmentsParams
    ): Promise<AppointmentAdminByClientView> {
        const returnData: AppointmentEventByClient[] = [];

        let filterData: Object = {
            dateDay: {
                $gte: dayjs(new Date()).clone().startOf("day").toDate(),
            },
            companyId: data.companyId,
            clientId: data.clientId,
        };

        if (data._id) {
            filterData = {
                _id: data._id,
            };
        }

        const appointments = await this.Appointments.AppointmentList.find(filterData);

        for (const appointment of appointments) {
            const client = await ClientsManager.clients.findOne({
                _id: String(appointment.clientId),
            });

            if (!client) {
                continue;
            }

            const userClient = await UsersManager.users.findOne({
                _id: client.userId ? String(client.userId) : undefined,
            });

            const employee = await EmployeesList.employees.findOne({
                _id: String(appointment.employeeId),
            });

            if (!employee) {
                continue;
            }

            const userEmployee = await UsersManager.users.findOne({
                _id: String(employee.userId),
            });

            if (!userEmployee) {
                continue;
            }

            const service = await ServiceList.services.findOne({
                _id: String(appointment.serviceId),
            });

            if (!service) {
                continue;
            }

            const clientName = joinStrings([client.firstName, client.lastName], " ");
            const clientUserName = userClient
                ? joinStrings([userClient.firstName, userClient.lastName], clientName)
                : joinStrings([client.firstName, client.lastName], " ");
            const clientAvatar = userClient
                ? getAvatar(client.userId, userClient.avatar, clientUserName)
                : clientUserName;

            const employeeFirstName = isNull<string>(userEmployee.firstName, "");
            const employeeLastName = isNull<string>(userEmployee.lastName, "");
            const employeeFullName = joinStrings([employeeFirstName, employeeLastName], " ");

            const employeeAvatar = userEmployee
                ? getAvatar(employee.userId, userEmployee.avatar, employeeFullName)
                : clientUserName;

            const discountData = await Discounts.getDiscountData(
                appointment.discountId,
                appointment.companyId
            );

            returnData.push({
                id: appointment.id,
                companyId: String(appointment.companyId),
                clientId: appointment.clientId,
                clientName: clientUserName,
                serviceId: appointment.serviceId,
                serviceName: service.name,
                serviceDescription: service.description,
                collaboratorId: employee.id,
                collaboratorName: employeeFullName,
                collaboratorAvatar: employeeAvatar,
                appointmentDate: dayjs(appointment.start).toISOString(),
                startTime: appointment.startHour,
                endTime: appointment.endHour,
                title: isNull<string>(clientName, clientUserName),
                avatar: clientAvatar,
                color: isNull<string>(employee.calendarColor, "#ff9900"),
                comments: appointment.comments,
                price: appointment.price,
                length: appointment.length,
                payment: appointment.payment,
                discountId: isNull<string>(appointment.discountId, ""),
                discountName: isNull<string>(discountData.name, ""),
                discountCode: isNull<string>(discountData.code, ""),
                discountValue: isNull<number>(discountData.value, 0),
                discountType: isNull<DiscountType>(discountData.type, DiscountType.PERCENTAGE),
                clientCtaCteBalance: 0,
            });
        }

        return {
            events: returnData,
        };
    }

    public async findAppointmentsByUser(
        data: FindAppointmentsByUserParams
    ): Promise<AppointmentAdminByClientView> {
        const returnData: AppointmentEventByClient[] = [];

        let filterData: Object = {
            dateDay: {
                $gte: dayjs(new Date()).clone().startOf("day").toDate(),
            },
            userId: data.sessionUser,
        };

        const appointments = await this.Appointments.AppointmentList.find(filterData).sort({
            start: 1,
        });

        for (const appointment of appointments) {
            const client = await ClientsManager.clients.findOne({
                _id: String(appointment.clientId),
            });

            if (!client) {
                continue;
            }

            const userClient = await UsersManager.users.findOne({
                _id: client.userId ? String(client.userId) : undefined,
            });

            const employee = await EmployeesList.employees.findOne({
                _id: String(appointment.employeeId),
            });

            if (!employee) {
                continue;
            }

            const userEmployee = await UsersManager.users.findOne({
                _id: String(employee.userId),
            });

            if (!userEmployee) {
                continue;
            }

            const service = await ServiceList.services.findOne({
                _id: String(appointment.serviceId),
            });

            if (!service) {
                continue;
            }

            const clientName = joinStrings([client.firstName, client.lastName], " ");
            const clientUserName = userClient
                ? joinStrings([userClient.firstName, userClient.lastName], clientName)
                : joinStrings([client.firstName, client.lastName], " ");
            const clientAvatar = userClient
                ? getAvatar(client.userId, userClient.avatar, clientUserName)
                : clientUserName;

            const employeeFirstName = isNull<string>(userEmployee.firstName, "");
            const employeeLastName = isNull<string>(userEmployee.lastName, "");
            const employeeFullName = joinStrings([employeeFirstName, employeeLastName], " ");

            const employeeAvatar = userEmployee
                ? getAvatar(employee.userId, userEmployee.avatar, employeeFullName)
                : clientUserName;

            const discountData = await Discounts.getDiscountData(
                appointment.discountId,
                appointment.companyId
            );

            returnData.push({
                id: appointment.id,
                companyId: String(appointment.companyId),
                clientId: appointment.clientId,
                clientName: clientUserName,
                serviceId: appointment.serviceId,
                serviceName: service.name,
                serviceDescription: service.description,
                collaboratorId: employee.id,
                collaboratorName: employeeFullName,
                collaboratorAvatar: employeeAvatar,
                appointmentDate: dayjs(appointment.start).toISOString(),
                startTime: appointment.startHour,
                endTime: appointment.endHour,
                title: isNull<string>(clientName, clientUserName),
                avatar: clientAvatar,
                color: isNull<string>(employee.calendarColor, "#ff9900"),
                comments: appointment.comments,
                price: appointment.price,
                length: appointment.length,
                payment: appointment.payment,

                discountId: isNull<string>(appointment.discountId, ""),
                discountName: isNull<string>(discountData.name, ""),
                discountCode: isNull<string>(discountData.code, ""),
                discountValue: isNull<number>(discountData.value, 0),
                discountType: isNull<DiscountType>(discountData.type, DiscountType.PERCENTAGE),
                clientCtaCteBalance: 0,
            });
        }

        return {
            events: returnData,
        };
    }

    public async findAppointmentsByCollaborator(
        data: FindAppointmentsByCollaboratorParams
    ): Promise<AppointmentAdminByClientView> {
        const returnData: AppointmentEventByClient[] = [];

        const employeeProfiles = await EmployeesList.employees.find({
            userId: data.sessionUser,
        });

        if (!employeeProfiles || employeeProfiles.length === 0) {
            return { events: returnData };
        }

        const employeeIds = employeeProfiles.map((e: any) => e.id || e._id);

        let filterData: any = {
            dateDay: {
                $gte: dayjs(new Date()).clone().startOf("day").toDate(),
            },
            employeeId: { $in: employeeIds },
        };

        const appointments = await this.Appointments.AppointmentList.find(filterData).sort({
            start: 1,
        });

        for (const appointment of appointments) {
            const client = await ClientsManager.clients.findOne({
                _id: String(appointment.clientId),
            });

            if (!client) {
                continue;
            }

            const userClient = await UsersManager.users.findOne({
                _id: client.userId ? String(client.userId) : undefined,
            });

            const employee = employeeProfiles.find((e: any) => String(e.id || e._id) === String(appointment.employeeId));

            if (!employee) {
                continue;
            }

            const userEmployee = await UsersManager.users.findOne({
                _id: String(employee.userId),
            });

            if (!userEmployee) {
                continue;
            }

            const service = await ServiceList.services.findOne({
                _id: String(appointment.serviceId),
            });

            if (!service) {
                continue;
            }

            const clientName = joinStrings([client.firstName, client.lastName], " ");
            const clientUserName = userClient
                ? joinStrings([userClient.firstName, userClient.lastName], clientName)
                : joinStrings([client.firstName, client.lastName], " ");
            const clientAvatar = userClient
                ? getAvatar(client.userId, userClient.avatar, clientUserName)
                : clientUserName;

            const employeeFirstName = isNull<string>(userEmployee.firstName, "");
            const employeeLastName = isNull<string>(userEmployee.lastName, "");
            const employeeFullName = joinStrings([employeeFirstName, employeeLastName], " ");

            const employeeAvatar = userEmployee
                ? getAvatar(employee.userId, userEmployee.avatar, employeeFullName)
                : clientUserName;

            const discountData = await Discounts.getDiscountData(
                appointment.discountId,
                appointment.companyId
            );

            returnData.push({
                id: appointment.id,
                companyId: String(appointment.companyId),
                clientId: appointment.clientId,
                clientName: clientUserName,
                serviceId: appointment.serviceId,
                serviceName: service.name,
                serviceDescription: service.description,
                collaboratorId: String(employee.id || (employee as any)._id),
                collaboratorName: employeeFullName,
                collaboratorAvatar: employeeAvatar,
                appointmentDate: dayjs(appointment.start).toISOString(),
                startTime: appointment.startHour,
                endTime: appointment.endHour,
                title: isNull<string>(clientName, clientUserName),
                avatar: clientAvatar,
                color: isNull<string>(employee.calendarColor, "#ff9900"),
                comments: appointment.comments,
                price: appointment.price,
                length: appointment.length,
                payment: appointment.payment,

                discountId: isNull<string>(appointment.discountId, ""),
                discountName: isNull<string>(discountData.name, ""),
                discountCode: isNull<string>(discountData.code, ""),
                discountValue: isNull<number>(discountData.value, 0),
                discountType: isNull<DiscountType>(discountData.type, DiscountType.PERCENTAGE),
                clientCtaCteBalance: 0,
            });
        }

        return {
            events: returnData,
        };
    }


    public async findAppointmentsByUserPaginated(
        data: FindAppointmentsByUserPaginatedParams
    ): Promise<PaginatedAppointmentEventByClientResult> {
        const returnData: AppointmentEventByClient[] = [];

        let filterData: Object = {
            dateDay: {
                $gte: dayjs(new Date()).clone().startOf("day").toDate(),
            },
            userId: data.sessionUser,
        };

        const total = await this.Appointments.AppointmentList.countDocuments(filterData);
        const limit = data.limit || 10;
        const page = data.page || 1;
        const skip = (page - 1) * limit;

        const appointments = await this.Appointments.AppointmentList.find(filterData)
            .sort({ start: 1 })
            .skip(skip)
            .limit(limit);

        for (const appointment of appointments) {
            const client = await ClientsManager.clients.findOne({
                _id: String(appointment.clientId),
            });

            if (!client) {
                continue;
            }

            const userClient = await UsersManager.users.findOne({
                _id: client.userId ? String(client.userId) : undefined,
            });

            const employee = await EmployeesList.employees.findOne({
                _id: String(appointment.employeeId),
            });

            if (!employee) {
                continue;
            }

            const userEmployee = await UsersManager.users.findOne({
                _id: String(employee.userId),
            });

            if (!userEmployee) {
                continue;
            }

            const service = await ServiceList.services.findOne({
                _id: String(appointment.serviceId),
            });

            if (!service) {
                continue;
            }

            const clientName = joinStrings([client.firstName, client.lastName], " ");
            const clientUserName = userClient
                ? joinStrings([userClient.firstName, userClient.lastName], clientName)
                : joinStrings([client.firstName, client.lastName], " ");
            const clientAvatar = userClient
                ? getAvatar(client.userId, userClient.avatar, clientUserName)
                : clientUserName;

            const employeeFirstName = isNull<string>(userEmployee.firstName, "");
            const employeeLastName = isNull<string>(userEmployee.lastName, "");
            const employeeFullName = joinStrings([employeeFirstName, employeeLastName], " ");

            const employeeAvatar = userEmployee
                ? getAvatar(employee.userId, userEmployee.avatar, employeeFullName)
                : clientUserName;

            const discountData = await Discounts.getDiscountData(
                appointment.discountId,
                appointment.companyId
            );

            returnData.push({
                id: appointment.id,
                companyId: String(appointment.companyId),
                clientId: appointment.clientId,
                clientName: clientUserName,
                serviceId: appointment.serviceId,
                serviceName: service.name,
                serviceDescription: service.description,
                collaboratorId: employee.id,
                collaboratorName: employeeFullName,
                collaboratorAvatar: employeeAvatar,
                appointmentDate: dayjs(appointment.start).toISOString(),
                startTime: appointment.startHour,
                endTime: appointment.endHour,
                title: isNull<string>(clientName, clientUserName),
                avatar: clientAvatar,
                color: isNull<string>(employee.calendarColor, "#ff9900"),
                comments: appointment.comments,
                price: appointment.price,
                length: appointment.length,
                payment: appointment.payment,

                discountId: isNull<string>(appointment.discountId, ""),
                discountName: isNull<string>(discountData.name, ""),
                discountCode: isNull<string>(discountData.code, ""),
                discountValue: isNull<number>(discountData.value, 0),
                discountType: isNull<DiscountType>(discountData.type, DiscountType.PERCENTAGE),
                clientCtaCteBalance: 0,
            });
        }

        return {
            data: returnData,
            page,
            pages: Math.ceil(total / limit),
        };
    }

    
public async findAppointmentsByCollaboratorPaginated(
        data: FindAppointmentsByCollaboratorPaginatedParams
    ): Promise<PaginatedAppointmentEventByClientResult> {
        const returnData: AppointmentEventByClient[] = [];

        const employeeProfiles = await EmployeesList.employees.find({
            userId: data.sessionUser,
        });

        if (!employeeProfiles || employeeProfiles.length === 0) {
            return { data: returnData, page: data.page || 1, pages: 0 };
        }

        const employeeIds = employeeProfiles.map((e: any) => e.id || e._id);

        let filterData: any = {
            dateDay: {
                $gte: dayjs(new Date()).clone().startOf("day").toDate(),
            },
            employeeId: { $in: employeeIds },
        };

        const total = await this.Appointments.AppointmentList.countDocuments(filterData);
        const limit = data.limit || 10;
        const page = data.page || 1;
        const skip = (page - 1) * limit;

        const appointments = await this.Appointments.AppointmentList.find(filterData)
            .sort({ start: 1 })
            .skip(skip)
            .limit(limit);

        for (const appointment of appointments) {
            const client = await ClientsManager.clients.findOne({
                _id: String(appointment.clientId),
            });

            if (!client) {
                continue;
            }

            const userClient = await UsersManager.users.findOne({
                _id: client.userId ? String(client.userId) : undefined,
            });

            const employee = employeeProfiles.find((e: any) => String(e.id || e._id) === String(appointment.employeeId));

            if (!employee) {
                continue;
            }

            const userEmployee = await UsersManager.users.findOne({
                _id: String(employee.userId),
            });

            if (!userEmployee) {
                continue;
            }

            const service = await ServiceList.services.findOne({
                _id: String(appointment.serviceId),
            });

            if (!service) {
                continue;
            }

            const clientName = joinStrings([client.firstName, client.lastName], " ");
            const clientUserName = userClient
                ? joinStrings([userClient.firstName, userClient.lastName], clientName)
                : joinStrings([client.firstName, client.lastName], " ");
            const clientAvatar = userClient
                ? getAvatar(client.userId, userClient.avatar, clientUserName)
                : clientUserName;

            const employeeFirstName = isNull<string>(userEmployee.firstName, "");
            const employeeLastName = isNull<string>(userEmployee.lastName, "");
            const employeeFullName = joinStrings([employeeFirstName, employeeLastName], " ");

            const employeeAvatar = userEmployee
                ? getAvatar(employee.userId, userEmployee.avatar, employeeFullName)
                : clientUserName;

            const discountData = await Discounts.getDiscountData(
                appointment.discountId,
                appointment.companyId
            );

            returnData.push({
                id: appointment.id,
                companyId: String(appointment.companyId),
                clientId: appointment.clientId,
                clientName: clientUserName,
                serviceId: appointment.serviceId,
                serviceName: service.name,
                serviceDescription: service.description,
                collaboratorId: String(employee.id || (employee as any)._id),
                collaboratorName: employeeFullName,
                collaboratorAvatar: employeeAvatar,
                appointmentDate: dayjs(appointment.start).toISOString(),
                startTime: appointment.startHour,
                endTime: appointment.endHour,
                title: isNull<string>(clientName, clientUserName),
                avatar: clientAvatar,
                color: isNull<string>(employee.calendarColor, "#ff9900"),
                comments: appointment.comments,
                price: appointment.price,
                length: appointment.length,
                payment: appointment.payment,

                discountId: isNull<string>(appointment.discountId, ""),
                discountName: isNull<string>(discountData.name, ""),
                discountCode: isNull<string>(discountData.code, ""),
                discountValue: isNull<number>(discountData.value, 0),
                discountType: isNull<DiscountType>(discountData.type, DiscountType.PERCENTAGE),
                clientCtaCteBalance: 0,
            });
        }

        return {
            data: returnData,
            page,
            pages: Math.ceil(total / limit),
        };
    }


    
public async updateAppointment(data: UpdateAppointmentParams): Promise<void> {
        const { notification = true, validation = false } = data;
        const sessionUser = await UsersManager.users.findOne({
            _id: data.sessionUser,
        });

        if (!sessionUser) {
            throw new Error("No se ha encontrado el usuario o no tiene permisos para realizar esta acción");
        }

        const checkAppointment = await this.Appointments.findOne({
            _id: data.id,
        });

        if (!checkAppointment) {
            throw new Error("El turno no existe");
        }

        const companyCheck = await CompaniesManager.companies.findOne({
            _id: String(checkAppointment.companyId),
        });

        if (!companyCheck) {
            throw new Error("La compañia no existe");
        }

        if (!(await EmployeesList.checkPermission(companyCheck, data.sessionUser, EmployeeRoles.ADMIN))) {
            throw new Error(NoPermissionMessage());
        }

        const serviceCheck = await ServicesManager.services.findOne({
            _id: String(checkAppointment.serviceId),
            companyId: String(checkAppointment.companyId),
        });

        if (!serviceCheck) {
            throw new Error("El servicio no existe o no pertenece a la compañia.");
        }

        const appointmentLength = data.length ? data.length : checkAppointment.length;

        const start = dayjs(data.start).clone();
        const end = dayjs(data.start)
            .clone()
            .add(serviceCheck.length * isNull<number>(appointmentLength, 1), "minutes");

        const startHour = start.clone().hour() + start.clone().minute() / 60;
        const endHour = end.clone().hour() + end.clone().minute() / 60;
        const dateDay = dayjs(data.start).clone().startOf("day").toDate();

        const originalConfig = this.getHeatMapConfig(checkAppointment, serviceCheck.length);
        const originalDateDay = dayjs(checkAppointment.dateDay).clone();

        const defaultFraction = Number(process.env.DEFAULT_COMPANY_SERVICE_FRACTION);

        const availabilityConfig: HeatMapConfig = {
            companyId: String(checkAppointment.companyId),
            serviceId: String(checkAppointment.serviceId),
            employeeId: String(checkAppointment.employeeId),
            heatMapDate: dayjs(data.start).startOf("day").toDate(),
            start: dayjs(data.start).toDate(),
            fraction: defaultFraction, //companyCheck.heatMapFraction ? companyCheck.heatMapFraction : defaultFraction,
            length: isNull<number>(appointmentLength, 1),
            serviceLimit: serviceCheck.limit,
            serviceLength: serviceCheck.length,
        };

        const availabilityData = {
            companyId: String(checkAppointment.companyId),
            employeeId: String(checkAppointment.employeeId),
            serviceId: String(checkAppointment.serviceId),
            start: dayjs(data.start).toDate(),
            length: isNull<number>(appointmentLength, 1),
            dateDay: dateDay,
            price: 0,
            comments: "",
            sessionUser: data.sessionUser,
        };

        const newStart = dayjs(data.start).toDate();

        if (validation) {
            const checkAvailable = await this.checkAvailability(availabilityConfig, availabilityData);

            //Solo chequeo disponibilidad si se cambia el start o el length del turno, ya que es una modificacion.
            if (newStart !== checkAppointment.start || data.length !== checkAppointment.length) {
                if (checkAvailable === false) {
                    throw new Error("No hay disponibilidad para el horario seleccionado");
                }
            }
        }

        checkAppointment.start = newStart;
        checkAppointment.startHour = startHour;
        checkAppointment.endHour = endHour;
        checkAppointment.dateDay = dateDay;

        if (data.price) {
            checkAppointment.price = data.price;
        }

        if (data.length) {
            checkAppointment.length = data.length;
        }

        if (data.comments) {
            checkAppointment.comments = data.comments;
        }

        try {
            await checkAppointment.save();

            const checkClient = await ClientsManager.clients.findOne({
                _id: String(checkAppointment.clientId),
            });

            if (!checkClient) {
                throw new Error("El cliente no existe");
            }

            const employeeCheck = await EmployeesList.employees.findOne({
                _id: String(checkAppointment.employeeId),
            });

            if (!employeeCheck) {
                throw new Error("El colaborador no existe");
            }

            if (notification && checkClient.userId) {
                //Envio la notificacion al cliente.
                await NotificationsManager.sendSystemNotification({
                userId: String(checkClient.userId),
                subject: "Turno actualizado",
                message: `Se ha actualizado un turno para el día ${dayjs(data.start).format(
                        "DD/MM/YYYY"
                    )} a las ${dayjs(data.start).format("HH:mm")}`,
                type: NotificationType.APPOINTMENT,
                code: String(checkAppointment._id)
            });

                //Intento enviar notificacion al cliente por Email
                try {
                    if (checkAppointment.id) {
                        await this.sendEmailNotification({
                            appointmentId: String(checkAppointment.id),
                            type: APPOINTMENT_NOTIFICATION_TYPE.REMINDER,
                            sessionUser: String(companyCheck.ownerId),
                        });
                    }
                } catch (err) {
                    console.log(
                        `Error al enviar la notificacion por Email para  ${companyCheck.name}(${companyCheck._id}):`,
                        err
                    );
                }

                //Intento enviar notificacion al cliente por Whatsapp
                try {
                    if (checkAppointment.id) {
                        await this.sendWapNotification({
                            appointmentId: String(checkAppointment.id),
                            type: APPOINTMENT_NOTIFICATION_TYPE.REMINDER,
                            sessionUser: String(companyCheck.ownerId),
                        });
                    }
                } catch (err) {
                    console.log(
                        `Error al enviar la notificacion por Whatsapp para  ${companyCheck.name}(${companyCheck._id}):`,
                        err
                    );
                }
            }

            //Envio la notificacion al profesional.
            await NotificationsManager.sendSystemNotification({
                userId: String(employeeCheck.userId),
                subject: "Turno actualizado",
                message: `Se ha actualizado un turno para el cliente ${ClientsManager.getClientFullName(
                    checkClient
                )} para el día ${dayjs(data.start).format("DD/MM/YYYY")} a las ${dayjs(data.start).format(
                    "HH:mm"
                )}`,
                type: NotificationType.APPOINTMENT,
                code: String(checkAppointment._id)
            });

            //Actualiza el heatmap de la fecha nueva.
            await this.updateHeatMap(availabilityConfig);

            if (!originalDateDay.isSame(dateDay)) {
                //actualizar el heatmap de la fecha original
                await this.updateHeatMap(originalConfig);
            }
        } catch (error) {
            console.log(error);
            throw new Error("Ha ocurrido un error al actualizar el turno.");
        }
    }

    private getHeatMapConfig(
        appointment: IAppointment,
        serviceLength: number,
        heatMapDate?: Date,
        start?: Date
    ): HeatMapConfig {
        const hmDate = heatMapDate ? heatMapDate : dayjs(appointment.start).startOf("day").toDate();
        const hmStart = start ? start : dayjs(appointment.start).toDate();

        const defaultFraction = Number(process.env.DEFAULT_COMPANY_SERVICE_FRACTION);

        const config: HeatMapConfig = {
            companyId: appointment.companyId,
            serviceId: appointment.serviceId,
            employeeId: appointment.employeeId,
            heatMapDate: hmDate,
            start: hmStart,
            fraction: defaultFraction,
            length: isNull<number>(appointment.length, 1),
            serviceLimit: 1, //el service limit no importa si no voy a validar si hay lugar.
            serviceLength: serviceLength,
        };

        return config;
    }

    public async deleteAppointment(data: DeleteAppointmentParams): Promise<void> {
        const { validation = false } = data;
        const sessionUser = await UsersManager.users.findOne({
            _id: data.sessionUser,
        });

        if (!sessionUser) {
            throw new Error("No se ha encontrado el usuario o no tiene permisos para realizar esta acción");
        }

        const checkAppointment = await this.Appointments.findOne({
            _id: data.id,
        });

        if (!checkAppointment) {
            throw new Error("El turno no existe");
        }

        if (checkAppointment.payment === true) {
            throw new Error("No se puede cancelar un turno que se encuentra pagado.");
        }

        const companyCheck = await CompaniesManager.companies.findOne({
            _id: String(checkAppointment.companyId),
        });

        if (!companyCheck) {
            throw new Error("La compañia no existe");
        }

        //Chequeo el empleado.
        const employeeCheck = await EmployeesList.employees.findOne({
            _id: String(checkAppointment.employeeId),
        });

        if (!employeeCheck) {
            throw new Error("El colaborador no existe");
        }

        //Si el sessionUser es el cliente, si puede borrarlo.
        const checkClient = await ClientsManager.clients.findOne({
            _id: String(checkAppointment.clientId),
        });

        if (!checkClient) {
            throw new Error("El cliente no existe");
        }

        //TODO: Check cancellationTime.
        if (
            !this.validateCancellationTime(companyCheck.cancellationTime, checkAppointment.start) &&
            validation
        ) {
            throw new Error(
                `No se puede cancelar el turno con menos de ${this.getCancellationTime(
                    companyCheck.cancellationTime
                )} horas de anticipación.`
            );
        }

        if (String(checkClient.userId) !== data.sessionUser) {
            //Solo valida si el usuario que intenta borrar no es el cliente.
            if (!(await EmployeesList.checkPermission(companyCheck, data.sessionUser, EmployeeRoles.ADMIN))) {
                throw new Error(NoPermissionMessage());
            }
        }

        const serviceCheck = await ServicesManager.services.findOne({
            _id: String(checkAppointment.serviceId),
            companyId: String(checkAppointment.companyId),
        });

        if (!serviceCheck) {
            throw new Error("El servicio no existe o no pertenece a la compañia.");
        }

        try {
            const config = this.getHeatMapConfig(checkAppointment, serviceCheck.length);

            await this.Appointments.AppointmentList.deleteOne({ _id: data.id });
            //Actualizar el heatmap.
            await this.updateHeatMap(config);

            if (checkClient.userId) {
                //Enviamos la notificacion de cancelacion al cliente
                await NotificationsManager.sendSystemNotification({
                userId: String(checkClient.userId),
                subject: "Turno Cancelado",
                message: `Se ha cancelado el turno para el día ${dayjs(checkAppointment.start).format(
                        "DD/MM/YYYY"
                    )} a las ${dayjs(checkAppointment.start).format("HH:mm")}`,
                type: NotificationType.APPOINTMENT,
                code: String(checkAppointment._id)
            });
            }

            //Enviamos la notificacion de cancelacion al profesional
            await NotificationsManager.sendSystemNotification({
                userId: String(employeeCheck.userId),
                subject: "Turno Cancelado",
                message: `Se ha cancelado un turno para el cliente ${ClientsManager.getClientFullName(
                    checkClient
                )} el día ${dayjs(checkAppointment.start).format("DD/MM/YYYY")} a las ${dayjs(
                    checkAppointment.start
                ).format("HH:mm")}`,
                type: NotificationType.APPOINTMENT,
                code: String(checkAppointment._id)
            });
        } catch (error) {
            console.log(error);
            throw new Error("Ha ocurrido un error al eliminar el turno.");
        }
    }

    public async getSchedules(data: FindAppointmentSchedulesParams): Promise<TextObjectFilterResult[]> {
        const returnData: TextObjectFilterResult[] = [];

        const dateDay = dayjs(data.dateDay).startOf("day");
        const weekDay = dateDay.weekday() + 1;

        const schedules: CollaboratorSchedulesView = await SchedulesList.findAllSchedulesByCollaborator({
            companyId: data.companyId,
            employeeId: data.employeeId,
            weekDay: weekDay,
        });

        const hasOverride = await SchedulesOverridesList.findOne({
            companyId: data.companyId,
            employeeId: data.employeeId,
            date: dateDay.toDate(),
        });

        const listOfSchedules = hasOverride ? hasOverride.schedules : schedules.schedules;

        const service = await ServiceList.services.findOne({
            _id: data.serviceId,
        });

        if (!service) {
            console.log("El servicio no existe");
            return [];
        }

        const defaultFraction = Number(process.env.DEFAULT_COMPANY_SERVICE_FRACTION);

        for (const schedule of listOfSchedules) {
            if (schedule.disabled) {
                continue;
            }

            const scheduleDataFrom = schedule.from.split(":");
            const scheduleDataTo = schedule.to.split(":");
            const hourFrom = parseInt(scheduleDataFrom[0]);
            const hourTo = parseInt(scheduleDataTo[0]);
            const minutesFrom = parseInt(scheduleDataFrom[1]);
            const minutesTo = parseInt(scheduleDataTo[1]);
            const startHour = dateDay.clone().hour(hourFrom).minute(minutesFrom);
            const endHour = dateDay.clone().hour(hourTo).minute(minutesTo);

            let currentHour = startHour.clone();
            while (currentHour.isBefore(endHour)) {
                const availabilityConfig: HeatMapConfig = {
                    companyId: data.companyId,
                    serviceId: data.serviceId,
                    employeeId: data.employeeId,
                    heatMapDate: dayjs(dateDay).startOf("day").toDate(),
                    start: dayjs(currentHour).toDate(),
                    fraction: defaultFraction,
                    length: 1,
                    serviceLimit: service.limit,
                    serviceLength: service.length,
                };

                const available = await this.checkAvailability(availabilityConfig, {
                    companyId: data.companyId,
                    serviceId: data.serviceId,
                    employeeId: data.employeeId,
                    userId: "",
                    start: dayjs(currentHour).toDate(),
                    startHour: currentHour.hour(),
                    dateDay: dateDay.toDate(),
                    length: 1,
                    price: 0,
                    sessionUser: "",
                });

                if (available) {
                    returnData.push({
                        id: currentHour.toISOString(),
                        name:
                            currentHour.format("HH:mm") +
                            "hs. - " +
                            currentHour.add(service.length, "minute").format("HH:mm") +
                            "hs.",
                    });
                }

                currentHour = currentHour
                    .clone()
                    .add(isNull<number>(service.length, defaultFraction), "minutes");
            }
        }

        return returnData;
    }

    private async tryToSendNotification(
        data: SendAppointmentNotificationParams
    ): Promise<AppointmentNotificationIntent> {
        const checkAppointment = await this.Appointments.findOne({
            _id: data.appointmentId,
        });

        if (!checkAppointment) {
            throw new Error("El turno no existe");
        }

        const companyCheck = await CompaniesManager.companies.findOne({
            _id: String(checkAppointment.companyId),
        });

        if (!companyCheck) {
            throw new Error("La compañia no existe");
        }

        if (!(await EmployeesList.checkPermission(companyCheck, data.sessionUser, EmployeeRoles.ADMIN))) {
            throw new Error(NoPermissionMessage());
        }

        const checkClient = await ClientsManager.clients.findOne({
            _id: String(checkAppointment.clientId),
        });

        if (!checkClient) {
            throw new Error("El cliente no existe");
        }

        const checkEmployee = await EmployeesList.employees.findOne({
            _id: String(checkAppointment.employeeId),
        });

        if (!checkEmployee) {
            throw new Error("El profesional no existe");
        }

        const checkEmployeeUser = await UsersManager.users.findOne({
            _id: String(checkEmployee.userId),
        });

        if (!checkEmployeeUser) {
            throw new Error("El profesional no tiene usuario asociado");
        }

        const checkService = await ServiceList.services.findOne({
            _id: String(checkAppointment.serviceId),
        });

        if (!checkService) {
            throw new Error("El servicio no existe");
        }

        let wapMessage = "";

        if (data.type == APPOINTMENT_NOTIFICATION_TYPE.REMINDER) {
            if (companyCheck.templateWapNotifId) {
                const template = await Templates.templates.findOne({
                    id: String(companyCheck.templateWapNotifId),
                    companyId: String(companyCheck._id),
                });

                if (template) {
                    wapMessage = template.template;
                }
            }

            if (!wapMessage) {
                wapMessage = process.env.WAP_REMINDER ? process.env.WAP_REMINDER : "";
            }
            wapMessage = wapMessage.replaceAll(
                "<---CLIENTE--->",
                ClientsManager.getClientFullName(checkClient)
            );
            wapMessage = wapMessage.replaceAll(
                "<---FECHA--->",
                dayjs(checkAppointment.start).format("DD/MM/YYYY")
            );
            wapMessage = wapMessage.replaceAll(
                "<---HORARIO--->",
                dayjs(checkAppointment.start).format("HH:mm") + "hs."
            );
            wapMessage = wapMessage.replaceAll(
                "<---DIRECCION--->",
                CompaniesManager.getCompanyAddress(companyCheck)
            );

            wapMessage = wapMessage.replaceAll(
                "<---PROFESIONAL--->",
                UsersManager.getUserFullName(checkEmployeeUser)
            );

            wapMessage = wapMessage.replaceAll("<---SERVICIO--->", checkService.name);
            wapMessage = wapMessage.replaceAll("<---PRECIO--->", formatCurrency(checkAppointment.price));

            wapMessage = wapMessage.replaceAll("<---ORGANIZACION--->", companyCheck.name);
            wapMessage = wapMessage.replaceAll("<---BR--->", "\r\n");
        } else if (data.type == APPOINTMENT_NOTIFICATION_TYPE.CREATION) {
            if (companyCheck.templateWapAltaId) {
                const template = await Templates.templates.findOne({
                    id: String(companyCheck.templateWapAltaId),
                    companyId: String(companyCheck._id),
                });

                if (template) {
                    wapMessage = template.template;
                }
            }

            if (!wapMessage) {
                wapMessage = process.env.WAP_ALTA ? process.env.WAP_ALTA : "";
            }

            wapMessage = wapMessage.replaceAll(
                "<---CLIENTE--->",
                ClientsManager.getClientFullName(checkClient)
            );
            wapMessage = wapMessage.replaceAll(
                "<---FECHA--->",
                dayjs(checkAppointment.start).format("DD/MM/YYYY")
            );
            wapMessage = wapMessage.replaceAll(
                "<---HORARIO--->",
                dayjs(checkAppointment.start).format("HH:mm") + "hs."
            );
            wapMessage = wapMessage.replaceAll(
                "<---DIRECCION--->",
                CompaniesManager.getCompanyAddress(companyCheck)
            );

            wapMessage = wapMessage.replaceAll(
                "<---PROFESIONAL--->",
                UsersManager.getUserFullName(checkEmployeeUser)
            );

            wapMessage = wapMessage.replaceAll("<---SERVICIO--->", checkService.name);
            wapMessage = wapMessage.replaceAll("<---PRECIO--->", formatCurrency(checkAppointment.price));

            wapMessage = wapMessage.replaceAll("<---ORGANIZACION--->", companyCheck.name);
            wapMessage = wapMessage.replaceAll("<---BR--->", "\r\n");
        }

        return {
            message: wapMessage,
            checkClient: checkClient,
            companyCheck: companyCheck,
        };
    }

    public async sendEmailNotification(data: SendAppointmentNotificationParams): Promise<void> {
        const sessionUser = await UsersManager.users.findOne({
            _id: data.sessionUser,
        });

        if (!sessionUser) {
            throw new Error("No se ha encontrado el usuario o no tiene permisos para realizar esta acción");
        }

        const { message, checkClient, companyCheck } = await this.tryToSendNotification(data);

        const canSend = await PlanSubscriptionsList.checkFeature({
            userId: String(companyCheck.ownerId),
            feature: PlanFeatures.MAIL_NOTIFICATIONS,
        });

        if (!canSend) {
            throw new Error("La organizacion no tiene un plan que soporte notificaciones por email");
        }

        const clientEmail = checkClient.email;

        if (!clientEmail || clientEmail == "") {
            throw new Error("El cliente no tiene un número telefónico asociado");
        }

        let subjectEmail = "";

        if (data.type == APPOINTMENT_NOTIFICATION_TYPE.REMINDER) {
            subjectEmail = "TurnosXpress :: Recordatorio";
        } else if (data.type == APPOINTMENT_NOTIFICATION_TYPE.CREATION) {
            subjectEmail = "TurnosXpress :: Alta de turno";
        }

        //Enviar email...
        await NotificationsManager.sendEmail({
            email: clientEmail,
            subject: subjectEmail,
            message: message,
        });
    }

    public async sendWapNotification(data: SendAppointmentNotificationParams): Promise<void> {
        const sessionUser = await UsersManager.users.findOne({
            _id: data.sessionUser,
        });

        if (!sessionUser) {
            throw new Error("No se ha encontrado el usuario o no tiene permisos para realizar esta acción");
        }

        const { message, checkClient, companyCheck } = await this.tryToSendNotification(data);

        const canSend = await PlanSubscriptionsList.checkFeature({
            userId: String(companyCheck.ownerId),
            feature: PlanFeatures.WAP_NOTIFICATIONS,
        });

        if (!canSend) {
            throw new Error("La organizacion no tiene un plan que soporte notificaciones por Whatsapp");
        }

        const wapNumber = await ClientsManager.getClientWapNumber(checkClient);

        if (!wapNumber || wapNumber == "") {
            throw new Error("El cliente no tiene un número telefónico asociado");
        }

        await NotificationsManager.sendWap({
            phoneNumber: wapNumber,
            message: message,
            companyId: String(companyCheck._id),
            sessionUser: data.sessionUser,
        });
    }

    public async sendWapSystemNotification(data: SendWapSystemNotificationParams): Promise<void> {
        if (!data.systemToken || data.systemToken == "") {
            throw new Error("No tiene permisos para realizar esta acción");
        }

        const systemToken = `${process.env.SYSTEM_KEY}`;

        if (systemToken != data.systemToken) {
            throw new Error("No tiene permisos para realizar esta acción");
        }

        const checkAppointment = await this.Appointments.findOne({
            _id: data.appointmentId,
        });

        if (!checkAppointment) {
            throw new Error("El turno no existe");
        }

        const company = await CompaniesManager.companies.findOne({
            _id: String(checkAppointment.companyId),
        });

        if (!company) {
            throw new Error("La organización no existe");
        }

        const { message, checkClient, companyCheck } = await this.tryToSendNotification({
            appointmentId: data.appointmentId,
            sessionUser: String(company.ownerId),
            type: APPOINTMENT_NOTIFICATION_TYPE.REMINDER,
        });

        const canSend = await PlanSubscriptionsList.checkFeature({
            userId: String(companyCheck.ownerId),
            feature: PlanFeatures.WAP_NOTIFICATIONS,
        });

        if (!canSend) {
            throw new Error("La organizacion no tiene un plan que soporte notificaciones por Whatsapp");
        }

        const wapNumber = await ClientsManager.getClientWapNumber(checkClient);

        if (!wapNumber || wapNumber == "") {
            throw new Error("El cliente no tiene un número telefónico asociado");
        }

        await NotificationsManager.sendWap({
            phoneNumber: wapNumber,
            message: message,
            companyId: String(companyCheck._id),
            sessionUser: String(company.ownerId),
        });
    }

    public async applyDiscount(data: ApplyAppointmentDiscountParams): Promise<void> {
        await validateSessionUser({ sessionUser: data.sessionUser });
        await validatePermissionsByCompany({
            companyId: data.companyId,
            sessionUser: data.sessionUser,
        });

        const appointment = await this.Appointments.AppointmentList.findOne({
            _id: data.appointmentId,
        });

        if (!appointment) {
            throw new Error("El turno no existe");
        }

        if (appointment.payment) {
            throw new Error("No se puede aplicar un descuento a un turno pagado");
        }

        const service = await ServicesManager.services.serviceList.findOne({
            _id: String(appointment.serviceId),
        });

        if (!service) {
            throw new Error("El servicio no existe");
        }

        const discount = await Discounts.discounts.discountsList.findOne({
            _id: data.discountId,
            companyId: data.companyId,
        });

        if (!discount) {
            throw new Error("El descuento no existe");
        }

        let price = service.price;

        if (service.discountId) {
            //Como el servicio ya tiene un descuento, aplico el descuento del turno sobre el precio original.
            //Ya que la idea es que los descuentos no sean acumulativos.
            price = service.originalPrice;
        }

        if (discount.type == DiscountType.FIXED) {
            price = price - discount.value;
        } else if (discount.type == DiscountType.PERCENTAGE) {
            price = price - price * (discount.value / 100);
        }

        appointment.discountId = data.discountId;
        appointment.price = price;
        await appointment.save();
    }

    public async deleteDiscount(data: DeleteAppointmentDiscountParams): Promise<void> {
        await validateSessionUser({ sessionUser: data.sessionUser });
        await validatePermissionsByCompany({
            companyId: data.companyId,
            sessionUser: data.sessionUser,
        });

        const appointment = await this.Appointments.AppointmentList.findOne({
            _id: data.appointmentId,
        });

        if (!appointment) {
            throw new Error("El turno no existe");
        }

        const service = await ServicesManager.services.serviceList.findOne({
            _id: String(appointment.serviceId),
        });

        if (!service) {
            throw new Error("El servicio no existe");
        }

        if (appointment.payment) {
            throw new Error("No se puede eliminar el descuento de un turno pagado");
        }

        appointment.set("discountId", null);
        appointment.price = service.originalPrice;
        await appointment.save();
    }

    public async getAvailableDates(data: GetAvailableDatesParams): Promise<GetAvailableDatesResult> {
        //Solamente validamos que sea un usuario registrado.
        await validateSessionUser({ sessionUser: data.sessionUser });

        const company = await CompaniesManager.companies.findOne({
            _id: data.companyId,
        });

        if (!company) {
            throw new Error("La organización no existe");
        }

        const employee = await EmployeesList.employees.findOne({
            _id: data.employeeId,
        });

        if (!employee) {
            throw new Error("El profesional no existe");
        }

        //Obtener rangos de fechas habilitados para la organizacion/profesional.
        //cuto startDate este entre nuestro dateFrom y dateTo en base a la fecha de
        //consulta y el rango de tiempo recibido por parametro.

        const length = data.length > 60 ? 60 : data.length;
        const dateFrom = dayjs().clone().startOf("week").add(-1, "days");
        const dateTo = dayjs().clone().add(length, "days").endOf("day");

        const schedulesEnabled = await SchedulesEnabledList.schedulesEnabled.schedulesEnabledList
            .find({
                companyId: String(data.companyId),
                employeeId: String(data.employeeId),
                startDate: { $gte: dateFrom.toDate(), $lte: dateTo.toDate() },
            })
            .exec();

        const returnAvailableDates: Date[] = [];
        let currentDate = dateFrom.clone().startOf("day");

        while (currentDate.isSameOrBefore(dateTo)) {
            const hasOverride = await SchedulesOverridesList.findOne({
                companyId: String(data.companyId),
                employeeId: String(data.employeeId),
                date: currentDate.toDate(),
            });

            if (hasOverride && hasOverride.schedules.length > 0) {
                returnAvailableDates.push(currentDate.toDate());
            } else if (!hasOverride) {
                // Check if the current date is within any enabled schedule range
                let isEnabled = false;
                if (schedulesEnabled) {
                    for (const scheduleItem of schedulesEnabled) {
                        const start = dayjs(scheduleItem.startDate).startOf("day");
                        const end = dayjs(scheduleItem.endDate).endOf("day");
                        if (currentDate.isSameOrAfter(start) && currentDate.isSameOrBefore(end)) {
                            isEnabled = true;
                            break;
                        }
                    }
                }

                if (isEnabled) {
                    const schedule = await SchedulesList.schedules.schedulesList.findOne({
                        companyId: String(data.companyId),
                        employeeId: String(data.employeeId),
                        weekDay: currentDate.day(),
                    });

                    if (schedule && schedule.schedules.length > 0) {
                        returnAvailableDates.push(currentDate.toDate());
                    }
                }
            }

            currentDate = currentDate.add(1, "day");
        }

        return {
            availableDates: returnAvailableDates,
            inRangeDates: [],
        } as GetAvailableDatesResult;
    }
}

const AppointmentList = new AppointmentManager();

export default AppointmentList;
