using System.Globalization; using System.Text.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Prometheus; using StackExchange.Redis; public class SofaDetector { private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true }; private readonly IConnectionMultiplexer _redis; private readonly IServiceProvider _services; private readonly SofaLabCache _labCache; private readonly SofaVasopressorResolver _vasopressors; private readonly SofaOptions _options; private readonly ClinicalMetrics _metrics; private readonly ILogger _logger; public SofaDetector( IConnectionMultiplexer redis, IServiceProvider services, SofaLabCache labCache, SofaVasopressorResolver vasopressors, IOptions options, ClinicalMetrics metrics, ILogger logger) { _redis = redis; _services = services; _labCache = labCache; _vasopressors = vasopressors; _options = options.Value; _metrics = metrics; _logger = logger; } public async Task ProcessObservationAsync( Guid encounterId, Guid patientId, string observationCode, decimal value, DateTimeOffset recordedAt, CancellationToken ct = default) { if (!SofaCalculator.TriggersRescore(observationCode)) return SofaScoringResult.NotSofaTrigger; if (SofaCalculator.IsSofaCode(observationCode)) { await _labCache.StoreAsync(encounterId, observationCode, value, recordedAt); } return await TryComputeScoreAsync(encounterId, patientId, ct); } public Task ProcessGcsScoredAsync( Guid encounterId, Guid patientId, CancellationToken ct = default) => TryComputeScoreAsync(encounterId, patientId, ct); private async Task TryComputeScoreAsync( Guid encounterId, Guid patientId, CancellationToken ct) { using var timer = _metrics.SofaScoringDuration.NewTimer(); using (var scope = _services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); if (!await db.Encounters.AnyAsync(e => e.Id == encounterId, ct)) { _logger.LogWarning( "Skipping SOFA score for unknown encounter {EncounterId}", encounterId); return SofaScoringResult.EncounterNotFound; } } var cached = await _labCache.GetAllAsync(encounterId); var staleComponents = new List(); var missingComponents = new List(); decimal? GetValue(string code) { if (!cached.TryGetValue(code, out var entry)) { missingComponents.Add(code); return null; } var status = _labCache.Classify(entry); if (status == SofaValueStatus.Expired) { missingComponents.Add(code); return null; } if (status == SofaValueStatus.Stale) staleComponents.Add(code); return entry.Value; } var pao2 = GetValue("PAO2_MMHG"); var fio2 = GetValue("FIO2_PCT"); var spo2 = GetValue("SPO2"); var supplementalO2 = GetValue("SUPPLEMENTAL_O2"); var onMechanicalVent = supplementalO2 is >= 1m; var usedSpO2Fallback = false; int respiratory; if (pao2 is not null && fio2 is not null) { respiratory = SofaCalculator.ScoreRespiratory(pao2, fio2, onMechanicalVent); } else if (_options.UseSpO2FiO2Fallback && spo2 is not null && fio2 is not null) { usedSpO2Fallback = true; respiratory = SofaCalculator.ScoreRespiratoryFromSpo2(spo2, fio2, onMechanicalVent); } else { respiratory = 0; if (pao2 is null) missingComponents.Add("PAO2_MMHG"); if (fio2 is null) missingComponents.Add("FIO2_PCT"); } var coagulation = SofaCalculator.ScoreCoagulation(GetValue("PLATELET_K_UL")); var liver = SofaCalculator.ScoreLiver(GetValue("BILIRUBIN_MG_DL")); decimal? map = null; var sbp = GetValue("SYSTOLIC_BP"); var dbp = GetValue("DIASTOLIC_BP"); if (sbp is not null && dbp is not null) map = MapCalculator.Calculate(sbp.Value, dbp.Value); var vasopressor = await _vasopressors.GetActiveVasopressorAsync(encounterId, ct); var cardiovascular = SofaCalculator.ScoreCardiovascular(map, vasopressor); var gcsTotal = await LoadGcsTotalAsync(encounterId); var cns = SofaCalculator.ScoreCns(gcsTotal); var creatinine = GetValue("CREATININE_MG_DL"); var urineMlH = GetValue("URINE_OUTPUT_ML_H"); decimal? urineMlDay = urineMlH is not null ? urineMlH * 24m : null; var renal = SofaCalculator.ScoreRenal(creatinine, urineMlDay); var result = SofaCalculator.ComputeTotal( respiratory, coagulation, liver, cardiovascular, cns, renal); var stalenessFlags = JsonSerializer.Serialize(new SofaStalenessFlags( staleComponents.Distinct().ToList(), missingComponents.Distinct().ToList(), usedSpO2Fallback), JsonOptions); var calculatedAt = DateTimeOffset.UtcNow; var (isBaseline, delta) = await ResolveBaselineAndDeltaAsync( encounterId, patientId, result, calculatedAt, SofaCalculator.CountPopulatedOrganSystems( hasRespiratory: (pao2 is not null && fio2 is not null) || (_options.UseSpO2FiO2Fallback && spo2 is not null && fio2 is not null), hasCoagulation: cached.ContainsKey("PLATELET_K_UL") && _labCache.Classify(cached["PLATELET_K_UL"]) != SofaValueStatus.Expired, hasLiver: cached.ContainsKey("BILIRUBIN_MG_DL") && _labCache.Classify(cached["BILIRUBIN_MG_DL"]) != SofaValueStatus.Expired, hasCardiovascular: (sbp is not null && dbp is not null) || vasopressor is not null, hasCns: gcsTotal is not null, hasRenal: (cached.ContainsKey("CREATININE_MG_DL") && _labCache.Classify(cached["CREATININE_MG_DL"]) != SofaValueStatus.Expired) || (cached.ContainsKey("URINE_OUTPUT_ML_H") && _labCache.Classify(cached["URINE_OUTPUT_ML_H"]) != SofaValueStatus.Expired)), ct); await PersistScoreAsync( encounterId, patientId, result, isBaseline, delta, stalenessFlags, calculatedAt, ct); var alertCreated = false; if (delta is >= 2) { alertCreated = await TryCreateAlertAsync( encounterId, patientId, AlertType.SofaSepsis, AlertSeverity.Critical, result, isBaseline ? null : delta, stalenessFlags, ct); } else if (delta == 1) { alertCreated = await TryCreateAlertAsync( encounterId, patientId, AlertType.SofaWarning, AlertSeverity.Warning, result, delta, stalenessFlags, ct); } _metrics.SofaScoresTotal .WithLabels(alertCreated ? "true" : "false") .Inc(); _logger.LogInformation( "SOFA score {Total} for encounter {Id} — baseline={Baseline} delta={Delta}", result.Total, encounterId, isBaseline, delta); return new SofaScoringResult( SofaOutcome.ScoreComputed, result, isBaseline, delta, alertCreated); } private async Task LoadGcsTotalAsync(Guid encounterId) { var cache = _redis.GetDatabase(); var gcsValues = await cache.StringGetAsync(GcsCalculator.AllComponentKeys(encounterId)); if (!gcsValues.All(v => v.HasValue)) return null; var eye = decimal.Parse(gcsValues[0]!, CultureInfo.InvariantCulture); var verbal = decimal.Parse(gcsValues[1]!, CultureInfo.InvariantCulture); var motor = decimal.Parse(gcsValues[2]!, CultureInfo.InvariantCulture); return GcsCalculator.ComputeTotal(eye, verbal, motor); } private async Task<(bool IsBaseline, int? Delta)> ResolveBaselineAndDeltaAsync( Guid encounterId, Guid patientId, SofaResult result, DateTimeOffset calculatedAt, int populatedOrganSystems, CancellationToken ct) { using var scope = _services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var existingBaseline = await db.SofaScores .AsNoTracking() .FirstOrDefaultAsync(s => s.EncounterId == encounterId && s.IsBaseline, ct); if (existingBaseline is null) { if (populatedOrganSystems >= 4) return (true, null); return (false, null); } var delta = result.Total - existingBaseline.TotalScore; return (false, delta); } private async Task PersistScoreAsync( Guid encounterId, Guid patientId, SofaResult result, bool isBaseline, int? delta, string stalenessFlags, DateTimeOffset calculatedAt, CancellationToken ct) { using var scope = _services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); db.SofaScores.Add(new SofaScore { Id = Guid.NewGuid(), EncounterId = encounterId, PatientId = patientId, TotalScore = result.Total, RespiratoryScore = result.Respiratory, CoagulationScore = result.Coagulation, LiverScore = result.Liver, CardiovascularScore = result.Cardiovascular, CnsScore = result.Cns, RenalScore = result.Renal, IsBaseline = isBaseline, DeltaFromBaseline = delta, StalenessFlags = stalenessFlags, CalculatedAt = calculatedAt, CreatedAt = DateTimeOffset.UtcNow }); await db.SaveChangesAsync(ct); } private async Task TryCreateAlertAsync( Guid encounterId, Guid patientId, AlertType alertType, AlertSeverity severity, SofaResult result, int? delta, string stalenessFlags, CancellationToken ct) { if (alertType == AlertType.SofaWarning) { var suppression = _services.GetRequiredService(); if (await suppression.IsSuppressedAsync(encounterId, alertType, ct)) return false; } using var scope = _services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); await using var tx = await db.Database.BeginTransactionAsync(ct); var alertId = Guid.NewGuid(); var triggeredAt = DateTimeOffset.UtcNow; var details = $"SOFA score {result.Total} (delta +{delta}). " + $"Components: Resp={result.Respiratory}, Coag={result.Coagulation}, " + $"Liver={result.Liver}, CV={result.Cardiovascular}, CNS={result.Cns}, Renal={result.Renal}. " + $"Staleness: {stalenessFlags}"; var affected = await db.Database.ExecuteSqlInterpolatedAsync($""" INSERT INTO clinical_alerts (id, encounter_id, patient_id, alert_type, severity, details, status, triggered_at) SELECT {alertId}, {encounterId}, {patientId}, {alertType.ToDbString()}, {severity.ToDbString()}, {details}, 'OPEN', {triggeredAt} WHERE NOT EXISTS ( SELECT 1 FROM clinical_alerts WHERE encounter_id = {encounterId} AND alert_type = {alertType.ToDbString()} AND status IN ('OPEN', 'ESCALATED') ) """, ct); if (affected == 0) { await tx.RollbackAsync(ct); return false; } db.OutboxEvents.Add(new OutboxEvent { Id = Guid.NewGuid(), Topic = "alert.generated", Payload = JsonSerializer.Serialize(new { alertId, encounterId, patientId, alertType = alertType.ToDbString(), severity = severity.ToDbString(), details, triggeredAt, sofaTotal = result.Total, sofaDelta = delta, partitionKey = encounterId.ToString() }), PartitionKey = encounterId.ToString(), CreatedAt = DateTimeOffset.UtcNow }); await db.SaveChangesAsync(ct); await tx.CommitAsync(ct); _metrics.ClinicalAlertsTotal .WithLabels(alertType.ToDbString(), severity.ToDbString()).Inc(); return true; } }