import { BadRequestError, BaseAdapter, ErrorCodes, ForbiddenError, ISOLATION_LEVELS, NotFoundError } from '@humand-packages/common';
import { DateTime } from 'luxon';
import { Service } from 'typedi';
import { PortsDI } from '../../../../../config/portsDI';
import { Instance } from '../../../../instances/business/models/instance';
import { OrganizationChartsService } from '../../../../organizationCharts/business/services/organizationChartsService';
import { SiteService } from '../../../../regions/business/services/sitesService';
import { User } from '../../../../users/business/models/user';
import { UsersService } from '../../../../users/business/services/usersService';
import ShiftAssignedDayRepositoryPort from '../../../infrastructure/ports/shiftManagement/shiftAssignedDayRepositoryPort';
import ShiftAssignmentRepositoryPort from '../../../infrastructure/ports/shiftManagement/shiftAssignmentRepositoryPort';
import { ShiftAssignRequestVC } from '../../../presentation/validationClasses/shiftManagement/shiftAssignVC';
import { ShiftBulkDeleteVC } from '../../../presentation/validationClasses/shiftManagement/shiftBulkDeleteVC';
import { ShiftVC } from '../../../presentation/validationClasses/shiftManagement/shiftCreationVC';
import { ShiftManagementCalendarFiltersVC } from '../../../presentation/validationClasses/shiftManagement/shiftManagementCalendarFiltersVC';
import { CategorizedHoursActionType } from '../../constants/categorizedHoursActionTypes';
import { CopyAndPasteRequest, PasteRequest, PasteRequestUser } from '../../interfaces/shiftManagement/copyAndPasteRequestInterface';
import { DuplicateShiftPeriodRequest } from '../../interfaces/shiftManagement/duplicateShiftPeriodRequestInterface';
import { GridCell, GridFilterRequest } from '../../interfaces/shiftManagement/gridFiltersInterface';
import { ShiftCreationInterface } from '../../interfaces/shiftManagement/shift';
import { ShiftCell } from '../../interfaces/shiftManagement/shiftCell';
import { ShiftMoveItem, ShiftMoveRequest } from '../../interfaces/shiftManagement/shiftMoveRequestInterface';
import { Shift } from '../../models/shiftManagement/shift';
import { ShiftAssignedDay, ShiftAssignedDayForCreation } from '../../models/shiftManagement/shiftAssignedDay';
import { RawShiftAssignedShiftForCreation, ShiftAssignedShift, ShiftAssignedShiftForCreation } from '../../models/shiftManagement/shiftAssignedShift';
import { TimeSlot } from '../../models/timeTracking/scheduleData';
import ScheduleCategorizedHoursPort from '../../ports/scheduleCategorizedHoursPort';
import { ShiftAssignedDayStatus } from '../../types/shiftAssignedDayStatus';
import {
    DateUserAssignedDayMap,
    UserAssignedDayMap,
    UserDateAssignedDayMap,
    UserDateAssignedDayRecord,
    UserDateExcludeCellRecord,
    UserDateIncludeCellRecord,
} from '../../types/shiftManagement/assignedDay';
import { ShiftAssignedShiftStatus } from '../../types/shiftManagement/assignedShiftStatus';
import { ShiftAssignedShiftType } from '../../types/shiftManagement/assignedShiftType';
import { searchByShift, searchByUser } from '../../types/shiftManagement/searchType';
import { ShiftAssignedShiftSource } from '../../types/shiftManagement/source';
import { hasTimeOverlap } from '../../utils/timeSlotOverlap';
import { TimeTrackingForShiftService } from '../timeTracking/timeTrackingForShift';
import ShiftAssignedDayService from './shiftAssignedDay';
import { ShiftService } from './shiftService';

const MAX_DAYS_BETWEEN_DATES = 31;
const MAX_USERS_COUNT = 1000;

@Service()
export default class ShiftAssignmentService extends BaseAdapter {
    constructor(
        @PortsDI(ShiftAssignedDayRepositoryPort) private readonly shiftAssignedDayRepository: ShiftAssignedDayRepositoryPort,
        @PortsDI(ShiftAssignmentRepositoryPort) private readonly shiftAssignmentRepository: ShiftAssignmentRepositoryPort,
        private readonly shiftService: ShiftService,
        private readonly organizationService: OrganizationChartsService,
        private readonly timeTrackingService: TimeTrackingForShiftService,
        private readonly usersService: UsersService,
        private readonly siteService: SiteService,
        private readonly shiftAssignedDayService: ShiftAssignedDayService,
        @PortsDI(ScheduleCategorizedHoursPort) private readonly scheduleCategorizedHoursPort: ScheduleCategorizedHoursPort,
    ) {
        super();
    }

    public assign = async (instance: Instance, user: User, params: ShiftAssignRequestVC, hasManageCapability: boolean): Promise<Shift | null> => {
        this.validateParams(params);
        const useShiftFromTemplate = !!params.shift.id;
        if (!useShiftFromTemplate) this.validateShiftConfiguration(params.shift);
        const userIdsInParams = this.getUserIdsInParams(params);
        await this.userCanPerformOverUsers(instance.getId(), user.getId(), userIdsInParams, hasManageCapability);
        if (params.allRows) {
            params.userIds = await this.getUserIdsByFilters(user, hasManageCapability, params.filters);
        }
        const shiftAssignedDays = await this.shiftAssignedDayRepository.getShiftAssignedDaysByGridFilters(instance.getId(), params);
        const userDateAssignedDayRecord = this.groupShiftAssignedDaysByUserAndDate(shiftAssignedDays);
        const shiftFromTemplate = useShiftFromTemplate ? await this.shiftService.getShiftById(params.shift.id!) : null;
        if (useShiftFromTemplate) {
            this.assertShiftExists(shiftFromTemplate);
            this.validateOverlappingInShiftFromTemplate(shiftFromTemplate, params, userDateAssignedDayRecord);
        } else {
            this.validateOverlappingInShiftFromParams(params.shift, params, userDateAssignedDayRecord);
        }
        const [shift, createdShiftAssignedDays] = await this.shiftAssignedDayRepository.getTransaction(async () => {
            const shiftType = this.evaluateShiftType(params.shift, useShiftFromTemplate);
            const shift =
                shiftType === ShiftAssignedShiftType.WORK
                    ? await this.getWorkShift(instance.getId(), useShiftFromTemplate, shiftFromTemplate, params.shift)
                    : null;
            if (shiftType === ShiftAssignedShiftType.WORK) this.assertShiftExists(shift);
            const shiftCells = this.buildShiftCellsForSingleShift(instance.getId(), params, shiftType, shift);
            const shiftAssignedDays = await this.persistShiftCells(instance.getId(), shiftCells, params);
            return [shift, shiftAssignedDays];
        });
        if (createdShiftAssignedDays.length > 0) {
            this.shiftAssignedDayService.emitShiftAssignedDayCreatedEvent(createdShiftAssignedDays, instance.getId(), false);
        }
        return shift;
    };

    private getWorkShift(instanceId: number, useShiftFromTemplate: boolean, shiftFromTemplate: Shift | null, shiftFromParams: ShiftVC) {
        return useShiftFromTemplate
            ? shiftFromTemplate
            : this.shiftService.createShift(instanceId, shiftFromParams.saveAsTemplate ?? false, shiftFromParams);
    }

    private evaluateShiftType(shift: ShiftVC, useShiftFromTemplate: boolean) {
        if (useShiftFromTemplate) return ShiftAssignedShiftType.WORK;
        return shift.isRest ? ShiftAssignedShiftType.REST : ShiftAssignedShiftType.WORK;
    }

    private assertShiftExists(shift: Shift | null): asserts shift is Shift {
        if (!shift) throw new NotFoundError('Shift not found', ErrorCodes.SHIFT_MANAGEMENT_SHIFT_NOT_FOUND);
    }

    private getUserIdsInParams(params: ShiftAssignRequestVC | ShiftBulkDeleteVC) {
        if (params.allRows && !params.cells?.length) {
            return [];
        }
        const userIds = [...(params.userIds || []), ...(params.cells?.map((cell) => cell.userId) || [])];
        return userIds;
    }

    private async userCanPerformOverUsers(instanceId: number, userId: number, userIds: number[], hasManageCapability: boolean) {
        if (!hasManageCapability) {
            const subordinateIds = await this.organizationService.getSubordinateIdsRecursive(userId, instanceId);
            if (userIds.some((id) => !subordinateIds.includes(id) && userId !== id)) {
                throw new ForbiddenError('The provided user has no access to this resource', ErrorCodes.ACCESS_DENIED);
            }
        }
    }

    private groupShiftAssignedDaysByUserAndDate(shiftAssignedDays: ShiftAssignedDay[]): UserDateAssignedDayRecord {
        const userDateAssignedDayRecord: UserDateAssignedDayRecord = shiftAssignedDays.reduce((acc, day) => {
            const { userId, dateString } = day;
            if (!acc[userId]) acc[userId] = {};
            if (!acc[userId][dateString]) acc[userId][dateString] = day;
            return acc;
        }, {} as UserDateAssignedDayRecord);

        return userDateAssignedDayRecord;
    }

    private groupShiftAssignedDaysByUserAndDateMap(shiftAssignedDays: ShiftAssignedDay[]): UserDateAssignedDayMap {
        const userDateAssignedDayMap: UserDateAssignedDayMap = new Map();
        for (const day of shiftAssignedDays) {
            const { userId, dateString } = day;
            if (!userDateAssignedDayMap.has(userId)) userDateAssignedDayMap.set(userId, new Map());
            userDateAssignedDayMap.get(userId)?.set(dateString, day);
        }
        return userDateAssignedDayMap;
    }

    private validateParams(params: ShiftAssignRequestVC) {
        const hasCells = !!params.cells?.length;
        const hasAllRows = !!params.allRows;
        const hasUserIds = !!params.userIds?.length;
        const hasDates = !!params.dates?.length;

        if (hasAllRows && !hasDates) {
            throw new BadRequestError('Dates are required when allRows is true', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
        }

        if (hasAllRows && hasUserIds) {
            throw new BadRequestError('User ids are not allowed when allRows is true', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
        }

        const hasBulk = hasAllRows || (hasUserIds && hasDates);
        if (!hasCells && !hasBulk) {
            throw new BadRequestError('You must specify either cells OR (userIds AND dates) OR allRows', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
        }
    }

    private validateShiftConfiguration(shift: ShiftVC) {
        const { timeSlots, isRest } = shift;
        if (isRest) {
            if (timeSlots && timeSlots.length > 0) {
                throw new BadRequestError('Rest shift must not have time slots', ErrorCodes.SHIFT_MANAGEMENT_INVALID_DAY_CONFIGURATION);
            }
            if (shift.name || shift.color) {
                throw new BadRequestError('Rest shift must not have a name or color', ErrorCodes.SHIFT_MANAGEMENT_INVALID_DAY_CONFIGURATION);
            }
        } else {
            if (!timeSlots || timeSlots.length === 0) {
                throw new BadRequestError('Shift must have time slots', ErrorCodes.SHIFT_MANAGEMENT_INVALID_DAY_CONFIGURATION);
            }
            if (!shift.color) {
                throw new BadRequestError('Shift must have a color', ErrorCodes.SHIFT_MANAGEMENT_INVALID_DAY_CONFIGURATION);
            }
        }

        if (!isRest) {
            const validatedSlots: { start: string; end: string }[] = [];

            for (const [startTimeRaw, endTimeRaw] of timeSlots) {
                const startTime = startTimeRaw.trim();
                const endTime = endTimeRaw.trim();

                if (startTime === endTime) {
                    throw new BadRequestError(
                        `Start time (${startTime}) must not be equal to end time (${endTime})`,
                        ErrorCodes.SHIFT_MANAGEMENT_INVALID_TIME_SLOTS,
                    );
                }

                for (const slot of validatedSlots) {
                    if (hasTimeOverlap(startTime, endTime, slot.start, slot.end)) {
                        throw new BadRequestError(
                            `Overlapping time slots found: [${startTime}, ${endTime}] overlaps with [${slot.start}, ${slot.end}]`,
                            ErrorCodes.SHIFT_MANAGEMENT_INVALID_TIME_SLOTS,
                        );
                    }
                }

                validatedSlots.push({ end: endTime, start: startTime });
            }
        }
    }

    private validateOverlappingInShiftFromTemplate(shift: Shift, filters: GridFilterRequest, userDateAssignedDayRecord: UserDateAssignedDayRecord) {
        this.validateOverlappingInShift(filters, shift.timeSlots, shift.isRest, userDateAssignedDayRecord);
    }

    private validateOverlappingInShiftFromParams(
        shift: ShiftCreationInterface,
        filters: GridFilterRequest,
        userDateAssignedDayRecord: UserDateAssignedDayRecord,
    ) {
        const newTimeSlots = shift.timeSlots ? this.shiftService.rawTimeSlotsToTimeSlot(shift.timeSlots) : [];
        this.validateOverlappingInShift(filters, newTimeSlots, shift.isRest, userDateAssignedDayRecord);
    }

    private validateOverlappingInShift(
        filters: GridFilterRequest,
        newTimeSlots: TimeSlot[],
        isRest: boolean | undefined,
        userDateAssignedDayRecord: UserDateAssignedDayRecord,
    ) {
        const { userIds, dates, cells, excludeCells } = filters;
        const hasBulkFilters = userIds?.length && dates?.length;
        const hasCellFilters = cells?.length;
        if (!(hasBulkFilters || hasCellFilters)) {
            return;
        }

        const hasExcludeCellFilters = excludeCells?.length;
        const userDateExcludeCellRecord = hasExcludeCellFilters ? this.getUserDateExcludeCellRecord(excludeCells) : {};
        const userDateIncludeCellRecord: UserDateIncludeCellRecord = {};

        if (hasCellFilters) {
            for (const cell of cells) {
                if (userDateExcludeCellRecord[cell.userId]?.[cell.date]) continue;
                const shiftAssignedDay = userDateAssignedDayRecord[cell.userId]?.[cell.date];
                this.validateOverlappingInPlanning(shiftAssignedDay, isRest, newTimeSlots);
                if (!userDateIncludeCellRecord[cell.userId]) userDateIncludeCellRecord[cell.userId] = {};
                userDateIncludeCellRecord[cell.userId][cell.date] = true;
            }
        }

        if (hasBulkFilters) {
            for (const userId of userIds) {
                for (const dateString of dates) {
                    if (userDateExcludeCellRecord[userId]?.[dateString]) continue;
                    if (userDateIncludeCellRecord[userId]?.[dateString]) continue;
                    const shiftAssignedDay = userDateAssignedDayRecord[userId]?.[dateString];
                    this.validateOverlappingInPlanning(shiftAssignedDay, isRest, newTimeSlots);
                }
            }
        }
    }

    private validateOverlappingInPlanning(shiftAssignedDay: ShiftAssignedDay, isRest: boolean | undefined, newTimeSlots: TimeSlot[]) {
        if (this.hasPlanning(shiftAssignedDay)) {
            if (isRest) {
                throw new BadRequestError(`Rest shift overlap`, ErrorCodes.SHIFT_MANAGEMENT_REST_SHIFT_OVERLAP);
            } else {
                const { isWorkday, timeSlots } = this.getScheduleData(shiftAssignedDay);
                if (isWorkday) {
                    this.validateOverlappingInTimeSlots(timeSlots, newTimeSlots);
                } else {
                    throw new BadRequestError(`Rest shift overlap`, ErrorCodes.SHIFT_MANAGEMENT_REST_SHIFT_OVERLAP);
                }
            }
        }
    }

    private hasPlanning(shiftAssignedDay: ShiftAssignedDay | undefined) {
        return (
            shiftAssignedDay &&
            shiftAssignedDay.shiftAssociations?.some((shiftAssignedShift) => shiftAssignedShift.status === ShiftAssignedShiftStatus.PUBLISHED)
        );
    }

    private validateOverlappingInTimeSlots(timeSlots: TimeSlot[], newTimeSlots: TimeSlot[]) {
        for (const existing of timeSlots) {
            for (const newTimeSlot of newTimeSlots) {
                if (hasTimeOverlap(existing.startTime, existing.endTime, newTimeSlot.startTime, newTimeSlot.endTime)) {
                    throw new BadRequestError(
                        `Overlapping time slots found: [${newTimeSlot.startTime}-${newTimeSlot.endTime}] overlaps with [${existing.startTime}-${existing.endTime}]`,
                        ErrorCodes.SHIFT_MANAGEMENT_INVALID_TIME_SLOTS,
                    );
                }
            }
        }
    }

    private getTimeSlotFromShiftAssignedShift = (shiftAssignedShift: ShiftAssignedShift | ShiftAssignedShiftForCreation): TimeSlot[] => {
        const timeSlots: TimeSlot[] = [];
        switch (shiftAssignedShift.source) {
            case ShiftAssignedShiftSource.INTEGRATION:
                timeSlots.push(...(shiftAssignedShift.timeSlots || []));
                break;
            case ShiftAssignedShiftSource.APP:
                timeSlots.push(...(shiftAssignedShift.shift?.timeSlots || []));
                break;
            default:
                break;
        }
        return timeSlots;
    };

    private getShiftAssignedDaysForCreation(instanceId: number, filters: GridFilterRequest) {
        const shiftAssignedDaysForCreation: ShiftAssignedDayForCreation[] = [];
        const { userIds, dates, cells, excludeCells } = filters;
        const hasBulkFilters = userIds?.length && dates?.length;
        const hasCellFilters = cells?.length;
        if (!(hasBulkFilters || hasCellFilters)) {
            return [];
        }
        const hasExcludeCellFilters = excludeCells?.length;
        const userDateExcludeCellRecord = hasExcludeCellFilters ? this.getUserDateExcludeCellRecord(excludeCells) : {};
        const userDateIncludeCellRecord: UserDateIncludeCellRecord = {};

        if (hasCellFilters) {
            for (const cell of cells) {
                if (userDateExcludeCellRecord[cell.userId]?.[cell.date]) continue;
                const shiftAssignedDayForCreation: ShiftAssignedDayForCreation = {
                    dateString: cell.date,
                    instanceId,
                    shiftAssociations: [],
                    status: ShiftAssignedDayStatus.PUBLISHED,
                    userId: cell.userId,
                };
                shiftAssignedDaysForCreation.push(shiftAssignedDayForCreation);
                if (!userDateIncludeCellRecord[cell.userId]) userDateIncludeCellRecord[cell.userId] = {};
                userDateIncludeCellRecord[cell.userId][cell.date] = true;
            }
        }

        if (hasBulkFilters) {
            for (const userId of userIds) {
                for (const dateString of dates) {
                    if (userDateExcludeCellRecord[userId]?.[dateString]) continue;
                    if (userDateIncludeCellRecord[userId]?.[dateString]) continue;
                    const shiftAssignedDayForCreation: ShiftAssignedDayForCreation = {
                        dateString,
                        instanceId,
                        shiftAssociations: [],
                        status: ShiftAssignedDayStatus.PUBLISHED,
                        userId,
                    };
                    shiftAssignedDaysForCreation.push(shiftAssignedDayForCreation);
                }
            }
        }

        return shiftAssignedDaysForCreation;
    }

    private getUserDateExcludeCellRecord(excludeCells: GridCell[]): UserDateExcludeCellRecord {
        const userDateExcludeCellRecord: UserDateExcludeCellRecord = {};
        for (const cell of excludeCells) {
            if (!userDateExcludeCellRecord[cell.userId]) userDateExcludeCellRecord[cell.userId] = {};
            userDateExcludeCellRecord[cell.userId][cell.date] = true;
        }
        return userDateExcludeCellRecord;
    }

    private getScheduleData(shiftAssignedDay: ShiftAssignedDay) {
        const timeSlots: TimeSlot[] = [];
        let isWorkday = false;
        for (const shiftAssignedShift of shiftAssignedDay.shiftAssociations ?? []) {
            if (shiftAssignedShift.status === ShiftAssignedShiftStatus.PUBLISHED) {
                switch (shiftAssignedShift.type) {
                    case ShiftAssignedShiftType.REST:
                        return { isWorkday: false, timeSlots: [] };
                    case ShiftAssignedShiftType.WORK:
                        timeSlots.push(...this.getTimeSlotFromShiftAssignedShift(shiftAssignedShift));
                        isWorkday = true;
                        break;
                    default:
                        break;
                }
            }
        }
        return { isWorkday, timeSlots };
    }

    /**
     * Builds one `ShiftCell` per targeted (user, date) cell for the single-shift
     * `assign` flow: the same shift (or REST) broadcast across every cell.
     */
    private buildShiftCellsForSingleShift = (
        instanceId: number,
        params: ShiftAssignRequestVC,
        shiftType: ShiftAssignedShiftType,
        shift: Shift | null,
    ): ShiftCell[] => {
        const isRest = shiftType === ShiftAssignedShiftType.REST;
        const timeSlots = !isRest && shift ? (shift.timeSlots as TimeSlot[]) : [];
        const assignedShift: RawShiftAssignedShiftForCreation = isRest
            ? {
                  instanceId,
                  source: ShiftAssignedShiftSource.APP,
                  status: ShiftAssignedShiftStatus.PUBLISHED,
                  type: ShiftAssignedShiftType.REST,
              }
            : {
                  instanceId,
                  shiftCode: shift?.code,
                  shiftVersion: shift?.version,
                  source: ShiftAssignedShiftSource.APP,
                  status: ShiftAssignedShiftStatus.PUBLISHED,
                  type: ShiftAssignedShiftType.WORK,
              };
        return this.getShiftAssignedDaysForCreation(instanceId, params).map((day) => ({
            assignedShifts: [{ ...assignedShift }],
            dateString: day.dateString,
            instanceId,
            isRest,
            timeSlots,
            userId: day.userId,
        }));
    };

    /**
     * Source-agnostic stamping primitive: authorizes the caller over the affected users,
     * validates each cell assignment against existing planning, then persists. Callers must
     * hand over fully-resolved `ShiftCell[]` (shift definitions already exist) —
     * this method knows nothing about templates, rotations or pastes.
     */
    public assignShiftCells = async (
        instance: Instance,
        user: User,
        shiftCells: ShiftCell[],
        hasManageCapability: boolean,
    ): Promise<ShiftAssignedDay[]> => {
        if (shiftCells.length === 0) return [];
        const userIds = [...new Set(shiftCells.map((cell) => cell.userId))];
        await this.userCanPerformOverUsers(instance.getId(), user.getId(), userIds, hasManageCapability);

        const dates = shiftCells.map((cell) => cell.dateString);
        const startDate = dates.reduce((a, b) => (a < b ? a : b));
        const endDate = dates.reduce((a, b) => (a > b ? a : b));

        const existingDays = await this.shiftAssignedDayRepository.getShiftAssignedDaysByGridFilters(instance.getId(), {
            endDate,
            startDate,
            userIds,
        });
        const existingByUserDate = this.groupShiftAssignedDaysByUserAndDate(existingDays);
        for (const cell of shiftCells) {
            const existingDay = existingByUserDate[cell.userId]?.[cell.dateString];
            this.validateOverlappingInPlanning(existingDay, cell.isRest, cell.timeSlots);
        }

        return this.persistShiftCells(instance.getId(), shiftCells, { endDate, startDate, userIds });
    };

    /**
     * Shared persist tail for every stamping flow: creates the day cells, attaches each
     * cell's assigned shifts (correlated by user+date), clears affected day summaries and
     * dispatches the categorized-hours recompute. Runs inside (or joins) a transaction.
     */
    private persistShiftCells = async (
        instanceId: number,
        shiftCells: ShiftCell[],
        summaryFilters: GridFilterRequest,
    ): Promise<ShiftAssignedDay[]> => {
        return this.shiftAssignedDayRepository.getTransaction(async () => {
            const daysForCreation: ShiftAssignedDayForCreation[] = shiftCells.map((cell) => ({
                dateString: cell.dateString,
                instanceId: cell.instanceId,
                shiftAssociations: [],
                status: ShiftAssignedDayStatus.PUBLISHED,
                userId: cell.userId,
            }));
            const createdDays = await this.shiftAssignedDayRepository.bulkCreateShiftAssignedDays(daysForCreation);

            const assignedShiftsByCell = new Map<string, RawShiftAssignedShiftForCreation[]>();
            for (const cell of shiftCells) assignedShiftsByCell.set(`${cell.userId}|${cell.dateString}`, cell.assignedShifts);

            const shiftsForCreation: ShiftAssignedShiftForCreation[] = [];
            for (const day of createdDays) {
                const assignedShifts = assignedShiftsByCell.get(`${day.userId}|${day.dateString}`) ?? [];
                for (const assignedShift of assignedShifts) {
                    shiftsForCreation.push({ ...assignedShift, assignedDayId: day.id });
                }
            }
            if (shiftsForCreation.length > 0) {
                await this.shiftAssignmentRepository.bulkCreateShiftAssignedShifts(shiftsForCreation);
            }
            await this.timeTrackingService.deleteTimeTrackingDaySummaries(instanceId, summaryFilters);
            await this.dispatchScheduleChangeForDays(instanceId, createdDays);
            return createdDays;
        });
    };

    public deleteShiftAssignedShift = async (
        shiftAssignedShiftId: number,
        shiftAssignedDayId: number,
        instanceId: number,
        logguedUserId: number,
        hasManageShiftsCapability: boolean,
    ): Promise<void> => {
        const shiftAssignedDay = await this.userCanDeleteShiftAssignedShift(shiftAssignedDayId, instanceId, logguedUserId, hasManageShiftsCapability);
        await this.shiftAssignmentRepository.getTransaction(async () => {
            const rowsDeleted = await this.shiftAssignmentRepository.deleteAssignedShift(shiftAssignedShiftId, instanceId);
            if (rowsDeleted > 0) {
                await this.timeTrackingService.deleteDaySummariesByShiftAssignedDayId(instanceId, shiftAssignedDayId);
                await this.scheduleCategorizedHoursPort.schedule({
                    type: CategorizedHoursActionType.SCHEDULE_CHANGE,
                    payload: {
                        instanceId,
                        userIds: [shiftAssignedDay.userId],
                        startDate: shiftAssignedDay.dateString,
                        endDate: shiftAssignedDay.dateString,
                    },
                });
            }
        });
    };

    private userCanDeleteShiftAssignedShift = async (
        shiftAssignedDayId: number,
        instanceId: number,
        userId: number,
        hasManageShiftsCapability: boolean,
    ): Promise<ShiftAssignedDay> => {
        const shiftAssignedDay = await this.shiftAssignedDayRepository.getShiftAssignedDayById(shiftAssignedDayId);
        if (!shiftAssignedDay) {
            throw new NotFoundError('Shift assigned day not found', ErrorCodes.NOT_FOUND);
        }
        await this.userCanPerformOverUsers(instanceId, userId, [shiftAssignedDay.userId], hasManageShiftsCapability);
        return shiftAssignedDay;
    };

    private async getUserIdsByFilters(user: User, hasManageCapability: boolean, filters?: ShiftManagementCalendarFiltersVC): Promise<number[]> {
        const hasSearchFilters = this.hasSearchFilters(filters);
        const hasUserIdsFilter = !!filters?.userIds?.length;
        const hasSegmentationItemIdsFilter = !!filters?.segmentationItemIds?.length;

        let userIds: number[] = [];
        if (hasUserIdsFilter) {
            userIds = filters?.userIds ?? [];
        } else {
            if (hasManageCapability) {
                userIds = await this.usersService.getUserIdsInInstance(user.getInstanceId());
            } else {
                userIds = await this.organizationService.getSubordinateIdsRecursive(user.getId(), user.getInstanceId());
            }
            if (userIds.length === 0) return [];
        }

        if (filters?.siteId) {
            const site = await this.siteService.getById(filters.siteId!, user.getInstanceId());
            const siteUserIds = site.getUsers().map((siteUser) => siteUser.getUserId());
            if (siteUserIds.length === 0) return [];
            userIds = userIds.filter((userId) => siteUserIds.includes(userId));
            if (userIds.length === 0) return [];
        }

        // get users from search (shift and user fields)
        let searchResultUserIds: number[] = [];

        if (hasSearchFilters) {
            // add the specified users to the result
            if (hasUserIdsFilter) {
                userIds = [...new Set([...userIds, ...filters.userIds!])];
            }

            if (searchByUser(filters?.searchType!)) {
                const userSearchIds = await this.usersService.getFilteredIds(
                    user,
                    {
                        ids: userIds,
                        search: filters?.search,
                    },
                    { excludeLoggedUser: false },
                );

                searchResultUserIds = [...searchResultUserIds, ...userSearchIds];
            }

            if (searchByShift(filters?.searchType!)) {
                const shiftAssignedDays = await this.shiftAssignedDayRepository.getShiftAssignedDaysByShiftName(
                    user.getInstanceId(),
                    filters?.search!,
                    filters?.startDate!,
                    filters?.endDate!,
                );

                if (shiftAssignedDays.length > 0) {
                    const shiftUserIds = [...new Set(shiftAssignedDays.map((assignedDay: ShiftAssignedDay) => assignedDay.userId))];
                    const filteredShiftUserIds = shiftUserIds.filter((userId) => userIds.includes(userId));
                    searchResultUserIds = [...searchResultUserIds, ...filteredShiftUserIds];
                }
            }

            userIds = [...new Set(searchResultUserIds)];
        }

        // filter by segmentation
        if (hasSegmentationItemIdsFilter) {
            if (userIds.length === 0) return [];
            userIds = await this.usersService.getFilteredIds(
                user,
                {
                    ids: userIds, // Apply segmentation to current user set
                    segmentationItemIds: filters.segmentationItemIds?.join(','),
                },
                { excludeLoggedUser: false },
            );
            if (hasUserIdsFilter && !hasSearchFilters) {
                userIds = [...new Set([...userIds, ...filters.userIds!])];
            }
        }

        if (hasUserIdsFilter && !hasSearchFilters && !hasSegmentationItemIdsFilter) {
            userIds = userIds.filter((userId) => filters.userIds?.includes(userId));
        }
        return userIds;
    }

    private hasSearchFilters(filters?: ShiftManagementCalendarFiltersVC): boolean {
        return !!filters?.search;
    }

    public bulkDelete = async (instance: Instance, user: User, params: ShiftBulkDeleteVC, hasManageCapability: boolean): Promise<void> => {
        this.validateBulkDeleteParams(params);
        const userIdsInParams = this.getUserIdsInParams(params);
        const shiftAssignedShifts = params.assignedShiftIds
            ? await this.shiftAssignedDayRepository.getShiftAssignedShifts(instance.getId(), params.assignedShiftIds)
            : [];
        const { assignedShiftGridCells, assignedShiftUserIds } = this.getShiftAssignedShiftRecords(shiftAssignedShifts);
        const userIdsAffected = [...new Set([...userIdsInParams, ...assignedShiftUserIds])];
        await this.userCanPerformOverUsers(instance.getId(), user.getId(), userIdsAffected, hasManageCapability);
        if (params.allRows) {
            params.userIds = await this.getUserIdsByFilters(user, hasManageCapability, params.filters);
        }
        return await this.shiftAssignmentRepository.getTransaction(async () => {
            await this.shiftAssignmentRepository.bulkDeleteShiftAssignedShifts(instance.getId(), params);
            const summaryFilters: GridFilterRequest = { ...params, cells: [...(params.cells ?? []), ...assignedShiftGridCells] };
            await this.timeTrackingService.deleteTimeTrackingDaySummaries(instance.getId(), summaryFilters);
            const { userIds, dates } = this.extractAffectedUserIdsAndDates(params, assignedShiftGridCells);
            if (userIds.length > 0 && dates.length > 0) {
                await this.scheduleCategorizedHoursPort.schedule({
                    type: CategorizedHoursActionType.SCHEDULE_CHANGE,
                    payload: {
                        instanceId: instance.getId(),
                        userIds,
                        startDate: dates.reduce((a, b) => (a < b ? a : b)),
                        endDate: dates.reduce((a, b) => (a > b ? a : b)),
                    },
                });
            }
        });
    };

    private extractAffectedUserIdsAndDates(params: ShiftBulkDeleteVC, assignedShiftGridCells: GridCell[]): { userIds: number[]; dates: string[] } {
        const allCells = [...(params.cells ?? []), ...assignedShiftGridCells];
        const cellUserIds = allCells.map((c) => c.userId);
        const cellDates = allCells.map((c) => c.date);
        const userIds = [...new Set([...(params.userIds ?? []), ...cellUserIds])];
        const dates = [...new Set([...(params.dates ?? []), ...cellDates])];
        return { userIds, dates };
    }

    private async dispatchScheduleChangeForDays(instanceId: number, shiftAssignedDays: ShiftAssignedDay[]): Promise<void> {
        if (shiftAssignedDays.length === 0) return;
        const dates = shiftAssignedDays.map((d) => d.dateString);
        const userIds = [...new Set(shiftAssignedDays.map((d) => d.userId))];
        const minDate = dates.reduce((a, b) => (a < b ? a : b));
        const maxDate = dates.reduce((a, b) => (a > b ? a : b));
        await this.scheduleCategorizedHoursPort.schedule({
            type: CategorizedHoursActionType.SCHEDULE_CHANGE,
            payload: { instanceId, userIds, startDate: minDate, endDate: maxDate },
        });
    }

    private getShiftAssignedShiftRecords(shiftAssignedShifts: ShiftAssignedShift[]): {
        assignedShiftGridCells: GridCell[];
        assignedShiftUserIds: number[];
    } {
        const userIds: Set<number> = new Set();
        const cellMap: Map<string, Map<number, GridCell>> = new Map();
        for (const shiftAssignedShift of shiftAssignedShifts) {
            if (!shiftAssignedShift.shiftAssignedDay) continue;
            const { userId, dateString } = shiftAssignedShift.shiftAssignedDay;
            userIds.add(userId);
            if (!cellMap.has(dateString)) cellMap.set(dateString, new Map());
            const userMap = cellMap.get(dateString)!;
            if (!userMap.has(userId)) userMap.set(userId, { date: dateString, userId });
        }
        const assignedShiftGridCells = Array.from(cellMap.values()).flatMap((userMap) => Array.from(userMap.values()));
        return { assignedShiftGridCells, assignedShiftUserIds: Array.from(userIds) };
    }

    private validateBulkDeleteParams(params: ShiftBulkDeleteVC) {
        const hasCells = !!params.cells?.length;
        const hasAllRows = !!params.allRows;
        const hasUserIds = !!params.userIds?.length;
        const hasDates = !!params.dates?.length;
        const hasAssignedShiftIds = !!params.assignedShiftIds?.length;

        if (hasAllRows && !hasDates) {
            throw new BadRequestError('Dates are required when allRows is true', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
        }

        if (hasAllRows && hasUserIds) {
            throw new BadRequestError('User ids are not allowed when allRows is true', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
        }

        const hasBulk = hasAllRows || (hasUserIds && hasDates);
        if (!(hasCells || hasBulk || hasAssignedShiftIds)) {
            throw new BadRequestError(
                'You must specify either cells OR (userIds AND dates) OR allRows OR assignedShiftIds',
                ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS,
            );
        }
    }

    public copyAndPaste = async (instance: Instance, user: User, params: CopyAndPasteRequest, hasManageCapability: boolean): Promise<void> => {
        this.validateCopyAndPasteParams(params);
        const userIdsInParams = this.getUserIdsInCopyAndPasteParams(params);
        await this.userCanPerformOverUsers(instance.getId(), user.getId(), userIdsInParams, hasManageCapability);
        // get shift assigned shifts from copy request to identify the dates to copy
        const shiftAssignedShifts = await this.shiftAssignedDayRepository.getShiftAssignedShifts(
            instance.getId(),
            params.copy.assignedShiftIds ?? [],
        );
        const { assignedShiftGridCells } = this.getShiftAssignedShiftRecords(shiftAssignedShifts);
        // get the number of dates to copy, sorted dates list
        const sortedDates = this.normalizeCopyGridCells(params, assignedShiftGridCells);

        const { targetUserIds, gridFilterRequest, pasteGridFilterRequest } = await this.generateGridFilterRequest(
            user,
            hasManageCapability,
            sortedDates,
            params,
        );
        const shiftAssignedDays = await this.shiftAssignedDayRepository.getShiftAssignedDaysByGridFilters(instance.getId(), gridFilterRequest);
        const userDateAssignedDayMap = this.groupShiftAssignedDaysByUserAndDateMap(shiftAssignedDays);
        const { copyUserDateAssignedDayMap, pasteUserDateAssignedDayMap } = this.classifyUserDateAssignedDayMap(
            userDateAssignedDayMap,
            params,
            targetUserIds,
        );
        this.validateOverlappingInPaste(sortedDates, params.paste, targetUserIds, copyUserDateAssignedDayMap, pasteUserDateAssignedDayMap);
        const shiftAssignedDaysForCreation = this.getShiftAssignedDaysForCreationCopyAndPaste(
            instance.getId(),
            sortedDates,
            params.paste,
            targetUserIds,
        );
        const lockKey = `copy-and-paste:${instance.getId()}:${user.getId()}`;
        return await this.shiftAssignmentRepository.transactionWithAdvisoryLock(
            lockKey,
            async () => {
                const shiftAssignedDays = await this.shiftAssignedDayRepository.bulkCreateShiftAssignedDays(shiftAssignedDaysForCreation);
                const shiftAssignedShiftsForCreation = this.getShiftAssignedShiftsForCreationInCopyAndPaste(
                    instance.getId(),
                    shiftAssignedDays,
                    sortedDates,
                    params.paste,
                    copyUserDateAssignedDayMap,
                    targetUserIds,
                );
                await this.shiftAssignmentRepository.bulkCreateShiftAssignedShifts(shiftAssignedShiftsForCreation);
                await this.timeTrackingService.deleteTimeTrackingDaySummaries(instance.getId(), pasteGridFilterRequest);
                await this.dispatchScheduleChangeForDays(instance.getId(), shiftAssignedDays);
            },
            {
                exponentialBackoff: true,
                isolationLevel: ISOLATION_LEVELS.READ_COMMITTED,
                tries: 1,
            },
        );
    };

    public move = async (instance: Instance, user: User, params: ShiftMoveRequest, hasManageCapability: boolean): Promise<void> => {
        this.validateMoveShape(params.moves);

        const shiftAssignedShiftSourceIds = params.moves.map((m) => m.shiftAssignedShiftId);
        const sourceShifts = await this.shiftAssignedDayRepository.getShiftAssignedShifts(instance.getId(), shiftAssignedShiftSourceIds);
        if (sourceShifts.length !== shiftAssignedShiftSourceIds.length) {
            throw new NotFoundError('One or more source shifts not found', ErrorCodes.SHIFT_MANAGEMENT_SHIFT_NOT_FOUND);
        }
        for (const shift of sourceShifts) {
            if (shift.status !== ShiftAssignedShiftStatus.PUBLISHED) {
                throw new BadRequestError(`Source shift ${shift.id} is not PUBLISHED`, ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
            }
        }

        const effectiveMoves = this.filterNoOpMoves(params.moves, sourceShifts);
        if (effectiveMoves.length === 0) return;

        const { assignedShiftGridCells: sourceCells, assignedShiftUserIds: sourceUserIds } = this.getShiftAssignedShiftRecords(sourceShifts);
        const targetUserIds = effectiveMoves.map((m) => m.targetUserId);
        const allUserIds = [...new Set([...sourceUserIds, ...targetUserIds])];
        await this.userCanPerformOverUsers(instance.getId(), user.getId(), allUserIds, hasManageCapability);

        const targetCells: GridCell[] = [
            ...new Map(effectiveMoves.map((m) => [`${m.targetUserId}:${m.targetDate}`, { date: m.targetDate, userId: m.targetUserId }])).values(),
        ];
        const movingShiftIdSet = new Set(shiftAssignedShiftSourceIds);

        const sourceDaysWithTimeSlots = await this.shiftAssignedDayRepository.getShiftAssignedDaysByGridFilters(instance.getId(), {
            cells: sourceCells,
        });
        const sourceShiftWithDataById = this.buildMovingShiftMap(sourceDaysWithTimeSlots, movingShiftIdSet);

        // overlap check outside the transaction.
        const existingTargetDays = await this.shiftAssignedDayRepository.getShiftAssignedDaysByGridFilters(instance.getId(), {
            cells: targetCells,
        });
        this.assertNoOverlapAtTargets(existingTargetDays, effectiveMoves, sourceShiftWithDataById, movingShiftIdSet);

        const lockKey = `move:${instance.getId()}`;
        await this.shiftAssignmentRepository.transactionWithAdvisoryLock(
            lockKey,
            async () => {
                const dayKey = (d: { userId: number; dateString: string }) => `${d.userId}:${d.dateString}`;
                const existingByKey = new Map<string, ShiftAssignedDay>(existingTargetDays.map((d) => [dayKey(d), d]));
                const cellsToCreate = targetCells.filter((c) => !existingByKey.has(`${c.userId}:${c.date}`));
                const createdDays =
                    cellsToCreate.length > 0
                        ? await this.shiftAssignedDayRepository.bulkCreateShiftAssignedDays(
                              cellsToCreate.map((c) => ({
                                  dateString: c.date,
                                  instanceId: instance.getId(),
                                  status: ShiftAssignedDayStatus.PUBLISHED,
                                  userId: c.userId,
                              })),
                          )
                        : [];
                for (const createdDay of createdDays) existingByKey.set(dayKey(createdDay), createdDay);

                // re-check overlap under lock to avoid cross-admin race.
                const reChecked = await this.shiftAssignedDayRepository.getShiftAssignedDaysByGridFilters(instance.getId(), {
                    cells: targetCells,
                });
                this.assertNoOverlapAtTargets(reChecked, effectiveMoves, sourceShiftWithDataById, movingShiftIdSet);

                const updates = effectiveMoves.map((m) => {
                    const day = existingByKey.get(`${m.targetUserId}:${m.targetDate}`);
                    if (!day) {
                        throw new BadRequestError(
                            `Target ShiftAssignedDay missing for user=${m.targetUserId} date=${m.targetDate}`,
                            ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS,
                        );
                    }
                    return { newAssignedDayId: day.id, shiftAssignedShiftId: m.shiftAssignedShiftId };
                });
                await this.shiftAssignmentRepository.bulkUpdateShiftAssignedShiftsAssignedDay(instance.getId(), updates);

                const allCells = [...sourceCells, ...targetCells];
                const summaryFilters: GridFilterRequest = { cells: allCells };
                await this.timeTrackingService.deleteTimeTrackingDaySummaries(instance.getId(), summaryFilters);

                // Recreate day summaries for every (user, date) the move touched, inside
                // the same transaction. Without this, calendar GETs that follow the move can race
                // with the lazy-create gate (replica lag, concurrent reads) and return a grid
                // with the affected cells missing.
                const allDates = allCells.map((c) => c.date);
                const fromMinDate = allDates.reduce((min, d) => (d < min ? d : min));
                const toMaxDate = allDates.reduce((max, d) => (d > max ? d : max));
                await this.timeTrackingService.createMissingDaySummariesForUsers(instance, allUserIds, fromMinDate, toMaxDate);

                const sourceDays = sourceShifts.map((s) => s.shiftAssignedDay).filter((d): d is ShiftAssignedDay => d !== undefined);
                const targetDays = Array.from(existingByKey.values());
                await this.dispatchScheduleChangeForDays(instance.getId(), sourceDays);
                await this.dispatchScheduleChangeForDays(instance.getId(), targetDays);
            },
            {
                exponentialBackoff: true,
                isolationLevel: ISOLATION_LEVELS.READ_COMMITTED,
                tries: 1,
            },
        );
    };

    /**
     * Builds a lookup of shifts being moved, keyed by id, from a set of ShiftAssignedDays whose
     * shiftAssociations are loaded with time slots.
     * @param days - The days containing the candidate shiftAssociations (source cells).
     * @param movingShiftIdSet - The ids of shifts being moved in this batch.
     * @returns A Map from shift id to the fully-loaded ShiftAssignedShift.
     */
    private buildMovingShiftMap(days: ShiftAssignedDay[], movingShiftIdSet: Set<number>): Map<number, ShiftAssignedShift> {
        const movingShiftById = new Map<number, ShiftAssignedShift>();
        for (const day of days) {
            for (const s of day.shiftAssociations ?? []) {
                const shift = s as ShiftAssignedShift;
                if (movingShiftIdSet.has(shift.id)) movingShiftById.set(shift.id, shift);
            }
        }
        return movingShiftById;
    }

    /**
     * For each move, validates that its source shift's time slots do not overlap with the time slots
     * of other shifts ending up in the target cell after this batch is applied — i.e.:
     *   - persisted shifts at the target that are NOT being moved out, plus
     *   - other shifts in this same batch whose target is this same cell.
     *
     * Shifts being moved out of the target in this batch are excluded so swaps and cascade swaps
     * (shift1 A→B + shift2 B→A; shift1 A→B + shift2 B→C + shift3 C→A) don't self-flag.
     * @param existingTargetDays - The currently-persisted ShiftAssignedDays at the target cells.
     * @param moves - The moves to validate.
     * @param sourceShiftWithDataById - Source shifts indexed by id, with time slots loaded.
     * @param movingShiftIdSet - The ids of shifts being moved in this batch.
     * @throws BadRequestError if any move would overlap with another shift ending up at its target cell.
     */
    private assertNoOverlapAtTargets(
        existingTargetDays: ShiftAssignedDay[],
        moves: ShiftMoveItem[],
        sourceShiftWithDataById: Map<number, ShiftAssignedShift>,
        movingShiftIdSet: Set<number>,
    ): void {
        for (const move of moves) {
            const movingShift = sourceShiftWithDataById.get(move.shiftAssignedShiftId);
            if (!movingShift) {
                throw new NotFoundError(`Source shift ${move.shiftAssignedShiftId} not found`, ErrorCodes.SHIFT_MANAGEMENT_SHIFT_NOT_FOUND);
            }

            // Look up the shifts persisted at the target cell, then keep only the ones that
            // will remain there after this batch runs. A shift whose id is in
            // movingShiftIdSet is already moved out in this same batch
            const persistedTargetDay = existingTargetDays.find((d) => d.userId === move.targetUserId && d.dateString === move.targetDate);
            const persistedShiftsStaying = ((persistedTargetDay?.shiftAssociations ?? []) as ShiftAssignedShift[]).filter(
                (s) => !movingShiftIdSet.has(s.id) && s.status === ShiftAssignedShiftStatus.PUBLISHED,
            );

            // Find the other shifts that will land at this same target cell because of
            // other moves in this same batch.
            const otherIncomingShifts = moves
                .filter(
                    (m) =>
                        m.shiftAssignedShiftId !== move.shiftAssignedShiftId &&
                        m.targetUserId === move.targetUserId &&
                        m.targetDate === move.targetDate,
                )
                .map((m) => sourceShiftWithDataById.get(m.shiftAssignedShiftId))
                .filter((s): s is ShiftAssignedShift => s !== undefined);

            const shiftsAtTargetAfterBatch = [...persistedShiftsStaying, ...otherIncomingShifts];
            if (shiftsAtTargetAfterBatch.length === 0) continue;

            // Build a snapshot of what the target cell will look like after this batch is applied
            const targetDayAfterBatch: ShiftAssignedDay = persistedTargetDay
                ? { ...persistedTargetDay, shiftAssociations: shiftsAtTargetAfterBatch }
                : ({ shiftAssociations: shiftsAtTargetAfterBatch } as ShiftAssignedDay);
            const movingShiftTimeSlots = this.getTimeSlotFromShiftAssignedShift(movingShift);
            this.validateOverlappingInPlannings(movingShift.type, movingShiftTimeSlots, targetDayAfterBatch);
        }
    }

    private getShiftAssignedShiftsForCreationInCopyAndPaste(
        instanceId: number,
        shiftAssignedDays: ShiftAssignedDay[],
        sortedDates: string[],
        pasteRequest: PasteRequest,
        copyUserDateAssignedDayMap: UserDateAssignedDayMap,
        targetUserIds: number[],
    ) {
        if (pasteRequest.allUsers) {
            const targetStartDate = pasteRequest.startDate;
            this.assertStartDate(targetStartDate);
            const pasteRequestUsers: PasteRequestUser[] = targetUserIds.map((targetUserId) => ({ startDate: targetStartDate, targetUserId }));
            return this.getShiftAssignedShiftsForCreationInCopyAndPasteWithPasteRequestUsers(
                instanceId,
                shiftAssignedDays,
                sortedDates,
                pasteRequestUsers,
                copyUserDateAssignedDayMap,
            );
        } else {
            const pasteRequestUsers = pasteRequest.users;
            if (!pasteRequestUsers) {
                throw new BadRequestError('Users are required when allUsers is false', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
            }
            return this.getShiftAssignedShiftsForCreationInCopyAndPasteWithPasteRequestUsers(
                instanceId,
                shiftAssignedDays,
                sortedDates,
                pasteRequestUsers,
                copyUserDateAssignedDayMap,
            );
        }
    }

    private getShiftAssignedShiftsForCreationInCopyAndPasteWithPasteRequestUsers(
        instanceId: number,
        shiftAssignedDays: ShiftAssignedDay[],
        sortedDates: string[],
        pasteRequestUsers: PasteRequestUser[],
        copyUserDateAssignedDayMap: UserDateAssignedDayMap,
    ) {
        const userDateAssignedDayMap = this.groupShiftAssignedDaysByUserAndDateMap(shiftAssignedDays);
        const shiftAssignedShiftsForCreation: ShiftAssignedShiftForCreation[] = [];
        sortedDates.forEach((date, index) => {
            for (const [_, copyDateAssignedDayMap] of copyUserDateAssignedDayMap.entries()) {
                const copyShiftAssignedDay = copyDateAssignedDayMap.get(date);
                if (copyShiftAssignedDay) {
                    const { type, shifts } = this.getPlanningShiftData(copyShiftAssignedDay.shiftAssociations);
                    if (type) {
                        pasteRequestUsers.forEach((pasteRequestUser) => {
                            const targetDate = DateTime.fromISO(pasteRequestUser.startDate).plus({ days: index }).toISODate();
                            if (targetDate) {
                                const targetShiftAssignedDay = userDateAssignedDayMap.get(pasteRequestUser.targetUserId)?.get(targetDate);
                                if (!targetShiftAssignedDay)
                                    throw new BadRequestError('Target shift assigned day not found', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
                                if (type === ShiftAssignedShiftType.REST) {
                                    shiftAssignedShiftsForCreation.push({
                                        assignedDayId: targetShiftAssignedDay.id,
                                        instanceId,
                                        source: ShiftAssignedShiftSource.APP,
                                        status: ShiftAssignedShiftStatus.PUBLISHED,
                                        type: ShiftAssignedShiftType.REST,
                                    });
                                } else if (type === ShiftAssignedShiftType.WORK) {
                                    for (const shift of shifts) {
                                        shiftAssignedShiftsForCreation.push({
                                            assignedDayId: targetShiftAssignedDay.id,
                                            instanceId,
                                            shiftCode: shift.code,
                                            shiftVersion: shift.version,
                                            source: ShiftAssignedShiftSource.APP,
                                            status: ShiftAssignedShiftStatus.PUBLISHED,
                                            type: ShiftAssignedShiftType.WORK,
                                        });
                                    }
                                }
                            }
                        });
                    }
                }
            }
        });
        return shiftAssignedShiftsForCreation;
    }

    private getPlanningShiftData(shiftAssignedShifts: ShiftAssignedShift[] | ShiftAssignedShiftForCreation[] | undefined): {
        type: ShiftAssignedShiftType | undefined;
        shifts: Shift[];
    } {
        if (!shiftAssignedShifts) return { shifts: [], type: undefined };
        const validShiftAssignedShifts = shiftAssignedShifts.filter(
            (shiftAssignedShift) =>
                shiftAssignedShift.status === ShiftAssignedShiftStatus.PUBLISHED && shiftAssignedShift.source === ShiftAssignedShiftSource.APP,
        );
        const shifts: Shift[] = [];
        for (const shiftAssignedShift of validShiftAssignedShifts) {
            if (shiftAssignedShift.type === ShiftAssignedShiftType.REST) {
                return { shifts: [], type: ShiftAssignedShiftType.REST };
            }
            if (shiftAssignedShift.shift) {
                shifts.push(shiftAssignedShift.shift);
            }
        }
        return { shifts, type: ShiftAssignedShiftType.WORK };
    }

    private getShiftAssignedDaysForCreationCopyAndPaste(
        instanceId: number,
        sortedDates: string[],
        pasteRequest: PasteRequest,
        targetUserIds: number[],
    ) {
        if (pasteRequest.allUsers) {
            const targetStartDate = pasteRequest.startDate;
            this.assertStartDate(targetStartDate);
            const pasteRequestUsers: PasteRequestUser[] = targetUserIds.map((targetUserId) => ({ startDate: targetStartDate, targetUserId }));
            return this.getShiftAssignedDaysForCreationCopyAndPasteWithPasteRequestUsers(instanceId, sortedDates, pasteRequestUsers);
        } else {
            const pasteRequestUsers = pasteRequest.users;
            if (!pasteRequestUsers) {
                throw new BadRequestError('Users are required when allUsers is false', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
            }
            return this.getShiftAssignedDaysForCreationCopyAndPasteWithPasteRequestUsers(instanceId, sortedDates, pasteRequestUsers);
        }
    }

    private getShiftAssignedDaysForCreationCopyAndPasteWithPasteRequestUsers(
        instanceId: number,
        sortedDates: string[],
        pasteRequestUsers: PasteRequestUser[],
    ) {
        const shiftAssignedDaysForCreation: ShiftAssignedDayForCreation[] = [];
        sortedDates.forEach((_, index) => {
            pasteRequestUsers.forEach((pasteRequestUser) => {
                const targetDate = DateTime.fromISO(pasteRequestUser.startDate).plus({ days: index }).toISODate();
                if (targetDate) {
                    shiftAssignedDaysForCreation.push({
                        dateString: targetDate,
                        instanceId,
                        status: ShiftAssignedDayStatus.PUBLISHED,
                        userId: pasteRequestUser.targetUserId,
                    });
                }
            });
        });
        return shiftAssignedDaysForCreation;
    }

    private validateCopyAndPasteParams(params: CopyAndPasteRequest) {
        const hasDates = !!params.copy.dates?.length;
        const hasAssignedShiftIds = !!params.copy.assignedShiftIds?.length;

        if (!(hasDates || hasAssignedShiftIds)) {
            throw new BadRequestError('You must specify either dates OR assignedShiftIds', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
        }

        const allUsers = !!params.paste.allUsers;

        if (allUsers) {
            this.assertStartDate(params.paste.startDate);
            if (params.paste.users) {
                throw new BadRequestError('Users are not allowed when allUsers is true', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
            }
        } else if (params.paste.users) {
            const targetUserIdsSet = new Set();
            for (const user of params.paste.users) {
                if (targetUserIdsSet.has(user.targetUserId))
                    throw new BadRequestError('Target user ids must be unique', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
                targetUserIdsSet.add(user.targetUserId);
            }
        } else {
            throw new BadRequestError('Users are required when allUsers is false', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
        }
    }

    private getUserIdsInCopyAndPasteParams(params: CopyAndPasteRequest) {
        return [params.copy.sourceUserId, ...(params.paste.users?.map((paste) => paste.targetUserId) || [])];
    }

    /**
     * Validates the shape of the move request, ensuring that:
     * - There are no duplicate source shiftAssignedShiftIds.
     * - The moves are not empty.
     *
     * Multiple moves may share a target cell (e.g. moving every shift out of
     * a multi-shift source cell into a single empty target cell).
     * @param moves - The moves to validate.
     * @throws BadRequestError if the moves are not valid.
     */
    private validateMoveShape(moves: ShiftMoveItem[]): void {
        if (moves.length === 0) {
            throw new BadRequestError('moves must not be empty', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
        }
        const sourceIds = new Set<number>();
        for (const move of moves) {
            if (sourceIds.has(move.shiftAssignedShiftId)) {
                throw new BadRequestError(
                    `Duplicate source shiftAssignedShiftId: ${move.shiftAssignedShiftId}`,
                    ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS,
                );
            }
            sourceIds.add(move.shiftAssignedShiftId);
        }
    }

    /**
     * Filters out moves that are no-ops, i.e. moves that target the same cell as the source.
     * @param moves - The moves to filter.
     * @param sourceShifts - The source shifts to use to determine if a move is a no-op.
     * @returns The filtered moves.
     */
    private filterNoOpMoves(moves: ShiftMoveItem[], sourceShifts: ShiftAssignedShift[]): ShiftMoveItem[] {
        const sourceCellByShiftId = new Map<number, { userId: number; dateString: string }>();
        for (const shift of sourceShifts) {
            const day = shift.shiftAssignedDay;
            if (!day) continue;
            sourceCellByShiftId.set(shift.id, { dateString: day.dateString, userId: day.userId });
        }
        return moves.filter((move) => {
            const source = sourceCellByShiftId.get(move.shiftAssignedShiftId);
            if (!source) return true;
            return !(source.userId === move.targetUserId && source.dateString === move.targetDate);
        });
    }

    private normalizeCopyGridCells(params: CopyAndPasteRequest, assignedShiftGridCells: GridCell[]): string[] {
        const dates = new Set<string>([...(params.copy.dates ?? [])]);

        for (const assignedShiftGridCell of assignedShiftGridCells) {
            if (assignedShiftGridCell.userId !== params.copy.sourceUserId)
                throw new BadRequestError('Assigned shift userId is not equal to source user id', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
            if (dates.has(assignedShiftGridCell.date)) continue;
            dates.add(assignedShiftGridCell.date);
        }

        if (dates.size === 0) {
            return [];
        }

        const missingDates = this.fillMissingDates(dates);

        const allDates = [...dates, ...missingDates];
        return allDates.sort();
    }

    private fillMissingDates(dates: Set<string>): string[] {
        if (dates.size === 0) return [];

        const sortedDates = [...dates].sort();
        const minDate = DateTime.fromISO(sortedDates[0]);
        const maxDate = DateTime.fromISO(sortedDates[sortedDates.length - 1]);

        const missingDates: string[] = [];
        let currentDate = minDate;
        const MAX_ITERATIONS = 366;
        let iterations = 0;

        while (currentDate <= maxDate && iterations < MAX_ITERATIONS) {
            const dateStr = currentDate.toISODate();
            if (dateStr && !dates.has(dateStr)) {
                missingDates.push(dateStr);
            }
            currentDate = currentDate.plus({ days: 1 });
            iterations++;
        }

        if (iterations >= MAX_ITERATIONS) {
            throw new BadRequestError('Date range exceeds 366 days.', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
        }

        return missingDates;
    }

    private assertStartDate(startDate: string | undefined): asserts startDate is string {
        if (!startDate) {
            throw new BadRequestError('Start date is required', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
        }
    }

    private async generateGridFilterRequest(
        user: User,
        hasManageCapability: boolean,
        sortedDates: string[],
        params: CopyAndPasteRequest,
    ): Promise<{ targetUserIds: number[]; gridFilterRequest: GridFilterRequest; pasteGridFilterRequest: GridFilterRequest }> {
        const allUsers = !!params.paste.allUsers;
        if (allUsers) {
            const userIds = await this.getUserIdsByFilters(user, hasManageCapability, params.paste.filters);
            //If the first date is the same as the start date, we need to exclude the source user because must cause overlapping with itself
            const shouldExcludeSourceUser = sortedDates.length > 0 && sortedDates[0] === params.paste.startDate;
            const excludeUserIdsSet = new Set([
                ...(params.paste.excludeTargetUserIds ?? []),
                ...(shouldExcludeSourceUser ? [params.copy.sourceUserId] : []),
            ]);
            const targetStartDate = params.paste.startDate;
            this.assertStartDate(targetStartDate);
            const targetDates = this.getTargetDates(sortedDates.length, targetStartDate);
            const filteredUserIds = excludeUserIdsSet.size > 0 ? userIds.filter((userId) => !excludeUserIdsSet.has(userId)) : userIds;
            const gridFilterRequest: GridFilterRequest = {
                allRows: allUsers,
                assignedShiftIds: params.copy.assignedShiftIds ?? [],
                cells: [...(params.copy.dates?.map((date) => ({ date, userId: params.copy.sourceUserId })) ?? [])],
                dates: targetDates,
                userIds: filteredUserIds,
            };

            const pasteGridFilterRequest: GridFilterRequest = {
                dates: targetDates,
                userIds: filteredUserIds,
            };
            return { gridFilterRequest, pasteGridFilterRequest, targetUserIds: filteredUserIds };
        } else {
            // generate the grid cells for the paste request
            const { gridCells: pasteGridCells, targetUserIds } = this.generatePasteGridCells(sortedDates.length, params.paste.users ?? []);
            const gridFilterRequest: GridFilterRequest = {
                assignedShiftIds: params.copy.assignedShiftIds ?? [],
                cells: [...(params.copy.dates?.map((date) => ({ date, userId: params.copy.sourceUserId })) ?? []), ...pasteGridCells],
            };
            const pasteGridFilterRequest: GridFilterRequest = {
                cells: pasteGridCells,
            };
            return { gridFilterRequest, pasteGridFilterRequest, targetUserIds };
        }
    }

    private generatePasteGridCells(datesCount: number, pasteRequests: PasteRequestUser[]): { gridCells: GridCell[]; targetUserIds: number[] } {
        const gridCells: GridCell[] = [];
        const targetUserIds: number[] = [];
        for (const pasteRequest of pasteRequests) {
            const targetDates = this.getTargetDates(datesCount, pasteRequest.startDate);
            for (const date of targetDates) {
                gridCells.push({ date, userId: pasteRequest.targetUserId });
                targetUserIds.push(pasteRequest.targetUserId);
            }
        }
        return { gridCells, targetUserIds };
    }

    private getTargetDates(datesCount: number, startDate: string) {
        const startDateDateTime = DateTime.fromISO(startDate);
        const targetDates: string[] = [];
        for (let i = 0; i < datesCount; i++) {
            const date = startDateDateTime.plus({ days: i }).toISODate();
            if (!date) throw new BadRequestError(`Invalid date in paste request - ${startDate}`, ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
            targetDates.push(date);
        }
        return targetDates;
    }

    private classifyUserDateAssignedDayMap(userDateAssignedDayMap: UserDateAssignedDayMap, params: CopyAndPasteRequest, targetUserIds: number[]) {
        const copyUserDateAssignedDayMap: UserDateAssignedDayMap = new Map();
        const pasteUserDateAssignedDayMap: UserDateAssignedDayMap = new Map();
        const targetUserIdsSet = new Set(targetUserIds);
        for (const [userId, dateAssignedDayMap] of userDateAssignedDayMap.entries()) {
            if (userId === params.copy.sourceUserId) {
                //Copy only the dates and assigned shifts that are in the copy request
                const dateAssignedDayMapCopy: Map<string, ShiftAssignedDay> = new Map();
                for (const [date, shiftAssignedDay] of dateAssignedDayMap.entries()) {
                    if (shiftAssignedDay) {
                        if (params.copy.dates?.includes(shiftAssignedDay.dateString)) {
                            dateAssignedDayMapCopy.set(date, shiftAssignedDay);
                        } else if (params.copy.assignedShiftIds?.length) {
                            const assignedShiftIdsSet = new Set(params.copy.assignedShiftIds);
                            const shiftAssociations = shiftAssignedDay.shiftAssociations?.filter((shiftAssociation) =>
                                assignedShiftIdsSet.has((shiftAssociation as ShiftAssignedShift).id),
                            );
                            if (shiftAssociations?.length) {
                                const copyShiftAssignedDay: ShiftAssignedDay = { ...shiftAssignedDay };
                                copyShiftAssignedDay.shiftAssociations = shiftAssociations;
                                dateAssignedDayMapCopy.set(date, copyShiftAssignedDay);
                            }
                        }
                    }
                }
                copyUserDateAssignedDayMap.set(userId, dateAssignedDayMapCopy);
            }

            if (targetUserIdsSet.has(userId)) pasteUserDateAssignedDayMap.set(userId, dateAssignedDayMap);
        }
        return { copyUserDateAssignedDayMap, pasteUserDateAssignedDayMap };
    }

    private validateOverlappingInPaste(
        sortedDates: string[],
        pasteRequest: PasteRequest,
        targetUserIds: number[],
        copyUserDateAssignedDayMap: UserDateAssignedDayMap,
        pasteUserDateAssignedDayMap: UserDateAssignedDayMap,
    ) {
        if (pasteRequest.allUsers) {
            const targetStartDate = pasteRequest.startDate;
            this.assertStartDate(targetStartDate);
            const pasteRequestUsers: PasteRequestUser[] = targetUserIds.map((targetUserId) => ({ startDate: targetStartDate, targetUserId }));
            this.validateOverlappingInPasteWithPasteRequestUsers(
                sortedDates,
                pasteRequestUsers,
                copyUserDateAssignedDayMap,
                pasteUserDateAssignedDayMap,
            );
        } else {
            const pasteRequestUsers = pasteRequest.users;
            if (!pasteRequestUsers) {
                throw new BadRequestError('Users are required when allUsers is false', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
            }
            this.validateOverlappingInPasteWithPasteRequestUsers(
                sortedDates,
                pasteRequestUsers,
                copyUserDateAssignedDayMap,
                pasteUserDateAssignedDayMap,
            );
        }
    }

    private validateOverlappingInPasteWithPasteRequestUsers(
        sortedDates: string[],
        pasteRequestUsers: PasteRequestUser[],
        copyUserDateAssignedDayMap: UserDateAssignedDayMap,
        pasteUserDateAssignedDayMap: UserDateAssignedDayMap,
    ) {
        sortedDates.forEach((date, index) => {
            for (const [userId, dateAssignedDayMap] of copyUserDateAssignedDayMap.entries()) {
                const sourceShiftAssignedDay = dateAssignedDayMap.get(date);
                if (sourceShiftAssignedDay) {
                    const { type, timeSlots } = this.getAssignedShiftData(sourceShiftAssignedDay.shiftAssociations);
                    if (type) {
                        for (const pasteRequestUser of pasteRequestUsers) {
                            const targetDate = DateTime.fromISO(pasteRequestUser.startDate).plus({ days: index }).toISODate();
                            if (targetDate) {
                                const targetShiftAssignedDay = pasteUserDateAssignedDayMap.get(pasteRequestUser.targetUserId)?.get(targetDate);
                                if (targetShiftAssignedDay) {
                                    try {
                                        this.validateOverlappingInPlannings(type, timeSlots, targetShiftAssignedDay);
                                    } catch (error) {
                                        if (error instanceof BadRequestError) {
                                            throw new BadRequestError(error.message, error.code, {
                                                sourceDate: date,
                                                sourceUserId: userId,
                                                targetDate,
                                                targetUserId: pasteRequestUser.targetUserId,
                                            });
                                        }
                                    }
                                }
                            } else {
                                throw new BadRequestError(
                                    `Invalid date in paste request ${pasteRequestUser.targetUserId} - ${pasteRequestUser.startDate}`,
                                    ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS,
                                );
                            }
                        }
                    }
                }
            }
        });
    }

    private validateOverlappingInPlannings(
        sourceType: ShiftAssignedShiftType | undefined,
        sourceTimeSlots: TimeSlot[],
        targetShiftAssignedDay: ShiftAssignedDay,
    ) {
        if (!targetShiftAssignedDay) return;
        const hasPlanning = this.hasPlanning(targetShiftAssignedDay);
        if (!hasPlanning) return;
        // rest shift cannot be copied to a planning day
        if (hasPlanning && sourceType === ShiftAssignedShiftType.REST) {
            throw new BadRequestError(`Rest shift overlap`, ErrorCodes.SHIFT_MANAGEMENT_REST_SHIFT_OVERLAP);
        }
        const { type, timeSlots } = this.getAssignedShiftData(targetShiftAssignedDay.shiftAssociations);
        // shift cannot be copied to a rest day
        if (type === ShiftAssignedShiftType.REST) {
            throw new BadRequestError(`Rest shift overlap`, ErrorCodes.SHIFT_MANAGEMENT_REST_SHIFT_OVERLAP);
        }
        this.validateOverlappingInTimeSlots(timeSlots, sourceTimeSlots);
    }

    private getAssignedShiftData(shiftAssignedShifts: ShiftAssignedShift[] | ShiftAssignedShiftForCreation[] | undefined) {
        if (!shiftAssignedShifts) return { timeSlots: [], type: undefined };
        const timeSlots: TimeSlot[] = [];
        for (const shiftAssignedShift of shiftAssignedShifts) {
            if (shiftAssignedShift.status === ShiftAssignedShiftStatus.PUBLISHED) {
                if (shiftAssignedShift.type === ShiftAssignedShiftType.REST) return { timeSlots: [], type: ShiftAssignedShiftType.REST };
                timeSlots.push(...this.getTimeSlotFromShiftAssignedShift(shiftAssignedShift));
            }
        }
        return { timeSlots, type: timeSlots.length > 0 ? ShiftAssignedShiftType.WORK : undefined };
    }

    public duplicateShiftPeriod = async (instance: Instance, user: User, params: DuplicateShiftPeriodRequest, hasManageCapability: boolean) => {
        const { sourceStartDate, sourceEndDate, targetStartDate, targetEndDate, filters } = params;
        this.validateDuplicateShiftPeriodParams(params);
        const userIds = await this.getUserIdsByFilters(user, hasManageCapability, filters);
        if (userIds.length > MAX_USERS_COUNT) {
            throw new BadRequestError(`Users count exceeds ${MAX_USERS_COUNT}`, ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
        }
        const sourceShiftAssignedDays = await this.shiftAssignedDayRepository.getShiftAssignedDaysByGridFilters(instance.getId(), {
            endDate: sourceEndDate,
            startDate: sourceStartDate,
            userIds,
        });
        // If there are no source shift assigned days, return
        if (sourceShiftAssignedDays.length === 0) return;
        const sourceDateUserAssignedDayMap = this.groupShiftAssignedDaysByDateAndUserMap(sourceShiftAssignedDays);
        const targetShiftAssignedDays = await this.shiftAssignedDayRepository.getShiftAssignedDaysByGridFilters(instance.getId(), {
            endDate: targetEndDate,
            startDate: targetStartDate,
            userIds,
        });
        const targetDateUserAssignedDayMap = this.groupShiftAssignedDaysByDateAndUserMap(targetShiftAssignedDays);
        const datesMap = this.getTargetDatesMapForDuplicateShiftPeriod(sourceStartDate, sourceEndDate, targetStartDate, targetEndDate);
        this.validateOverlappingInDuplicateShiftPeriod(sourceDateUserAssignedDayMap, targetDateUserAssignedDayMap, datesMap);
        const shiftAssignedDaysForCreation = this.getShiftAssignedDaysForCreationForDuplicateShiftPeriod(
            instance.getId(),
            Array.from(datesMap.keys()),
            userIds,
        );
        const lockKey = `duplicate-shift-period:${instance.getId()}:${user.getId()}`;
        return await this.shiftAssignmentRepository.transactionWithAdvisoryLock(
            lockKey,
            async () => {
                const shiftAssignedDays = await this.shiftAssignedDayRepository.bulkCreateShiftAssignedDays(shiftAssignedDaysForCreation);
                const shiftAssignedShiftsForCreation = this.getShiftAssignedShiftsForCreationInDuplicateShiftPeriod(
                    instance.getId(),
                    shiftAssignedDays,
                    datesMap,
                    sourceDateUserAssignedDayMap,
                );
                await this.shiftAssignmentRepository.bulkCreateShiftAssignedShifts(shiftAssignedShiftsForCreation);
                await this.timeTrackingService.deleteTimeTrackingDaySummaries(instance.getId(), {
                    endDate: targetEndDate,
                    startDate: targetStartDate,
                    userIds,
                });
                await this.dispatchScheduleChangeForDays(instance.getId(), shiftAssignedDays);
            },
            {
                exponentialBackoff: true,
                isolationLevel: ISOLATION_LEVELS.READ_COMMITTED,
                tries: 1,
            },
        );
    };

    private getShiftAssignedDaysForCreationForDuplicateShiftPeriod(instanceId: number, dates: string[], userIds: number[]) {
        const shiftAssignedDaysForCreation: ShiftAssignedDayForCreation[] = [];
        dates.forEach((date) => {
            for (const userId of userIds) {
                shiftAssignedDaysForCreation.push({
                    dateString: date,
                    instanceId,
                    status: ShiftAssignedDayStatus.PUBLISHED,
                    userId,
                });
            }
        });
        return shiftAssignedDaysForCreation;
    }

    private getShiftAssignedShiftsForCreationInDuplicateShiftPeriod(
        instanceId: number,
        shiftAssignedDays: ShiftAssignedDay[],
        datesMap: Map<string, string>,
        sourceDateUserAssignedDayMap: DateUserAssignedDayMap,
    ) {
        const dateUserAssignedDayMap = this.groupShiftAssignedDaysByDateAndUserMap(shiftAssignedDays);
        const shiftAssignedShiftsForCreation: ShiftAssignedShiftForCreation[] = [];
        for (const [targetDate, sourceDate] of datesMap.entries()) {
            const sourceDateAssignedDayMap = sourceDateUserAssignedDayMap.get(sourceDate);
            if (!sourceDateAssignedDayMap) continue;
            for (const [sourceUserId, sourceShiftAssignedDay] of sourceDateAssignedDayMap.entries()) {
                const targetShiftAssignedDay = dateUserAssignedDayMap.get(targetDate)?.get(sourceUserId);
                if (!targetShiftAssignedDay)
                    throw new BadRequestError('Target shift assigned day not found', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
                const { type, shifts } = this.getPlanningShiftData(sourceShiftAssignedDay.shiftAssociations);
                if (type) {
                    if (type === ShiftAssignedShiftType.REST) {
                        shiftAssignedShiftsForCreation.push({
                            assignedDayId: targetShiftAssignedDay.id,
                            instanceId,
                            source: ShiftAssignedShiftSource.APP,
                            status: ShiftAssignedShiftStatus.PUBLISHED,
                            type: ShiftAssignedShiftType.REST,
                        });
                    } else if (type === ShiftAssignedShiftType.WORK) {
                        for (const shift of shifts) {
                            shiftAssignedShiftsForCreation.push({
                                assignedDayId: targetShiftAssignedDay.id,
                                instanceId,
                                shiftCode: shift.code,
                                shiftVersion: shift.version,
                                source: ShiftAssignedShiftSource.APP,
                                status: ShiftAssignedShiftStatus.PUBLISHED,
                                type: ShiftAssignedShiftType.WORK,
                            });
                        }
                    }
                }
            }
        }
        return shiftAssignedShiftsForCreation;
    }

    private validateDuplicateShiftPeriodParams(params: DuplicateShiftPeriodRequest) {
        const sourceStartDateTime = DateTime.fromISO(params.sourceStartDate);
        const sourceEndDateTime = DateTime.fromISO(params.sourceEndDate);
        const targetStartDateTime = DateTime.fromISO(params.targetStartDate);
        const targetEndDateTime = DateTime.fromISO(params.targetEndDate);

        const sourceDaysDiff = sourceEndDateTime.diff(sourceStartDateTime, 'days').days;
        const targetDaysDiff = targetEndDateTime.diff(targetStartDateTime, 'days').days;

        if (sourceDaysDiff > MAX_DAYS_BETWEEN_DATES) {
            throw new BadRequestError(`Source date range exceeds ${MAX_DAYS_BETWEEN_DATES} days`, ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
        }
        if (targetDaysDiff > MAX_DAYS_BETWEEN_DATES) {
            throw new BadRequestError(`Target date range exceeds ${MAX_DAYS_BETWEEN_DATES} days`, ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
        }
        if (sourceStartDateTime >= targetStartDateTime) {
            throw new BadRequestError('Source start date must be before target start date', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
        }
        if (sourceEndDateTime >= targetEndDateTime) {
            throw new BadRequestError('Source end date must be before target end date', ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
        }
    }

    private groupShiftAssignedDaysByDateAndUserMap(shiftAssignedDays: ShiftAssignedDay[]): DateUserAssignedDayMap {
        const dateUserAssignedDayMap: DateUserAssignedDayMap = new Map();
        for (const day of shiftAssignedDays) {
            const { userId, dateString } = day;
            if (!dateUserAssignedDayMap.has(dateString)) dateUserAssignedDayMap.set(dateString, new Map());
            dateUserAssignedDayMap.get(dateString)?.set(userId, day);
        }
        return dateUserAssignedDayMap;
    }

    private validateOverlappingInDuplicateShiftPeriod(
        sourceDateUserAssignedDayMap: DateUserAssignedDayMap,
        targetDateUserAssignedDayMap: DateUserAssignedDayMap,
        datesMap: Map<string, string>,
    ) {
        for (const [targetDate, sourceDate] of datesMap.entries()) {
            const sourceDateAssignedDayMap = sourceDateUserAssignedDayMap.get(sourceDate);
            if (sourceDateAssignedDayMap) {
                const targetDateAssignedDayMap = targetDateUserAssignedDayMap.get(targetDate);
                if (targetDateAssignedDayMap) {
                    this.validateOverlappingInDuplicateShiftPeriodForDate(targetDate, sourceDateAssignedDayMap, targetDateAssignedDayMap);
                }
            }
        }
    }

    private validateOverlappingInDuplicateShiftPeriodForDate(
        date: string,
        sourceUserAssignedDayMap: UserAssignedDayMap,
        targetUserAssignedDayMap: UserAssignedDayMap,
    ) {
        for (const [sourceUserId, sourceShiftAssignedDay] of sourceUserAssignedDayMap.entries()) {
            const { type, timeSlots } = this.getAssignedShiftData(sourceShiftAssignedDay.shiftAssociations);
            if (type) {
                const targetShiftAssignedDay = targetUserAssignedDayMap.get(sourceUserId);
                if (targetShiftAssignedDay) {
                    try {
                        this.validateOverlappingInPlannings(type, timeSlots, targetShiftAssignedDay);
                    } catch (error) {
                        if (error instanceof BadRequestError) {
                            throw new BadRequestError(error.message, error.code, {
                                date,
                                userId: sourceUserId,
                            });
                        }
                    }
                }
            }
        }
    }

    private getTargetDatesMapForDuplicateShiftPeriod(
        sourceStartDate: string,
        sourceEndDate: string,
        targetStartDate: string,
        targetEndDate: string,
    ): Map<string, string> {
        const sourceStartDateDateTime = DateTime.fromISO(sourceStartDate);
        const sourceEndDateDateTime = DateTime.fromISO(sourceEndDate);
        const targetStartDateDateTime = DateTime.fromISO(targetStartDate);
        const targetEndDateDateTime = DateTime.fromISO(targetEndDate);

        // Build array of source dates
        const sourceDates: string[] = [];
        let currentSourceDate = sourceStartDateDateTime;
        let sourceIterations = 0;
        while (currentSourceDate <= sourceEndDateDateTime && sourceIterations < MAX_DAYS_BETWEEN_DATES) {
            sourceDates.push(currentSourceDate.toISODate()!);
            currentSourceDate = currentSourceDate.plus({ days: 1 });
            sourceIterations++;
        }

        // Map each target date to a source date cyclically
        const targetToSourceDateMap = new Map<string, string>();
        let currentTargetDate = targetStartDateDateTime;
        let targetIterations = 0;

        while (currentTargetDate <= targetEndDateDateTime && targetIterations < MAX_DAYS_BETWEEN_DATES) {
            const targetDateStr = currentTargetDate.toISODate();
            if (!targetDateStr)
                throw new BadRequestError(`Invalid date in duplicate shift period - ${targetStartDate}`, ErrorCodes.SHIFT_MANAGEMENT_INVALID_PARAMS);
            const sourceDateStr = sourceDates[targetIterations % sourceDates.length];
            targetToSourceDateMap.set(targetDateStr, sourceDateStr);

            currentTargetDate = currentTargetDate.plus({ days: 1 });
            targetIterations++;
        }

        return targetToSourceDateMap;
    }
}
