using adas_core.Application.Services.Interfaces; using adas_core.Domain.Enums; using adas_core.Domain.Models; using adas_core.Domain.Models.AppSettings; using adas_core.Domain.Models.Pumps; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MongoDB.Bson; using MongoDB.Driver; namespace adas_core.Application.Customizations.HUVH.UCIA; public class CalculatedObservations : ICalculatedObservations { private const string CodingSystem = "ADAS"; private readonly List? _antibioticList; private readonly List? _antidepressantsList; private readonly List? _antihypertensivesList; private readonly List? _antipsicoticList; private readonly List? _anxiolyticsList; private readonly List? _inotropicMedicines; private readonly ILogger _logger; private readonly Lazy _medicineService; private readonly List? _neuroMedicationList; private readonly Lazy _observationService; private readonly List? _sedationList; private readonly List? _serumColloidList; private readonly List? _serumCrystalloidList; private readonly Lazy _treatmentService; public CalculatedObservations(IServiceProvider serviceProvider) { _treatmentService = serviceProvider.GetRequiredService>(); _medicineService = serviceProvider.GetRequiredService>(); _observationService = serviceProvider.GetRequiredService>(); _logger = serviceProvider.GetRequiredService>(); var apiSettings = serviceProvider.GetRequiredService>(); //apiSettings; _sedationList = apiSettings.Value.Sedation; _inotropicMedicines = apiSettings.Value.InotropicMedicines; _antibioticList = apiSettings.Value.AntibioticList; _anxiolyticsList = apiSettings.Value.AnxiolyticList; _antipsicoticList = apiSettings.Value.AntipsicoticList; _antidepressantsList = apiSettings.Value.AntidepressantsList; _neuroMedicationList = apiSettings.Value.NeuroMedicationList; _serumCrystalloidList = apiSettings.Value.SerumCrystalloidList; _serumColloidList = apiSettings.Value.SerumColloidList; _antihypertensivesList = apiSettings.Value.AntihypertensivesList; } public Task CalculateActiveBolus(ObjectId patientId) { throw new NotImplementedException(); } public async Task Map(T obs, bool onlyByName = false) where T : BasePatientObservation { if (obs.Name is "PSI" or "RASS" or "TS") _ = CalculateOverSedation(obs); if (obs.Name is "Inspiratory_Pressure" or "Compliancia" or "Air_Flow") _ = CalculateVentilationMode(obs); if (obs.Name is "ECMO_Location" or "Location_Drainage_Cannula" or "Location_Return_Cannula") obs = (T)await CalculateEcmoLocation(obs); if (obs.Name is "EVN" or "ANI" or "ESCID") _ = CalculateOverAnalgesia(obs); if (obs.Name is "Pmeset" or "PEEP") _ = CalculateDrivingPressure(obs); if (obs.Name is "SpO2_FiO2_Ratio" or "FR") _ = CalculateRoxIndex(obs); if (obs.Name is "Diuresis") _ = CalculateDiuresis(obs); if (obs.Name is "Rehabilitation") obs = (T)await CalculateRehabilitationAlarm(obs); return obs; } public async Task Map(PatientTreatment treatment) { var order = treatment.PlacerOrder?.EntityIdentifier; //aquí almacenamos el número de orden if (string.IsNullOrEmpty(order)) return treatment; //comprobamos el estado de los tratamientos anteriores var oldTreatments = await CheckExpiredPatientTreatments(treatment); treatment.OrderControl = oldTreatments.Any() ? OrderControlType.Xo : OrderControlType.Nw; if (treatment.EndTime != null) //el tratamiento ha expirado treatment.OrderControl = OrderControlType.Dc; await CheckTreatmentMedicines(treatment); return treatment; } public async Task Map(PumpObservation pumpObservation) { return await Task.FromResult(pumpObservation); } public Task CalculateMedicineObservation(List activeMedicines, ObjectId patientId) { //Esto viene del schedulerService para chequear los medicamentos activos CheckMedicationCategories(patientId, activeMedicines); return Task.CompletedTask; } public async Task> GetActiveTreatmentsByPatient(ObjectId id) { var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(id); return activeTreatments; } public Task Map(PatientDiagnosis diagnosis) { throw new NotImplementedException(); } public async Task FixTimeInconsistencyWithLast(PatientObservation newObservation) { if (string.IsNullOrEmpty(newObservation.Name)) { _logger.LogError( "Error FixTimeInconsistencyWithLast. Observation name is null or empty. Observation: {newObservation}", newObservation); return newObservation; } var lastObservations = await _observationService.Value.FindLastObservations(newObservation.PatientId, 1, [newObservation.Name]); var lastObservation = lastObservations.FirstOrDefault(); if (lastObservation != null && DateTime.Compare( new DateTime(lastObservation.Time.Year, lastObservation.Time.Month, lastObservation.Time.Day, lastObservation.Time.Hour, lastObservation.Time.Minute, lastObservation.Time.Second), new DateTime(newObservation.Time.Year, newObservation.Time.Month, newObservation.Time.Day, newObservation.Time.Hour, newObservation.Time.Minute, newObservation.Time.Second) ) >= 0) newObservation.Time = lastObservation.Time.AddSeconds(1); return newObservation; } public Task> PreMapList(List listToInsert) { return Task.FromResult(listToInsert); } public Task MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert) { return Task.FromResult(obs); } public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code) { throw new NotImplementedException(); } private async Task CalculateRehabilitationAlarm(BasePatientObservation obs) { //En rojo si el grado es de 0 a 2 incluidos, por más de 7 días if (obs is not PatientObservation pobs || !int.TryParse(pobs.Value.ToString(), out var pobsValue) || pobsValue > 2) return obs; var result = await _observationService.Value.FindLastObservations(obs.PatientId, 6, ["Rehabilitation"]); var obsInAlert = result.Count(o => int.TryParse(o.Value.ToString(), out var oValue) && oValue is >= 0 and <= 2); if (obsInAlert == 6) pobs.Status = StatusEnum.Type.Alert; return obs; } private async Task CalculateDiuresis(BasePatientObservation obs) { // Calculado como el sumatorio de la Diuresis de las últimas 6h // entre el último peso medido del paciente y entre 6. Medido en ml/kg/h // Valor = sum(Diuresis últimas 6 horas) / Weight / 6 if (obs is not PatientObservation pobs) return; double weightValue = 0; var numValues = pobs.Name switch { "Diuresis" => 2, // Si la observación entrante es "Diuresis", necesitamos las 2 últimas para completar 6h. "Weight" => 3, // Si la observación entrante es "Weight", necesitamos las 3 últimas de "Diuresis". _ => 0 }; if (numValues == 0) return; // Si no es ni Diuresis ni Weight, no aplicamos cálculo. // Obtener las observaciones de diuresis necesarias var diuresisObservations = await _observationService.Value.FindLastObservations(pobs.PatientId, numValues, ["Diuresis"]); var diuresisValues = diuresisObservations .Select(o => double.TryParse(o.Value.ToString(), out var value) ? value : 0) .ToList(); if (pobs.Name == "Diuresis") { // Si la observación entrante es Diuresis, la agregamos if (double.TryParse(pobs.Value.ToString(), out var currentDiuresis)) diuresisValues.Insert(0, currentDiuresis); // Buscar el último peso registrado var weightObservations = await _observationService.Value.FindLastObservations(pobs.PatientId, 1, ["Weight"]); var weightObs = weightObservations.FirstOrDefault(); if (weightObs != null && double.TryParse(weightObs.Value.ToString(), out var wObsValue)) weightValue = wObsValue; } else if (pobs.Name == "Weight" && double.TryParse(pobs.Value.ToString(), out var wValue)) { // Si la observación entrante es Weight, asignamos su valor directamente. weightValue = wValue; } // Validaciones antes de calcular if (diuresisValues.Count < 3) { _logger.LogWarning("Calculated Diuresis lacks sufficient values. Required: 3, Found: {count}", diuresisValues.Count); return; } if (weightValue <= 0) { _logger.LogWarning("Calculated Diuresis aborted due to invalid weight value: {weight}", weightValue); return; } // Cálculo del índice de diuresis var diuresisIndex = diuresisValues.Sum() / weightValue / 6; var diuresisObs = new PatientObservation { PatientId = pobs.PatientId, Time = DateTime.UtcNow, CodingSystem = CodingSystem, Name = "Calculated_Diuresis", Value = diuresisIndex }; await _observationService.Value.InsertObservation(diuresisObs, mapObs: false); } private async Task CalculateRoxIndex(BasePatientObservation obs) { // Cálculo dividiendo el ratio SpO2/FiO2 entre la FR // Valor = (SpO2_FiO2_Ratio) / FR if (obs is not PatientObservation pobs) return; List? requiredObservations = pobs.Name switch { "SpO2_FiO2_Ratio" => ["FR"], // Si la observación actual es SpO2_FiO2_Ratio, necesitamos FR "FR" => ["SpO2_FiO2_Ratio"], // Si la observación actual es FR, necesitamos SpO2_FiO2_Ratio _ => null }; if (requiredObservations is null) return; var result = await _observationService.Value.FindLastObservations(pobs.PatientId, 1, requiredObservations); var targetObs = result.FirstOrDefault(); if (targetObs == null || !double.TryParse(targetObs.Value.ToString(), out var targetValue)) return; // Determinar los valores necesarios para el cálculo var spo2FiO2Ratio = pobs.Name == "SpO2_FiO2_Ratio" ? double.TryParse(pobs.Value.ToString(), out var ratio) ? ratio : 0 : targetValue; var fr = pobs.Name == "FR" ? double.TryParse(pobs.Value.ToString(), out var frValue) ? frValue : 0 : targetValue; if (fr == 0) // Evita división por cero return; var roxIndexObs = new PatientObservation { PatientId = pobs.PatientId, Time = DateTime.UtcNow, CodingSystem = CodingSystem, Name = "Rox_Index", Value = spo2FiO2Ratio / fr }; await _observationService.Value.InsertObservation(roxIndexObs, mapObs: false); } private async Task CalculateDrivingPressure(BasePatientObservation obs) { //Diferencia entre la Pmeset y la PEEP //Calculo Valor = Pmeset - PEEP try { if (obs is not PatientObservation pobs) return; var targetObservation = pobs.Name switch { "Pmeset" => "PEEP", "PEEP" => "Pmeset", _ => null }; if (targetObservation is null) return; var resultList = await _observationService.Value.FindLastObservations(obs.PatientId, 1, [targetObservation]); var targetObs = resultList.FirstOrDefault(); if (targetObs == null || !int.TryParse(targetObs.Value.ToString(), out var targetValue)) return; var peepValue = pobs.Name == "PEEP" ? int.TryParse(pobs.Value.ToString(), out var peep) ? peep : 0 : targetValue; var pmesetValue = pobs.Name == "Pmeset" ? int.TryParse(pobs.Value.ToString(), out var pmeset) ? pmeset : 0 : targetValue; var drivingPressureObs = new PatientObservation { PatientId = obs.PatientId, Time = DateTime.UtcNow, CodingSystem = CodingSystem, Name = "Driving_Pressure", Value = pmesetValue - peepValue }; await _observationService.Value.InsertObservation(drivingPressureObs, mapObs: false); } catch (Exception e) { Console.WriteLine(e); throw; } } private async Task CalculateOverAnalgesia(BasePatientObservation? obs, ObjectId? patientId = null) { // Mandar observación 'SOBREANALGESIA' // 0 <= EVN <= 3 o ESCID = 3 o ANI > 70 // y // Perfusiones de morfina, remifentanilo y fentanilo sostenidas > 24h. try { List requiredObservations = ["EVN", "ESCID", "ANI"]; // Si patientId es null, la información proviene de una observación. // Si tiene valor, proviene de un tratamiento. if (!patientId.HasValue) { if (obs is not PatientObservation pobs || !int.TryParse(pobs.Value.ToString(), out _)) return; patientId = pobs.PatientId; } var observations = await _observationService.Value.FindLastObservations(patientId.Value, 1, requiredObservations); // Verificar si alguna observación cumple la condición var condition1 = observations.Any(observation => int.TryParse(observation.Value.ToString(), out var obsValue) && observation.Name switch { "EVN" when obsValue is >= 0 and <= 3 => true, "ESCID" when obsValue == 3 => true, "ANI" when obsValue > 70 => true, _ => false }); if (!condition1) return; // Verificar tratamientos activos y umbral de analgesia var treatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(patientId.Value); var patientTreatments = treatments.ToList(); if (!patientTreatments.Any()) return; var morfina = patientTreatments.FirstOrDefault(t => t != null && t.RequestedGiveCodes.Any(c => c.Text is "Morfina")); var remifentanilo = patientTreatments.FirstOrDefault(t => t != null && t.RequestedGiveCodes.Any(c => c.Text is "Remifentanilo")); var fentanilo = patientTreatments.FirstOrDefault(t => t != null && t.RequestedGiveCodes.Any(c => c.Text is "Fentanilo")); if (morfina != null && !ExceedsAnalgesiaThreshold(morfina) && remifentanilo != null && !ExceedsAnalgesiaThreshold(remifentanilo) && fentanilo != null && !ExceedsAnalgesiaThreshold(fentanilo)) return; var overAnalgesiaObs = new PatientObservation { PatientId = patientId.Value, Time = DateTime.UtcNow, CodingSystem = CodingSystem, Name = "Over_Analgesia", Value = bool.TrueString }; await _observationService.Value.InsertObservation(overAnalgesiaObs, mapObs: false); } catch (Exception ex) { _logger.LogError("Error calculating Over_Analgesia. Exception: {ex}", ex); throw; } } private Task CalculateEcmoLocation(BasePatientObservation obs) { //Las opciones pueden ser VV, VA o ECMO. //Cuando llegue VV se muestra VV. //Cuando llegue VA se muestra VA. //Cuando llegue los siguientes parámetros, se debe mostrar ECMO: ECCO2r, VVDL, VA+V, VVDL+V, VV+V, VVA if (obs is not PatientObservation pobs) return Task.FromResult(obs); var ecmoValues = new List { "ECCO2r", "VVDL", "VA+V", "VVDL+V", "VV+V", "VVA" }; var obsValue = pobs.Value.ToString(); if (obsValue != null && ecmoValues.Contains(obsValue)) pobs.Value = "ECMO"; return Task.FromResult(obs); } private async Task CalculateVentilationMode(BasePatientObservation obs) { //- Si llega valor de PI pero NO COMPL, el tipo de ventilación es VMNI //- Si llega valor de PS pero no llega COMPL, el tipo de ventilación es VMNI //- Si llega COMPL, el tipo de ventilación es VMI //- Si llega Flujo, leer el tipo de ventilación de CCC(CNAF o CTAF) var mode = string.Empty; switch (obs.Name) { case "Inspiratory_Pressure": var complObs = await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["Compliancia"]); if (!complObs.Any()) mode = "VMNI"; break; case "Compliancia": mode = "VMI"; break; case "Air_Flow": var cccVentMode = await _observationService.Value.FindByPatientIdAndCodingSystemAsync(obs.PatientId, "CCC", "Ventilation_Mode"); if (!await cccVentMode.AnyAsync()) return; mode = cccVentMode.First().Value.ToString() ?? string.Empty; break; default: return; } if (string.IsNullOrEmpty(mode)) return; var ventilationModeObservation = new PatientObservation { PatientId = obs.PatientId, Time = DateTime.UtcNow, CodingSystem = CodingSystem, Name = "Ventilation_Mode", Value = mode }; _ = _observationService.Value.InsertObservation(ventilationModeObservation, mapObs: false); } private async Task CalculateOverSedation(BasePatientObservation? obs, ObjectId? patientId = null) { //Mandar observación 'SOBRESEDACIÓN' //cuando: //(RASS =-4 o RASS =-5) y //PSI < 25 y //TS > 5 y //(Propofol > 3 o Midazolam > 0,05 ó Isoflorano > 10) try { var pobsName = string.Empty; //si patientId viene null viene la observación //Si tiene valor viene de un tratamiento if (!patientId.HasValue) { if (obs is not PatientObservation pobs || !int.TryParse(pobs.Value.ToString(), out var pobsValue)) return; patientId = pobs.PatientId; pobsName = pobs.Name; switch (pobsName) { case "RASS" when pobsValue is not (-4 or -5): case "PSI" when pobsValue >= 25: case "TS" when pobsValue <= 5: return; } } var requiredObservations = pobsName switch { "RASS" => ["PSI", "TS"], "PSI" => ["RASS", "TS"], "TS" => ["RASS", "PSI"], _ => new List { "RASS", "PSI", "TS" } }; var result = await _observationService.Value.FindLastObservations(patientId.Value, 1, requiredObservations); var observations = result.ToDictionary(r => r.Name?.ToString() ?? "null"); if ((pobsName != "RASS" && (!observations.TryGetValue("RASS", out var rassObs) || !int.TryParse(rassObs.Value.ToString(), out var rassValue) || rassValue is not (-4 or -5))) || (pobsName != "PSI" && (!observations.TryGetValue("PSI", out var psiObs) || !int.TryParse(psiObs.Value.ToString(), out var psiValue) || psiValue >= 25)) || (pobsName != "TS" && (!observations.TryGetValue("TS", out var tsObs) || !int.TryParse(tsObs.Value.ToString(), out var tsValue) || tsValue <= 5))) return; var treatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(patientId.Value); if (!treatments.Any(ExceedsSedationThreshold)) return; var overSedationObs = new PatientObservation { PatientId = patientId.Value, Time = DateTime.UtcNow, CodingSystem = CodingSystem, Name = "Over_Sedation", Value = bool.TrueString }; await _observationService.Value.InsertObservation(overSedationObs, mapObs: false); } catch (Exception ex) { _logger.LogError("Error calculating Over_Sedation. Exception: {ex}", ex); throw; } } private static bool ExceedsAnalgesiaThreshold(PatientTreatment? treatment) { // Perfusiones de morfina, remifentanilo o fentanilo sostenidas > 24 h return treatment is { StartTime: not null } && treatment.StartTime.Value.AddSeconds(86400) < DateTime.UtcNow; } private static bool ExceedsSedationThreshold(PatientTreatment? treatment) { return (treatment != null && treatment.RequestedGiveCodes.Any(c => c.Text == "Propofol" && treatment.RequestedGiveAmountMinimum > 3)) || (treatment != null && treatment.RequestedGiveCodes.Any(c => c.Text == "Midazolam" && treatment.RequestedGiveAmountMinimum > 0.05)) || (treatment != null && treatment.RequestedGiveCodes.Any(c => c.Text == "Isoflorano" && treatment.RequestedGiveAmountMinimum > 10)); } private async Task> CheckExpiredPatientTreatments(PatientTreatment treatment) { //si el tratamiento ha expirado actualizamos el OrderControl a DC y devolvemos las que siguen activas var result = new List(); var oldTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(treatment.PatientId); foreach (var t in oldTreatments.Where(t => t != null)) if (t is { EndTime: not null } && t.EndTime > DateTime.UtcNow) { t.OrderControl = OrderControlType.Dc; _ = _treatmentService.Value.UpdateTreatment(t); } else if (t != null) { result.Add(t); } return result; } private async Task CheckTreatmentMedicines(PatientTreatment treatment) { var newMedicines = (await Task.WhenAll(treatment.RequestedGiveCodes .Select(async code => await _medicineService.Value.GetByCode(code.Identifier)))) .Where(m => m != null) .ToList(); if (newMedicines.Count == 0) return; var activeTreatments = (await GetActiveTreatmentsByPatient(treatment.PatientId)).ToList(); var activeMedicines = (await _medicineService.Value.GetMedicinesOfTreatments(activeTreatments)).ToList(); // Combina ambas listas y selecciona solo elementos únicos con el mismo nombre var uniqueMedicines = newMedicines .Concat(activeMedicines) .GroupBy(m => m?.Name) .Select(g => g.First()) .ToList(); // uniqueMedicines contiene la lista de medicamentos activos de un paciente CheckMedicationCategories(treatment.PatientId, uniqueMedicines as List); } private void CheckMedicationCategories(ObjectId patientId, List uniqueMedicines) { var medicationCategories = new Dictionary?> { { "Sedation_Medication_Multivalue", _sedationList }, { "Inotropic_Medication_Multivalue", _inotropicMedicines }, { "Antibiotic_Medication_Multivalue", _antibioticList }, { "Anxiolytic_Medication_Multivalue", _anxiolyticsList }, { "Antipsychotic_Medication_Multivalue", _antipsicoticList }, { "Antidepressants_Medication_Multivalue", _antidepressantsList }, { "Neuro_Medication_Multivalue", _neuroMedicationList }, { "Crystalloid_Serum_Medication_Multivalue", _serumCrystalloidList }, { "Colloid_Serum_Medication_Multivalue", _serumColloidList }, { "Antihypertensives_Medication_Multivalue", _antihypertensivesList } }; foreach (var category in medicationCategories) { var observations = uniqueMedicines? .Where(m => category.Value != null && !string.IsNullOrEmpty(m.Name) && m.Name != null && category.Value.Contains(m.Name)) .ToList(); if (observations != null && observations.Any()) _ = CreateMedicationMultivalueObservation(observations, patientId, category.Key); } } private async Task CreateMedicationMultivalueObservation(IEnumerable medications, ObjectId patientId, string obsName) { var newObs = new PatientObservation { PatientId = patientId, Name = obsName, CodingSystem = CodingSystem, Value = medications.Select(m => m?.Name).ToArray(), Time = DateTime.Now }; var lastObsList = await _observationService.Value.FindLastObservations(patientId, 1, [obsName]); var lastObs = lastObsList.FirstOrDefault(); if (lastObs is { Expired: false }) { //expiramos la anterior lastObs.Expired = true; _ = _observationService.Value.UpdateObservation(lastObs); } await _observationService.Value.InsertObservation(newObs, mapObs: false); //mapObs:no volvemos a mapear la observación } }