using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.HybridCache; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using Microsoft.Extensions.Logging; using MMBLL.Scheduling.Engine; using MMDAL.CustomCode.Scheduling; using MMDAL.DTO.Scheduling; using System.Text.Json; using static GB5Shared.GB5Constant.Constant; namespace MMBLL.Scheduling { public class SchedulingRunBLL : ISchedulingRunBLL { private readonly ISchedulingRunDAL _SchedulingRunDAL; private readonly ISchedulingEngine _SchedulingEngine; private readonly AutoNumber _AutoNumber; private readonly IQueryExecutor _QueryExecutor; private readonly KeyInvalidate _KeyInvalidate; private readonly ILogger _Logger; public SchedulingRunBLL( ISchedulingRunDAL schedulingRunDAL, ISchedulingEngine schedulingEngine, AutoNumber autoNumber, IQueryExecutor queryExecutor, KeyInvalidate keyInvalidate, ILogger logger) { _SchedulingRunDAL = schedulingRunDAL; _SchedulingEngine = schedulingEngine; _AutoNumber = autoNumber; _QueryExecutor = queryExecutor; _KeyInvalidate = keyInvalidate; _Logger = logger; } // ── ProcessSchedulingRunAsync ───────────────────────────────────────── public async Task ProcessSchedulingRunAsync( SchedulingRunTriggerDTO trigger, int schedulingRunId, LoginDTO loginDTO, IProgress progress, CancellationToken ct) { var runLog = new SchedulingRunLogDTO { SchedulingRunId = schedulingRunId, TenantId = trigger.TenantId, TriggeredByUserId = trigger.UserId, HorizonFromDate = trigger.HorizonFrom, HorizonToDate = trigger.HorizonTo, Status = 1, // Running StartedAt = DateTime.UtcNow }; var Trans = await _QueryExecutor.BeginTransactionAsync(loginDTO); try { await _SchedulingRunDAL.SaveSchedulingRunLogAsync(runLog, loginDTO, Trans); await _QueryExecutor.CommitAsync(Trans); } catch (Exception) { await _QueryExecutor.RollbackAsync(Trans); throw; } SchedulingEngineResult engineResult; try { engineResult = await _SchedulingEngine .RunAsync(trigger, schedulingRunId, loginDTO, progress, ct) .ConfigureAwait(false); } catch (Exception ex) { _Logger.LogError(ex, "Scheduling engine failed for run {RunId}", schedulingRunId); await _SchedulingRunDAL.UpdateSchedulingRunStatusAsync(schedulingRunId, 3, loginDTO); throw; } // Bulk insert the output — chunked at 5k rows inside DAL if (engineResult.CommittedPlan.Count > 0) { // Assign ResourcePlanIds from auto-number batch var planAuto = await _AutoNumber.GetNumberAsync( engineResult.CommittedPlan.Count, AUTONUMBERCONSTANT.RESOURCEPLAN, loginDTO); int planId = planAuto.StartNumber; foreach (var row in engineResult.CommittedPlan) { row.ResourcePlanId = planId++; row.ResourcePlanCombineId = planAuto.StartNumber; } await _SchedulingRunDAL .BulkInsertResourcePlanAsync(engineResult.CommittedPlan, loginDTO, ct) .ConfigureAwait(false); } if (engineResult.BatchChainRows.Count > 0) { var chainAuto = await _AutoNumber.GetNumberAsync( engineResult.BatchChainRows.Count, AUTONUMBERCONSTANT.BATCHCHAIN, loginDTO); int chainId = chainAuto.StartNumber; foreach (var row in engineResult.BatchChainRows) row.BatchChainId = chainId++; await _SchedulingRunDAL .BulkInsertBatchChainAsync(engineResult.BatchChainRows, loginDTO, ct) .ConfigureAwait(false); } // ── Phase 6f: Mount task persistence ───────────────────────────── // TASKID is not an IDENTITY column — pre-generate IDs via AutoNumber before inserting. if (engineResult.MountTasks.Count > 0) { var mountAuto = await _AutoNumber .GetNumberAsync(engineResult.MountTasks.Count, AUTONUMBERCONSTANT.TASK, loginDTO) .ConfigureAwait(false); int taskId = mountAuto.StartNumber; foreach (var mt in engineResult.MountTasks) { mt.TaskId = taskId++; // Resolve ResourcePlanId from the CommittedPlan index recorded at engine time if (mt.CommittedPlanStartIndex < engineResult.CommittedPlan.Count) mt.ResourcePlanId = engineResult.CommittedPlan[mt.CommittedPlanStartIndex].ResourcePlanId; await _SchedulingRunDAL .SaveMountTaskAsync(mt, loginDTO) .ConfigureAwait(false); if (mt.ResourcePlanId != 0) await _SchedulingRunDAL .UpdateResourcePlanMountIdAsync(mt.ResourcePlanId, mt.TaskId, loginDTO) .ConfigureAwait(false); } } // ── Phase 8: Nesting plan schedule update ───────────────────────── // ResourceLevel=2 rows: IndentDetailId holds the NestingPlanId (set as eu.SourceDetailId in engine) foreach (var row in engineResult.CommittedPlan.Where(r => r.ResourceLevel == 2 && r.IndentDetailId > 0)) { await _SchedulingRunDAL.UpdateNestingPlanScheduleAsync( row.IndentDetailId, row.ScheduledStartDate, row.ScheduledEndDate, row.ResourcePlanCombineId, loginDTO).ConfigureAwait(false); } // ── Phase 9: Monitor group sequence assignment ───────────────────── // ResourceLevel=0 rows (monitor-only) with Particulars=MonitorGroup name var monitorGroups = engineResult.CommittedPlan .Where(r => r.ResourceLevel == 0 && !string.IsNullOrEmpty(r.Particulars)) .GroupBy(r => r.Particulars!); foreach (var grp in monitorGroups) { short seq = 1; foreach (var row in grp) { if (row.IndentDetailId > 0) await _SchedulingRunDAL.UpdateIndentDetailMonitorGroupAsync( row.IndentDetailId, grp.Key, seq++, loginDTO).ConfigureAwait(false); } } // ── Phase 10: Mark indent details as scheduled ──────────────────── var scheduledDetailIds = engineResult.CommittedPlan .Where(r => r.ResourceLevel == 1 && r.IndentDetailId > 0) .Select(r => r.IndentDetailId) .Distinct(); foreach (var detailId in scheduledDetailIds) await _SchedulingRunDAL .UpdateIndentDetailScheduleStatusAsync(detailId, 1, loginDTO) .ConfigureAwait(false); // ── Decision log persistence ────────────────────────────────────── if (engineResult.DecisionLog.Count > 0) await _SchedulingRunDAL .BulkInsertDecisionLogAsync(engineResult.DecisionLog, loginDTO, ct) .ConfigureAwait(false); // ── Trace persistence (only when TraceEnabled) ──────────────────── if (trigger.TraceEnabled && engineResult.TraceLog.Count > 0) { foreach (var entry in engineResult.TraceLog) { entry.SchedulingRunId = schedulingRunId; entry.TenantId = trigger.TenantId; } await _SchedulingRunDAL .BulkInsertTraceAsync(engineResult.TraceLog, loginDTO, ct) .ConfigureAwait(false); } // Update run log to Completed decimal avgUtil = engineResult.RCCPReport.Any() ? engineResult.RCCPReport.Average(r => r.OverloadPct) : 0m; runLog.Status = 2; // Completed runLog.MachinesScheduled = engineResult.CommittedPlan .Select(r => r.MachineId).Distinct().Count(); runLog.UtilizationPct = Math.Round(avgUtil, 2); runLog.DurationMs = engineResult.DurationMs; runLog.RccpJson = JsonSerializer.Serialize(engineResult.RCCPReport); runLog.CompletedAt = DateTime.UtcNow; await _SchedulingRunDAL .UpdateSchedulingRunCompleteAsync(runLog, loginDTO) .ConfigureAwait(false); return new SchedulingRunSummaryDTO { SchedulingRunId = schedulingRunId, EUsScheduled = engineResult.EUsScheduled, EUsFailed = engineResult.EUsFailed, UtilizationByWorkCenter = engineResult.RCCPReport, DurationMs = engineResult.DurationMs, RCCPWarnings = engineResult.RCCPReport .Where(r => r.IsOverloaded) .Select(r => $"WorkCenter {r.WorkCenterId} at {r.OverloadPct:F1}%") .ToList() }; } // ── Reschedule ──────────────────────────────────────────────────────── public async Task RescheduleAsync( SchedulingRunTriggerDTO trigger, LoginDTO loginDTO, CancellationToken ct) { if (trigger.HorizonFrom.Date < DateTime.UtcNow.Date) throw new ArgumentException("HorizonFrom cannot be in the past — cannot reschedule history."); // Reset SCHEDULESTATUS=0 for indent details whose plans will be deleted await _SchedulingRunDAL .ResetIndentDetailScheduleStatusForHorizonAsync(trigger.HorizonFrom, loginDTO, ct) .ConfigureAwait(false); await _SchedulingRunDAL .DeleteFutureUnlockedResourcePlanAsync(trigger.HorizonFrom, loginDTO, ct) .ConfigureAwait(false); var auto = await _AutoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.SCHEDULINGRUN, loginDTO); int runId = auto.StartNumber; var progress = new Progress(_ => { }); // no SignalR from BLL reschedule path await ProcessSchedulingRunAsync(trigger, runId, loginDTO, progress, ct).ConfigureAwait(false); return SuccessResponse.SaveSuccess; } // ── Read methods ────────────────────────────────────────────────────── public async Task GetSchedulingRunLog( int runId, LoginDTO loginDTO, CancellationToken ct = default) { try { return await _SchedulingRunDAL.GetSchedulingRunLogAsync(runId, loginDTO, ct).ConfigureAwait(false); } catch (Exception) { throw; } } public async Task> GetSchedulingRunLogList( LoginDTO loginDTO, CancellationToken ct = default) { try { return await _SchedulingRunDAL.GetSchedulingRunLogListAsync(loginDTO, ct).ConfigureAwait(false); } catch (Exception) { throw; } } public async Task> GetResourcePlanByIndent( int indentDetailId, LoginDTO loginDTO, CancellationToken ct = default) { try { return await _SchedulingRunDAL .GetResourcePlanByIndentAsync(indentDetailId, loginDTO, ct) .ConfigureAwait(false); } catch (Exception) { throw; } } public async Task> GetResourcePlanByMachine( int machineId, DateTime dateFrom, DateTime dateTo, LoginDTO loginDTO, CancellationToken ct = default) { try { return await _SchedulingRunDAL .GetResourcePlanByMachineAsync(machineId, dateFrom, dateTo, loginDTO, ct) .ConfigureAwait(false); } catch (Exception) { throw; } } public async Task LockResourcePlan(int resourcePlanId, LoginDTO loginDTO) { try { await _SchedulingRunDAL .UpdateIsLockedAsync(resourcePlanId, 1, loginDTO) .ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } catch (Exception) { throw; } } public async Task GetESERunTraceAsync( int runId, LoginDTO loginDTO, CancellationToken ct = default) { try { var headerTask = _SchedulingRunDAL.GetSchedulingRunLogAsync(runId, loginDTO, ct); var traceTask = _SchedulingRunDAL.GetTraceForRunAsync(runId, loginDTO, ct); await Task.WhenAll(headerTask, traceTask).ConfigureAwait(false); var result = new { Header = headerTask.Result, Trace = traceTask.Result }; return JsonSerializer.Serialize(result); } catch (Exception ex) { _Logger.LogError(ex, "GetESERunTraceAsync failed for RunId {RunId}", runId); throw; } } public async Task GetSchedulingRunDetailLogAsync( int runId, LoginDTO loginDTO, CancellationToken ct = default) { try { var rows = await _SchedulingRunDAL .GetDecisionLogForRunAsync(runId, loginDTO, ct) .ConfigureAwait(false); return JsonSerializer.Serialize(rows); } catch (Exception ex) { _Logger.LogError(ex, "GetSchedulingRunDetailLogAsync failed for RunId {RunId}", runId); throw; } } // ── Plan Lifecycle ──────────────────────────────────────────────────── public async Task FirmPlanAsync( FirmPlanDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { try { GB5Trace.Step("firm-plan", new { dto.SchedulingRunId, dto.WorkCenterId }); if (dto.ResourcePlanIds is { Count: > 0 }) await _SchedulingRunDAL .FirmPlanByIdsAsync(dto.ResourcePlanIds, loginDTO, ct) .ConfigureAwait(false); else if (dto.SchedulingRunId.HasValue) await _SchedulingRunDAL .FirmPlanByRunAsync(dto.SchedulingRunId.Value, loginDTO, ct) .ConfigureAwait(false); else if (dto.WorkCenterId.HasValue) await _SchedulingRunDAL .FirmPlanByWorkCenterAsync(dto.WorkCenterId.Value, dto.HorizonFrom, dto.HorizonTo, loginDTO, ct) .ConfigureAwait(false); else throw new ArgumentException("FirmPlan requires ResourcePlanIds, SchedulingRunId, or WorkCenterId."); return SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("firm-plan-failed", ex); _Logger.LogError(ex, "FirmPlanAsync failed"); throw; } } public async Task ReleasePlanAsync( IEnumerable resourcePlanIds, LoginDTO loginDTO, CancellationToken ct = default) { try { var ids = resourcePlanIds.ToList(); GB5Trace.Step("release-plan", new { Count = ids.Count }); await _SchedulingRunDAL.ReleasePlanAsync(ids, loginDTO, ct).ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("release-plan-failed", ex); _Logger.LogError(ex, "ReleasePlanAsync failed"); throw; } } public async Task StartWorkOrderAsync( int resourcePlanId, DateTime actualStart, LoginDTO loginDTO, CancellationToken ct = default) { try { GB5Trace.Step("start-workorder", new { resourcePlanId }); await _SchedulingRunDAL .StartWorkOrderAsync(resourcePlanId, actualStart, loginDTO, ct) .ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("start-workorder-failed", ex); _Logger.LogError(ex, "StartWorkOrderAsync failed for ResourcePlanId {ResourcePlanId}", resourcePlanId); throw; } } public async Task StopWorkOrderAsync( StopWorkOrderDTO dto, LoginDTO loginDTO, CancellationToken ct = default) { try { GB5Trace.Step("stop-workorder", new { dto.ResourcePlanId, dto.ReleaseResource }); // 1. Update TRESOURCEPLAN — PLANSTATUS=4, optionally ISLOCKED=0 await _SchedulingRunDAL.StopWorkOrderAsync(dto, loginDTO, ct).ConfigureAwait(false); // 2. If reset → TINDENTDETAIL.SCHEDULESTATUS=0 (re-enters planning queue) if (dto.ResetToUnscheduled) { var planDetail = await _SchedulingRunDAL .GetPlanMachineAndDetailAsync(dto.ResourcePlanId, loginDTO, ct) .ConfigureAwait(false); if (planDetail?.IndentDetailId > 0) await _SchedulingRunDAL .UpdateIndentDetailScheduleStatusAsync(planDetail.IndentDetailId, 0, loginDTO) .ConfigureAwait(false); } return SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("stop-workorder-failed", ex); _Logger.LogError(ex, "StopWorkOrderAsync failed for ResourcePlanId {ResourcePlanId}", dto.ResourcePlanId); throw; } } } }