using MMDAL.DTO.Scheduling; namespace MMBLL.Scheduling.Engine { // Run-scoped in-memory state — created fresh per SchedulingEngine.RunAsync call, // discarded when the run completes. Never cached in the Scoped service field. public class SchedulingContext { // MachineId → list of committed slots (includes locked pre-existing slots on reschedule) public Dictionary> BookedSlots { get; } = new(); // RootBatchNumber → the committed slot of the last segment of that batch // Used by BatchContinuity=CarryForward to locate the successor's earliest start public Dictionary BatchChainMap { get; } = new(); // Working calendar cache: (WorkCenterId, Date) → windows for that date // Key is WorkCenterId (not MachineId) because BuildDayWindowsAsync only varies by work center. // Populated on-demand by CalendarService; avoids repeated DB hits per work-center-date pair. public Dictionary<(int WorkCenterId, DateOnly Date), IReadOnlyList> CalendarCache { get; } = new(); // Accumulated output — written to DB after all EUs are processed public List CommittedPlan { get; } = new(); public List BatchChainRows { get; } = new(); public List MountTasks { get; } = new(); public List DecisionLog { get; } = new(); // Per-machine: ID of the pattern that was last mounted (null if none tracked) public Dictionary LastMountedPatternId { get; } = new(); // Per-pattern: MachineId currently holding the physical mould. // Prevents the same physical mould being planned on two machines simultaneously. public Dictionary PatternCurrentMachine { get; } = new(); public void ClaimPattern(int patternId, int machineId) => PatternCurrentMachine[patternId] = machineId; public bool IsPatternOnAnotherMachine(int patternId, int machineId) => PatternCurrentMachine.TryGetValue(patternId, out var owner) && owner.HasValue && owner.Value != machineId; // MachineId → blocked windows (downtime + breakdowns) for the full scheduling run. // Populated on first GetWorkingWindowsAsync call per machine; cached for all subsequent EU/candidates. public Dictionary> BlockedWindowsCache { get; } = new(); // Trace — populated only when TraceEnabled (set from SchedulingRunTriggerDTO.TraceEnabled) public bool TraceEnabled { get; set; } public List TraceLog { get; } = new(); public void AddTrace(int? euId, string stepCode, byte phase, string? dataJson, int elapsedMs = 0) { if (!TraceEnabled) return; TraceLog.Add(new SchedulingTraceEntryDTO { SeqNo = TraceLog.Count + 1, EuId = euId, StepCode = stepCode, Phase = phase, Data = dataJson, ElapsedMs = elapsedMs }); } public void BookSlot(int machineId, SlotProposal slot) { if (!BookedSlots.TryGetValue(machineId, out var list)) { list = new List(); BookedSlots[machineId] = list; } // Maintain sorted order so GetFreeSegments can do a single forward pass int idx = list.BinarySearch(slot, SlotStartComparer.Instance); list.Insert(idx < 0 ? ~idx : idx, slot); } public IReadOnlyList GetBookedSlots(int machineId) => BookedSlots.TryGetValue(machineId, out var list) ? list : Array.Empty(); internal sealed class SlotStartComparer : IComparer { public static readonly SlotStartComparer Instance = new(); public int Compare(SlotProposal? x, SlotProposal? y) => x!.Start.CompareTo(y!.Start); } } }