import crypto from 'crypto';
import {
    BadRequestError,
    BaseAdapter,
    BaseXlsxPort,
    ENV_OPTIONAL_NUMBER_KEYS_MAPPING,
    ErrorCodes,
    EventEmitterPort,
    Events,
    ISO_DATE,
    LanguagesValues,
    Modules,
    OrderItem,
    TranslationsPort,
    WORKDAYS,
    allPromises,
} from '@humand-packages/common';
import moment from 'moment';
import { Inject, Service } from 'typedi';
import { v4 as uuidv4 } from 'uuid';
import { PortsDI } from '../../../../config/portsDI';
import { UpdateWorkSchedule, UpdateWorkScheduleDataResult } from '../../../../types/bulkUploads/checkDataResult';
import { MethodParams } from '../../../../types/bulkUploads/runJobDTO';
import { AttachmentsService } from '../../../attachments/business/services/attachmentsService';
import { JobErrorCodes } from '../../../jobs/business/constants/jobErrors';
import { JobErrorData } from '../../../jobs/business/types/jobErrors';
import { SiteService } from '../../../regions/business/services/sitesService';
import { User } from '../../../users/business/models/user';
import { UserInterface } from '../../../users/business/models/userInterface';
import { UsersService } from '../../../users/business/services/usersService';
import { UserDAO } from '../../../users/infrastructure/dataAccessObjects/userDAO';
import { userDAOtoUser } from '../../../users/infrastructure/mappers/usersMapper';
import { WorkScheduleUsersDAOCreationAttributes } from '../../infrastructure/dataAccessObjects/workScheduleUsers';
import { TIME_REGEX, compareTimeSlots } from '../../infrastructure/mappers/workScheduleMapper';
import { WorkScheduleRepositoryPort } from '../../infrastructure/ports/WorkScheduleRepositoryPort';
import { BulkUploadFileTranslationKeys } from '../../presentation/constants/bulkUpload';
import { CategorizedHoursActionType } from '../constants/categorizedHoursActionTypes';
import { ScheduleFilter } from '../interfaces/timeTrackingDaySummaryFilters';
import { WorkScheduleUserAssignmentAttributes } from '../interfaces/WorkScheduleUserAssignmentAttributes';

import { WorkScheduleUserAssignmentFilters } from '../interfaces/workScheduleUserAssignmentFilters';
import { TimeSlot } from '../models/timeTracking/scheduleData';
import {
    AssignedWorkSchedule,
    ScheduledDays,
    WorkSchedule,
    WorkScheduleCreationAttributes,
    WorkScheduleCreationParameters,
    WorkScheduleUpdateAttributes,
    WorkScheduleUserAssignment,
    allWorkScheduleTranslationKeys,
    generalWorkScheduleTranslationKeys,
    workScheduleMandatoryUpdateTranslationKeys,
} from '../models/workSchedule';
import { WorkSchedulesSearchFilters } from '../models/workScheduleSearchFilters';
import ScheduleCategorizedHoursPort from '../ports/scheduleCategorizedHoursPort';
import WorkScheduleServicePort from '../ports/workScheduleServicePort';

const INITIAL_VERSION = 1;
const YES_VALUES = ['si', 'sí', 'sim', 'yes', 'y'];
const enumOrder = [WORKDAYS.MONDAY, WORKDAYS.TUESDAY, WORKDAYS.WEDNESDAY, WORKDAYS.THURSDAY, WORKDAYS.FRIDAY, WORKDAYS.SATURDAY, WORKDAYS.SUNDAY];

@Service()
export class WorkScheduleServiceAdapter extends BaseAdapter implements WorkScheduleServicePort {
    constructor(
        @PortsDI(WorkScheduleRepositoryPort) private readonly workScheduleRepository: WorkScheduleRepositoryPort,
        private readonly usersService: UsersService,
        private readonly eventEmitter: EventEmitterPort,
        private readonly xlsxPort: BaseXlsxPort,
        private readonly attachmentsService: AttachmentsService,
        private readonly siteService: SiteService,
        private readonly translationsPort: TranslationsPort,
        @PortsDI(ScheduleCategorizedHoursPort) private readonly scheduleCategorizedHoursPort: ScheduleCategorizedHoursPort,
    ) {
        super();
    }

    @Inject(ENV_OPTIONAL_NUMBER_KEYS_MAPPING.MAX_BULK_UPDATE_WORK_SCHEDULES_RECORDS)
    private maxBulkUpdateWorkScheduleRecords!: number;

    createSchedule = async (params: WorkScheduleCreationParameters): Promise<WorkSchedule> => {
        if (params.scheduledDays.length === 0) {
            throw new BadRequestError('Work schedule must have at least one scheduled day', ErrorCodes.WORK_SCHEDULE_WITH_NO_DAYS);
        }
        if (params.generalTimeSlots && params.generalTimeSlots?.length > 0) {
            params.scheduledDays.forEach((day) => (day.timeSlots = params.generalTimeSlots!));
        }
        const scheduleCreated = await this.workScheduleRepository.createSchedule({ ...params, code: uuidv4(), version: INITIAL_VERSION });
        return scheduleCreated;
    };

    listWorkSchedules = (
        params: WorkSchedulesSearchFilters,
        pagination: { limit: number; offset: number },
        order?: OrderItem[],
    ): Promise<{ count: number; rows: WorkSchedule[] }> => {
        return this.workScheduleRepository.listWorkSchedules(params, pagination, order);
    };

    getWorkSchedule = (id: number, instanceId: number, timezone: string): Promise<WorkSchedule> => {
        return this.workScheduleRepository.getWorkSchedule(id, instanceId, timezone, true, false);
    };

    getWorkScheduleByUser = (userId: number, instanceId: number, timezone: string): Promise<WorkSchedule | undefined> => {
        return this.workScheduleRepository.getWorkScheduleByUser(userId, instanceId, timezone);
    };

    getWorkScheduleByUserIds = async (
        userIds: number[],
        instanceId: number,
        timezone: string,
    ): Promise<{ [userId: number]: AssignedWorkSchedule }> => {
        const assignedWorkSchedules = await this.workScheduleRepository.getWorkSchedulesByUsers(userIds, instanceId, timezone);
        return assignedWorkSchedules.reduce(
            (acc, assignedWorkSchedule) => {
                acc[assignedWorkSchedule.userId] = assignedWorkSchedule;
                return acc;
            },
            {} as { [userId: number]: AssignedWorkSchedule },
        );
    };

    assignWorkScheduleToUser = async (userId: number, workScheduleId: number, instanceId: number, timezone: string) => {
        const schedule = await this.getWorkSchedule(workScheduleId, instanceId, timezone);
        await this.assignUsersToSchedule(
            {
                collaborators: [userId],
                id: workScheduleId,
                instanceId,
                scheduledDays: schedule.scheduledDays.map((day) => day.dayOfWeek),
                startDate: moment.tz(timezone).format(ISO_DATE),
            },
            timezone,
        );
        return schedule;
    };

    unassignWorkScheduleFromUser = async (userId: number, instanceId: number, timezone: string) => {
        const schedule = await this.getWorkScheduleByUser(userId, instanceId, timezone);
        if (!schedule) {
            return;
        }
        await this.workScheduleRepository.unassignUsers([userId]);
    };

    getAssignedWorkSchedulesByUser = (userId: number, instanceId: number, fromDate: string, toDate: string): Promise<AssignedWorkSchedule[]> => {
        return this.workScheduleRepository.getAssignedWorkSchedulesByUser(userId, instanceId, fromDate, toDate);
    };

    getAssignedWorkSchedulesBySchedule = (
        id: number,
        instanceId: number,
        pagination: { limit: number; offset: number },
    ): Promise<{ count: number; rows: AssignedWorkSchedule[] }> => {
        return this.workScheduleRepository.getAssignedWorkSchedulesBySchedule(id, instanceId, pagination);
    };

    async eliminateWorkSchedule(id: number, instanceId: number): Promise<void> {
        const startDate = moment().format(ISO_DATE);
        const affectedUsers = await this.workScheduleRepository.getTransaction(async () => {
            const userIds = await this.workScheduleRepository.eliminateWorkSchedule(id);
            if (userIds.length > 0) {
                await this.scheduleCategorizedHoursPort.schedule({
                    payload: { endDate: startDate, instanceId, startDate, userIds },
                    type: CategorizedHoursActionType.SCHEDULE_CHANGE,
                });
            }
            return userIds;
        });
        this.eventEmitter.emitEvent(Events.WORK_SCHEDULE_NEW_USERS, {
            instanceId,
            startDate,
            userIds: affectedUsers,
        });
    }

    async updateWorkSchedule(instanceId: number, id: number, attributes: WorkScheduleUpdateAttributes, timezone: string): Promise<WorkSchedule> {
        const oldSchedule = await this.workScheduleRepository.getWorkSchedule(id, instanceId, timezone);
        const newSchedule: WorkScheduleCreationAttributes & { id?: number } = { ...oldSchedule, ...attributes };
        delete newSchedule.id;
        if (attributes.generalTimeSlots && attributes.generalTimeSlots?.length > 0) {
            newSchedule.scheduledDays.forEach((day) => (day.timeSlots = newSchedule.generalTimeSlots!));
        } else if (attributes.scheduledDays?.length && oldSchedule.generalTimeSlots?.length) {
            delete newSchedule.generalTimeSlots;
        }
        if (this.shouldCreateNewVersion(oldSchedule, newSchedule)) {
            let affectedUsers: number[] = [];
            const startDate = moment.tz(timezone).format(ISO_DATE);
            const resultSchedule = await this.workScheduleRepository.getTransaction(async () => {
                const scheduleCreated = await this.workScheduleRepository.createSchedule({
                    ...newSchedule,
                    code: oldSchedule.code,
                    version: oldSchedule.version + 1,
                });
                affectedUsers = await this.workScheduleRepository.eliminateWorkSchedule(id, scheduleCreated.id);
                if (affectedUsers.length > 0) {
                    await this.scheduleCategorizedHoursPort.schedule({
                        payload: { endDate: startDate, instanceId, startDate, userIds: affectedUsers },
                        type: CategorizedHoursActionType.SCHEDULE_CHANGE,
                    });
                }
                return scheduleCreated;
            });
            this.eventEmitter.emitEvent(Events.WORK_SCHEDULE_NEW_USERS, {
                instanceId,
                startDate,
                userIds: affectedUsers,
            });
            return resultSchedule;
        }
        const workSchedule = oldSchedule;
        if (oldSchedule.name !== newSchedule.name || oldSchedule.description !== newSchedule.description) {
            await this.workScheduleRepository.updateWorkSchedule(id, { description: newSchedule.description, name: newSchedule.name });
            workSchedule.name = newSchedule.name;
            workSchedule.description = newSchedule.description;
        }
        return workSchedule;
    }

    assignUsersToSchedule = async (params: WorkScheduleUserAssignmentAttributes, timezone: string) => {
        const startDate = moment.tz(params.startDate, timezone).format(ISO_DATE);
        const endDate = params.endDate ? moment.tz(params.endDate, timezone).format(ISO_DATE) : undefined;
        await this.workScheduleRepository.assignUsers(params, timezone);
        await this.scheduleCategorizedHoursPort.schedule({
            payload: {
                endDate: endDate ?? moment.tz(timezone).format(ISO_DATE),
                instanceId: params.instanceId,
                startDate,
                userIds: params.collaborators,
            },
            type: CategorizedHoursActionType.SCHEDULE_CHANGE,
        });
        this.eventEmitter.emitEvent(Events.WORK_SCHEDULE_NEW_USERS, {
            endDate,
            instanceId: params.instanceId,
            startDate,
            userIds: params.collaborators,
        });
        if (moment().isBetween(params.startDate, params.endDate ?? moment('9999-12-31'), 'day', '[]')) {
            await this.updateUsersWorkdays(
                params.instanceId,
                params.collaborators,
                params.scheduledDays.sort((a, b) => enumOrder.indexOf(a) - enumOrder.indexOf(b)),
            );
        }
    };

    getWorkSchedulesByXlsxBuffer = async (buffer: Buffer, user: UserInterface): Promise<Record<string, string>[]> => {
        const workbook = await this.xlsxPort.getWorkbookByBuffer(buffer);
        const translations = this.getWorkScheduleTranslations(user.language);
        const expectedHeaders = [
            ...workScheduleMandatoryUpdateTranslationKeys.map((key) => translations[key]),
            ...generalWorkScheduleTranslationKeys.map((key) => translations[key]),
        ];
        const { recordArray } = this.xlsxPort.worksheetToRecordArray(workbook.worksheets[0], {
            expectedHeaders,
        });
        this.xlsxPort.verifyMaxRecordCount(recordArray, this.maxBulkUpdateWorkScheduleRecords, 'Work schedules');
        return recordArray;
    };

    getWorkScheduleTranslations = (language: LanguagesValues): Record<string, string> => {
        const workScheduleTranslations = this.translationsPort.translateModule(
            language,
            Modules.TIME_TRACKING,
            workScheduleMandatoryUpdateTranslationKeys.map((key) => ({ key })),
        );
        const generalWorkScheduleTranslations = this.translationsPort.translateModule(
            language,
            Modules.TRANSLATION,
            generalWorkScheduleTranslationKeys.map((key) => ({ key })),
        );
        return { ...workScheduleTranslations, ...generalWorkScheduleTranslations };
    };

    getUpdateWorkSchedulesDataByXlsx = async (inputData: Record<string, unknown>, params: MethodParams): Promise<UpdateWorkScheduleDataResult> => {
        const translations = this.getWorkScheduleTranslations(params.user.language);
        const errors: JobErrorData[] = [];
        const scheduleDataRow = inputData as Record<string, string>;
        const employeeInternalId = scheduleDataRow[translations[BulkUploadFileTranslationKeys.EMPLOYEE_INTERNAL_ID]]?.toString().trim();
        let user: UserDAO | null = null;
        if (!employeeInternalId) {
            errors.push({ code: JobErrorCodes.BULK_UPDATE_WORK_SCHEDULE_MISSING_EMPLOYEE_INTERNAL_ID, description: {} });
        } else {
            user = await this.usersService.findByEmployeeInternalId(params.instanceId, employeeInternalId);
            if (!user) {
                errors.push({ code: JobErrorCodes.BULK_UPDATE_PROFILE_FIELDS_BAD_USER, description: { employeeInternalId } });
            }
        }
        const scheduleDays: ScheduledDays[] = [];
        const badDays: string[] = [];
        const dayColumns = allWorkScheduleTranslationKeys.map((key) => translations[key]);
        dayColumns.shift(); // remove user column
        dayColumns.forEach((day) => {
            if (!scheduleDataRow[day]) return;
            const lowerWorkDaysData = scheduleDataRow[day].toString().toLowerCase();
            const workdaysDict = this.getWorkdaysTranslations(dayColumns);
            if (YES_VALUES.includes(lowerWorkDaysData)) {
                scheduleDays.push({ dayOfWeek: workdaysDict[day], timeSlots: [] });
            } else {
                const timeSlots: TimeSlot[] = this.parseTimeSlots(lowerWorkDaysData, badDays, day);
                if (timeSlots.length) {
                    scheduleDays.push({ dayOfWeek: workdaysDict[day], timeSlots });
                }
            }
        });
        if (badDays.length)
            errors.push({
                code: JobErrorCodes.BULK_UPDATE_WORK_SCHEDULE_BAD_DAY_VALUE,
                description: { days: badDays, rowNumber: params.rowNumber },
            });
        let hash: string | undefined = undefined;
        if (scheduleDays.length) hash = this.generateScheduleDaysHash(scheduleDays);
        return { data: { hash, scheduleDays, userId: user?.getId() ?? 0 }, errors };
    };

    // Only after preparing it all to operate on multiple schedules at once, did i realize the job runs row per row.
    // This method is ready to support multiple schedules, but it will only be used for one at a time until we make jobs support multiple row batches.
    bulkUpdateWorkSchedules = async (updateUsersData: UpdateWorkSchedule[], params: MethodParams): Promise<void> => {
        const timezone = params.instance!.getTimezone();
        const startDate = (params.metadata?.startDate as string) ?? moment.tz(timezone).format(ISO_DATE);
        const endDate = (params.metadata?.endDate as string) || undefined;
        const hashes: Set<string> = new Set();
        updateUsersData.forEach((userData) => {
            if (userData.hash) hashes.add(userData.hash);
        });
        const existingSchedules = await this.workScheduleRepository.listSimilarAutoCreatedWorkSchedules(params.instanceId, Array.from(hashes));
        const schedulesToCreate: WorkScheduleCreationParameters[] = [];
        const usersByHash: Record<string, number[]> = {};
        const scheduleByHash: Record<string, WorkSchedule> = {};
        const userIdsToRemove: number[] = [];
        updateUsersData.forEach((userData) => {
            if (!userData.scheduleDays.length || !userData.hash) {
                userIdsToRemove.push(userData.userId);
                return;
            }
            usersByHash[userData.hash] = usersByHash[userData.hash] ?? [];
            usersByHash[userData.hash].push(userData.userId);
            const existingSchedule = existingSchedules.find((schedule) => this.areScheduledDaysSame(schedule, userData));
            if (existingSchedule) {
                scheduleByHash[userData.hash] = existingSchedule;
                return;
            }
            const existingCreationSchedule = schedulesToCreate.find((schedule) => this.areScheduledDaysSame(schedule, userData));
            if (!existingCreationSchedule) {
                schedulesToCreate.push({
                    autoCreated: true,
                    createdBy: params.user.id,
                    description: '',
                    hash: userData.hash,
                    instanceId: params.instanceId,
                    name: '',
                    scheduledDays: userData.scheduleDays,
                });
            }
        });
        await allPromises(
            schedulesToCreate.map((schedule) => async () => {
                const created = await this.workScheduleRepository.createSchedule({ ...schedule, code: uuidv4(), version: INITIAL_VERSION });
                scheduleByHash[schedule.hash!] = created;
            }),
        );
        // assign users to schedules
        await allPromises(
            Object.keys(scheduleByHash).map((hash) => {
                const userIds = usersByHash[hash];
                const schedule = scheduleByHash[hash];
                if (!userIds || !schedule) return () => Promise.resolve();
                return () =>
                    this.assignUsersToSchedule(
                        {
                            collaborators: userIds,
                            endDate,
                            id: schedule.id,
                            instanceId: params.instanceId,
                            scheduledDays: schedule.scheduledDays.map((day) => day.dayOfWeek),
                            startDate,
                        },
                        params.instance!.getTimezone(),
                    );
            }),
        );
        if (userIdsToRemove.length) {
            await this.workScheduleRepository.unassignUsers(userIdsToRemove.sort().filter(this.notDuplicate));
        }
    };

    migrateWorkdaysToWorkSchedules = async (
        devopsUser: UserInterface,
        limit: number,
    ): Promise<{ [instanceId: number]: { instanceName: string; amountMigrated: number } }> => {
        const returnData: { [instanceId: number]: { instanceName: string; amountMigrated: number } } = {};
        const instanceData: { instance: { id: number; name: string }; users: { id: number; workdays: WORKDAYS[] }[] }[] =
            await this.workScheduleRepository.getUsersWithoutWorkSchedulesWithWorkdays(limit);
        if (!instanceData.length) return {};
        for (const data of instanceData) {
            const usersData: UpdateWorkSchedule[] = data.users.map((user) => {
                const scheduleDays: ScheduledDays[] = user.workdays.map((day) => ({ dayOfWeek: day, timeSlots: [] }));
                return { hash: this.generateScheduleDaysHash(scheduleDays), scheduleDays, userId: user.id };
            });
            // Piggyback on existing logic
            await this.bulkUpdateWorkSchedules(usersData, {
                instance: { getTimezone: () => 'utc' },
                instanceId: data.instance.id,
                user: { id: devopsUser.id } as UserDAO,
            } as MethodParams);
            returnData[data.instance.id] = { amountMigrated: usersData.length, instanceName: data.instance.name };
        }
        return returnData;
    };

    bulkCreateAssignments(assignments: WorkScheduleUsersDAOCreationAttributes[]): Promise<void> {
        return this.workScheduleRepository.bulkCreateAssignments(assignments);
    }

    public deleteExistingWorkSchedules = async (instanceId: number): Promise<void> => {
        let continueIterating = false;
        const limit = 1000;
        await this.workScheduleRepository.getTransaction(async () => {
            do {
                const { count, rows: workSchedulesIds } = await this.workScheduleRepository.listWorkScheduleIdsByInstance(instanceId, {
                    limit,
                    offset: 0, // deleted schedules are not obtained again
                });
                await allPromises(workSchedulesIds.map((id) => () => this.workScheduleRepository.eliminateWorkSchedule(id)));
                continueIterating = count > limit && limit === workSchedulesIds.length;
            } while (continueIterating);
        });
    };

    // TODO link to something updating this every day or even more often
    private async updateUsersWorkdays(instanceId: number, users: number[], workdays: WORKDAYS[]) {
        await this.usersService.updateWorkdaysForMultipleUsers(users, workdays);
    }

    private notDuplicate(index: number, item: number, array: number[]): unknown {
        return !index || item != array[index - 1];
    }

    private areScheduledDaysSame(schedule: WorkScheduleCreationParameters, userData: UpdateWorkSchedule): unknown {
        return schedule.hash && schedule.hash === userData.hash && !this.areScheduleDaysDifferent(schedule.scheduledDays, userData.scheduleDays);
    }

    private parseTimeSlots(lowerWorkDaysData: string, badDays: string[], day: string): TimeSlot[] {
        // Example: "9:00, 12:00; \r\n13:00, 18:00"
        return lowerWorkDaysData
            .split(';')
            .filter((timeSlot) => {
                const [startTime, endTime] = timeSlot.trim().split(',');
                if (!startTime && !endTime) {
                    return false;
                } else if (!startTime || !endTime || !startTime.match(TIME_REGEX) || !endTime.match(TIME_REGEX)) {
                    badDays.push(day);
                    return false;
                }
                return true;
            })
            .map((timeSlot) => {
                const timeFormat = 'HH:mm';
                const [startTime, endTime] = timeSlot
                    .trim()
                    .split(',')
                    .map((time) => time.trim().padStart(timeFormat.length, '0'));
                return { endTime, startTime };
            });
    }

    private getWorkdaysTranslations(xlsxWorkdays: string[]): Record<string, WORKDAYS> {
        return {
            [xlsxWorkdays[0]]: WORKDAYS.MONDAY,
            [xlsxWorkdays[1]]: WORKDAYS.TUESDAY,
            [xlsxWorkdays[2]]: WORKDAYS.WEDNESDAY,
            [xlsxWorkdays[3]]: WORKDAYS.THURSDAY,
            [xlsxWorkdays[4]]: WORKDAYS.FRIDAY,
            [xlsxWorkdays[5]]: WORKDAYS.SATURDAY,
            [xlsxWorkdays[6]]: WORKDAYS.SUNDAY,
        };
    }

    private generateScheduleDaysHash(scheduleDays: ScheduledDays[]): string {
        const sortedDays = scheduleDays.sort((a, b) => a.dayOfWeek.localeCompare(b.dayOfWeek));
        const serialized = sortedDays.reduce((acc, day) => {
            const timeSlots = day.timeSlots.sort(compareTimeSlots);
            const dayString = `${day.dayOfWeek}:${timeSlots.map((slot) => `${slot.startTime}-${slot.endTime}`).join(',')}`;
            return acc + dayString;
        }, '');
        const sha256 = crypto.createHash('sha256');
        sha256.update(serialized);
        return sha256.digest('base64');
    }

    private shouldCreateNewVersion(oldSchedule: WorkSchedule, newSchedule: WorkScheduleUpdateAttributes) {
        if (oldSchedule.scheduledDays.length != newSchedule.scheduledDays?.length) {
            return true;
        }
        return this.areScheduleDaysDifferent(oldSchedule.scheduledDays, newSchedule.scheduledDays);
    }

    private areScheduleDaysDifferent(oldScheduleDays: ScheduledDays[], newScheduleDays: ScheduledDays[]): boolean {
        oldScheduleDays.sort((a, b) => a.dayOfWeek.localeCompare(b.dayOfWeek));
        newScheduleDays.sort((a, b) => a.dayOfWeek.localeCompare(b.dayOfWeek));
        for (let i = 0; i < oldScheduleDays.length; i++) {
            const oldDay = oldScheduleDays[i];
            const newDay = newScheduleDays[i];
            if (!newDay || oldDay.timeSlots.length != newDay.timeSlots.length) {
                return true;
            } else if (this.areTimeSlotsDifferent(oldDay.timeSlots, newDay.timeSlots)) {
                return true;
            }
        }
        return false;
    }

    private areTimeSlotsDifferent(a: TimeSlot[], b: TimeSlot[]): boolean {
        a.sort(compareTimeSlots);
        b.sort(compareTimeSlots);
        for (let i = 0; i < a.length; i++) {
            if (a[i].startTime != b[i].startTime || a[i].endTime != b[i].endTime) {
                return true;
            }
        }
        return false;
    }

    public getAllUsersAssignedToWorkSchedule = async (
        id: number,
        instanceId: number,
        loggedUser: UserInterface,
        pagination: { limit: number; offset: number },
        timezone: string,
        filters: WorkScheduleUserAssignmentFilters,
    ): Promise<{ count: number; rows: WorkScheduleUserAssignment[] }> => {
        let hasUsersFilters = false;
        const userFilters = {
            departmentIds: filters.departmentIds,
            excludeDepartmentIds: filters.excludeDepartmentIds,
            ids: filters.userIds,
            search: filters.search,
            segmentationItemIds: filters.itemIds?.join(','),
        };
        if (
            userFilters.ids?.length ||
            userFilters.segmentationItemIds?.length ||
            userFilters.search ||
            userFilters.departmentIds?.length ||
            userFilters.excludeDepartmentIds?.length
        ) {
            hasUsersFilters = true;
            filters.userIds = await this.usersService.getFilteredIds(loggedUser as User, userFilters, { excludeLoggedUser: false });
        }

        if (!hasUsersFilters) {
            filters.userIds = await this.usersService.getUserIdsInInstance(instanceId);
        }

        if (filters.userIds?.length && (filters.regionIds?.length || filters.siteIds?.length)) {
            filters.userIds = await this.siteService.filterUsers(filters.userIds, {
                instanceId,
                regionIds: filters.regionIds ?? [],
                siteIds: filters.siteIds ?? [],
            });
        }

        const { count, rows } = await this.workScheduleRepository.getAllUsersAssignedToWorkSchedule(
            id,
            instanceId,
            filters.userIds ?? [],
            pagination,
            timezone,
        );
        await this.addUserDataToAssignments(instanceId, rows);
        return { count, rows };
    };

    public getAllUserAssignmentsInWorkSchedule = async (
        id: number,
        instanceId: number,
        userId: number,
        pagination: { limit: number; offset: number },
        timezone: string,
    ): Promise<{ count: number; rows: WorkScheduleUserAssignment[] }> => {
        const { count, rows } = await this.workScheduleRepository.getAllUserAssignmentsInWorkSchedule(id, instanceId, userId, pagination, timezone);
        await this.addUserDataToAssignments(instanceId, rows);
        return { count, rows };
    };

    private addUserDataToAssignments = async (instanceId: number, assignments: WorkScheduleUserAssignment[]): Promise<void> => {
        const userIds = assignments.map((assignment) => assignment.userId);
        const { rows: users } = await this.usersService.getUsersNotInList(instanceId, userIds, [], {
            pagination: { limit: userIds.length, offset: 0 },
        });
        if (users.length) await this.attachmentsService.addSignedUrlToModelWithImages(instanceId, users, ['profilePicture']);
        const usersMap: Record<number, UserDAO> = {};
        users.forEach((user) => {
            usersMap[user.id] = user;
        });
        assignments.forEach((assignment) => {
            assignment.user = userDAOtoUser(usersMap[assignment.userId]);
        });
    };

    public deleteWorkScheduleUser = async (id: number, workScheduleId: number, instanceId: number): Promise<void> => {
        const workScheduleUser = await this.workScheduleRepository.getWorkScheduleUser(id, workScheduleId);
        if (workScheduleUser) {
            await this.workScheduleRepository.deleteWorkScheduleUser(id, workScheduleId);
            await this.scheduleCategorizedHoursPort.schedule({
                payload: {
                    endDate: workScheduleUser.endDate ? workScheduleUser.endDateString : moment().format(ISO_DATE),
                    instanceId,
                    startDate: workScheduleUser.startDateString,
                    userIds: [workScheduleUser.userId],
                },
                type: CategorizedHoursActionType.SCHEDULE_CHANGE,
            });
            this.eventEmitter.emitEvent(Events.WORK_SCHEDULE_NEW_USERS, {
                endDate: workScheduleUser.endDate ? workScheduleUser.endDateString : undefined,
                instanceId,
                startDate: workScheduleUser.startDateString,
                userIds: [workScheduleUser.userId],
            });
        }
    };

    public getUserIdsByScheduleCriteria = async (
        instanceId: number,
        userIds: number[],
        fromDate: string,
        toDate: string,
        filters: ScheduleFilter,
    ): Promise<number[]> => {
        return this.workScheduleRepository.getUserIdsByScheduleCriteria(instanceId, userIds, fromDate, toDate, filters);
    };
}
