public class ReplayEngine { private readonly VigilCareApiClient _client; private readonly ApiPoller? _poller; private DateTimeOffset _scenarioStartTime; public ReplayEngine(VigilCareApiClient client, ApiPoller? poller) { _client = client; _poller = poller; } public async Task RunAsync( ScenarioFile scenario, ReplayOptions options, CancellationToken ct = default) { var result = new ReplayResult(scenario.Scenario.Id); var startTime = DateTimeOffset.UtcNow; _scenarioStartTime = startTime; // --- Phase 1: Setup --- SimulatorConsole.Header(scenario.Scenario.Name, scenario.Scenario.Description); PatientResponse patient; EncounterResponse encounter; if (options.DryRun) { SimulatorConsole.DryRun("Would register patient: " + $"{scenario.Patient.FirstName} {scenario.Patient.LastName}"); SimulatorConsole.DryRun("Would open encounter: " + $"{scenario.Encounter.Department} / {scenario.Encounter.EncounterType}"); } else { patient = await _client.RegisterPatientAsync(new RegisterPatientRequest( scenario.Patient.FirstName, scenario.Patient.LastName, DateOnly.Parse(scenario.Patient.DateOfBirth), scenario.Patient.Gender)); encounter = await _client.OpenEncounterAsync(patient.Id, new OpenEncounterRequest( scenario.Encounter.EncounterType, scenario.Encounter.Department, scenario.Encounter.AttendingPhysician, scenario.Encounter.RoomBed, scenario.Encounter.AdmissionReason)); SimulatorConsole.Info($"Patient registered: {patient.Id} ({patient.Mrn})"); SimulatorConsole.Info($"Encounter opened: {encounter.Id} ({encounter.Status})"); result.PatientId = patient.Id; result.EncounterId = encounter.Id; } // --- Phase 2: Replay events --- double lastOffset = 0; var clusters = scenario.Events .GroupBy(e => e.OffsetMinutes) .OrderBy(g => g.Key) .ToList(); foreach (var cluster in clusters) { ct.ThrowIfCancellationRequested(); var deltaMinutes = cluster.Key - lastOffset; if (deltaMinutes > 0 && options.Speed > 0 && !options.DryRun) { var delayMs = (int)(deltaMinutes * 60_000 / options.Speed); SimulatorConsole.Wait(deltaMinutes, delayMs); await Task.Delay(delayMs, ct); } var simTimestamp = FormatSimTime(cluster.Key); var observationEvents = cluster.Where(e => e.Type == "observation").ToList(); var otherEvents = cluster.Where(e => e.Type != "observation").ToList(); if (observationEvents.Count > 0) await ReplayObservationCluster(observationEvents, simTimestamp, options, result); foreach (var evt in otherEvents) { switch (evt.Type) { case "order": await ReplayOrder( evt, simTimestamp, options, result, scenario.Encounter.AttendingPhysician); break; case "medication": await ReplayMedication(evt, simTimestamp, options, result); break; case "order_result": await ReplayOrderResult(evt, simTimestamp, options, result); break; } } lastOffset = cluster.Key; if (options.Poll && !options.DryRun && _poller is not null) { await Task.Delay(options.PollIntervalSeconds * 1000, ct); await _poller.PollAndDisplayAsync(result.EncounterId, simTimestamp); } } // --- Phase 3: Summary --- result.Duration = DateTimeOffset.UtcNow - startTime; SimulatorConsole.Summary(result); return result; } private async Task ReplayObservationCluster( List cluster, string simTime, ReplayOptions options, ReplayResult result) { var observations = new List(); var offsetMinutes = cluster[0].OffsetMinutes; var recordedAt = _scenarioStartTime.AddMinutes(offsetMinutes); foreach (var evt in cluster) { var code = evt.Data.GetProperty("code").GetString()!; var value = evt.Data.GetProperty("value").GetDecimal(); var unit = evt.Data.GetProperty("unit").GetString()!; var source = evt.Data.TryGetProperty("source", out var s) ? s.GetString()! : "Manual"; SimulatorConsole.Event(simTime, $"{code} {value} {unit}"); result.ObservationsSent++; if (!options.DryRun) { observations.Add(new IngestObservationRequest( code, value, unit, ToApiSource(source), recordedAt, null)); } } if (!options.DryRun && observations.Count > 0) await _client.SendObservationBatchAsync(result.EncounterId, observations); } private async Task ReplayMedication( ScenarioEvent evt, string simTime, ReplayOptions options, ReplayResult result) { var drugName = evt.Data.GetProperty("drugName").GetString()!; var dose = evt.Data.GetProperty("dose").GetDecimal(); var doseUnit = evt.Data.GetProperty("doseUnit").GetString()!; var route = evt.Data.GetProperty("route").GetString()!; var administeredBy = evt.Data.GetProperty("administeredBy").GetString()!; if (options.DryRun) { SimulatorConsole.DryRun($"[{simTime}] MEDICATION {drugName} {dose}{doseUnit} {route}"); return; } var sent = await _client.TrySendMedicationAsync(result.EncounterId, new CreateMedicationAdministrationRequest(drugName, dose, doseUnit, route, null, administeredBy)); if (sent) { SimulatorConsole.Event(simTime, $"MEDICATION {drugName} {dose}{doseUnit} ({route}) sent"); result.MedicationsSent++; } else { SimulatorConsole.Warn($"[{simTime}] MEDICATION skipped — endpoint not available (Phase 15 required)"); result.MedicationsSkipped++; } } private async Task ReplayOrder( ScenarioEvent evt, string simTime, ReplayOptions options, ReplayResult result, string defaultOrderedBy) { var description = evt.Data.GetProperty("description").GetString()!; var orderType = evt.Data.GetProperty("orderType").GetString()!; var orderedBy = evt.Data.TryGetProperty("orderedBy", out var ob) ? ob.GetString()! : defaultOrderedBy; if (options.DryRun) { SimulatorConsole.DryRun($"[{simTime}] ORDER {orderType}: {description}"); return; } var placed = await _client.TryCreateOrderAsync(result.EncounterId, new CreateOrderRequest(orderType, description, orderedBy)); if (placed) { SimulatorConsole.Event(simTime, $"ORDER {description} placed"); result.OrdersPlaced++; } else { SimulatorConsole.Warn($"[{simTime}] ORDER skipped — failed to place '{description}'"); } } private async Task ReplayOrderResult( ScenarioEvent evt, string simTime, ReplayOptions options, ReplayResult result) { var orderDesc = evt.Data.GetProperty("orderDescription").GetString()!; var resultSummary = evt.Data.TryGetProperty("resultSummary", out var rs) ? rs.GetString() : null; if (options.DryRun) { SimulatorConsole.DryRun($"[{simTime}] ORDER_RESULT {orderDesc}"); return; } var ok = await _client.TryResultOrderAsync( result.EncounterId, orderDesc, resultSummary); if (ok) { SimulatorConsole.Event(simTime, $"ORDER_RESULT {orderDesc} → resulted"); result.OrdersResulted++; } else { SimulatorConsole.Warn($"[{simTime}] ORDER_RESULT skipped — order '{orderDesc}' not found or already resulted"); } } private static string FormatSimTime(double offsetMinutes) { var hours = (int)(offsetMinutes / 60); var mins = (int)(offsetMinutes % 60); return $"{hours:D2}:{mins:D2}"; } private static string ToApiSource(string source) => source switch { "Device" or "DEVICE" or "device" or "monitor" => "DEVICE", "Manual" or "MANUAL" or "manual" => "MANUAL", "Lab" or "LAB" or "lab" => "LAB", _ => throw new InvalidOperationException($"Unknown observation source: '{source}'") }; }