import fs from "fs";
import path from "path";
import { rm } from "fs/promises";
import axios from "axios";
import {
    IUser,
    IUserView,
    SignUpParams,
    LoginParams,
    LoginByTokenParams,
    LoginByGoogleParams,
    SetUserFileParams,
    VerificationUserParams,
    PaginateUsersParams,
    PaginateUsersResults,
    UpdateUserParams,
    FindUsersParams,
    ChangePasswordParams,
    CollaboratorView,
    FindCollaboratorByEmailParams,
    RecoveryAccountParams,
    IGoogleData,
    DeleteUserParams,
    SavePayerEmailParams,
    GetPayerEmailParams,
    PayerEmailResult,
    CheckUserPhoneParams,
    UserAssistanceInfo,
    SysAdminPaginateUsersParams,
    SysAdminUpdateProfileParams,
    SysAdminSetVerifiedParams,
    SysAdminDeleteUserParams,
    SysAdminOrganizationsStatusParams,
    SysAdminUserOrganizationsResult,
} from "./Users.Interface";
import { UsersMongooseAdapter } from "./Users.Adapter.Mongoose";
import { IUsersAdapter, IUsersManager } from "./Users.Interface";
import { ISession } from "../Session.model";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import Joi from "joi";
import hashPassword from "../../helpers/HashPassword";
import { isNull } from "../../helpers/IsNull";
import { joinStrings } from "../../helpers/String";
import { NoPermissionMessage } from "../../helpers/NoPermissionMessage";
import { NotificationsManager } from "../Notifications/Notifications";
import sharp from "sharp";
import getAvatar from "../../helpers/getAvatar";
import PlanSubscriptionsList from "../../Models/PlanSubscriptions/PlanSubscriptons";
import { ISubscriptionInfo } from "../PlanSubscriptions/PlanSubscriptions.interface";
import MetricsList from "../Metrics/Metrics";
import PlansList from "../Plans/Plans";
import CompaniesList from "../Companies/Companies";
import EmployeesList from "../Employees/Employee";
import SchedulesList from "../Schedules/Schedules";
import SchedulesEnabledList from "../SchedulesEnabled/SchedulesEnabled";
import EmployeesServicesList from "../EmployeesServices/EmployeesServices";
import AppointmentList from "../Appointments/Appointments";
import MessageConversationList from "../MessageConversations/MessagesConversations";
import SystemNoficationsList from "../SystemNotifications/SystemNotification";
import HeatMapList from "../HeatMap/HeatMap";
import ClientsList from "../Clients/Clients";
import RepeatsList from "../Repeats/Repeats";
import { OAuth2Client } from "google-auth-library";
import SchedulesDisabledList from "../SchedulesDisabled/SchedulesDisabled";

const cleanEmail = (email: string): string => {
    return email.toLowerCase().trim();
};

class UsersManager implements IUsersManager {
    users: IUsersAdapter;

    constructor() {
        this.users = new UsersMongooseAdapter();
    }

    private async getOrganizationSubscriptions(
        userId: string
    ): Promise<Record<string, ISubscriptionInfo | null>> {
        const subscriptions: Record<string, ISubscriptionInfo | null> = {};

        // 1. Get companies where user is owner
        const ownerCompanies = await CompaniesList.companies.find({ ownerId: userId });
        for (const company of ownerCompanies) {
            if (company.id) {
                const sub = await PlanSubscriptionsList.getSubscriptionByUser({
                    sessionUser: company.ownerId,
                });
                subscriptions[company.id] = sub;
            }
        }

        // 2. Get companies where user is employee
        const employeeRecords = await EmployeesList.employees.find({ userId: userId });
        for (const employee of employeeRecords) {
            const company = await CompaniesList.companies.findOne({ _id: employee.companyId });
            if (company && company.id && !subscriptions[company.id]) {
                const sub = await PlanSubscriptionsList.getSubscriptionByUser({
                    sessionUser: company.ownerId,
                });
                subscriptions[company.id] = sub;
            }
        }

        return subscriptions;
    }

    public async checkUserPhone(data: CheckUserPhoneParams): Promise<boolean> {
        const sessionUser = await this.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"
            );
        }

        if (!sessionUser.phoneAreaCode || sessionUser.phoneAreaCode == "") {
            return false;
        }

        if (!sessionUser.phoneNumber || sessionUser.phoneNumber == "") {
            return false;
        }

        return true;
    }

    public async getPayerEmail(data: GetPayerEmailParams): Promise<PayerEmailResult> {
        const sessionUser = await this.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"
            );
        }

        return {
            mpPayerEmail: sessionUser.mpPayerEmail ? cleanEmail(sessionUser.mpPayerEmail) : "",
        };
    }

    public async savePayerEmail(data: SavePayerEmailParams): Promise<void> {
        const { email } = data;

        // Define a validation profile using Joi
        const validationProfile = Joi.object({
            email: Joi.string().email().required(),
            sessionUser: Joi.string().required(),
        });

        // Validate the data using the validation profile
        const { error } = validationProfile.validate(data, {
            abortEarly: false,
        });

        // Throw an error if validation fails
        if (error) {
            throw new Error(error.message);
        }

        const sessionUser = await this.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"
            );
        }

        sessionUser.mpPayerEmail = cleanEmail(email);

        await sessionUser.save();
    }

    public async deleteUser(data: DeleteUserParams): Promise<void> {
        const userCheck = await this.users.findOne({ _id: data.sessionUser });

        if (!userCheck) {
            throw new Error("El usuario que intenta dar de baja no existe.");
        }

        const ownerOrgs = await CompaniesList.companies.find({
            ownerId: data.sessionUser,
        });

        //No puede tener organizaciones creadas.
        if (ownerOrgs.length > 0) {
            throw new Error(
                "Para darte de baja primero tenes que eliminar las organizaciones que creaste."
            );
        }

        //No debe tener una suscripcion.
        const sub = await PlanSubscriptionsList.getSubscriptionByUser({
            sessionUser: data.sessionUser,
        });

        if (sub) {
            if (sub.plan.price > 0) {
                throw new Error("Para darte de baja primero tenes que cancelar tu suscripción.");
            }
        }

        try {
            //Si es colaborador en una o mas organizaciones, tenemos que:
            const employees = await EmployeesList.employees.find({
                userId: data.sessionUser,
            });

            for (const employee of employees) {
                const organization = await CompaniesList.companies.findOne({
                    _id: employee.companyId,
                });

                if (!organization) {
                    continue;
                }

                const collaboratorUser = await this.users.findOne({
                    _id: employee.userId,
                });

                if (!collaboratorUser) {
                    continue;
                }

                //1. Eliminar sus horarios de la organizacion.
                await SchedulesList.deleteSchedulesByEmployee({
                    employeeId: String(employee.id),
                });
                //2. Eliminar los horarios habilitados de la organizacion.
                await SchedulesEnabledList.deleteSchedulesEnabledByEmployee({
                    employeeId: String(employee.id),
                });

                //3. Eliminar los horarios deshabilitados de la organizacion.
                await SchedulesDisabledList.deleteSchedulesDisabledByEmployee({
                    employeeId: String(employee.id),
                });

                //3. Eliminar los servicios asignados al colaborador.
                await EmployeesServicesList.deleteEmployeeServiceByEmployee({
                    employeeId: String(employee.id),
                });
                //4. Cambiar los turnos que tenga al duenio de la organizacion.
                const appointments = await AppointmentList.Appointments.find({
                    employeeId: String(employee.id),
                });

                for (const appointment of appointments) {
                    await AppointmentList.changeEmployeeToOwner({
                        appointmentId: String(appointment.id),
                        companyId: String(employee.companyId),
                    });
                }

                //5. Eliminar el HeatMap del Colaborador.
                await HeatMapList.deleteHeatMapByEmployee({
                    employeeId: String(employee.id),
                });

                //6. Eliminar el colaborador de la organizacion.
                await EmployeesList.deleteEmployee({
                    employeeId: String(employee.id),
                });

                //7.Recalcular las metricas del duenio.
                await MetricsList.calculateMetrics({
                    userId: String(organization.ownerId),
                });

                //Enviamos la notificacion al duenio de la organizacion a la que pertenecia el colaborador.
                await SystemNoficationsList.createNotification({
                    userId: String(organization.ownerId),
                    subject: "Colaborador eliminado",
                    message: `El colaborador ${UserList.getUserFullName(
                        collaboratorUser
                    )} ha sido eliminado de tu organización. y sus turnos se han transferido a ti ya que se ha dado de baja de la plataforma.`,
                });
            }

            //Eliminar las repeticiones de turnos.
            await RepeatsList.deleteRepeatsByUser({
                userId: data.sessionUser,
            });

            //Eliminar los clientes creados con el usuario.
            const clients = await ClientsList.clients.find({
                userId: data.sessionUser,
            });

            for (const client of clients) {
                if (!client.id) {
                    continue;
                }

                //Tengo que pasarlo como sesionUSer al duenio de la organizacion. para que pueda eliminar.
                const orgClient = await CompaniesList.companies.findOne({
                    _id: String(client.companyId),
                });

                if (!orgClient) {
                    continue;
                }

                //En este caso borra el cliente pero no chequea el saldo de la cuenta.
                await ClientsList.deleteClient({
                    clientId: String(client.id),
                    checkCashOnAccount: false,
                    sessionUser: String(orgClient.ownerId),
                });
            }

            //Eliminar las conversaciones
            await MessageConversationList.deleteConversationsByUser({
                userId: data.sessionUser,
            });

            //Eliminar las suscripciones
            await PlanSubscriptionsList.deletePlanSuscriptionByUser({
                sessionUser: data.sessionUser,
            });

            //Eliminar las notificaciones
            await SystemNoficationsList.deleteNotificationsByUser({
                userId: data.sessionUser,
            });

            //Eliminar foto de perfil.
            await this.deleteUserFiles(data.sessionUser);

            //Elimminar el usuario.
            await this.users.delete(data.sessionUser);
        } catch (e) {
            console.log("Error al eliminar el usuario:", e);
        }
    }

    private async deleteUserFiles(userId: string): Promise<void> {
        const rootDir =
            process.env.NODE_ENV !== "production"
                ? process.env.UPLOAD_USERS_FILES_ROOT_DEV
                : process.env.UPLOAD_USERS_FILES_ROOT_PRO;

        if (!rootDir) {
            throw new Error("No se ha encontrado la ruta de los archivos");
        }

        const userDir = path.join(__dirname, rootDir, userId);

        try {
            await rm(userDir, { recursive: true, force: true });
        } catch (err) {
            console.error(`Error al eliminar el directorio ${userDir}:`, err);
        }
    }

    public getUserFullName(data: IUser): string {
        return joinStrings([data.firstName, data.lastName], " ");
    }

    private getVerificationCode(): string {
        return Math.floor(Math.random() * 1000000000)
            .toString()
            .substring(0, 6);
    }

    public async signUp(data: SignUpParams): Promise<IUser> {
        // Destructure email and password from the data object
        const {
            email,
            password,
            firstName,
            lastName,
            phoneCountryCode = "549",
            phoneAreaCode,
            phoneNumber,
        } = data;

        // Define a validation profile using Joi
        const validationProfile = Joi.object({
            email: Joi.string().email().required(),
            password: Joi.string().alphanum().min(8).required(),
            firstName: Joi.string().required(),
            lastName: Joi.string().required(),
            phoneAreaCode: Joi.string().required(),
            phoneNumber: Joi.string().required(),
        });

        // Validate the data using the validation profile
        const { error } = validationProfile.validate(data, {
            abortEarly: false,
        });

        // Throw an error if validation fails
        if (error) {
            throw new Error(error.message);
        }

        const user = await this.users.findOne({ email: cleanEmail(email) });

        if (user) {
            throw new Error("El usuario ya se encuentra registrado.");
        }

        const passwordCifrado = await hashPassword(password);

        const verificationCode = this.getVerificationCode();

        const newUserData: IUser = {
            email: cleanEmail(email),
            firstName: firstName,
            lastName: lastName,
            phoneCountryCode: phoneCountryCode,
            phoneAreaCode: phoneAreaCode,
            phoneNumber: phoneNumber,
            password: passwordCifrado,
            verificationCode: verificationCode,
        };

        const newUser = await this.users.create(newUserData);

        const signUpUrl = "https://turnosxpress.com.ar/landing/login/verification";

        await NotificationsManager.sendEmail({
            email: cleanEmail(email),
            subject: "TurnosXpress :: Bienvenido/a",
            message: `Hola ${firstName}, Gracias por registrarte en turnosXpress. Primero debes activar tu cuenta ingresando en ${signUpUrl} . Tu código de verificación es: ${verificationCode}`,
        });

        return newUser;
    }

    public async recoveryAccount(data: RecoveryAccountParams): Promise<void> {
        const validationProfile = Joi.object({
            email: Joi.string().email().required().messages({
                "string.email": "El correo ingresado no es valido",
                "any.required": "El correo es requerido",
                "string.empty": "El correo es requerido",
            }),
            recoveryCode: Joi.string().length(6).optional().messages({
                "string.length": "El código de verificacion debe tener 6 digitos",
            }),
            newPassword: Joi.string().alphanum().min(8).optional(),
            sendRecoveryCode: Joi.boolean().required().messages({
                "any.required": "Faltan parametros requeridos",
                "any.empty": "Faltan parametros requeridos",
            }),
        });

        // Validate the data using the validation profile
        const { error } = validationProfile.validate(data, {
            abortEarly: false,
        });

        // Throw an error if validation fails
        if (error) {
            throw new Error(error.message);
        }

        const user = await this.users.findOne({ email: cleanEmail(data.email) });

        if (!user) {
            throw new Error("El email ingresado no se encuentra registrado");
        }

        if (data.sendRecoveryCode) {
            const code = this.getVerificationCode();

            user.recoveryCode = code;

            await user.save();

            await NotificationsManager.sendEmail({
                email: cleanEmail(data.email),
                subject: "TurnosXpress :: Recuperación de cuenta",
                message: `Hola ${user.firstName}, Te enviamos un código de verificación para recuperar tu cuenta. Tu código de verificación es: ${code}. Ingresa https://turnosxpress.com.ar/landing/recover-account/verify para establecer una nueva clave`,
            });

            return;
        }

        if (!data.recoveryCode) {
            throw new Error("Debes ingresar el código de verificación");
        }

        if (user.recoveryCode !== data.recoveryCode) {
            throw new Error("El código de verificación es incorrecto");
        }

        if (!data.newPassword) {
            throw new Error("Debes ingresar una nueva clave");
        }

        const passwordCifrado = await hashPassword(data.newPassword);

        user.password = passwordCifrado;
        user.recoveryCode = undefined;
        await user.save();
    }

    public async verificateAndLoginUser(data: VerificationUserParams): Promise<ISession> {
        const { email, password, verificationCode } = data;

        // Define a validation profile using Joi
        const validationProfile = Joi.object({
            email: Joi.string().email().required(),
            verificationCode: Joi.string().required(),
            password: Joi.string().alphanum().min(8).required(),
        });

        // Validate the data using the validation profile
        const { error } = validationProfile.validate(data, {
            abortEarly: false,
        });

        // Throw an error if validation fails
        if (error) {
            throw new Error(error.message);
        }

        const userTest = await this.users.findOne({ email: cleanEmail(email) });

        if (!userTest) {
            throw new Error("Usuario no encontrado");
        }

        if (userTest.verificationCode !== verificationCode) {
            throw new Error("El código de verificación es incorrecto");
        }

        userTest.verificated = true;

        await userTest.save();

        try {
            const freePlan = await PlansList.plans.findOne({ price: 0 });
            if (freePlan) {
                const hasSub = await PlanSubscriptionsList.planSuscriptions.findOne({ sessionUser: String(userTest._id) });
                if (!hasSub) {
                    const startDate = new Date();
                    const endDate = new Date(startDate.getTime() + 10 * 365 * 24 * 60 * 60 * 1000); // 10 years
                    await PlanSubscriptionsList.planSuscriptions.create({
                        userId: String(userTest._id),
                        planId: String(freePlan._id),
                        mpPlanId: String(freePlan._id),
                        startDate: startDate,
                        endDate: endDate,
                        autoRenew: true,
                        mpStatus: "authorized",
                        mpInitPoint: "/landing/dashboard"
                    });
                }
            }
        } catch (e) {
            console.error("Error asignando plan gratuito:", e);
        }

        return await this.login({
            email: cleanEmail(email),
            password: password,
        });
    }

    public async login(data: LoginParams): Promise<ISession> {
        const { email, password } = data;

        // Define a validation profile using Joi
        const validationProfile = Joi.object({
            email: Joi.string().email().required(),
            password: Joi.string().alphanum().min(8).required(),
        });

        // Validate the data using the validation profile
        const { error } = validationProfile.validate(data, {
            abortEarly: false,
        });

        // Throw an error if validation fails
        if (error) {
            throw new Error(error.message);
        }

        const userTest = await this.users.findOne({ email: cleanEmail(email) });

        if (!userTest) {
            throw new Error("Usuario no encontrado");
        }

        if (!userTest.password) {
            throw new Error("Clave no proporcionada");
        }

        const passwordTest = await bcrypt.compare(password, userTest.password);

        if (!passwordTest) {
            throw new Error("Credenciales invalidas");
        }

        if (!userTest.verificated) {
            throw new Error("Debes verificar tu cuenta. Revisa la bandeja de entrada de tu correo");
        }
        //....

        userTest.lastLoginDate = new Date();
        await userTest.save();

        const subscription = await PlanSubscriptionsList.getSubscriptionByUser({
            sessionUser: userTest.id,
        });

        const metrics = await MetricsList.getMetrics(userTest.id);

        const userFullName = joinStrings([userTest.firstName, userTest.lastName], " ");

        // Create a payload object with session data
        const payload: ISession = {
            loged: true,
            userEmail: cleanEmail(userTest.email),
            userFullName: userFullName,
            userId: isNull<string>(userTest.id, ""),
            userToken: "",
            avatar: getAvatar(userTest.id, userTest.avatar, userFullName),
            subscription: subscription,
            organizationSubscriptions: await this.getOrganizationSubscriptions(userTest.id),
            metrics: metrics,
        };

        //console.log(payload);

        // Generate a JWT token using the payload and a secret word
        const token = jwt.sign(payload, `${process.env.SECRET_WORD}`, {
            expiresIn: `${process.env.TOKEN_SESSION_TIME}`,
        });

        // Create a user object by merging the payload and the token
        const user: ISession = { ...payload, ...{ userToken: token } };

        // Return the user object
        return user;
    }

    public async loginByToken({ token }: LoginByTokenParams): Promise<ISession> {
        try {
            // Verify the token using the secret word. This also decodes it.
            const decodedPayload = jwt.verify(token, `${process.env.SECRET_WORD}`) as ISession & {
                iat: number;
                exp: number;
            };

            const userTest = await this.users.findOne({
                email: cleanEmail(decodedPayload.userEmail),
            });

            if (!userTest) {
                throw new Error("Ha ocurrido un error al reestablecer la sesión");
            }

            const subscription = await PlanSubscriptionsList.getSubscriptionByUser({
                sessionUser: decodedPayload.userId,
            });

            const metrics = await MetricsList.getMetrics(decodedPayload.userId);

            const userFullName = joinStrings([userTest.firstName, userTest.lastName], " ");

            // Create the session payload. This ensures data is up-to-date.
            const payload: ISession = {
                loged: true,
                userEmail: cleanEmail(userTest.email),
                userFullName: userFullName,
                userId: isNull<string>(userTest.id, ""),
                userToken: "", // Will be filled next
                avatar: getAvatar(userTest.id, userTest.avatar, userFullName),
                subscription: subscription,
                organizationSubscriptions: await this.getOrganizationSubscriptions(decodedPayload.userId),
                metrics: metrics,
            };

            const nowInSeconds = Math.floor(Date.now() / 1000);
            const fiveMinutesInSeconds = 5 * 60;
            let finalToken = token; // Assume we reuse the old token

            // If the token expires in less than 5 minutes, generate a new one.
            if (decodedPayload.exp < nowInSeconds + fiveMinutesInSeconds) {
                finalToken = jwt.sign(payload, `${process.env.SECRET_WORD}`, {
                    expiresIn: `${process.env.TOKEN_SESSION_TIME}`,
                });
            }

            // Return the session object with the appropriate token
            return { ...payload, userToken: finalToken };
        } catch (error) {
            // Throw an error if the token is invalid or expired
            throw new Error("Invalid token");
        }
    }

    private async getGoogleInfo(data: LoginByGoogleParams): Promise<IGoogleData> {
        const profileResponse = await axios.get("https://www.googleapis.com/oauth2/v2/userinfo", {
            headers: {
                Authorization: `Bearer ${data.token}`,
            },
        });

        return profileResponse.data as IGoogleData;
    }

    public async loginByAndroid(data: LoginByGoogleParams): Promise<ISession> {
        const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;

        if (!GOOGLE_CLIENT_ID) {
            throw new Error("Error en el servidor, no se ha configurado el CLIENT_ID de Google");
        }

        const client = new OAuth2Client(GOOGLE_CLIENT_ID);

        const ticket = await client.verifyIdToken({
            idToken: data.token,
            audience: GOOGLE_CLIENT_ID, // Specify the CLIENT_ID of the app that accesses the backend
        });

        const payload = ticket.getPayload();
        if (!payload) {
            throw new Error("Invalid token");
        }

        const googleUserId = payload.sub;
        const email = payload.email;
        const name = payload.name;

        if (!email) {
            throw new Error("Invalid token");
        }

        const userCheck = await this.users.findOne({
            email: cleanEmail(email),
        });

        if (userCheck) {
            userCheck.external_id = googleUserId;
            userCheck.external_service = "google";
            userCheck.verificated = true;
            await userCheck.save();

            const subscription = await PlanSubscriptionsList.getSubscriptionByUser({
                sessionUser: userCheck.id,
            });

            const metrics = await MetricsList.getMetrics(userCheck.id);

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

            const payload: ISession = {
                loged: true,
                userEmail: cleanEmail(userCheck.email),
                userFullName: userFullName,
                userId: isNull<string>(String(userCheck.id), ""),
                userToken: "",
                avatar: getAvatar(userCheck.id, userCheck.avatar, userFullName),
                subscription: subscription,
                organizationSubscriptions: await this.getOrganizationSubscriptions(String(userCheck.id)),
                metrics: metrics,
            };

            // Generate a JWT token using the payload and a secret word
            const token = jwt.sign(payload, `${process.env.SECRET_WORD}`, {
                expiresIn: `${process.env.TOKEN_SESSION_TIME}`,
            });

            // Create a user object by merging the payload and the token
            return { ...payload, ...{ userToken: token } };
        } else {
            const newUserData: IUser = {
                email: cleanEmail(email),
                firstName: name,
                external_id: googleUserId,
                external_service: "google",
            };

            const newUser = await this.users.create(newUserData);

            if (!newUser.id) {
                throw new Error("Error creating user");
            }

            const subscription = await PlanSubscriptionsList.getSubscriptionByUser({
                sessionUser: newUser.id,
            });

            const metrics = await MetricsList.getMetrics(newUser.id);

            const userFullName = joinStrings([newUser.firstName, newUser.lastName], " ");

            const payload: ISession = {
                loged: true,
                userEmail: cleanEmail(newUser.email),
                userFullName: userFullName,
                userId: isNull<string>(String(newUser.id), ""),
                userToken: "",
                avatar: getAvatar(
                    isNull<string>(String(newUser.id), ""),
                    newUser.avatar,
                    userFullName
                ),
                subscription: subscription,
                organizationSubscriptions: await this.getOrganizationSubscriptions(String(newUser.id)),
                metrics: metrics,
            };

            // Generate a JWT token using the payload and a secret word
            const token = jwt.sign(payload, `${process.env.SECRET_WORD}`, {
                expiresIn: `${process.env.TOKEN_SESSION_TIME}`,
            });

            // Create a user object by merging the payload and the token
            return { ...payload, ...{ userToken: token } };
        }
    }

    public async loginByGoogle(data: LoginByGoogleParams): Promise<ISession> {
        try {
            const googleData = await this.getGoogleInfo(data);

            //TODO: creo que debo verificar por email.

            // const userCheck = await this.users.findOne({
            //     external_id: googleId,
            //     external_service: "google",
            //     email: email,
            // });

            const userCheck = await this.users.findOne({
                email: cleanEmail(googleData.email),
            });

            if (userCheck) {
                userCheck.external_id = googleData.id;
                userCheck.external_service = "google";
                userCheck.verificated = true;
                await userCheck.save();

                const subscription = await PlanSubscriptionsList.getSubscriptionByUser({
                    sessionUser: userCheck.id,
                });

                const metrics = await MetricsList.getMetrics(userCheck.id);

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

                const payload: ISession = {
                    loged: true,
                    userEmail: cleanEmail(userCheck.email),
                    userFullName: userFullName,
                    userId: isNull<string>(String(userCheck.id), ""),
                    userToken: "",
                    avatar: getAvatar(userCheck.id, userCheck.avatar, userFullName),
                    subscription: subscription,
                    organizationSubscriptions: await this.getOrganizationSubscriptions(String(userCheck.id)),
                    metrics: metrics,
                };

                // Generate a JWT token using the payload and a secret word
                const token = jwt.sign(payload, `${process.env.SECRET_WORD}`, {
                    expiresIn: `${process.env.TOKEN_SESSION_TIME}`,
                });

                // Create a user object by merging the payload and the token
                return { ...payload, ...{ userToken: token } };
            } else {
                const newUserData: IUser = {
                    email: cleanEmail(googleData.email),
                    firstName: googleData.name,
                    external_id: googleData.id,
                    external_service: "google",
                };

                const newUser = await this.users.create(newUserData);

                if (!newUser.id) {
                    throw new Error("Error creating user");
                }

                const subscription = await PlanSubscriptionsList.getSubscriptionByUser({
                    sessionUser: newUser.id,
                });

                const metrics = await MetricsList.getMetrics(newUser.id);

                const userFullName = joinStrings([newUser.firstName, newUser.lastName], " ");

                const payload: ISession = {
                    loged: true,
                    userEmail: cleanEmail(newUser.email),
                    userFullName: userFullName,
                    userId: isNull<string>(String(newUser.id), ""),
                    userToken: "",
                    avatar: getAvatar(
                        isNull<string>(String(newUser.id), ""),
                        newUser.avatar,
                        userFullName
                    ),
                    subscription: subscription,
                    organizationSubscriptions: await this.getOrganizationSubscriptions(String(newUser.id)),
                    metrics: metrics,
                };

                // Generate a JWT token using the payload and a secret word
                const token = jwt.sign(payload, `${process.env.SECRET_WORD}`, {
                    expiresIn: `${process.env.TOKEN_SESSION_TIME}`,
                });

                // Create a user object by merging the payload and the token
                return { ...payload, ...{ userToken: token } };
            }

            return {} as ISession;
        } catch (error) {
            console.log(error);
            throw new Error("An error occurred while trying to login with Google");
        }
    }

    public async changePassword(data: ChangePasswordParams): Promise<void> {
        const sessionUser = await this.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"
            );
        }

        let passwordTest = false;

        if (sessionUser.password) {
            passwordTest = await bcrypt.compare(data.oldPassword, sessionUser.password);
        }

        if (!passwordTest) {
            throw new Error("Credenciales invalidas");
        }

        if (!sessionUser.verificated) {
            throw new Error("Debes verificar tu cuenta. Revisa la bandeja de entrada de tu correo");
        }

        const validationProfile = Joi.object({
            newPassword: Joi.string().alphanum().min(8).required(),
        });

        // Validate the data using the validation profile
        const { error } = validationProfile.validate(
            { newPassword: data.newPassword },
            {
                abortEarly: true,
            }
        );

        // Throw an error if validation fails
        if (error) {
            throw new Error(error.message);
        }

        const passwordCifrado = await hashPassword(data.newPassword);

        sessionUser.password = passwordCifrado;
        sessionUser.recoveryCode = undefined;
        await sessionUser.save();

        await NotificationsManager.sendEmail({
            email: cleanEmail(sessionUser.email),
            subject: "TurnosXpress :: Contraseña actualizada",
            message: `Hola ${sessionUser.firstName}, has actualizado tu contraseña con exito`,
        });
    }

    public async paginate(filters: PaginateUsersParams): Promise<PaginateUsersResults> {
        return await this.users.paginate(filters);
    }

    public async findOne(data: FindUsersParams): Promise<IUserView> {
        const sessionUser = await this.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"
            );
        }

        return {
            id: sessionUser.id,
            email: cleanEmail(sessionUser.email),
            firstName: sessionUser.firstName,
            lastName: sessionUser.lastName,
            fullName: this.getFullName(sessionUser),
            avatar: getAvatar(sessionUser.id, sessionUser.avatar, this.getFullName(sessionUser)),
            phoneNumber: isNull<string>(sessionUser.phoneNumber, ""),
            phoneCountryCode: isNull<string>(sessionUser.phoneCountryCode, ""),
            phoneAreaCode: isNull<string>(sessionUser.phoneAreaCode, ""),
            formattedPhoneNumber: this.getFormattedPhoneNumber(sessionUser),
            street: isNull<string>(sessionUser.street, ""),
            streetNumber: isNull<string>(sessionUser.streetNumber, ""),
            builingFloor: isNull<string>(sessionUser.builingFloor, ""),
            buildingApartament: isNull<string>(sessionUser.buildingApartament, ""),
            block: isNull<string>(sessionUser.block, ""),
            city: isNull<string>(sessionUser.city, ""),
            state: isNull<string>(sessionUser.state, ""),
            country: isNull<string>(sessionUser.country, ""),
            zipCode: isNull<string>(sessionUser.zipCode, ""),
        };
    }

    public async findByEmail(
        data: FindCollaboratorByEmailParams
    ): Promise<CollaboratorView | null> {
        const user = await this.users.findOne({ email: cleanEmail(data.email) });

        if (!user) {
            return null;
        }

        return {
            id: user.id,
            email: cleanEmail(user.email),
            fullName: this.getFullName(user),
            avatar: getAvatar(user.id, user.avatar, this.getFullName(user)),
        };
    }

    private getFormattedPhoneNumber(user: IUser): string {
        return `(${isNull<string>(user.phoneCountryCode, "")})-${isNull<string>(
            user.phoneAreaCode,
            ""
        )}-${isNull<string>(user.phoneNumber, "")}`;
    }

    private getFullName(user: IUser): string {
        return joinStrings([user.firstName, user.lastName], " ");
    }

    public async updateUser(data: UpdateUserParams): Promise<void> {
        const sessionUser = await this.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"
            );
        }

        if (data.sessionUser != data.id) {
            throw new Error(NoPermissionMessage());
        }

        await this.users.update(data);

        await NotificationsManager.sendEmail({
            email: cleanEmail(sessionUser.email),
            subject: "TurnosXpress :: Usuario actualizado",
            message: `Hola ${sessionUser.firstName}, has actualizado tu perfil con éxito`,
        });
    }

    public async setUserIcon(data: SetUserFileParams): Promise<void> {
        const sessionUser = await this.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"
            );
        }

        if (!data.file) {
            throw new Error("No se ha enviado el archivo");
        }

        const rootDir =
            process.env.NODE_ENV !== "production"
                ? process.env.UPLOAD_USERS_FILES_ROOT_DEV
                : process.env.UPLOAD_USERS_FILES_ROOT_PRO;

        const uploadPath = path.join(__dirname, rootDir + data.sessionUser);

        if (!fs.existsSync(uploadPath)) {
            fs.mkdirSync(uploadPath, { recursive: true });
        }

        const fileExtension = data.file.originalname.split(".").pop();
        const newFileName = "avatar." + fileExtension;

        const filePath = path.join(uploadPath, newFileName);

        await sharp(data.file.buffer)
            .resize({ width: 96, height: 96 })
            .rotate()
            .webp()
            .toFile(filePath);

        sessionUser.avatar = newFileName;
        await sessionUser.save();
    }

    public async getUserAssistanceInfo(data: FindUsersParams): Promise<UserAssistanceInfo> {
        const sessionUser = await this.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"
            );
        }

        if (!data.email) {
            throw new Error("Debe proporcionar el email del usuario");
        }

        const returnValue: UserAssistanceInfo = {
            message: "",
            email: "",
            fullName: "",
            verificated: false,
            activationCode: undefined,
            recoveryCode: undefined,
        };

        const usserAssitance = await this.users.findOne({ email: cleanEmail(data.email) });

        if (!usserAssitance) {
            returnValue.message = "No se ha encontrado un usuario con ese email";
            return returnValue;
        }

        if (usserAssitance.verificated && !usserAssitance.recoveryCode) {
            returnValue.email = cleanEmail(usserAssitance.email);
            returnValue.message =
                "El usuario ya se encuentra activo. Si no puedes ingresar, intenta recuperar tu cuenta.";
            returnValue.verificated = true;
            return returnValue;
        }

        if (usserAssitance.verificated && usserAssitance.recoveryCode) {
            returnValue.email = cleanEmail(usserAssitance.email);
            returnValue.message =
                "El usuario ya se encuentra activo y ha intentado cambiar su clave de acceso. Antes de proporcionar el código de recuperación, asegurate de verificar que se trate realmente del cliente, para evitar que una persona no autorizada acceda a su cuenta.";
            returnValue.verificated = true;
            returnValue.recoveryCode = usserAssitance.recoveryCode;
            return returnValue;
        }

        returnValue.email = cleanEmail(usserAssitance.email);
        returnValue.fullName = this.getFullName(usserAssitance);
        returnValue.verificated = false;
        returnValue.activationCode = usserAssitance.verificationCode;
        returnValue.message = `El usuario no se encuentra activo.`;

        return returnValue;
    }

    // --- SysAdmin Methods ---

    public async sysAdminPaginateUsers(
        data: SysAdminPaginateUsersParams
    ): Promise<PaginateUsersResults> {
        return await this.users.sysAdminPaginate(data);
    }

    public async sysAdminUpdateProfile(data: SysAdminUpdateProfileParams): Promise<void> {
        const { userId, ...updateData } = data;
        const user = await this.users.findOne({ _id: userId });
        if (!user) {
            throw new Error("Usuario no encontrado");
        }

        await this.users.update({
            id: userId,
            sessionUser: userId, // Mocking sessionUser because update params require it
            firstName: updateData.firstName,
            lastName: updateData.lastName,
            street: updateData.street,
            streetNumber: updateData.streetNumber,
            builingFloor: updateData.builingFloor,
            buildingApartament: updateData.buildingApartament,
            block: updateData.block,
            city: updateData.city,
            state: updateData.state,
            country: updateData.country,
            zipCode: updateData.zipCode,
            phoneCountryCode: updateData.phoneCountryCode,
            phoneAreaCode: updateData.phoneAreaCode,
            phoneNumber: updateData.phoneNumber,
        });
    }

    public async sysAdminSetVerifiedStatus(data: SysAdminSetVerifiedParams): Promise<void> {
        const user = await this.users.findOne({ _id: data.userId });
        if (!user) {
            throw new Error("Usuario no encontrado");
        }
        user.verificated = data.isVerified;
        if (data.isVerified) {
            user.verificationCode = undefined;
        }
        await user.save();
    }

    public async sysAdminDeleteUser(data: SysAdminDeleteUserParams): Promise<void> {
        const userCheck = await this.users.findOne({ _id: data.userId });

        if (!userCheck) {
            throw new Error("El usuario que intenta dar de baja no existe.");
        }

        const ownerOrgs = await CompaniesList.companies.find({
            ownerId: data.userId,
        });

        //No puede tener organizaciones creadas.
        if (ownerOrgs.length > 0) {
            throw new Error(
                "Para dar de baja al usuario primero tienes que eliminar las organizaciones que creó (manualmente)."
            );
        }

        //No debe tener una suscripcion.
        const sub = await PlanSubscriptionsList.getSubscriptionByUser({
            sessionUser: data.userId,
        });

        if (sub) {
            if (sub.plan.price > 0) {
                throw new Error("Para dar de baja al usuario primero tienes que cancelar su suscripción (manualmente).");
            }
        }

        try {
            //Si es colaborador en una o mas organizaciones, tenemos que:
            const employees = await EmployeesList.employees.find({
                userId: data.userId,
            });

            for (const employee of employees) {
                const organization = await CompaniesList.companies.findOne({
                    _id: employee.companyId,
                });

                if (!organization) continue;

                const collaboratorUser = await this.users.findOne({
                    _id: employee.userId,
                });

                if (!collaboratorUser) continue;

                await SchedulesList.deleteSchedulesByEmployee({ employeeId: String(employee.id) });
                await SchedulesEnabledList.deleteSchedulesEnabledByEmployee({ employeeId: String(employee.id) });
                await SchedulesDisabledList.deleteSchedulesDisabledByEmployee({ employeeId: String(employee.id) });
                await EmployeesServicesList.deleteEmployeeServiceByEmployee({ employeeId: String(employee.id) });

                const appointments = await AppointmentList.Appointments.find({ employeeId: String(employee.id) });
                for (const appointment of appointments) {
                    await AppointmentList.changeEmployeeToOwner({
                        appointmentId: String(appointment.id),
                        companyId: String(employee.companyId),
                    });
                }

                await HeatMapList.deleteHeatMapByEmployee({ employeeId: String(employee.id) });
                await EmployeesList.deleteEmployee({ employeeId: String(employee.id) });
                await MetricsList.calculateMetrics({ userId: String(organization.ownerId) });

                await SystemNoficationsList.createNotification({
                    userId: String(organization.ownerId),
                    subject: "Colaborador eliminado por SysAdmin",
                    message: `El colaborador ${this.getUserFullName(collaboratorUser)} ha sido dado de baja por la administración y sus turnos se han transferido a ti.`,
                });
            }

            await RepeatsList.deleteRepeatsByUser({ userId: data.userId });

            const clients = await ClientsList.clients.find({ userId: data.userId });
            for (const client of clients) {
                if (!client.id) continue;

                const orgClient = await CompaniesList.companies.findOne({ _id: String(client.companyId) });
                if (!orgClient) continue;

                await ClientsList.deleteClient({
                    clientId: String(client.id),
                    checkCashOnAccount: false,
                    sessionUser: String(orgClient.ownerId),
                });
            }

            await MessageConversationList.deleteConversationsByUser({ userId: data.userId });
            await PlanSubscriptionsList.deletePlanSuscriptionByUser({ sessionUser: data.userId });
            await SystemNoficationsList.deleteNotificationsByUser({ userId: data.userId });
            await this.deleteUserFiles(data.userId);

            await this.users.delete(data.userId);
        } catch (e) {
            console.log("Error al eliminar el usuario (SysAdmin):", e);
            throw e;
        }
    }

    public async sysAdminOrganizationsStatus(
        data: SysAdminOrganizationsStatusParams
    ): Promise<SysAdminUserOrganizationsResult> {
        const result: SysAdminUserOrganizationsResult = {
            clientIn: [],
            collaboratorIn: [],
        };

        const clients = await ClientsList.clients.find({ userId: data.userId });
        for (const client of clients) {
            const org = await CompaniesList.companies.findOne({ _id: String(client.companyId) });
            if (org) {
                result.clientIn.push({ companyId: String(org.id), companyName: org.name });
            }
        }

        const employees = await EmployeesList.employees.find({ userId: data.userId });
        for (const emp of employees) {
            const org = await CompaniesList.companies.findOne({ _id: String(emp.companyId) });
            if (org) {
                result.collaboratorIn.push({ companyId: String(org.id), companyName: org.name });
            }
        }

        return result;
    }
}

const UserList = new UsersManager();

export default UserList;
