import UsersManager from "../Users/Users";
import CompaniesManager from "../Companies/Companies";
import { NoPermissionMessage } from "../../helpers/NoPermissionMessage";
import { NotificationsManager } from "../Notifications/Notifications";
import { RepeatsAdapterMongoose } from "./Repeats.Adapter.Mongoose";
import {
    AddRepeatDisabledDateParams,
    CountRepeatsParams,
    CreateRepeatsParams,
    DeleteRepeatsByClientParams,
    DeleteRepeatsByCompanyParams,
    DeleteRepeatsByUserParams,
    DeleteRepeatsParams,
    FindRepeatsParams,
    IRepeat,
    IRepeatsManager,
    RepeatView,
    GetPendingRepeatsParams,
    PendingRepeatView,
} from "./Repeats.Interface";
import { EmployeeRoles } from "../Employees/Employees.Interface";
import ServiceList from "../Services/Service";
import ClientsList from "../Clients/Clients";
import EmployeesList from "../Employees/Employee";
import UserList from "../Users/Users";
import dayjs from "dayjs";
import localeData from "dayjs/plugin/localeData";
import "dayjs/locale/es";
import weekday from "dayjs/plugin/weekday";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
import { isNull } from "../../helpers/IsNull";
import getAvatar from "../../helpers/getAvatar";
import MetricsList from "../Metrics/Metrics";
import AppointmentList from "../Appointments/Appointments";
import Discounts from "../Discounts/Discounts";
import { NotificationType } from "../../Models/SystemNotifications/SystemNotification.Interface";

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

class RepeatsManager implements IRepeatsManager {
    repeats: RepeatsAdapterMongoose;

    constructor() {
        this.repeats = new RepeatsAdapterMongoose();
    }

    private numberToTime(decimal: number): string {
        const hours = Math.floor(decimal);
        const minutes = Math.round((decimal - hours) * 60);
        const paddedMinutes = minutes.toString().padStart(2, "0");
        return `${hours}:${paddedMinutes}hs`;
    }

    public async createRepeat(data: CreateRepeatsParams): Promise<IRepeat> {
        //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 EmployeesList.checkPermission(
                companyCheck,
                data.sessionUser,
                EmployeeRoles.ADMIN
            ))
        ) {
            throw new Error(NoPermissionMessage());
        }

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

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

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

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

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

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

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

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

        const checkClient = await ClientsList.clients.findOne({
            _id: data.clientId,
        });

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

        const checkRepeat = await this.repeats.findOne({
            companyId: data.companyId,
            clientId: data.clientId,
            weekDay: data.weekDay,
            startHour: data.startHour,
        });

        if (checkRepeat) {
            throw new Error("Ya existe una reserva previa.");
        }

        const newRepeat = await this.repeats.create({
            ...data,
            ...{ length: checkService.length },
        });

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

        const humanDay = dayjs().localeData().weekdays()[data.weekDay];
        const humanHour = this.numberToTime(data.startHour);

        if (checkClient.userId) {
            await NotificationsManager.sendSystemNotification({
                userId: String(checkClient.userId),
                subject: "Se ha creado una nueva reserva con renovación automática",
                message: `La organización ${
                    companyCheck.name
                } ha asignado una reserva con renovación automática todos los ${humanDay} a las ${humanHour} para ${
                    checkService.name
                } con  ${UserList.getUserFullName(employeeUser)}.`,
                type: NotificationType.SYSTEM
            });
        }

        await NotificationsManager.sendSystemNotification({
                userId: String(checkEmployee.userId),
                subject: "Se ha creado una nueva reserva con renovación automática",
                message: `${ClientsList.getClientFullName(
                checkClient
            )} tiene una reservación con renovación automática todos los ${humanDay} a las ${humanHour} para ${
                checkService.name
            } con vos.`,
                type: NotificationType.SYSTEM
            });

        return newRepeat;
    }

    public async deleteRepeat(data: DeleteRepeatsParams): 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 checkRepeat = await this.repeats.findOne({
            _id: data.repeatId,
        });

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

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

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

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

        await this.repeats.delete(data.repeatId);
    }

    public async deleteRepeatsByCompany(data: DeleteRepeatsByCompanyParams): Promise<void> {
        await this.repeats.repeatsList.deleteMany({ companyId: data.companyId });
    }

    public async deleteRepeatsByClient(data: DeleteRepeatsByClientParams): Promise<void> {
        await this.repeats.repeatsList.deleteMany({ clientId: data.clientId });
    }

    public async deleteRepeatsByUser(data: DeleteRepeatsByUserParams): Promise<void> {
        //obtengo los clientes asociados al usuario.
        const clients = await ClientsList.clients.find({
            userId: data.userId,
        });

        for (const client of clients) {
            await this.deleteRepeatsByClient({
                clientId: String(client.id),
            });
        }
    }

    public async getRepeatsByClient(data: FindRepeatsParams): Promise<RepeatView[]> {
        const returnValue: RepeatView[] = [];

        const repeats = await this.repeats.find(data);

        for (const repeat of repeats) {
            if (!repeat.id) {
                continue;
            }

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

            if (!companyCheck) {
                continue;
            }

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

            if (!employeeCheck) {
                continue;
            }

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

            if (!employeeUser) {
                continue;
            }

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

            if (!serviceCheck) {
                continue;
            }

            const clientCheck = await ClientsList.clients.findOne({
                _id: repeat.clientId,
            });

            if (!clientCheck) {
                continue;
            }

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

            returnValue.push({
                id: repeat.id,
                weekDay: repeat.weekDay,
                weekDayName: dayjs().localeData().weekdays()[repeat.weekDay],
                startHour: repeat.startHour,
                startHourFormatted: this.numberToTime(repeat.startHour),
                length: repeat.length,
                companyId: repeat.companyId,
                companyName: companyCheck.name,
                employeeId: repeat.employeeId,
                employeeName: UserList.getUserFullName(employeeUser),
                employeeCalendarColor: isNull<string>(employeeCheck.calendarColor, "#000000"),
                employeeAvatar: getAvatar(
                    employeeCheck.userId,
                    employeeUser.avatar,
                    UserList.getUserFullName(employeeUser)
                ),
                serviceId: repeat.serviceId,
                serviceName: serviceCheck.name,
                discountId: isNull<string>(repeat.discountId, ""),
                discountName: discountData.name,
                discountType: discountData.type,
                discountValue: discountData.value,
                discountCode: discountData.code,
                clientId: repeat.clientId,
                clientName: ClientsList.getClientFullName(clientCheck),
            });
        }

        return returnValue;
    }

    public async countRepeats(data: CountRepeatsParams): Promise<number> {
        let count = 0;
        const countDateStart = dayjs(data.countDate);
        const countDateEnd = countDateStart.clone().add(data.heatMapFraction, "minutes");
        const weekDay = dayjs(data.countDate).weekday() + 1;
        const weekStart = dayjs(data.countDate).startOf("week");

        const repeats = await this.repeats.find({
            companyId: data.companyId,
            employeeId: data.employeeId,
            weekDay: weekDay,
        });

        for (const repeat of repeats) {
            if (!repeat.id) {
                continue;
            }

            const repeatHour = Math.floor(repeat.startHour);
            const repeatMinutes = (repeat.startHour - repeatHour) * 60;
            const repeatWeekDay = weekDay - 1;

            if (repeatWeekDay < 0) {
                continue;
            }

            const repeatDateStart = weekStart
                .add(repeatWeekDay, "d")
                .startOf("day")
                .hour(repeatHour)
                .minute(repeatMinutes)
                .second(0)
                .millisecond(0);

            //Si la fecha esta deshabilitada no tengo que contarla.
            if (repeat.disabledDates) {
                if (this.existDisabledDate(repeatDateStart.toDate(), repeat.disabledDates)) {
                    continue;
                }
            }

            //TODO: Si el turno ya esta creado no tengo que contarlo...
            const searchDate = repeatDateStart.clone().toDate();

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

            if (appointmentCreated) {
                continue;
            }

            const repeatDateEnd = repeatDateStart.clone().add(repeat.length, "minutes");

            if (
                repeatDateStart.isSameOrBefore(countDateStart) &&
                repeatDateEnd.isSameOrAfter(countDateEnd)
            ) {
                count++;
            }
        }

        return count;
    }

    public async getPendingRepeatsCurrentWeek(data: GetPendingRepeatsParams): Promise<PendingRepeatView[]> {
        const returnValue: PendingRepeatView[] = [];

        const weekStart = dayjs().startOf("week");
        const filters: any = {};
        if (data.companyId) {
            const companyCheck = await CompaniesManager.companies.findOne({ _id: data.companyId });
            if (!companyCheck) {
                throw new Error("La compañia no existe");
            }
            filters.companyId = data.companyId;
        }

        if (data.filterBySessionUser) {
            const employeeQuery: any = { userId: data.sessionUser };
            if (data.companyId) {
                employeeQuery.companyId = data.companyId;
            }
            const sessionEmployees = await EmployeesList.employees.find(employeeQuery);
            if (sessionEmployees && sessionEmployees.length > 0) {
                filters.employeeId = { $in: sessionEmployees.map(e => e.id) };
            } else {
                // If user is not an employee, they can't have personal repeats
                return []; 
            }
        } else if (data.employeeId) {
            filters.employeeId = data.employeeId;
        }

        const repeats = await this.repeats.find(filters);

        for (const repeat of repeats) {
            if (!repeat.id) {
                continue;
            }

            const repeatHour = Math.floor(repeat.startHour);
            const repeatMinutes = Math.round((repeat.startHour - repeatHour) * 60);
            
            // dayjs() .weekday() logic: 0 to 6
            // repeat.weekDay is likely 1-7 or 0-6? 
            // In countRepeats: const weekDay = dayjs(data.countDate).weekday() + 1;
            // and repeat.weekDay is checked against it. So repeat.weekDay corresponds to weekday()+1
            
            const repeatWeekDay = repeat.weekDay - 1;

            if (repeatWeekDay < 0 || repeatWeekDay > 6) {
                continue; // Should not happen if data is correct
            }

            const repeatDateStart = weekStart
                .clone()
                .add(repeatWeekDay, "d")
                .startOf("day")
                .hour(repeatHour)
                .minute(repeatMinutes)
                .second(0)
                .millisecond(0);

            // Si la fecha es en el pasado, no la mostramos como pendiente para crear
            if (repeatDateStart.isBefore(dayjs().startOf("day"))) {
                continue;
            }
            
            const searchDate = repeatDateStart.toDate();

            // Si la fecha esta deshabilitada no tengo que devolverla
            if (repeat.disabledDates && this.existDisabledDate(searchDate, repeat.disabledDates)) {
                continue;
            }

            // Check if appointment is already created
            const appointmentCreated = await AppointmentList.Appointments.findOne({
                companyId: String(repeat.companyId),
                clientId: String(repeat.clientId),
                serviceId: String(repeat.serviceId),
                employeeId: String(repeat.employeeId),
                start: searchDate,
            });

            if (appointmentCreated) {
                continue;
            }

            // Prepare the view
            const employeeCheck = await EmployeesList.employees.findOne({ _id: repeat.employeeId });
            if (!employeeCheck) continue;

            const employeeUser = await UsersManager.users.findOne({ _id: employeeCheck.userId });
            if (!employeeUser) continue;

            const serviceCheck = await ServiceList.services.findOne({ _id: repeat.serviceId });
            if (!serviceCheck) continue;

            const clientCheck = await ClientsList.clients.findOne({ _id: repeat.clientId });
            if (!clientCheck) continue;

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

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

            returnValue.push({
                id: repeat.id,
                weekDay: repeat.weekDay,
                weekDayName: dayjs().localeData().weekdays()[repeat.weekDay],
                startHour: repeat.startHour,
                startHourFormatted: this.numberToTime(repeat.startHour),
                length: repeat.length,
                companyId: repeat.companyId,
                companyName: companyCheck ? companyCheck.name : "",
                employeeId: repeat.employeeId,
                employeeName: UserList.getUserFullName(employeeUser),
                employeeCalendarColor: isNull<string>(employeeCheck.calendarColor, "#000000"),
                employeeAvatar: getAvatar(employeeCheck.userId, employeeUser.avatar, UserList.getUserFullName(employeeUser)),
                serviceId: repeat.serviceId,
                serviceName: serviceCheck.name,
                discountId: isNull<string>(repeat.discountId, ""),
                discountName: discountData.name,
                discountType: discountData.type,
                discountValue: discountData.value,
                discountCode: discountData.code,
                clientId: repeat.clientId,
                clientName: ClientsList.getClientFullName(clientCheck),
                pendingDate: searchDate
            });
        }

        // Sort by pendingDate
        returnValue.sort((a, b) => a.pendingDate.getTime() - b.pendingDate.getTime());

        return returnValue;
    }

    public existDisabledDate(checkDate: Date, disabledDates: Date[]): boolean {
        return disabledDates.some((fecha) => fecha.getTime() === checkDate.getTime());
    }

    public async addDisabledDate(data: AddRepeatDisabledDateParams): Promise<void> {
        const repeat = await this.repeats.findOne({
            _id: data.repeatId,
        });

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

        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: String(repeat.companyId),
        });

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

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

        if (!repeat.disabledDates) {
            repeat.disabledDates = [];
        }

        if (this.existDisabledDate(data.disableDate, repeat.disabledDates)) {
            console.log("La fecha ya estaba deshabilitada");
            return;
        }

        repeat.disabledDates.push(new Date(data.disableDate));

        await repeat.save();
    }
}

const RepeatsList = new RepeatsManager();

export default RepeatsList;
