using GB5Shared.DTO.Framework.Login; using MMDAL.CustomCode.Scheduling; using MMDAL.DTO.Scheduling; namespace MMBLL.Scheduling.Engine { public class CalendarService : ICalendarService { private readonly IWorkCenterShiftMapDAL _ShiftMapDAL; private readonly ICalendarDAL _CalendarDAL; public CalendarService(IWorkCenterShiftMapDAL shiftMapDAL, ICalendarDAL calendarDAL) { _ShiftMapDAL = shiftMapDAL; _CalendarDAL = calendarDAL; } // Pre-populates the CalendarCache for all (workCenterId, date) pairs in the horizon // using a single batch DB query. Call before the EU scheduling loop to eliminate // per-day lazy loading during scheduling. public async Task PrePopulateCalendarCacheAsync( IEnumerable workCenterIds, DateTime horizonFrom, DateTime horizonTo, SchedulingContext context, LoginDTO login, CancellationToken ct) { var wcList = workCenterIds.ToList(); if (wcList.Count == 0) return; var patterns = (await _ShiftMapDAL .GetShiftPatternsForHorizonAsync(wcList, horizonFrom, horizonTo, login, ct) .ConfigureAwait(false)).ToList(); for (var date = horizonFrom.Date; date <= horizonTo.Date; date = date.AddDays(1)) { var dayOfWeek = (byte)date.DayOfWeek; var wcGroups = patterns .Where(p => p.DayOfWeek == dayOfWeek && p.EffectiveFrom.Date <= date && (p.EffectiveTo == null || p.EffectiveTo.Value.Date > date)) .GroupBy(p => p.WorkCenterId); foreach (var group in wcGroups) { var cacheKey = (group.Key, DateOnly.FromDateTime(date)); if (context.CalendarCache.ContainsKey(cacheKey)) continue; context.CalendarCache[cacheKey] = BuildWindowsFromPatterns(group, date); } } } public async Task> GetWorkingWindowsAsync( int machineId, int workCenterId, DateTime from, DateTime to, SchedulingContext context, LoginDTO login, CancellationToken ct) { var result = new List(); // Blocked windows (downtime + breakdowns) are cached per machine for the run. // First call for this machine fetches from DB; all subsequent calls reuse the cached list. if (!context.BlockedWindowsCache.TryGetValue(machineId, out var blockedWindows)) { var downtime = await _CalendarDAL.GetPlannedDowntimeAsync(machineId, from, to, login, ct).ConfigureAwait(false); var breakdowns = await _CalendarDAL.GetBreakdownWindowsAsync(machineId, from, to, login, ct).ConfigureAwait(false); blockedWindows = downtime.Concat(breakdowns).ToList(); context.BlockedWindowsCache[machineId] = blockedWindows; } // Walk day-by-day using the per-(WorkCenterId, Date) calendar cache. // Pre-populated by PrePopulateCalendarCacheAsync at run start; cache misses fall back to DB. for (var date = from.Date; date <= to.Date; date = date.AddDays(1)) { var cacheKey = (workCenterId, DateOnly.FromDateTime(date)); if (!context.CalendarCache.TryGetValue(cacheKey, out var dayWindows)) { dayWindows = await BuildDayWindowsAsync(workCenterId, date, login, ct).ConfigureAwait(false); context.CalendarCache[cacheKey] = dayWindows; } foreach (var window in dayWindows) { var clipped = ClipToRange(window, from, to); if (clipped is null) continue; // Subtract blocked windows from the working window var segments = SubtractBlocked(clipped, blockedWindows); result.AddRange(segments); } } // Merge adjacent/overlapping free segments so non-interruptible jobs can span consecutive shifts. return MergeAdjacentWindows(result); } // ── Private helpers ─────────────────────────────────────────────────── private static IReadOnlyList BuildWindowsFromPatterns( IEnumerable patterns, DateTime date) { var windows = new List(); foreach (var p in patterns) { var shiftStart = date.AddMinutes(p.ShiftStartTime); var shiftEnd = date.AddMinutes(p.ShiftEndTime); if (shiftEnd <= shiftStart) shiftEnd = shiftEnd.AddDays(1); // overnight if (p.BreakStartTime > 0 && p.BreakEndTime > p.BreakStartTime) { var breakStart = date.AddMinutes(p.BreakStartTime); var breakEnd = date.AddMinutes(p.BreakEndTime); if (shiftStart < breakStart) windows.Add(new TimeWindow(shiftStart, breakStart)); if (breakEnd < shiftEnd) windows.Add(new TimeWindow(breakEnd, shiftEnd)); } else { windows.Add(new TimeWindow(shiftStart, shiftEnd)); } } return windows; } private async Task> BuildDayWindowsAsync( int workCenterId, DateTime date, LoginDTO login, CancellationToken ct) { byte dayOfWeek = (byte)date.DayOfWeek; // 0=Sunday…6=Saturday var shiftMaps = await _ShiftMapDAL .GetShiftMapForDate(workCenterId, date, dayOfWeek, login, ct) .ConfigureAwait(false); var windows = new List(); foreach (var sm in shiftMaps) { var shiftStart = date.AddMinutes(sm.ShiftStartTime); var shiftEnd = date.AddMinutes(sm.ShiftEndTime); // Handle overnight shifts (end < start means crosses midnight) if (shiftEnd <= shiftStart) shiftEnd = shiftEnd.AddDays(1); if (sm.BreakStartTime > 0 && sm.BreakEndTime > sm.BreakStartTime) { // Split shift around the break window var breakStart = date.AddMinutes(sm.BreakStartTime); var breakEnd = date.AddMinutes(sm.BreakEndTime); if (shiftStart < breakStart) windows.Add(new TimeWindow(shiftStart, breakStart)); if (breakEnd < shiftEnd) windows.Add(new TimeWindow(breakEnd, shiftEnd)); } else { windows.Add(new TimeWindow(shiftStart, shiftEnd)); } } return windows; } private static TimeWindow? ClipToRange(TimeWindow window, DateTime from, DateTime to) { var start = window.TaskStart < from ? from : window.TaskStart; var end = window.TaskEnd > to ? to : window.TaskEnd; return start < end ? new TimeWindow(start, end) : null; } // Merges adjacent or overlapping TimeWindows so a single contiguous block is visible to SlotFinder. // Needed when consecutive shifts produce adjacent free segments after downtime subtraction. private static IReadOnlyList MergeAdjacentWindows(List windows) { if (windows.Count <= 1) return windows; var sorted = windows.OrderBy(w => w.TaskStart).ToList(); var merged = new List { sorted[0] }; for (int i = 1; i < sorted.Count; i++) { var last = merged[^1]; var current = sorted[i]; if (current.TaskStart <= last.TaskEnd) merged[^1] = new TimeWindow(last.TaskStart, current.TaskEnd > last.TaskEnd ? current.TaskEnd : last.TaskEnd); else merged.Add(current); } return merged; } // Subtracts all blocked windows from a base window, returning the remaining free segments. private static IEnumerable SubtractBlocked( TimeWindow baseWindow, IEnumerable blocked) { var segments = new List { baseWindow }; foreach (var block in blocked.Where(b => b.Overlaps(baseWindow))) { var next = new List(); foreach (var seg in segments) { if (!block.Overlaps(seg)) { next.Add(seg); continue; } if (seg.TaskStart < block.TaskStart) next.Add(new TimeWindow(seg.TaskStart, block.TaskStart)); if (block.TaskEnd < seg.TaskEnd) next.Add(new TimeWindow(block.TaskEnd, seg.TaskEnd)); } segments = next; } return segments; } } }