public sealed class SimulationRunState { private readonly object _gate = new(); private DateTimeOffset? _completedAt; public Guid RunId { get; init; } public string ScenarioId { get; init; } = null!; public string ScenarioName { get; init; } = null!; public double Speed { get; init; } public string? SessionId { get; init; } public string StartedByUserId { get; init; } = null!; public DateTimeOffset StartedAt { get; init; } public double TotalOffsetMinutes { get; init; } public string PatientDisplayName { get; init; } = null!; public SimulationRunStatus Status { get; private set; } = SimulationRunStatus.Pending; public Guid? PatientId { get; private set; } public Guid? EncounterId { get; private set; } public int ObservationsSent { get; private set; } public int MedicationsSent { get; private set; } public int OrdersPlaced { get; private set; } public double LastOffsetMinutes { get; private set; } public double ProgressPercent { get; private set; } public string? FailureReason { get; private set; } public double ElapsedRealSeconds { get { lock (_gate) { var end = _completedAt ?? DateTimeOffset.UtcNow; return (end - StartedAt).TotalSeconds; } } } public void MarkRunning() { lock (_gate) Status = SimulationRunStatus.Running; } public void SetIds(Guid? patientId, Guid? encounterId) { lock (_gate) { if (patientId.HasValue) PatientId = patientId; if (encounterId.HasValue) EncounterId = encounterId; } } public void UpdateOffset(double offsetMinutes) { lock (_gate) { LastOffsetMinutes = offsetMinutes; ProgressPercent = TotalOffsetMinutes <= 0 ? 100 : Math.Clamp(offsetMinutes / TotalOffsetMinutes * 100.0, 0, 100); } } public void ApplyResultCounters(int observationsSent, int medicationsSent, int ordersPlaced) { lock (_gate) { ObservationsSent = observationsSent; MedicationsSent = medicationsSent; OrdersPlaced = ordersPlaced; } } public void NoteEvent(string description) { lock (_gate) { if (description.StartsWith("MEDICATION", StringComparison.Ordinal)) MedicationsSent++; else if (description.StartsWith("ORDER ", StringComparison.Ordinal)) OrdersPlaced++; else if (!description.StartsWith("ORDER_RESULT", StringComparison.Ordinal) && !description.StartsWith("ACK ", StringComparison.Ordinal)) ObservationsSent++; } } public void MarkTerminal(SimulationRunStatus status, string? failureReason = null) { lock (_gate) { Status = status; FailureReason = failureReason; _completedAt = DateTimeOffset.UtcNow; if (status == SimulationRunStatus.Completed) ProgressPercent = 100; } } public SimulationRunState Snapshot() { lock (_gate) { var copy = new SimulationRunState { RunId = RunId, ScenarioId = ScenarioId, ScenarioName = ScenarioName, Speed = Speed, SessionId = SessionId, StartedByUserId = StartedByUserId, StartedAt = StartedAt, TotalOffsetMinutes = TotalOffsetMinutes, PatientDisplayName = PatientDisplayName, }; copy.Status = Status; copy.PatientId = PatientId; copy.EncounterId = EncounterId; copy.ObservationsSent = ObservationsSent; copy.MedicationsSent = MedicationsSent; copy.OrdersPlaced = OrdersPlaced; copy.LastOffsetMinutes = LastOffsetMinutes; copy.ProgressPercent = ProgressPercent; copy.FailureReason = FailureReason; copy._completedAt = _completedAt; return copy; } } }