import { SchedulesAdapterMongoose } from "./Schedules.Adapter.Mongoose";
import {
    CreateScheduleParams,
    UpdateScheduleParams,
    DeleteScheduleParams,
    FindSchedulesParams,
    ISchedule,
    ISchedulesManager,
    PaginateSchedulesParams,
    PaginateSchedulesResults,
    CollaboratorSchedulesView,
    AvailableSchedulesParams,
    DeleteScheulesByCompanyParams,
    DeleteSchedulesByEmployeeParams,
    ScheduleItem,
} from "./Schedules.Interface";
import { EmployeeRoles } from "../Employees/Employees.Interface";
import UsersManager from "../Users/Users";
import CompaniesManager from "../Companies/Companies";
import EmployeesList from "../Employees/Employee";
import { NoPermissionMessage } from "../../helpers/NoPermissionMessage";
import { joinStrings } from "../../helpers/String";
import { isNull } from "../../helpers/IsNull";
import getAvatar from "../../helpers/getAvatar";
import { NotificationsManager } from "../Notifications/Notifications";
import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface";

class SchedulesManager implements ISchedulesManager {
    schedules: SchedulesAdapterMongoose;

    constructor() {
        this.schedules = new SchedulesAdapterMongoose();
    }

    public async deleteSchedulesByEmployee(data: DeleteSchedulesByEmployeeParams): Promise<void> {
        await this.schedules.schedulesList.deleteMany({ employeeId: data.employeeId });
    }

    public async deleteSchedulesByCompany(data: DeleteScheulesByCompanyParams): Promise<void> {
        await this.schedules.schedulesList.deleteMany({ companyId: data.companyId });
    }

    public async createSchedule(data: CreateScheduleParams): Promise<ISchedule> {
        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");
        }

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

        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.");
        }

        const ScheduleCheck = await this.schedules.findOne({
            employeeId: data.employeeId,
            companyId: data.companyId,
            weekDay: data.weekDay,
        });

        if (ScheduleCheck) {
            throw new Error("El servicio ya fue asignado para este empleado");
        }

        const newEmplyeeService = await this.schedules.create(data);

        const strHorarios = data.schedules.map((horario) => {
            return `De ${horario.from} a ${horario.to}`;
        });

        await NotificationsManager.sendSystemNotification({
                userId: String(employeeCheck.userId),
                subject: "Se han agregado horarios de trabajo",
                message: `Se han registrado nuevos horarios en la organización ${companyCheck.name
                }. Horarios: ${strHorarios.join(", ")}`,
                type: NotificationType.SCHEDULE,
                code: String(employeeCheck._id)
            });

        return newEmplyeeService;
    }

    public async updateSchedule(data: UpdateScheduleParams): 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 companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId });

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

        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 (
            !(await EmployeesList.checkPermission(
                companyCheck,
                data.sessionUser,
                EmployeeRoles.ADMIN
            ))
        ) {
            throw new Error(NoPermissionMessage());
        }

        const scheduleUpd = await this.schedules.findOne({
            employeeId: data.employeeId,
            companyId: data.companyId,
            weekDay: data.weekDay,
        });

        if (!scheduleUpd) {
            this.createSchedule(data);
            return;
        }

        scheduleUpd.schedules = data.schedules;

        await scheduleUpd.save();

        const strHorarios = data.schedules.map((horario) => {
            return `De ${horario.from} a ${horario.to}`;
        });

        await NotificationsManager.sendSystemNotification({
                userId: String(employeeCheck.userId),
                subject: "Se han agregado horarios de trabajo",
                message: `Se han registrado nuevos horarios en la organización ${companyCheck.name
                }. Horarios: ${strHorarios.join(", ")}`,
                type: NotificationType.SCHEDULE,
                code: String(employeeCheck._id)
            });
    }

    public async paginate(filters: PaginateSchedulesParams): Promise<PaginateSchedulesResults> {
        return await this.schedules.paginate(filters);
    }

    public async find(filters: FindSchedulesParams): Promise<ISchedule[]> {
        return await this.schedules.find(filters);
    }

    private async getScheduleByFrom(data: AvailableSchedulesParams): Promise<ScheduleItem | null> {
        const schedule = await this.schedules.findOne({
            employeeId: data.employeeId,
            companyId: data.companyId,
            weekDay: data.weekDay,
        });

        if (schedule) {
            for (const horario of schedule.schedules) {
                if (horario.disabled) {
                    continue;
                }
                if (horario.from == data.from) {
                    return horario;
                }
                if (horario.from < data.from && horario.to > data.from) {
                    return horario;
                }
            }
        }

        return null;
    }

    private checkScheduleTo(schedule: ScheduleItem, to: string): boolean {
        const toSchedule = schedule.to.split(":");

        const toHoras = parseInt(toSchedule[0]);
        const toMinutos = parseInt(toSchedule[1]);
        const toTime = toHoras + toMinutos / 60;

        const dataTo = to.split(":");
        const dataToHoras = parseInt(dataTo[0]);
        const dataToMinutos = parseInt(dataTo[1]);
        const dataToTime = dataToHoras + dataToMinutos / 60;

        // Si este rango cubre el from to del potencial turno retorna true.
        if (dataToTime <= toTime) {
            return true;
        }

        return false;
    }

    public async isAvailable(data: AvailableSchedulesParams): Promise<boolean> {
        // Buscar un rango horario que coincida con el from.
        const scheduleFrom = await this.getScheduleByFrom(data);

        if (scheduleFrom) {
            // Si este rango cubre el from to del potencial turno retorna true.
            if (this.checkScheduleTo(scheduleFrom, data.to)) {
                return true;
            }

            // En caso de que no lo cubra, lo que hacemos es chequear si en ese rango que comienza con el from
            // es posible convinarlo con otro rango chequeando que exista un rango que comience donde termina el primero.
            // Entonces podemos crear un nuevo rango que convine los dos encontrados para formar uno mas grande.
            const scheduleTo = await this.getScheduleByFrom({
                employeeId: data.employeeId,
                companyId: data.companyId,
                weekDay: data.weekDay,
                from: scheduleFrom.to,
                to: data.to,
            });

            if (!scheduleTo) {
                return false;
            }

            if (this.checkScheduleTo(scheduleTo, data.to)) {
                return true;
            }
        }

        return false;
    }

    public async findAllSchedulesByCollaborator(
        filters: FindSchedulesParams
    ): Promise<CollaboratorSchedulesView> {
        const employeeCheck = await EmployeesList.employees.findOne({
            _id: filters.employeeId,
            companyId: filters.companyId,
        });

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

        const userCheck = await UsersManager.users.findOne({
            _id: employeeCheck.userId,
        });

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

        const schedule = await SchedulesList.schedules.findOne({
            companyId: filters.companyId,
            employeeId: filters.employeeId,
            weekDay: filters.weekDay,
        });

        const fullName = joinStrings([userCheck.firstName, userCheck.lastName], " ");

        return {
            companyId: isNull<string>(filters.companyId, ""),
            employeeId: isNull<string>(filters.employeeId, ""),
            weekDay: isNull<number>(filters.weekDay, 1),
            scheduleId: schedule ? schedule.id : "",
            schedules: schedule ? schedule.schedules : [],
            fullName: fullName,
            avatar: getAvatar(userCheck.id, userCheck.avatar, fullName),
        };
    }

    public async delete(data: DeleteScheduleParams): 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 companyCheck = await CompaniesManager.companies.findOne({ _id: data.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 deleteItem = await this.schedules.findOne({
            _id: data.id,
        });

        if (!deleteItem) {
            throw new Error("Ha ocurrido un error al borrar el horario");
        }

        const emplyeeId = deleteItem.employeeId;

        const employeeCheck = await EmployeesList.employees.findOne({
            companyId: data.companyId,
            _id: emplyeeId,
        });
        if (!employeeCheck) {
            throw new Error("El empleado no existe o no pertenece a la compañia.");
        }

        const strHorarios = deleteItem.schedules.map((horario) => {
            return `De ${horario.from} a ${horario.to}`;
        });

        await this.schedules.delete(String(deleteItem._id));

        await NotificationsManager.sendSystemNotification({
                userId: String(employeeCheck.userId),
                subject: "Se han agregado horarios de trabajo",
                message: `Se han registrado nuevos horarios en la organización ${companyCheck.name
                }. Horarios: ${strHorarios.join(", ")}`,
                type: NotificationType.SCHEDULE,
                code: String(employeeCheck._id)
            });
    }
}

const SchedulesList = new SchedulesManager();

export default SchedulesList;
