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 Serilog; namespace adas_core.Application.Customizations.HPAZ; public class CalculatedObservations(IServiceProvider serviceProvider) : ICalculatedObservations { private readonly Lazy _alarmService = serviceProvider.GetRequiredService>(); private readonly ApiSettings _apiSettings = serviceProvider.GetRequiredService>().Value; //apiSettings; private readonly IConfigObservationService _configObservationService = serviceProvider.GetRequiredService(); //configObservationService; private readonly ILogger _logger = serviceProvider.GetRequiredService>(); //Use GroupedObservation to avoid circular dependency with _observationService private readonly Lazy _observationService = serviceProvider.GetRequiredService>(); //observationService; private readonly List _pressBloodArteryMean = ["TAm"]; public Task CalculateActiveBolus(ObjectId patientId) { return Task.CompletedTask; } public Task CalculateMedicineObservation(List activeMedicines, ObjectId patientId) { return Task.CompletedTask; } public Task> GetActiveTreatmentsByPatient(ObjectId id) { return Task.FromResult>(new List()); } /// /// Maps the obs to an event or alarm depending on its type and inserts it if needed /// /// Type /// Observation /// True if the observation needs to generate an alert /// Mapped observation public async Task Map(T source, bool onlyByName) where T : BasePatientObservation { _logger.LogDebug("Mapping {obsName} mode observation {obs}", source.Name, source); if (source is not BasePatientObservationValue obs) return source; var name = obs is PatientObservationAlarm obsAlarm ? obsAlarm.Event : obs.Name; name ??= string.Empty; //No generamos alertas si viene onlyByName if (name == "Resp_Mode") obs = CalculateVentilationMode(obs); if (name is "Sattc" or "FiO2") await CalculateSf(obs, name); if (name is "FiO2" or "P_VAM" or "PaO2_Tidal") await CalculateOxygenationIndex(obs, name); if (name is "FiO2" or "PaO2_Tidal") await CalculatePf(obs, name); if (name == "TEST_ALARMA") await _alarmService.Value.CalculateAlarmTest(obs, name); obs = await CheckObsWithSameTimeExistsAndIncrementTime(obs); if (obs is PatientObservation pobs) { pobs = await CheckAlarmConfig(pobs); return pobs as T; } return obs as T; } public async Task Map(PumpObservation pumpObservation) { try { if (pumpObservation.Status is PumpEnum.Status.Alarm or PumpEnum.Status.Warning) { PatientObservation? patientObservation = null; string? name = null; //La oclusión es para todos los tipos var volumetricAirOcclusionAlarm = _apiSettings.VolumetricAirOcclusionAlarm ?? null; if (volumetricAirOcclusionAlarm != null && pumpObservation.AlarmType != null && volumetricAirOcclusionAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty)) { name = "VolumetricAirOcclusion"; //alerta oclusión aire para bombas volumétricas patientObservation = await CreatePumpAlarmObservation(name, pumpObservation); } var listInotropicMedicines = _apiSettings.InotropicMedicines ?? null; if (listInotropicMedicines is { Count: > 0 } && pumpObservation.DrugName != null && listInotropicMedicines.Contains(pumpObservation.DrugName)) //INOTRÓPICOS { var inotropicEndInfusionAlarm = _apiSettings.InotropicEndInfusionAlarm ?? null; if (inotropicEndInfusionAlarm != null && pumpObservation.AlarmType != null && inotropicEndInfusionAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty)) { //Inotrópicos próxima a fin de infusión // Y //proximo fin de infusión, oclusión, y presencia aire…. //Solo inotrópicos //Está en la línea 19 del excel y abarca la que hay en la línea 9 // TODO: revisar en los logs que nombre de type (Buscar por crud xmlns) name = "InotropicEndInfusion"; patientObservation = await CreatePumpAlarmObservation(name, pumpObservation); } } else { var pumPressureAlarm = _apiSettings.PumPressureAlarm ?? null; if (pumPressureAlarm != null && pumpObservation.AlarmType != null && pumPressureAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty)) { name = "Pressure"; //alerta de presión(la alerta de la bomba se programa + -30mmHg por encima de la presión de la línea). patientObservation = await CreatePumpAlarmObservation(name, pumpObservation); } var endContinuousInfusionAlarm = _apiSettings.EndContinuousInfusionAlarm ?? null; if (endContinuousInfusionAlarm != null && pumpObservation.AlarmType != null && endContinuousInfusionAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty)) { name = "EndContinuousInfusion"; //próximo a fin de infusión /fin de infusión //información de bombas solo perfusión continua patientObservation = await CreatePumpAlarmObservation(name, pumpObservation); } var volumetricAirInLineAlarm = _apiSettings.VolumetricAirInLineAlarm ?? null; if (volumetricAirInLineAlarm != null && pumpObservation.AlarmType != null && volumetricAirInLineAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty)) { name = "VolumetricAirInLine"; //alerta aire para bombas volumétricas (burbujas de aire para la bomba) patientObservation = await CreatePumpAlarmObservation(name, pumpObservation); } } if (patientObservation != null && name != null) { _ = _observationService.Value.InsertObservation(patientObservation); _ = _alarmService.Value.SendAlarm(patientObservation, name, AlarmEnum.Name.Pump, AlarmEnum.Severity.None, AlarmEnum.Type.Auto); } } } catch (Exception ex) { _logger.LogError("Error mapping alarm pump observation. Exception: {exMessage}", ex.Message); } return pumpObservation; } public Task Map(PatientTreatment treatment) { throw new NotImplementedException(); } public Task Map(PatientDiagnosis diagnosis) { throw new NotImplementedException(); } public async Task FixTimeInconsistencyWithLast(PatientObservation newObservation) { if (string.IsNullOrEmpty(newObservation.Name)) { _logger.LogError( "Error FixTimeInconsistencyWithLast new observation name is null or empty: {newObservation}.", newObservation); return newObservation; } var lasObservations = await _observationService.Value.FindLastObservations(newObservation.PatientId, 1, [newObservation.Name]); var lastObservation = lasObservations.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 listToMap) { var mappedObsList = listToMap .Select(obs => obs.DeepCopy()) .Select(obs => _configObservationService.Map(obs).Result) .Where(obs => obs != null) .ToList(); // mappedObsList contains observation mapped // Check blue code _ = CalculateBlueCodeList(mappedObsList); return Task.FromResult(listToMap); } public Task MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert) { if (!string.IsNullOrEmpty(alarmToInsert.EventId)) obs.Code = alarmToInsert.EventId; if (!string.IsNullOrEmpty(alarmToInsert.Event)) obs.Name = alarmToInsert.Event; obs.Value = alarmToInsert.Value; return Task.FromResult(obs); } Task ICalculatedObservations.SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code) { throw new NotImplementedException(); } private async Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code, AlarmEnum.Type type) { await _alarmService.Value.SendAlarm(obs, name, code, AlarmEnum.Severity.None, type); } /// /// Initial check to ignore old observations generating blue codes /// /// Observation /// True if it needs to be ignored private bool CheckIfObservationIsOlderAndShouldBeIgnored(PatientObservation obs) { if (obs.Time.ToUniversalTime().AddMinutes(_apiSettings.IgnoreCalcObservationsOlderInMinutesThan) < DateTime.UtcNow) { Log.Debug( "ignoring calc old observation: name:{obsName} obs_time:{obsTime} system_time {DateTimeNow} id:{obsId}", obs.Name ?? "null", obs.Time, DateTime.Now, obs.Id); return true; } return false; } private async Task CheckAlarmConfig(PatientObservation pobs) { var configObs = await _configObservationService.Get(new PatientObservation { Name = pobs.Name, PatientId = pobs.PatientId }); pobs.Alarm = configObs?.Alarm ?? null; return pobs; } private static PatientObservation CreateAlarmObservation(string name, PatientObservation pobs) { return new PatientObservation { CodingSystem = "ADAS_ALARM", Code = name, Name = $"Alarm_{name}", Value = pobs.Value, PatientId = pobs.PatientId, Time = pobs.Time, Alarm = pobs.Alarm }; } /// /// Checks if a blue code observation Lists needs to be generated and inserts it. /// /// public async Task CalculateBlueCodeList(List? obsList) { //Las 3 observaciones vienen siempre juntas en el mismo mensaje //Solo se comprueba si se cumplen 2 de las 3 condiciones var condition = 0; var patientId = obsList?.FirstOrDefault()?.PatientId; if (patientId == null) return; var fcObs = obsList?.FirstOrDefault(o => o is { Name: "FC" }); var sattcObs = obsList?.FirstOrDefault(o => o is { Name: "Sattc" }); var tamObs = obsList?.FirstOrDefault(o => o is { Name: "TAm" }); // FC < 60 if (fcObs != null && !CheckIfObservationIsOlderAndShouldBeIgnored(fcObs) && double.TryParse(fcObs.Value.ToString(), out var cardiacBeatRate) && cardiacBeatRate < 60) { Log.Debug("BlueCode condition: FC {fc} for patient: {obsPatientid} fc increase limit", cardiacBeatRate, fcObs.PatientId); condition++; } // SpO2 < 80 (Sattc) if (sattcObs != null && !CheckIfObservationIsOlderAndShouldBeIgnored(sattcObs) && double.TryParse(sattcObs.Value.ToString(), out var spO2Value) && spO2Value < 80) { Log.Debug("Blue code condition: SpO2 {sp} for patient: {obsPatientid} Sattc increase limit", spO2Value, sattcObs.PatientId); condition++; } if (condition == 0) return; // Early exit if 2 conditions are met if (condition == 2) { _ = SendBlueCode(fcObs); return; } // TAm actual es un 50% por debajo de la medición de 30 seg antes if (tamObs != null && !CheckIfObservationIsOlderAndShouldBeIgnored(tamObs) && double.TryParse(tamObs.Value.ToString(), out var tamObsValue)) { // Buscamos el anterior var lastPressBloodObsList = await _observationService.Value.FindLastObservations(tamObs.PatientId, 1, _pressBloodArteryMean); var lastPressBloodObs = lastPressBloodObsList.FirstOrDefault(); // Diferencia entre la última observación es < 30 s if (lastPressBloodObs != null && double.TryParse(lastPressBloodObs.Value.ToString(), out var lastPressBloodValue) && (tamObs.Time - lastPressBloodObs.Time).TotalSeconds <= 35 && tamObsValue <= lastPressBloodValue / 2) { Log.Debug("Blue code condition: TAm decrease limit over 30s for patient: {obsPatientid}", tamObs.PatientId); condition++; } } if (condition >= 2) _ = SendBlueCode(tamObs); } private async Task SendBlueCode(PatientObservation? pobs) { if (pobs == null) return; Log.Debug("Blue code Alarm for patient: {obsPatientid}", pobs.PatientId); var blueCodeObservation = CreateAlarmObservation("BlueCode", pobs); blueCodeObservation = await CheckAlarmConfig(blueCodeObservation); _ = _observationService.Value.InsertObservation(blueCodeObservation); _ = SendAlarm(blueCodeObservation, "BlueCode", AlarmEnum.Name.Blue, AlarmEnum.Type.Auto); } //PRVC : donde aparezca cambiar y mostrar en su lugar: VCRP //FLUJO ALTO: poner OAF en su lugar private BasePatientObservationValue CalculateVentilationMode(BasePatientObservationValue obs) { var value = obs is PatientObservationAlarm oAlarm ? oAlarm.Value : obs is PatientObservation o ? o.Value : null; if (value != null && value.ToString()!.Contains("PRVC")) obs.Value = value.ToString()!.Replace("PRVC", "VCRP"); else if (value != null && value.ToString()!.Contains("FLUJ.ALTO")) obs.Value = "OAF"; _logger.LogDebug("Calculating patient id: {id} ventilation mode: {obsName}, value : {obsValue}", obs.PatientId, obs.Name, obs.Value); return obs; } //TODO duplicado con el calculated del 12o, sacar a un utils private async Task CheckObsWithSameTimeExistsAndIncrementTime( BasePatientObservationValue obs) { if (obs is not PatientObservation pobs) return obs; var existObsWithSameTime = await _observationService.Value.FindAnyWithSameDate(pobs.PatientId, pobs.Time, pobs.Name); if (existObsWithSameTime != null && existObsWithSameTime.Any()) pobs.Time = pobs.Time.AddSeconds(1); return pobs; } //S/F Calc ( craneal saturation / FiO2 private async Task CalculateSf(BasePatientObservationValue obs, string name) { try { PatientObservation? saturation = null; PatientObservation? fio2 = null; var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["FiO2", "Sattc"]); switch (name) { case "FiO2": fio2 = (PatientObservation)obs; saturation = values.FirstOrDefault(o => o.Name == "Sattc"); break; case "Sattc": saturation = (PatientObservation)obs; fio2 = values.FirstOrDefault(o => o.Name == "FiO2"); break; } if (fio2 != null && saturation != null && !fio2.Expired && double.TryParse(fio2.Value.ToString(), out var fio) && !saturation.Expired && double.TryParse(saturation.Value.ToString(), out var sat) && sat <= 97) { if (fio == 0) { _logger.LogError("Error calculating SF. FiO2: {fio}", fio); return; } var sfValue = sat / fio; sfValue = Math.Round(sfValue, 4); var sfObs = new PatientObservation { PatientId = obs.PatientId, Name = "Sattc_FiO2", CodingSystem = "ADAS", Value = sfValue, Time = obs.Time }; await _observationService.Value.InsertObservation(sfObs); } } catch (Exception ex) { _logger.LogError("Error calculate S/F observation. Exception: {exMessage}", ex.Message); } } /// /// Oxygenation index is calculated => P_VAM x FiO2 x 100 / PaO2_Tidal /// the method is called when it receives P_VAM or FiO2 or PaO2_Tidal and tries to take the other values to perform the /// calculation if they are not /// in database then does nothing. /// public async Task CalculateOxygenationIndex(BasePatientObservationValue obs, string name) { var toSearchList = new List(); PatientObservation? pVam = null; PatientObservation? fiO2 = null; PatientObservation? paO2 = null; toSearchList.AddRange(new List { "P_VAM", "FiO2", "PaO2_Tidal" }); var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList); try { switch (name) { case "P_VAM": pVam = (PatientObservation)obs; fiO2 = values.FirstOrDefault(o => o.Name == "FiO2"); paO2 = values.FirstOrDefault(o => o.Name == "PaO2_Tidal"); break; case "FiO2": fiO2 = (PatientObservation)obs; pVam = values.FirstOrDefault(o => o.Name == "P_VAM"); paO2 = values.FirstOrDefault(o => o.Name == "PaO2_Tidal"); break; case "PaO2_Tidal": paO2 = (PatientObservation)obs; fiO2 = values.FirstOrDefault(o => o.Name == "FiO2"); pVam = values.FirstOrDefault(o => o.Name == "P_VAM"); break; } //Controlar que FiO2 no haya expirada al recogerla if (fiO2 != null && paO2 != null && pVam != null && int.TryParse(pVam.Value.ToString(), out var pvam) && int.TryParse(fiO2.Value.ToString(), out var fio2) && int.TryParse(paO2.Value.ToString(), out var pao2)) { if (pao2 == 0) { _logger.LogError( "Error calculate Oxygenation Index observation. PaO2: {pao2}, observation: {obsName}", pao2, name); return; } var oxygenationIndexValue = pvam * fio2 * 100 / pao2; var oxygenationIndexObs = new PatientObservation { PatientId = obs.PatientId, Name = "Oxygenation_Index", CodingSystem = "ADAS", Value = oxygenationIndexValue, Time = obs.Time }; await _observationService.Value.InsertObservation(oxygenationIndexObs); } } catch (Exception ex) { _logger.LogError("Error calculate Oxygenation Index observation. Exception: {exMessage}", ex.Message); } } //P/F Calc ( PaO2_Tidal / FiO2 private async Task CalculatePf(BasePatientObservationValue obs, string name) { try { PatientObservation? paO2 = null; PatientObservation? fio2 = null; var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["FiO2", "PaO2_Tidal"]); switch (name) { case "FiO2": fio2 = (PatientObservation)obs; paO2 = values.FirstOrDefault(o => o.Name == "PaO2_Tidal"); break; case "PaO2_Tidal": paO2 = (PatientObservation)obs; fio2 = values.FirstOrDefault(o => o.Name == "FiO2"); break; } if (fio2 != null && paO2 != null && !fio2.Expired && !paO2.Expired && double.TryParse(paO2.Value.ToString(), out var pa) && double.TryParse(fio2.Value.ToString(), out var fi)) { if (fi == 0) { _logger.LogError("Error Calculating PF. FiO2 value is 0. Observation:{obs}", obs); return; } var sfValue = pa / fi; sfValue = Math.Round(sfValue, 4); var sfObs = new PatientObservation { PatientId = obs.PatientId, Name = "PaO2_FiO2", CodingSystem = "ADAS", Value = sfValue, Time = obs.Time }; await _observationService.Value.InsertObservation(sfObs); } } catch (Exception ex) { _logger.LogError("Error calculate P/F observation. Exception: {exMessage}", ex.Message); } } private async Task CreatePumpAlarmObservation(string name, PumpObservation pumpObservation) { var positionPump = pumpObservation.IsAux != null && !pumpObservation.IsAux.Value ? $"Rack {pumpObservation.GatewayNumber} Bomba {pumpObservation.Number}" : $"Rack Aux {pumpObservation.GatewayNumber} Bomba {pumpObservation.Number}"; PatientObservation patientObservation = new() { CodingSystem = "ADAS_ALARM", Code = $"Pump_{name}", Name = $"Alarm_Pump_{name}", Value = pumpObservation.DrugName ?? positionPump, Time = pumpObservation.Time }; if (pumpObservation.PatientId != null) patientObservation.PatientId = pumpObservation.PatientId.Value; return await CheckAlarmConfig(patientObservation); } }