import { EmployeesAdapterMongoose } from "./Employees.Adapter.Mongoose";
import {
    CreateEmployeeParams,
    IEmployee,
    IEmployeesManager,
    EmployeeRoles,
    ValidateEmployeeParams,
    PaginateEmployeesParams,
    PaginateEmployeesResults,
    CompanyEmployeesView,
    FindEmployeesParams,
    FindEmployeesByIdParams,
    UpdateEmployeeRolesParams,
    UpdateEmployeeParams,
    DeleteEmployeesByCompany,
    DeleteEmployeeParams,
} from "./Employees.Interface";
import UsersManager from "../Users/Users";
import { ICompanyDocument } from "../Companies/Companies.Adapter.Mongoose";
import CompaniesManager from "../Companies/Companies";
import { NoPermissionMessage } from "../../helpers/NoPermissionMessage";
import { joinStrings } from "../../helpers/String";
import { isNull } from "../../helpers/IsNull";
import getAvatar from "../../helpers/getAvatar";
import { TextObjectFilterResult } from "../../Models/TextObjectFilter.model";
import MetricsList from "../../Models/Metrics/Metrics";
import { NotificationsManager } from "../Notifications/Notifications";
import EmployeesServicesList from "../EmployeesServices/EmployeesServices";
import SchedulesList from "../Schedules/Schedules";
import { IncompleteCollaboratorView } from "./Employees.Interface";
import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface";

class EmployeeManager implements IEmployeesManager {
    employees: EmployeesAdapterMongoose;

    constructor() {
        this.employees = new EmployeesAdapterMongoose();
    }

    public async deleteEmployee(data: DeleteEmployeeParams): Promise<void> {
        await this.employees.employeeList.deleteOne({ _id: data.employeeId });
    }

    public async deleteEmployeesByCompany(data: DeleteEmployeesByCompany): Promise<void> {
        await this.employees.employeeList.deleteMany({ companyId: data.companyId });
    }

    public async checkRoleById(
        companyId: string,
        employeeId: string,
        role: EmployeeRoles
    ): Promise<boolean> {
        //Check if role exist in roles array.
        const check = await this.employees.findOne({ companyId: companyId, userId: employeeId });

        if (!check) {
            return false;
        }

        if (check && check.roles) {
            return check.roles.includes(role);
        }

        return false;
    }

    public async checkPermission(
        company: ICompanyDocument,
        employeeId: string,
        role: EmployeeRoles
    ): Promise<boolean> {
        //Si el data.sessionUser es el owner de la empresa tiene permiso.
        if (String(company.ownerId) == String(employeeId)) {
            return true;
        }

        //Si el data.sessionUser tiene el rol admin de la empresa tiene permiso.
        return await this.checkRoleById(String(company._id), employeeId, role);
    }

    public async createEmployee(data: CreateEmployeeParams): Promise<IEmployee> {
        //Primero tengo que verificar que el usuario que intenta registrar un empleado
        //tenga permisos para hacerlo y que sea un usuario valido. Este dato viene de
        //el campo data.sessionUser.

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

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

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

        const employeeCheck = await this.employees.findOne({
            companyId: data.companyId,
            userId: data.userId,
        });

        if (employeeCheck) {
            throw new Error("El usuario ya pertenece a la compañia");
        }

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

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

        const newEmplyee = await this.employees.create(data);

        await NotificationsManager.sendSystemNotification({
                userId: String(data.userId),
                subject: "Su usuario ha sido vinculado a una organización",
                message: `La compañía ${companyCheck.name} lo ha vinculado como colaborador. Es necesario que acepte la invitación para poder formar parte de esta organización.`,
                type: NotificationType.EMPLOYEE,
                code: String((newEmplyee as any)._id)
            });

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

        return newEmplyee;
    }

    public async updateEmployeeRoles(data: UpdateEmployeeRolesParams): 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 checkPermission = await this.checkPermission(
            companyCheck,
            data.sessionUser,
            EmployeeRoles.ADMIN
        );

        if (!checkPermission) {
            throw new Error(NoPermissionMessage());
        }

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

        if (!employee) {
            throw new Error("Ha ocurrido un error al validar el empleado.");
        }

        if (String(employee.userId) == data.sessionUser) {
            if (String(companyCheck.ownerId) != data.sessionUser) {
                throw new Error(NoPermissionMessage());
            }
        }

        employee.roles = data.roles;

        await employee.save();
    }

    //No exponer este metodo.
    public async validateFull(data: ValidateEmployeeParams): Promise<void> {
        const employee = await this.employees.findOne({
            _id: data.employeeId,
        });

        if (!employee) {
            throw new Error("Ha ocurrido un error al validar el empleado.");
        }

        employee.hostOk = true;
        employee.guestOk = true;

        await employee.save();
    }

    public async validateGuest(data: ValidateEmployeeParams): 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 employee = await this.employees.findOne({
            _id: data.employeeId,
        });

        if (!employee) {
            throw new Error("Ha ocurrido un error al validar el empleado.");
        }

        if (String(sessionUser.id) != employee.userId) {
            throw new Error("Solo el destinatario puede aceptar la vinculación.");
        }

        employee.guestOk = true;

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

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

        await employee.save();

        await NotificationsManager.sendSystemNotification({
                userId: String(employee.userId),
                subject: "Has aceptado la invitación!",
                message: `Ahora que haz aceptado la invitación ya estas habilitado para trabajar con ${checkCompany.name}.`,
                type: NotificationType.EMPLOYEE,
                code: String(employee._id)
            });
    }

    public async rejectGuest(data: ValidateEmployeeParams): 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 employee = await this.employees.findOne({
            _id: data.employeeId,
        });

        if (!employee) {
            throw new Error("Ha ocurrido un error al validar el empleado.");
        }

        if (String(sessionUser.id) != employee.userId) {
            throw new Error("Solo el destinatario puede rechazar la vinculación.");
        }

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

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

        await this.employees.employeeList.deleteOne({ _id: data.employeeId });

        await NotificationsManager.sendSystemNotification({
                userId: String(checkCompany.ownerId),
                subject: "Invitación rechazada",
                message: `El usuario ${UsersManager.getUserFullName(sessionUser)} ha rechazado la invitación para unirse a ${checkCompany.name}.`,
                type: NotificationType.EMPLOYEE,
                code: String(employee._id)
            });
    }

    public async validateHost(data: ValidateEmployeeParams): 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 employee = await this.employees.findOne({
            _id: data.employeeId,
        });

        if (!employee) {
            throw new Error("Ha ocurrido un error al validar el empleado.");
        }

        //Si el data.sessionUser tiene el rol admin de la empresa tiene permiso.
        const checkPermission = await this.checkRoleById(
            employee.companyId,
            data.sessionUser,
            EmployeeRoles.ADMIN
        );

        if (!checkPermission) {
            throw new Error(NoPermissionMessage());
        }

        const employUserCheck = await UsersManager.users.findOne({
            _id: employee.userId,
        });
        if (!employUserCheck) {
            throw new Error("El usuario no existe");
        }

        const companyCheck = await CompaniesManager.companies.findOne({
            _id: employee.companyId,
        });
        if (!companyCheck) {
            throw new Error("La organización no existe");
        }

        employee.hostOk = true;

        await employee.save();

        await NotificationsManager.sendSystemNotification({
                userId: String(data.sessionUser),
                subject: "Has habilitado a un colaborador!",
                message: `Se ha enviado una notificación a ${UsersManager.getUserFullName(
                employUserCheck
            )} para formar parte de ${companyCheck.name}.`,
                type: NotificationType.EMPLOYEE,
                code: String(employee._id)
            });
    }

    public async paginate(filters: PaginateEmployeesParams): Promise<PaginateEmployeesResults> {
        return await this.employees.paginate(filters);
    }

    public async findByCompanyId(data: FindEmployeesParams): Promise<CompanyEmployeesView[]> {
        const employees = await this.employees.find(data);

        const view = await Promise.all(
            employees.map(async (employee) => {
                const userData = await UsersManager.users.findOne({ _id: employee.userId });

                const strFirstName = userData ? isNull<string>(userData.firstName, "") : "";
                const strLastName = userData ? isNull<string>(userData.lastName, "") : "";
                const strFullName = joinStrings([strFirstName, strLastName], " ");
                const strAvatar = userData
                    ? getAvatar(userData.id, userData.avatar, strFullName)
                    : strFullName;

                const boolGuestOk = employee.guestOk || false;
                const boolHostOk = employee.hostOk || false;
                const boolFullOk = boolGuestOk && boolHostOk;

                return {
                    id: isNull<string>(employee.id, ""),
                    companyId: employee.companyId,
                    userId: employee.userId,
                    firstName: strFirstName,
                    lastName: strLastName,
                    fullName: strFullName,
                    avatar: strAvatar,
                    email: userData ? userData.email : "",
                    roles: employee.roles ? employee.roles : [],
                    guestOk: boolGuestOk,
                    hostOk: boolHostOk,
                    fullOk: boolFullOk,
                    calendarColor: isNull<string>(employee.calendarColor, "#ff9900"),
                };
            })
        );

        return view;
    }

    public async findById(data: FindEmployeesByIdParams): Promise<CompanyEmployeesView> {
        const employee = await this.employees.findOne({ _id: data.id });

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

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

        const strFirstName = userData ? isNull<string>(userData.firstName, "") : "";
        const strLastName = userData ? isNull<string>(userData.lastName, "") : "";
        const strFullName = joinStrings([strFirstName, strLastName], " ");
        const strAvatar = userData
            ? getAvatar(userData.id, userData.avatar, strFullName)
            : strFullName;

        const boolGuestOk = employee.guestOk || false;
        const boolHostOk = employee.hostOk || false;
        const boolFullOk = boolGuestOk && boolHostOk;

        return {
            id: isNull<string>(employee.id, ""),
            companyId: employee.companyId,
            userId: employee.userId,
            firstName: strFirstName,
            lastName: strLastName,
            fullName: strFullName,
            avatar: strAvatar,
            email: userData ? userData.email : "",
            roles: employee.roles ? employee.roles : [],
            guestOk: boolGuestOk,
            hostOk: boolHostOk,
            fullOk: boolFullOk,
            calendarColor: isNull<string>(employee.calendarColor, "#ff9900"),
        };
    }

    public async textObjectFilter(data: FindEmployeesParams): Promise<TextObjectFilterResult[]> {
        const employees = await this.employees.find(data);
        const formattedResult: TextObjectFilterResult[] = [];

        for (const colaborador of employees) {
            const userInfo = await UsersManager.users.findOne({ _id: colaborador.userId });

            if (!userInfo) {
                continue;
            }

            const userFullName = joinStrings([userInfo.firstName, userInfo.lastName], " ");
            const strAvatar = userInfo
                ? getAvatar(userInfo.id, userInfo.avatar, userFullName)
                : userFullName;

            formattedResult.push({
                id: isNull<string>(colaborador.id, ""),
                name: userFullName,
                image: strAvatar,
            });
        }

        return formattedResult;
    }

    public async update(data: UpdateEmployeeParams): 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 employee = await this.employees.findOne({
            _id: data.id,
        });

        if (!employee) {
            throw new Error("Ha ocurrido un error al validar el empleado.");
        }

        const companyCheck = await CompaniesManager.companies.findOne({ _id: employee.companyId });
        if (!companyCheck) {
            throw new Error("La organización no existe");
        }

        //Si el data.sessionUser tiene el rol admin de la empresa o es owner tiene permiso.
        const checkPermission = await this.checkPermission(
            companyCheck,
            data.sessionUser,
            EmployeeRoles.ADMIN
        );

        if (!checkPermission) {
            throw new Error(NoPermissionMessage());
        }

        if (data.calendarColor) {
            employee.calendarColor = data.calendarColor;
        }

        if (data.limit) {
            employee.limit = data.limit;
        }

        await employee.save();
    }

    public async getIncompleteSetup(sessionUser: string): Promise<IncompleteCollaboratorView[]> {
        const incompleteCollaborators: IncompleteCollaboratorView[] = [];
        const userCompanies = await CompaniesManager.getByUserId({ sessionUser });

        for (const company of userCompanies) {
            if (company.isAdmin || company.isOwner) {
                const employees = await this.findByCompanyId({ companyId: company.id } as any);
                for (const employee of employees) {
                    if (employee.hostOk && employee.guestOk) {
                        const services = await EmployeesServicesList.find({ employeeId: employee.id } as any);
                        const schedules = await SchedulesList.find({ employeeId: employee.id, companyId: company.id } as any);

                        const missingServices = services.length === 0;
                        const missingSchedules = schedules.length === 0;

                        if (missingServices || missingSchedules) {
                            incompleteCollaborators.push({
                                employeeId: employee.id,
                                companyId: company.id,
                                companyName: company.name,
                                name: employee.fullName,
                                missingServices,
                                missingSchedules
                            });
                        }
                    }
                }
            }
        }
        return incompleteCollaborators;
    }
}

const EmployeesList = new EmployeeManager();

export default EmployeesList;
