import {
    BadRequestError,
    BaseController,
    CapabilityName,
    CapabilityType,
    ENV_OPTIONAL_NUMBER_KEYS_MAPPING,
    ErrorCodes,
    FilterableMandatoryPaginableVC,
    ForbiddenError,
    I18n,
    ISO_DATE,
    LoggerPort,
    mimeExtensions,
    Modules,
    reportExtensionToMimeType,
    RequestUnprocessableEntityError,
    TranslationsPort,
    UserStatus,
} from '@humand-packages/common';
import { NextFunction, Request } from 'express';
import { ACCEPTED, CREATED, OK } from 'http-status-codes';
import moment, { Moment } from 'moment';
import { Inject, Service } from 'typedi';
import { PortsDI } from '../../../../config/portsDI';
import { getFilename } from '../../../../helpers/spreadsheet';
import { EmailsService } from '../../../../services/emails';
import { StorageService } from '../../../../services/storage';
import { DeprecatedHumandResponse } from '../../../../types/locals';
import { DepartmentsService } from '../../../departments/business/services/departmentsService';
import { DepartmentSC } from '../../../departments/presentation/serializationClasses/departmentSC';
import { FeatureFlagCode } from '../../../featureFlags/business/enums/featureFlagCode';
import { FeatureFlagService } from '../../../featureFlags/business/services/featureFlagService';
import { Instance } from '../../../instances/business/models/instance';
import { OrganizationChartsService } from '../../../organizationCharts/business/services/organizationChartsService';
import { PermissionsService } from '../../../permissions/business/services/permissionsService';
import { RegionsService } from '../../../regions/business/services/regionService';
import { SiteService } from '../../../regions/business/services/sitesService';
import { RegionSC } from '../../../regions/presentation/serializationClasses/regionSC';
import { User } from '../../../users/business/models/user';
import { UsersService } from '../../../users/business/services/usersService';
import { UserDAO } from '../../../users/infrastructure/dataAccessObjects/userDAO';
import { userDAOtoUser } from '../../../users/infrastructure/mappers/usersMapper';
import { PublicUserSerializer } from '../../../users/presentation/serializationClasses/publicUserSerializer';
import { FilterPublicUserListMapper } from '../../../users/presentation/validationClasses/filterPublicUserListMapper';
import { MAX_DAYS_FROM_NOW_SCHEDULE } from '../../business/constants/timeTracking';
import { TimeTrackingDaySummaryReportFilters } from '../../business/interfaces/timeTrackingDaySummaryFilters';
import { TimeTrackingDaySummary } from '../../business/models/daySummary';
import { PairedEntry } from '../../business/models/PairedEntry';
import { ShiftAssignedDay } from '../../business/models/shiftManagement/shiftAssignedDay';
import { TimeTrackingEntryPrototype } from '../../business/models/timeTrackingEntry';
import { WorkSchedule } from '../../business/models/workSchedule';
import { TimeTrackingEmployeePort } from '../../business/ports/employeeTablePorts';
import WorkScheduleServicePort from '../../business/ports/workScheduleServicePort';
import { DaySummaryDetailService } from '../../business/services/daySummaryDetailService';
import { TimeTrackingHourCategorizationService } from '../../business/services/hourCategorizationService';
import { ReportServicePort } from '../../business/services/report';
import ShiftCalendarService from '../../business/services/shiftManagement/shiftCalendar';
import TimeTrackingService from '../../business/services/timeTracking';
import { TimeTrackingIncidenceNotificationService } from '../../business/services/timeTracking/timeTrackingIncidenceNotificationService';
import TimeTrackingUserSettingService from '../../business/services/timeTracking/timeTrackingUserSetting';
import { TimeTrackingEntryType } from '../../business/types/entry';
import { isUserGeneratedSource, TimeTrackingEntrySource } from '../../business/types/source';
import { State } from '../../infrastructure/constants/state';
import { TIME_TRACKING_APP_GRANTS_DEFINITION } from '../constants/timeTrackingGrants';
import { TimeTrackingPermissions } from '../helpers/timeTrackingPermissions';
import { EmployeeTableSC } from '../serializationClasses/EmployeeTableSC';
import { PairedEntrySC } from '../serializationClasses/PairedEntrySC';
import { ScheduleCalendarSC } from '../serializationClasses/ScheduleCalendarSC';
import { TimeTrackingDaySummarySC } from '../serializationClasses/TimeTrackingDaySummarySC';
import { TimeTrackingEntrySC } from '../serializationClasses/TimeTrackingEntrySC';
import { TimeTrackingSimplifiedHourCategoriesSC } from '../serializationClasses/TimeTrackingSimplifiedHourCategoriesSC';
import { TimeTrackingResponse } from '../types/response';
import { CloseEntriesRequestVC } from '../validationClasses/closeEntriesRequestVC';
import { CreateOfflineTimeTrackingEntriesRequestsVC } from '../validationClasses/createOfflineTimeTrackingEntriesRequestsVC';
import { CreateTimeTrackingEntryRequestsVC } from '../validationClasses/createTimeTrackingEntryRequestsVC';
import { GetDaySummaryDetailVC } from '../validationClasses/getDaySummaryDetailVC';
import { GetTimeTrackingEntryRequestsVC } from '../validationClasses/getTimeTrackingEntryRequestsVC';
import { GetHourCategoriesVC } from '../validationClasses/hourCategorization/GetHourCategoriesVC';
import { TimeTrackingGetNotificationScheduleVC } from '../validationClasses/timeTracking/timeTrackingGetNotificationScheduleVC';
import {
    TimeTrackingDaySummaryFiltersVC,
    TimeTrackingDaySummaryWithReportFiltersVC,
    TimeTrackingDaySummaryWithScheduleFiltersVC,
    TimeTrackingReportType,
} from '../validationClasses/timeTrackingDaySummaryFiltersVC';
import { UpdateTimeTrackingEntryRequestsVC } from '../validationClasses/updateTimeTrackingEntryRequestsVC';

export const DEFAULT_LIMIT = 20; // 10 pairs
export const DEFAULT_OFFSET = 0;
export const DEFAULT_BATCH_ENTRIES_LIMIT = 10000;
export const DATE_FORMAT = 'DD/MM/YYYY';
export const DEFAULT_AUTO_CLOSE_DAYS = 3;
export const DEFAULT_DAYS_LIMIT = 31;
const filenameKey = 'reports.filename';
const DUPLICATE_TYPE_ERROR = "Can't create two entries of the same type in a row";

@Service()
export class TimeTrackingController extends BaseController {
    constructor(
        @PortsDI(TimeTrackingService) private readonly timeTrackingService: TimeTrackingService,
        @PortsDI(ReportServicePort) private readonly reportService: ReportServicePort,
        @PortsDI(WorkScheduleServicePort) private readonly workScheduleService: WorkScheduleServicePort,
        @PortsDI(TimeTrackingEmployeePort) private readonly employeeAdapter: TimeTrackingEmployeePort,
        private readonly permissionService: PermissionsService,
        private readonly organizationService: OrganizationChartsService,
        private readonly usersService: UsersService,
        private readonly siteService: SiteService,
        private readonly storageService: StorageService,
        private readonly emailsService: EmailsService,
        private readonly departmentsService: DepartmentsService,
        protected readonly regionsService: RegionsService,
        private readonly translationsPort: TranslationsPort,
        @Inject(ENV_OPTIONAL_NUMBER_KEYS_MAPPING.NOTIFICATION_DELAY) notificationDelay: number,
        logger: LoggerPort,
        private readonly shiftCalendarService: ShiftCalendarService,
        private readonly timeTrackingUserSettingService: TimeTrackingUserSettingService,
        private readonly featureFlagService: FeatureFlagService,
        private readonly timeTrackingHourCategorizationService: TimeTrackingHourCategorizationService,
        private readonly daySummaryDetailService: DaySummaryDetailService,
        private readonly incidenceNotificationService: TimeTrackingIncidenceNotificationService,
        private readonly timeTrackingPermissions: TimeTrackingPermissions,
    ) {
        super(logger, notificationDelay);
    }

    private reportDownloadUrlExpire = 3600; // 1 hour

    public createEntry = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { bodyParams, loggedUser, instanceOfLoggedUser, loggedBotApp } = res.locals;
        const params = bodyParams as CreateTimeTrackingEntryRequestsVC;
        await this.endpointHandlerWithGenerics(
            next,
            async () => {
                this.permissionService.validateViewTimeTracking(loggedUser);
                const hasTimeTrackingDesertEagleEnabledFeatureFlag = await this.getTimeTrackingDesertEagleEnabledFeatureFlag(loggedUser.instanceId);
                if (params.userId && params.userId !== loggedUser.id) {
                    this.permissionService.validateManageTimeTracking(loggedUser);
                }
                const createdEntry = await this.timeTrackingService.createEntryTransactional(
                    loggedUser as UserDAO,
                    instanceOfLoggedUser,
                    params,
                    loggedBotApp,
                    hasTimeTrackingDesertEagleEnabledFeatureFlag,
                );
                res.json(createdEntry);
                res.status(CREATED).end();
                return { createdEntry, instance: instanceOfLoggedUser };
            },
            async ({ createdEntry, instance }) => {
                await this.incidenceNotificationService.evaluateEntry(createdEntry, instance);
            },
        );
    };

    public updateEntries = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { bodyParams, loggedUser, instanceOfLoggedUser } = res.locals;
        const user = userDAOtoUser(loggedUser as UserDAO);
        const params = bodyParams as UpdateTimeTrackingEntryRequestsVC;
        await this.endpointHandlerWithGenerics(next, async () => {
            const userIds = params.entries.map((entry) => entry.userId);
            this.permissionService.validateViewTimeTracking(user);
            const hasTimeTrackingDesertEagleEnabledFeatureFlag = await this.getTimeTrackingDesertEagleEnabledFeatureFlag(loggedUser.instanceId);
            await this.performCapabilityValidations(user, { userIds }, CapabilityName.MANAGE_TIME_TRACKING);
            const result = await this.timeTrackingService.updateEntries(
                instanceOfLoggedUser,
                user,
                params.summaryId,
                params.entries.map((entry) => ({ ...entry, time: new Date(entry.time) })),
                hasTimeTrackingDesertEagleEnabledFeatureFlag,
            );
            res.json(result);
            res.status(OK).end();
        });
    };

    public syncEntries = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { bodyParams, loggedUser, instanceOfLoggedUser, loggedBotApp } = res.locals;
        const params = bodyParams as CreateOfflineTimeTrackingEntriesRequestsVC;
        await this.endpointHandlerWithGenerics(
            next,
            async () => {
                const entries = params.entries
                    .filter((entry) => !entry.source || isUserGeneratedSource(entry.source))
                    .map((entry) => {
                        const time = entry.time ? moment(entry.time, true).toDate() : new Date();
                        return this.createTimeTrackingEntryModel(loggedUser as UserDAO, entry, time, loggedUser.id);
                    })
                    .sort((a, b) => b.time.getTime() - a.time.getTime());
                if (entries.some((entry, index) => index > 0 && entry.type === entries[index - 1].type)) {
                    throw new RequestUnprocessableEntityError(DUPLICATE_TYPE_ERROR, ErrorCodes.TIME_TRACKING_ENTRY_DUPLICATE);
                }
                const createdEntries = await this.timeTrackingService.syncEntries(entries, loggedUser.id, instanceOfLoggedUser, loggedBotApp);
                res.json(createdEntries);
                res.status(CREATED).end();
                return { createdEntries, userId: loggedUser.id, instance: instanceOfLoggedUser };
            },
            async ({ createdEntries, userId, instance }) => {
                await this.incidenceNotificationService.evaluateEntries(createdEntries, userId, instance);
            },
        );
    };

    public listEntries = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser } = res.locals;
        const params = res.locals.queryParams as GetTimeTrackingEntryRequestsVC;
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateViewTimeTracking(loggedUser);
            if (params.userId && params.userId !== loggedUser.id) {
                this.permissionService.validateManageTimeTracking(loggedUser);
            }
            const date = params.date ? moment(params.date) : moment();
            date.tz(instanceOfLoggedUser.getTimezone()); // always use instance timezone
            const result = await this.timeTrackingService.listDayEntries(params.userId ?? loggedUser.id, date.format(ISO_DATE), instanceOfLoggedUser);
            if (params.individualRecords?.toLowerCase() === 'true') {
                res.json(
                    new TimeTrackingEntrySC().serializePaginatedList({
                        count: result.length,
                        limit: 0,
                        offset: 0,
                        rows: result,
                    }),
                );
            } else {
                const pairedResults = PairedEntry.getPairedResults(result);
                res.json(
                    new PairedEntrySC().serializePaginatedList({
                        count: result.length,
                        limit: 0,
                        offset: 0,
                        rows: pairedResults,
                    }),
                );
            }
            res.status(OK).end();
        });
    };

    public getRecentDaySummary = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser } = res.locals;
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateViewTimeTracking(loggedUser);
            const date = moment();
            date.tz(instanceOfLoggedUser.getTimezone()); // always use instance timezone
            const previousDayMoment = date.clone().subtract(1, 'day');
            const daySummaries = await this.timeTrackingService.getOrCreateDaySummariesByUser(
                loggedUser.id,
                instanceOfLoggedUser,
                previousDayMoment.format(ISO_DATE),
                date.format(ISO_DATE),
                true,
            );
            daySummaries.sort((a, b) => b.dateString.localeCompare(a.dateString)); // descending order
            const workSchedule = await this.workScheduleService.getWorkScheduleByUser(
                loggedUser.id,
                loggedUser.instanceId,
                instanceOfLoggedUser.getTimezone(),
            );
            if (daySummaries.length === 0) {
                this.logger.warn('Time Tracking | No day summary found for the day');
                res.json({ entries: [], estimatedHours: 0, workSchedule });
                res.status(OK).end();
                return;
            }
            this.fakeTempDayChangeEntries(daySummaries, date, loggedUser.id);
            res.json({
                daySummary: daySummaries[0],
                entries: PairedEntry.getPairedResults(daySummaries[0].timeTrackingEntries),
                estimatedHours: daySummaries[0].estimatedHours,
                previousDaySummary: daySummaries[1],
                workSchedule,
            });
            res.status(OK).end();
        });
    };

    public getDaySummaryDetail = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser, queryParams } = res.locals;
        const params = queryParams as GetDaySummaryDetailVC;
        const user = userDAOtoUser(loggedUser as UserDAO);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateViewTimeTracking(user);
            const detail = await this.daySummaryDetailService.getDaySummaryDetail(user, instanceOfLoggedUser, params.userId, params.date);
            const body = new TimeTrackingDaySummarySC().serialize(detail);
            const grants = await this.timeTrackingPermissions.getTargetGrants(loggedUser, params.userId);
            res.status(OK).json({ ...body, grants });
        });
    };

    private fakeTempDayChangeEntries = (daySummaries: TimeTrackingDaySummary[], date: Moment, userId: number) => {
        if (daySummaries.length < 2 || daySummaries[1].state !== State.OPEN) return;
        const dayChange = moment(daySummaries[1]?.dayChange);
        const autoClose = daySummaries[1]?.autoCloseTime ? moment(daySummaries[1].autoCloseTime) : dayChange.clone().add(1, 'hour');
        if (dayChange.isBefore(autoClose) && Math.max(date.diff(dayChange, 'minutes'), 0) < 10) {
            const templateEntry = { comment: 'Temp day change', id: 0, source: TimeTrackingEntrySource.DAY_CHANGE, userId };
            daySummaries[1].timeTrackingEntries.push({ ...templateEntry, time: dayChange.toDate(), type: TimeTrackingEntryType.END });
            daySummaries[0].timeTrackingEntries.push({
                ...templateEntry,
                time: dayChange.clone().add(1, 'ms').toDate(),
                type: TimeTrackingEntryType.START,
            });
        } else if (autoClose.isBefore(dayChange) && Math.max(date.diff(autoClose, 'minutes'), 0) < 30) {
            const templateEntry = { comment: 'Temp auto close', id: 0, source: TimeTrackingEntrySource.AUTO_CLOSE, userId };
            daySummaries[1].timeTrackingEntries.push({ ...templateEntry, time: autoClose.toDate(), type: TimeTrackingEntryType.END });
        }
    };

    private createTimeTrackingEntryModel(
        loggedUser: UserDAO,
        params: CreateTimeTrackingEntryRequestsVC,
        time: Date,
        userId: number,
    ): TimeTrackingEntryPrototype {
        return {
            comment: params.comment,
            createdBy: loggedUser.id,
            facialRecognitionSkipped: params.facialRecognitionSkipped ?? false,
            instanceId: loggedUser.instanceId,
            isRemoteWork: params.isRemoteWork,
            location: params.location,
            siteId: params.siteId,
            source: params.source ?? TimeTrackingEntrySource.APP,
            time: time,
            type: params.type,
            userId: userId,
        };
    }

    public closeEntriesForDate = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        await this.devopsEndpointHandlerWithGenerics(
            next,
            async () => {
                const { loggedUser } = res.locals;
                this.permissionService.validateDevOps(loggedUser);
                const params = res.locals.queryParams as CloseEntriesRequestVC;
                const toDate = params.date ? moment(params.date, true).toDate() : moment().toDate();
                const fromDate = params.fromDate
                    ? moment(params.fromDate, true).toDate()
                    : moment(toDate).subtract(DEFAULT_AUTO_CLOSE_DAYS, 'days').toDate();

                const { autoCloseBatches } = await this.timeTrackingService.closeEntriesForDate(
                    toDate,
                    fromDate,
                    params.limit || DEFAULT_BATCH_ENTRIES_LIMIT,
                );
                res.status(OK).end();
                return { autoCloseBatches };
            },
            async ({ autoCloseBatches }) => {
                await this.incidenceNotificationService.evaluateAutoCloseEntries(autoCloseBatches);
            },
        );
    };

    // DEPRECATED
    public report = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser } = res.locals;
        const user = userDAOtoUser(loggedUser as UserDAO);
        await this.endpointHandlerWithGenerics(next, async () => {
            const params = res.locals.queryParams as TimeTrackingDaySummaryWithReportFiltersVC;
            this.permissionService.validateViewTimeTracking(user);
            await this.performCapabilityValidations(user, params, CapabilityName.MANAGE_TIME_TRACKING);
            const filters = {
                departmentIds: params.departmentIds,
                fromDate: params.startDate,
                groupIds: params.groupIds,
                hasHolidays: params.hasHolidays,
                hasTimeOff: params.hasTimeOff,
                incidences: params.incidences ?? [],
                itemIds: params.itemIds,
                search: params.search,
                status: params.status ?? [],
                toDate: params.endDate,
                userCustomFields: params.userCustomFields,
                userFields: params.userFields,
                userIds: params.userIds,
            } as TimeTrackingDaySummaryReportFilters;
            let workbook;
            if (params.type === TimeTrackingReportType.CONSOLIDATED) {
                workbook = await this.reportService.consolidatedReport(user, instanceOfLoggedUser, filters);
            } else {
                workbook = await this.reportService.detailedReport(user, instanceOfLoggedUser, filters);
            }
            const translations = I18n.translate((user as User).language, [{ key: filenameKey }]);
            const reportName = `${translations[filenameKey]} [${moment(params.startDate).format(DATE_FORMAT)} - ${moment(params.endDate).format(
                DATE_FORMAT,
            )}]`;
            res.setHeader('Content-Type', reportExtensionToMimeType[mimeExtensions.xlsx]);
            res.setHeader('Content-Disposition', `attachment; filename="${reportName}.xlsx"`);
            await workbook.xlsx.write(res);
            res.status(OK);
            res.end();
            return { fromDate: params.startDate, instanceId: (user as User).getInstanceId(), toDate: params.endDate };
        });
    };

    public reportToEmail = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser } = res.locals;
        const user = userDAOtoUser(loggedUser as UserDAO);
        const params = res.locals.queryParams as TimeTrackingDaySummaryWithReportFiltersVC;
        const email = params.email ?? loggedUser.email;
        await this.endpointHandlerWithGenerics(
            next,
            async () => {
                if (!email) throw new BadRequestError('No email to send report provided', ErrorCodes.BAD_REQUEST);
                this.permissionService.validateViewTimeTracking(user);
                await this.performCapabilityValidations(user, params, CapabilityName.MANAGE_TIME_TRACKING);

                if (params.regionIds?.length || params.siteIds?.length) {
                    params.siteIds = await this.siteService.filterSites({
                        instanceId: instanceOfLoggedUser.getId(),
                        regionIds: params.regionIds,
                        siteIds: params.siteIds,
                    });
                }

                const filters = {
                    departmentIds: params.departmentIds,
                    excludeDepartmentIds: params.excludeDepartmentIds,
                    excludeSiteIds: params.excludeSiteIds,
                    fromDate: params.startDate,
                    groupIds: params.groupIds,
                    hasHolidays: params.hasHolidays,
                    hasTimeOff: params.hasTimeOff,
                    hourCategoryIds: params.hourCategoryIds,
                    incidences: params.incidences ?? [],
                    includeAutoCloseIncidence: params.includeAutoCloseIncidence,
                    includeClockInOut: params.includeClockInOut,
                    includeKiosk: params.includeKiosk,
                    includeScheduledTime: params.includeScheduledTime,
                    itemIds: params.itemIds,
                    search: params.search,
                    siteIds: params.siteIds,
                    status: params.status ?? [],
                    toDate: params.endDate,
                    userCustomFields: params.userCustomFields,
                    userFields: params.userFields,
                    userIds: params.userIds,
                } as TimeTrackingDaySummaryReportFilters;
                res.status(ACCEPTED).json().end();
                try {
                    let workbook;
                    if (params.type === TimeTrackingReportType.CONSOLIDATED) {
                        workbook = await this.reportService.consolidatedReport(user, instanceOfLoggedUser, filters);
                    } else {
                        workbook = await this.reportService.detailedReport(user, instanceOfLoggedUser, filters);
                    }
                    const translations = this.translationsPort.translateModule((user as User).getLanguage(), Modules.TIME_TRACKING, [
                        { key: filenameKey },
                    ]);
                    const reportName = `${translations[filenameKey]}_[${moment(params.startDate).format(ISO_DATE)}_${moment(params.endDate).format(
                        ISO_DATE,
                    )}]`;
                    const storageKey = await this.storageService.uploadReportFile({
                        file: workbook['xlsx'],
                        filename: getFilename(reportName),
                    });
                    const reportSignedUrl = await this.storageService.getSignedUrlOfKey(
                        loggedUser.instanceId,
                        storageKey,
                        true,
                        this.reportDownloadUrlExpire,
                    );
                    return { reportSignedUrl };
                } catch (error) {
                    return { error: error as Error };
                }
            },
            async (notificationParams) => {
                const { error, reportSignedUrl } = notificationParams;
                if (error === undefined && reportSignedUrl === undefined) return;
                if (error === undefined) {
                    const translations = this.translationsPort.translateModule(user.getLanguage(), Modules.TIME_TRACKING, [
                        { key: 'reports.emailNotification.title' },
                        { key: 'reports.emailNotification.description' },
                    ]);
                    await this.emailsService.sendNewReportEmail({
                        email: email!,
                        formMessage: translations['reports.emailNotification.description'],
                        formTitle: translations['reports.emailNotification.title'],
                        instanceId: loggedUser.instanceId,
                        language: loggedUser.language,
                        url: reportSignedUrl!,
                    });
                } else {
                    await this.emailsService.sendFailedReportEmail(email!, loggedUser.language, loggedUser.instanceId);
                    throw error;
                }
            },
        );
    };

    private async performCapabilityValidations(user: User, params: Partial<TimeTrackingDaySummaryFiltersVC>, capability: CapabilityType) {
        const capabilities = user.getGeneralPermissions() ?? {};
        const hasManagementCapability = capabilities[capability];
        if (params.userIds?.length) {
            const subordinateIds = await this.organizationService.getSubordinateIdsRecursive(user.getId(), user.getInstanceId());
            if (!hasManagementCapability && params.userIds.some((id) => !subordinateIds.includes(id) && user.id !== id)) {
                throw new ForbiddenError('The provided user has no access to this resource', ErrorCodes.ACCESS_DENIED);
            }
        }
        // avoid searching for users that are not your subordinates if you lack permission
        if (params.search && !params.userIds?.length && !hasManagementCapability) {
            params.userIds = await this.organizationService.getSubordinateIdsRecursive(user.getId(), user.getInstanceId());
        }
        // If no filters, then show all users in instance if you have the permission
        if (this.hasNoFilters(params) && hasManagementCapability) {
            const statusFilter = params.includeDeactivated
                ? [UserStatus.ACTIVE, UserStatus.UNCLAIMED, UserStatus.DEACTIVATED]
                : [UserStatus.ACTIVE, UserStatus.UNCLAIMED];
            params.userIds = await this.usersService.getUserIdsInInstance(user.getInstanceId(), statusFilter);
        }
    }

    private hasNoFilters(params: Partial<TimeTrackingDaySummaryFiltersVC>) {
        return !params.search && !params.userIds?.length && !params.itemIds?.length && !params.siteIds?.length;
    }

    private async performCapabilityValidationsForCalendar(user: User, params: Partial<TimeTrackingDaySummaryFiltersVC>, capability: CapabilityType) {
        const capabilities = user.getGeneralPermissions() ?? {};
        const hasManagementCapability = capabilities[capability];
        if (params.userIds?.length) {
            const subordinateIds = await this.organizationService.getSubordinateIdsRecursive(user.getId(), user.getInstanceId());
            if (!hasManagementCapability && params.userIds.some((id) => !subordinateIds.includes(id) && user.id !== id)) {
                throw new ForbiddenError('The provided user has no access to this resource', ErrorCodes.ACCESS_DENIED);
            }
        }
        // avoid searching for users that are not your subordinates if you lack permission
        if (params.search && !params.userIds?.length && !hasManagementCapability) {
            params.userIds = await this.organizationService.getSubordinateIdsRecursive(user.getId(), user.getInstanceId());
        }
        // If no filters, then show all users in instance if you have the permission
        if (this.hasNoFiltersCalendar(params) && hasManagementCapability) {
            params.userIds = await this.usersService.getUserIdsInInstance(user.getInstanceId());
        }
    }

    private hasNoFiltersCalendar(params: Partial<TimeTrackingDaySummaryFiltersVC>) {
        return !params.userIds?.length && !params.itemIds?.length;
    }

    public listEmployeeSummaries = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser, requiredLimitOffsetPagination: pagination } = res.locals;
        const user = userDAOtoUser(loggedUser as UserDAO);
        await this.endpointHandlerWithGenerics(next, async () => {
            const params = res.locals.queryParams as TimeTrackingDaySummaryWithScheduleFiltersVC;
            this.permissionService.validateViewTimeTracking(user);
            if (params.regionIds?.length || params.siteIds?.length) {
                params.siteIds = await this.siteService.filterSites({
                    instanceId: instanceOfLoggedUser.getId(),
                    regionIds: params.regionIds,
                    siteIds: params.siteIds,
                });
            }
            const hasManageCapability = this.permissionService.hasAnyCapability(user, [CapabilityName.MANAGE_TIME_TRACKING]);
            await this.performCapabilityValidations(user, params, CapabilityName.MANAGE_TIME_TRACKING);
            const results = await this.employeeAdapter.listEmployeeSummaries(
                user,
                instanceOfLoggedUser,
                {
                    departmentIds: params.departmentIds,
                    fromDate: params.startDate,
                    hasHolidays: params.hasHolidays,
                    hasTimeOff: params.hasTimeOff,
                    incidences: params.incidences,
                    itemIds: params.itemIds,
                    limit: pagination.limit,
                    offset: pagination.offset,
                    includeDeactivated: params.includeDeactivated,
                    search: params.search,
                    siteIds: params.siteIds,
                    status: params.status,
                    toDate: params.endDate,
                    userIds: params.userIds,
                },
                true,
                hasManageCapability,
            );
            const scheduleIds = new Set<number>();
            const shiftAssignedDayIds = new Set<number>();
            results.rows.forEach((employeeSummary) => {
                employeeSummary.daySummaries.forEach((summary) => {
                    if (summary.workScheduleId) scheduleIds.add(summary.workScheduleId);
                    if (summary.shiftAssignedDayId) shiftAssignedDayIds.add(summary.shiftAssignedDayId);
                });
            });
            const schedules = await this.getScheduleData((user as User).instanceId, scheduleIds);
            const shiftAssignedDays = await this.getShiftAssignedDaysData(shiftAssignedDayIds);
            const grantsMap = await this.timeTrackingPermissions.getTargetsGrants(
                loggedUser,
                results.rows.map((row) => row.userId),
            );
            const body = new EmployeeTableSC().serializePaginatedList({
                count: results.count,
                extraData: {
                    grants: grantsMap,
                    schedules,
                    shiftAssignedDays,
                    sites: results.sites,
                },
                limit: pagination.limit,
                offset: pagination.offset,
                rows: results.rows,
            });
            res.json(body);
            res.status(OK);
            res.end();
        });
    };

    private async getScheduleData(instanceId: number, scheduleIds: Set<number>) {
        const { rows: schedules } = await this.workScheduleService.listWorkSchedules(
            { ids: Array.from(scheduleIds), includeDeleted: true, instanceId },
            { limit: 10000, offset: 0 },
        );
        const schedulesById = new Map<number, WorkSchedule>();
        schedules.forEach((schedule) => schedulesById.set(schedule.id, schedule));
        return schedulesById;
    }

    private async getShiftAssignedDaysData(shiftAssignedDayIds: Set<number>) {
        const shiftAssignedDays = await this.shiftCalendarService.getShiftAssignedDaysByIds(Array.from(shiftAssignedDayIds));
        const shiftAssignedDaysById = new Map<number, ShiftAssignedDay>();
        shiftAssignedDays.forEach((shiftAssignedDay) => shiftAssignedDaysById.set(shiftAssignedDay.id, shiftAssignedDay));
        return shiftAssignedDaysById;
    }

    public listEmployeesCalendar = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser, requiredLimitOffsetPagination: pagination } = res.locals;
        const user = userDAOtoUser(loggedUser as UserDAO);
        await this.endpointHandlerWithGenerics(next, async () => {
            const params = res.locals.queryParams as TimeTrackingDaySummaryWithScheduleFiltersVC;
            this.permissionService.validateManageWorkSchedules(user);
            await this.performCapabilityValidationsForCalendar(user, params, CapabilityName.MANAGE_WORK_SCHEDULES);
            if (params.regionIds?.length || params.siteIds?.length) {
                params.userIds = await this.siteService.filterUsers(params.userIds, {
                    instanceId: instanceOfLoggedUser.getId(),
                    regionIds: params.regionIds,
                    siteIds: params.siteIds,
                });
                if (!params.userIds?.length) {
                    res.json(
                        new ScheduleCalendarSC().serializePaginatedList({
                            count: 0,
                            extraData: { schedulesById: {} },
                            limit: pagination.limit,
                            offset: pagination.offset,
                            rows: [],
                        }),
                    );
                    res.status(OK);
                    res.end();
                    return;
                }
            }
            const employeeSummaries = await this.employeeAdapter.listEmployeeCalendars(user, instanceOfLoggedUser, {
                departmentIds: params.departmentIds,
                excludeDepartmentIds: params.excludeDepartmentIds,
                excludeScheduleCodes: params.excludeScheduleCodes,
                fromDate: params.startDate,
                hasHolidays: false,
                hasSchedule: params.hasSchedule,
                hasTimeOff: false,
                incidences: [],
                itemIds: params.itemIds,
                limit: pagination.limit,
                offset: pagination.offset,
                onlyNoSchedule: params.onlyNoSchedule,
                scheduleCodes: params.scheduleCodes,
                search: params.search,
                status: [],
                toDate: params.endDate,
                userIds: params.userIds,
            });
            const scheduleIds = employeeSummaries.rows
                .flatMap((employee) => employee.daySummaries.map((daySummary) => daySummary.workScheduleId))
                .filter(Boolean) as number[];
            const { rows: schedules } = await this.workScheduleService.listWorkSchedules(
                { ids: scheduleIds, includeDeleted: true, instanceId: instanceOfLoggedUser.getId() },
                { limit: 10000, offset: 0 },
            );
            const schedulesById = schedules.reduce((acc: Record<string, WorkSchedule>, schedule: WorkSchedule) => {
                acc[schedule.id] = schedule;
                return acc;
            }, {});
            // Limit and offset are used for user pagination, not for calendar pagination.
            res.json(
                new ScheduleCalendarSC().serializePaginatedList({
                    count: employeeSummaries.count,
                    extraData: { schedulesById },
                    limit: pagination.limit,
                    offset: pagination.offset,
                    rows: employeeSummaries.rows,
                }),
            );
            res.status(OK);
            res.end();
        });
    };

    public listDepartments = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction) => {
        const { loggedUser, requiredLimitOffsetPagination: limitOffsetPagination, sortingOptions, queryParams } = res.locals;
        const { q } = queryParams as FilterableMandatoryPaginableVC;
        const user = userDAOtoUser(loggedUser as UserDAO);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateAnyCapability(user, [CapabilityName.VIEW_TIME_TRACKING, CapabilityName.MANAGE_TIME_TRACKING]);
            const { rows, count } = await this.departmentsService.getDepartments({
                instanceId: (user as User).getInstanceId(),
                pagination: limitOffsetPagination,
                q,
                sortingOptions: sortingOptions ?? [],
            });
            const serializedDepartment = new DepartmentSC().serializePaginatedList({
                count,
                limit: limitOffsetPagination.limit,
                offset: limitOffsetPagination.offset,
                rows,
            });
            res.status(OK).send(serializedDepartment);
        });
    };

    public listRegions = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, limitOffsetPagination } = res.locals;
        const user = userDAOtoUser(loggedUser as UserDAO) as User;
        await this.endpointHandler(next, async () => {
            this.permissionService.validateAnyCapability(user, [CapabilityName.VIEW_TIME_TRACKING, CapabilityName.MANAGE_TIME_TRACKING]);
            const { regions, totalCount } = await this.regionsService.findAll(user.getInstanceId(), limitOffsetPagination);

            res.status(OK).json(
                new RegionSC().serializePaginatedList({
                    count: totalCount,
                    limit: limitOffsetPagination.limit!,
                    offset: limitOffsetPagination.offset!,
                    rows: regions,
                }),
            );
        });
    };

    public listUsers = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        const queryParams = res.locals.queryParams as FilterPublicUserListMapper;
        const { limitOffsetPagination, sortingOptions, loggedUser } = res.locals;
        const user = userDAOtoUser(loggedUser);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateViewTimeTracking(user);
            const hasManageCapability = this.permissionService.hasAnyCapability(user, [CapabilityName.MANAGE_TIME_TRACKING]);
            const { rows, count } = await this.employeeAdapter.listUsers(
                res.locals.loggedUser,
                queryParams,
                { pagination: limitOffsetPagination, sortingOptions },
                hasManageCapability,
            );
            res.status(OK).json(
                new PublicUserSerializer().serializePaginatedList(
                    rows,
                    limitOffsetPagination.limit as number,
                    limitOffsetPagination.offset as number,
                    count,
                ),
            );
        });
    };

    public listNotificationSchedule = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser } = res.locals;
        const params = res.locals.queryParams as TimeTrackingGetNotificationScheduleVC;
        const user = userDAOtoUser(loggedUser as UserDAO);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateViewTimeTracking(user);
            this.validateStartAndEndDate(params, instanceOfLoggedUser);
            const notificationSchedule = await this.timeTrackingService.getNotificationSchedule((user as User).getId(), instanceOfLoggedUser, params);
            res.status(OK).json(notificationSchedule);
        });
    };

    private validateStartAndEndDate(params: TimeTrackingGetNotificationScheduleVC, instance: Instance) {
        // if not present, 7 days from now
        if (!params.startDate) {
            params.startDate = moment.tz(instance.getTimezone()).format(ISO_DATE);
        }
        if (!params.endDate) {
            params.endDate = moment.tz(instance.getTimezone()).add(MAX_DAYS_FROM_NOW_SCHEDULE, 'day').format(ISO_DATE);
        }
        if (moment(params.startDate).isAfter(moment(params.endDate))) {
            throw new BadRequestError('Start date must be before or equal to end date', ErrorCodes.INVALID_DATE_RANGE);
        }
    }

    public getMyUserSetting = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        const { loggedUser } = res.locals;
        const user = userDAOtoUser(loggedUser as UserDAO);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateViewTimeTracking(user);

            const userSetting = await this.timeTrackingUserSettingService.getTimeTrackingUserSettingWithKioskPolicy(
                user.getInstanceId(),
                user.getId(),
            );

            res.json(userSetting);
            res.status(OK).end();
        });
    };

    private getTimeTrackingDesertEagleEnabledFeatureFlag = async (instanceId: number): Promise<boolean> => {
        const featureFlag = await this.featureFlagService.getBooleanFeatureFlag(FeatureFlagCode.TIME_TRACKING_DESERT_EAGLE_ENABLED, instanceId);
        return featureFlag.value;
    };

    public listHourCategories = async (_req: Request, res: TimeTrackingResponse, next: NextFunction): Promise<void> => {
        const { loggedUser, instanceOfLoggedUser, requiredLimitOffsetPagination, queryParams } = res.locals;
        const { search } = queryParams as GetHourCategoriesVC;
        const user = userDAOtoUser(loggedUser);
        await this.endpointHandlerWithGenerics(next, async () => {
            this.permissionService.validateViewTimeTracking(user);
            const result = await this.timeTrackingHourCategorizationService.listHourCategories(
                instanceOfLoggedUser.getId(),
                requiredLimitOffsetPagination,
                undefined,
                search,
            );
            res.json(
                new TimeTrackingSimplifiedHourCategoriesSC().serializePaginatedList({
                    count: result.count,
                    limit: requiredLimitOffsetPagination.limit,
                    offset: requiredLimitOffsetPagination.offset,
                    rows: result.rows,
                }),
            );
            res.status(OK).end();
        });
    };

    public appGetGrants = async (_req: Request, res: DeprecatedHumandResponse, next: NextFunction): Promise<void> => {
        await this.endpointHandlerWithGenerics(next, async () => {
            const grants = await this.permissionService.calculateGrants(res.locals.loggedUser, TIME_TRACKING_APP_GRANTS_DEFINITION);
            res.status(OK).json({ grants });
        });
    };
}
