using GB5Shared.DTO.Framework.Login; using MMDAL.CustomCode.Scheduling; using MMDAL.DTO.Scheduling; namespace MMBLL.Scheduling.Engine { public class CandidateGenerator : ICandidateGenerator { private readonly ICandidateDAL _CandidateDAL; public CandidateGenerator(ICandidateDAL candidateDAL) { _CandidateDAL = candidateDAL; } public async Task> GetCandidatesAsync( ExecutionUnit eu, SchedulingContext context, LoginDTO login, CancellationToken ct) { // If the EU is pinned to a specific machine, return just that machine //if (eu.FixedMachineId.HasValue) if (eu.FixedMachineId.HasValue && eu.FixedMachineId.Value != -1) { var fixed_ = await BuildCandidateForMachine(eu.FixedMachineId.Value, eu, context, login, ct).ConfigureAwait(false); return fixed_ is null ? Array.Empty() : new[] { fixed_ }; } var machines = await _CandidateDAL .GetMachinesForWorkCenterAsync(eu.WorkCenterId, login, ct) .ConfigureAwait(false); var candidates = new List(); foreach (var machine in machines) { var candidate = await BuildCandidateForMachine(machine.MachineId, eu, context, login, ct).ConfigureAwait(false); if (candidate is not null) candidates.Add(candidate); } // Sort: machines with the already-mounted pattern first (lower changeover cost), // then by utilization ascending (prefer less-busy machines among equals) return candidates .OrderBy(c => c.PatternAlreadyMounted ? 0 : 1) .ThenBy(c => c.UtilizationRatio) .ToList(); } private async Task BuildCandidateForMachine( int machineId, ExecutionUnit eu, SchedulingContext context, LoginDTO login, CancellationToken ct) { // Lazy-load prior-run physical mount state on first encounter. // context.LastMountedPatternId is set by: // (a) this lazy load — prior-run state from TRESOURCEPLAN, loaded once per machine per run // (b) changeover block in SchedulingEngine — updated when machine mounts a new pattern this run // Current-run plans live in CommittedPlan only (not yet in TRESOURCEPLAN), so the DB read // always returns prior-run state; current-run conflicts use ClaimPattern/PatternCurrentMachine. int? lastMounted; if (context.LastMountedPatternId.TryGetValue(machineId, out var lm)) { lastMounted = lm; } else { lastMounted = await _CandidateDAL .GetLastMountedPatternAsync(machineId, login, ct) .ConfigureAwait(false); context.LastMountedPatternId[machineId] = lastMounted; // cache — DB only hit once per machine per run } // Register the machine's currently-mounted pattern as an initial ownership claim. // TryAdd prevents overwriting a pattern already committed to another machine this run. if (lastMounted.HasValue && !context.PatternCurrentMachine.ContainsKey(lastMounted.Value)) context.PatternCurrentMachine.TryAdd(lastMounted.Value, machineId); int? patternId = -1; int balanceCount = int.MaxValue; bool alreadyMounted = false; if (eu.FixedPatternId.HasValue) { // Pattern explicitly specified on the EU patternId = eu.FixedPatternId.Value; alreadyMounted = patternId == lastMounted; } else if (eu.IsPatternRequired == 0) { // Process requires a pattern/mould — filter by item+SKU to find applicable moulds var patterns = await _CandidateDAL .GetPatternCompatibilityAsync(machineId, eu.ProcessId, eu.ItemId, eu.SkuId, login, ct) .ConfigureAwait(false); // Exclude patterns already committed to another machine in this run var compatible = patterns .Where(p => p.PatternId.HasValue && !context.IsPatternOnAnotherMachine(p.PatternId.Value, machineId)) .ToList(); if (!compatible.Any()) return null; // required but none available for this item+machine → reject // Prefer already-mounted pattern; otherwise pick highest balance count var mounted = compatible.FirstOrDefault(p => p.PatternId == lastMounted); if (mounted is not null) { patternId = mounted.PatternId; balanceCount = mounted.PatternBalanceCount; alreadyMounted = true; } else { var best = compatible.OrderByDescending(p => p.PatternBalanceCount).First(); patternId = best.PatternId; balanceCount = best.PatternBalanceCount; } } // Utilization ratio: booked minutes vs a nominal 8-hour reference window var booked = context.GetBookedSlots(machineId); int bookedMinutes = booked.Sum(s => s.DurationMinutes); double utilization = bookedMinutes / 480.0; // normalize against 8h reference return new MachineCandidateDTO { MachineId = machineId, WorkCenterId = eu.WorkCenterId, PatternId = patternId, PatternBalanceCount = balanceCount, PatternAlreadyMounted = alreadyMounted, UtilizationRatio = utilization, TenantId = login.ClientId }; } } }