using GB5Shared.DTO.Framework.Login; using MMDAL.DTO.Scheduling; namespace MMBLL.Scheduling.Engine { public class SlotFinder : ISlotFinder { // Look-ahead limit: if no slot found within this many calendar days, give up public const int MaxLookAheadDays = 365; private readonly ICalendarService _CalendarService; public SlotFinder(ICalendarService calendarService) { _CalendarService = calendarService; } public async Task<(IReadOnlyList Proposals, string RejectReason)> FindSlotAsync( ExecutionUnit eu, MachineCandidateDTO candidate, SchedulingContext context, LoginDTO login, CancellationToken ct) { // IsInterruptible: 0=Yes (interruptible) 1=No — IS* TINYINT convention return eu.IsInterruptible == 0 ? await FindInterruptibleSlotsAsync(eu, candidate, context, login, ct).ConfigureAwait(false) : await FindNonInterruptibleSlotAsync(eu, candidate, context, login, ct).ConfigureAwait(false); } // ── Mode 1: Non-interruptible ───────────────────────────────────────── private async Task<(IReadOnlyList Proposals, string RejectReason)> FindNonInterruptibleSlotAsync( ExecutionUnit eu, MachineCandidateDTO candidate, SchedulingContext context, LoginDTO login, CancellationToken ct) { var searchFrom = eu.ComputedEarliestStart; var searchTo = searchFrom.AddDays(MaxLookAheadDays); var windows = await _CalendarService.GetWorkingWindowsAsync( candidate.MachineId, candidate.WorkCenterId, searchFrom, searchTo, context, login, ct).ConfigureAwait(false); if (!windows.Any()) return (Array.Empty(), "NoShift"); var booked = context.GetBookedSlots(candidate.MachineId); foreach (var window in windows) { var freeSegments = GetFreeSegments(window, booked); foreach (var free in freeSegments) { if (free.DurationMinutes >= eu.DurationMinutes) { var slotEnd = free.TaskStart.AddMinutes(eu.DurationMinutes); return (new[] { new SlotProposal( MachineId: candidate.MachineId, PatternId: candidate.PatternId, Start: free.TaskStart, End: slotEnd, ParallelNumber: 1, PatternAlreadyMounted: candidate.PatternAlreadyMounted) }, string.Empty); } } } return (Array.Empty(), "FullyBooked"); } // ── Mode 2: Interruptible ───────────────────────────────────────────── private async Task<(IReadOnlyList Proposals, string RejectReason)> FindInterruptibleSlotsAsync( ExecutionUnit eu, MachineCandidateDTO candidate, SchedulingContext context, LoginDTO login, CancellationToken ct) { var searchFrom = eu.ComputedEarliestStart; var searchTo = searchFrom.AddDays(MaxLookAheadDays); var windows = await _CalendarService.GetWorkingWindowsAsync( candidate.MachineId, candidate.WorkCenterId, searchFrom, searchTo, context, login, ct).ConfigureAwait(false); if (!windows.Any()) return (Array.Empty(), "NoShift"); var booked = context.GetBookedSlots(candidate.MachineId); var segments = new List(); int remaining = eu.DurationMinutes; foreach (var window in windows) { if (remaining <= 0) break; var freeSegments = GetFreeSegments(window, booked); foreach (var free in freeSegments) { if (remaining <= 0) break; if (free.DurationMinutes < eu.MinimumRunMinutes) continue; int take = Math.Min(remaining, free.DurationMinutes); segments.Add(new SlotProposal( MachineId: candidate.MachineId, PatternId: candidate.PatternId, Start: free.TaskStart, End: free.TaskStart.AddMinutes(take), ParallelNumber: 1, PatternAlreadyMounted: candidate.PatternAlreadyMounted)); remaining -= take; } } return remaining <= 0 ? (segments, string.Empty) : (Array.Empty(), "FullyBooked"); } // ── Mode 3: Parallel (SPLITS > 1) ──────────────────────────────────── public async Task> FindParallelSlotsAsync( ExecutionUnit eu, IReadOnlyList candidates, SchedulingContext context, LoginDTO login, CancellationToken ct) { int n = candidates.Count; var searchFrom = eu.ComputedEarliestStart; var searchTo = searchFrom.AddDays(MaxLookAheadDays); int minutesEach = (int)Math.Ceiling((double)eu.DurationMinutes / n); // Find the first time window where ALL n machines are simultaneously free // Load windows for each candidate in parallel var windowTasks = candidates.Select(c => _CalendarService.GetWorkingWindowsAsync( c.MachineId, c.WorkCenterId, searchFrom, searchTo, context, login, ct)).ToArray(); var allWindows = await Task.WhenAll(windowTasks).ConfigureAwait(false); // Intersect free windows across all machines to find simultaneous availability // Start with candidate 0's free windows, intersect with each subsequent candidate var intersected = GetFreeWindowList(allWindows[0], context.GetBookedSlots(candidates[0].MachineId)); for (int i = 1; i < n; i++) { var free_i = GetFreeWindowList(allWindows[i], context.GetBookedSlots(candidates[i].MachineId)); intersected = IntersectWindowLists(intersected, free_i); } // Find first intersected window large enough for minutesEach foreach (var window in intersected) { if (window.DurationMinutes >= minutesEach) { var proposals = new List(); for (int i = 0; i < n; i++) { proposals.Add(new SlotProposal( MachineId: candidates[i].MachineId, PatternId: candidates[i].PatternId, Start: window.TaskStart, End: window.TaskStart.AddMinutes(minutesEach), ParallelNumber: i + 1, PatternAlreadyMounted: candidates[i].PatternAlreadyMounted)); } return proposals; } } return Array.Empty(); } // ── Helpers ─────────────────────────────────────────────────────────── // booked is guaranteed sorted by Start (SchedulingContext.BookSlot maintains order). // Single forward pass: no LINQ sort, no allocation beyond the yielded windows. private static IEnumerable GetFreeSegments( TimeWindow window, IReadOnlyList booked) { if (booked.Count == 0) { yield return window; yield break; } var cursor = window.TaskStart; for (int j = 0; j < booked.Count; j++) { var s = booked[j]; if (s.Start >= window.TaskEnd) break; // sorted: nothing after this can overlap if (s.End <= window.TaskStart) continue; // before this window var occStart = s.Start < window.TaskStart ? window.TaskStart : s.Start; var occEnd = s.End > window.TaskEnd ? window.TaskEnd : s.End; if (cursor < occStart) yield return new TimeWindow(cursor, occStart); if (occEnd > cursor) cursor = occEnd; } if (cursor < window.TaskEnd) yield return new TimeWindow(cursor, window.TaskEnd); } private static List GetFreeWindowList( IReadOnlyList shiftWindows, IReadOnlyList booked) { var result = new List(); foreach (var w in shiftWindows) result.AddRange(GetFreeSegments(w, booked)); return result; } private static List IntersectWindowLists( List a, List b) { var result = new List(); foreach (var wa in a) { foreach (var wb in b) { var start = wa.TaskStart > wb.TaskStart ? wa.TaskStart : wb.TaskStart; var end = wa.TaskEnd < wb.TaskEnd ? wa.TaskEnd : wb.TaskEnd; if (start < end) result.Add(new TimeWindow(start, end)); } } return result; } } }