using adas_core.Application.Exceptions; using adas_core.Application.Repositories.Interfaces; using adas_core.Application.Services.Interfaces; using adas_core.Application.Subscriptions; using adas_core.Domain.Enums; using adas_core.Domain.Models; using adas_core.Domain.Models.AppSettings; using adas_core.Domain.Models.Filter; using adas_core.Domain.Models.MongoModels; using adas_core.Domain.Models.Pumps; using adas_core.Domain.Models.Responses; using adas_core.Domain.Utils; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MongoDB.Bson; using Patient = adas_core.Domain.Models.MongoModels.Patient; namespace adas_core.Application.Services { /// /// Servicio maestro de gestión de bombas /// - Procesa ApiRequest (HL7/Alaris transformado) /// - Histórico clínico (pump_observations) /// - Histórico de alarmas (pump_alarm_events) /// - Alarmas activas (pump_alarm_state) /// - Snapshot (pump_state) /// - Broadcast de snapshots (PumpState + PumpAlarmState) /// public class PumpService( IPumpObservationRepository pumpObservationRepository, IPumpStateRepository pumpStateRepo, IPumpAlarmEventRepository alarmEventRepo, IPumpAlarmStateRepository alarmStateRepo, IPumpArchiveRepository pumpArchiveRepo, IPatientService patientService, IConfigPumpsService configPumpsService, IOptions apiSettings, ILogger logger, ISubscribersService subscribersService, IClientMessageService clientMessageService, Lazy calculatedObservationsService, IHttpContextAccessor httpContextAccessor, ILocalAuditService auditService, IConfigUnitsService configUnitsService) : IPumpService { // Settings private readonly int _pumpExpiresSeconds = apiSettings.Value.PumpExpiresSeconds; private readonly bool _sendPumpsZero = apiSettings.Value.SendPumpsZero; // ====================================================================== // ENTRYPOINT // ====================================================================== public async Task SaveRequest(ApiRequest req) { // Normalizar single vs list (Alaris puede mandar 1 sola) if (req.PumpObservation != null && (req.PumpObservations == null || req.PumpObservations.Count == 0)) req.PumpObservations = [req.PumpObservation]; if (req.PumpObservations == null || req.PumpObservations.Count == 0) { logger.LogWarning("ApiRequest contains 0 PumpObservations"); return; } // Busca paciente (lookup/create) a partir de PatientNumber / Patient / PatientId string var resolvedPatient = await patientService.FindPatientByApiRequest(req); var foundPatientId = resolvedPatient?.Id; foreach (var pobs in req.PumpObservations) { UpdatePatientFromRequest(req, pobs, foundPatientId); // Regla Alaris: si el origen es AlarisPump y no hay PatientId -> descartar if (string.Equals(req.Type, "AlarisPump", StringComparison.OrdinalIgnoreCase) && pobs.PatientId == null) { logger.LogWarning("AlarisPump: Observation descartada por ausencia de PatientId. DeviceId={device}", pobs.DeviceId); continue; } if (pobs.Time == DateTime.MinValue) pobs.Time = DateTime.UtcNow; try { // 1) Procesar por tipo HL7 o según ObservationType (Alaris) switch (req.Type) { case "ORU_R01": // PCD-01 case "ORU_R42": // PCD-10 pobs.MessageType = PumpEnum.PumpMessageType.Observation; await ProcessObservation(pobs); break; case "ORU_R40": // PCD-04 pobs.MessageType = PumpEnum.PumpMessageType.Alarm; await ProcessAlarm(pobs); break; case "AlarisPump": if (pobs.MessageType == PumpEnum.PumpMessageType.Alarm) await ProcessAlarm(pobs); else await ProcessObservation(pobs); break; default: logger.LogWarning("Tipo de request desconocido para PumpService: {type}", req.Type); break; } // 2) Snapshot de bomba var state = await UpdatePumpState(pobs); // 3) Alarmas activas del dispositivo var activeAlarms = await alarmStateRepo.FindAllActiveByDeviceAsync(pobs.DeviceId!); // 4) Broadcast de snapshots (PumpState + todas las PumpAlarmState) await SendSnapshotsBroadcast(state, activeAlarms, pobs.PatientId ?? foundPatientId, req); // 5) Retención (sobre histórico de observaciones) - reglas await DoRetentionActions(pobs); } catch (Exception ex) { logger.LogError(ex, "Error procesando observation/alarm DeviceId={deviceId}", pobs.DeviceId); } } } // ====================================================================== // OBSERVACIONES (PCD-01 / PCD-10) // ====================================================================== private async Task ProcessObservation(PumpObservation obs) { logger.LogDebug("Insertando OBSERVATION DeviceId={dev} Time={time}", obs.DeviceId, obs.Time); obs.Id = ObjectId.GenerateNewId(); obs.Expires = _pumpExpiresSeconds; var mapped = await MapPumpObservation(obs); if (mapped != null) { await pumpObservationRepository.InsertAsync(mapped); await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, mapped); } } // ====================================================================== // ALARMAS (PCD-04) // ====================================================================== private async Task ProcessAlarm(PumpObservation obs) { logger.LogDebug("Insertando ALARM DeviceId={dev}, Phase={phase}, Type={type}", obs.DeviceId, obs.EventPhase, obs.AlarmType); var alarmEvent = new PumpAlarmEvent { Id = ObjectId.GenerateNewId(), DeviceId = obs.DeviceId, RackId = obs.RackId, DeviceTypeMdc = obs.DeviceTypeMdc, DeviceIp = obs.DeviceIp, PillarAssembly = obs.PillarAssembly, PillarRackSlot = obs.PillarRackSlot, Time = obs.Time, InfusionId = obs.InfusionId, AlarmType = obs.AlarmType, AlarmTypeMdc = obs.AlarmTypeMdc, AlarmDescription = obs.AlarmDescription, AlarmPriority = obs.AlarmPriority, AlarmState = obs.AlarmState, AlarmInactivationState = obs.AlarmInactivationState, EventPhase = obs.EventPhase, AlertSourceMdc = obs.AlertSourceMdc, PatientId = obs.PatientId }; await alarmEventRepo.InsertAsync(alarmEvent); await UpdateAlarmState(obs); } private async Task UpdateAlarmState(PumpObservation obs) { if (obs.EventPhase == null) return; var phaseLower = obs.EventPhase.Value.ToString().ToLowerInvariant(); // Cierre if (phaseLower == "end") { await alarmStateRepo.RemoveAsync(obs.DeviceId!, obs.AlarmType, obs.AlarmTypeMdc); return; } // Start/continue → upsert var state = new PumpAlarmState { Id = ObjectId.GenerateNewId(), DeviceId = obs.DeviceId, AlarmType = obs.AlarmType, AlarmCodeMdc = obs.AlarmTypeMdc, AlarmDescription = obs.AlarmDescription, AlarmPriority = obs.AlarmPriority, AlarmState = obs.AlarmState, LastPhase = obs.EventPhase, FirstSeen = DateTime.UtcNow, LastUpdated = DateTime.UtcNow, AlertSourceMdc = obs.AlertSourceMdc, InfusionId = obs.InfusionId, PatientId = obs.PatientId }; await alarmStateRepo.UpsertActiveAsync(state); } // ====================================================================== // SNAPSHOT (devuelve el PumpState actualizado) // ====================================================================== private async Task UpdatePumpState(PumpObservation obs) { var current = await pumpStateRepo.FindByDeviceIdAsync(obs.DeviceId!) ?? new PumpState { Id = ObjectId.GenerateNewId(), DeviceId = obs.DeviceId }; MergePumpState(current, obs); current.LastUpdated = DateTime.UtcNow; await pumpStateRepo.UpsertAsync(current); return current; } private static void MergePumpState(PumpState state, PumpObservation obs) { // Identidad / físico if (!string.IsNullOrWhiteSpace(obs.RackId)) state.RackId = obs.RackId; if (!string.IsNullOrWhiteSpace(obs.DeviceTypeMdc)) state.DeviceTypeMdc = obs.DeviceTypeMdc; if (!string.IsNullOrWhiteSpace(obs.DeviceIp)) state.DeviceIp = obs.DeviceIp; if (!string.IsNullOrWhiteSpace(obs.PillarAssembly)) state.PillarAssembly = obs.PillarAssembly; if (!string.IsNullOrWhiteSpace(obs.PillarRackSlot)) state.PillarRackSlot = obs.PillarRackSlot; // Infusión if (!string.IsNullOrWhiteSpace(obs.InfusionId)) state.InfusionId = obs.InfusionId; // Estado if (obs.InfusingStatus != null) { state.InfusingStatus = obs.InfusingStatus; state.IsInfusing = obs.IsInfusing ?? false; } if (obs.Status != null) state.Status = obs.Status; if (obs.PumpMode != null) state.PumpMode = obs.PumpMode; if (!string.IsNullOrWhiteSpace(obs.ActiveSourceInfo)) state.ActiveSourceInfo = obs.ActiveSourceInfo; if (!string.IsNullOrWhiteSpace(obs.InfusionModeDetail)) state.InfusionModeDetail = obs.InfusionModeDetail; if (!string.IsNullOrWhiteSpace(obs.NotDeliveringReason)) state.NotDeliveringReason = obs.NotDeliveringReason; if (!string.IsNullOrWhiteSpace(obs.Source)) state.Source = obs.Source; // Métricas if (HasValue(obs.FlowFluid)) state.FlowFluid = obs.FlowFluid; if (HasValue(obs.Rate)) state.Rate = obs.Rate; if (HasValue(obs.VolumeInfused)) state.VolumeInfused = obs.VolumeInfused; if (HasValue(obs.FluidDelivTotal)) state.FluidDelivTotal = obs.FluidDelivTotal; if (HasValue(obs.FluidDelivTotalSet)) state.FluidDelivTotalSet = obs.FluidDelivTotalSet; if (HasValue(obs.VolumeRemaining)) state.VolumeRemaining = obs.VolumeRemaining; if (HasValue(obs.Vtbi)) state.Vtbi = obs.Vtbi; if (HasValue(obs.TimeRemaining)) state.TimeRemaining = obs.TimeRemaining; if (HasValue(obs.TimeProgrammed)) state.TimeProgrammed = obs.TimeProgrammed; // Medicación if (!string.IsNullOrWhiteSpace(obs.DrugName)) state.DrugName = obs.DrugName; if (!string.IsNullOrWhiteSpace(obs.DrugId)) state.DrugId = obs.DrugId; if (HasValue(obs.Concentration)) state.Concentration = obs.Concentration; if (HasValue(obs.DoseRate)) state.DoseRate = obs.DoseRate; if (HasValue(obs.DrugAmount)) state.DrugAmount = obs.DrugAmount; if (HasValue(obs.DrugDoseDelivered)) state.DrugDoseDelivered = obs.DrugDoseDelivered; if (HasValue(obs.PatientWeight)) state.PatientWeight = obs.PatientWeight; if (obs.Syringe != null) state.Syringe = obs.Syringe; // Eventos / Alarmas resumen if (obs.Event != null) state.Event = obs.Event; if (obs.EventPhase != null) state.EventPhase = obs.EventPhase; if (obs.AlarmType != null) state.AlarmType = obs.AlarmType; if (!string.IsNullOrWhiteSpace(obs.AlarmDescription)) state.AlarmDescription = obs.AlarmDescription; if (!string.IsNullOrWhiteSpace(obs.AlarmState)) state.AlarmState = obs.AlarmState; if (!string.IsNullOrWhiteSpace(obs.AlarmInactivationState)) state.AlarmInactivationState = obs.AlarmInactivationState; if (!string.IsNullOrWhiteSpace(obs.AlarmPriority)) state.AlarmPriority = obs.AlarmPriority; if (!string.IsNullOrWhiteSpace(obs.AlarmTypeMdc)) state.AlarmCodeMdc = obs.AlarmTypeMdc; // Paciente state.PatientId = obs.PatientId; } private static bool HasValue(CommonPumpTypes.PumpValue? v) => v is { Value: not null }; // MAP public async Task MapPumpObservation(PumpObservation obs) { var obs2 = await configPumpsService.Map(obs); var obs3 = await configUnitsService.Map(obs2); var obs4 = await calculatedObservationsService.Value.Map(obs3); if (obs4 == null) logger.LogDebug("Mapping ignorado para obs"); return obs3; } //Métodos public Task SaveRequestAsync(ApiRequest req) => SaveRequest(req); // Últimas N observaciones por paciente public async Task> FindLastPumpObservations(ObjectId patientId, int num = 1) { var list = await pumpObservationRepository.FindByPatientId(patientId); if (list is List pumpObservations) return pumpObservations is { Count: 0 } ? [] : pumpObservations.OrderByDescending(x => x.Time).Take(num).ToList(); return []; } // Última fecha de observación por paciente (para todos los pacientes) public async Task> FindAllLastPatientObservationTime() { return await pumpObservationRepository.FindAllLastPatientObservationTimeAsync(); } // Borrado completo por paciente (observaciones + alarmas + alarmState) public async Task DeleteByPatientId(ObjectId id) { logger.LogDebug("Delete Pump data by Patient Id {id}", id); await pumpObservationRepository.DeleteByPatientId(id); await alarmEventRepo.DeleteByPatientId(id); await alarmStateRepo.DeleteByPatientId(id); } // Archivo → por entidad paciente public async Task Archive(Patient patient) => await ArchiveByPatientId(patient.Id); // Archivo → por PatientId (mueve a archive_pumpobservations y elimina del activo) public async Task ArchiveByPatientId(ObjectId id) { var list = await pumpObservationRepository.FindByPatientId(id); var pumpObservations = list.ToList(); if (pumpObservations.Count != 0) await pumpArchiveRepo.InsertManyAsync(pumpObservations); await DeleteByPatientId(id); logger.LogDebug("Archived Pump observations & deleted active data by Patient Id {id}", id); } // Actualización masiva public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId) { if (string.IsNullOrWhiteSpace(nameId)) throw new ArgumentException("nameId no puede ser nulo o vacío.", nameof(nameId)); var updatedObs = await pumpObservationRepository.UpdateManyObjectIdByFieldAsync(nameId, id, oldId); // actualizar también alarmas activas e históricas _ = await alarmEventRepo.UpdateManyObjectIdByFiledNameAsync(nameId, id, oldId); _ = await alarmStateRepo.UpdateManyObjectIdByFieldNameAsync(nameId, id, oldId); logger.LogInformation( "UpdateManyObjectId completado. Campo={field}, oldId={oldId}, newId={newId}. Obs actualizadas={obsUpdated}", nameId, oldId, id, updatedObs); // Si quieres auditar el cambio: await auditService.CreateAuditLogAsync( httpContextAccessor.HttpContext?.User!, new { Field = nameId, OldId = oldId, NewId = id, Scope = "PumpObservation" }, null); } // ConfigPumpsService public async Task?> GetItemsById(string id) => await configPumpsService.GetConfigItems(id); public async Task?> GetAllPumpConfig() => await configPumpsService.GetAllPumpConfigs() ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); public async Task GetPumpConfigsById(string id) => await configPumpsService.GetPumpConfigById(id) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); public async Task UpdatePumpConfig(ConfigPumps config) { var oldConfig = await configPumpsService.GetPumpConfigById(config.Id); await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldConfig, config); return await configPumpsService.UpdatePumpConfig(config) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); } public async Task InsertPumpConfig(ConfigPumps config) { await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, config); return await configPumpsService.InsertPumpConfig(config) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed); } public async Task DeletePumpConfig(ConfigPumps config) { var result = await configPumpsService.DeletePumpConfig(config); if (!result) throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, config, null); return result; } // Paginación de observaciones public async Task?> GetPaginatedPump(PaginationFilter filter) { // Implementación compatible sin nuevos métodos en los repos: // 1) Si llega PatientId, paginamos en memoria desde FindByPatientId. // 2) Si llega DeviceId, usamos FindByDeviceIdAsync y paginamos en memoria. // 3) Si no hay filtro, devolvemos vacío para evitar lecturas completas. var page = filter.PageNumber <= 0 ? 1 : filter.PageNumber; var size = filter.PageSize <= 0 ? 20 : filter.PageSize; var fr = filter.FilteredRequest; if (fr == null) return new PaginationResponse([], page, size, 0); List all; if (!string.IsNullOrWhiteSpace(fr.PatientId)) { if (!ObjectId.TryParse(fr.PatientId, out var patientId)) return new PaginationResponse([], page, size, 0); var list = await pumpObservationRepository.FindByPatientId(patientId); all = list.ToList(); if (fr.StartDate.HasValue) all = all.Where(o => o.Time >= fr.StartDate.Value).ToList(); if (fr.EndDate.HasValue) all = all.Where(o => o.Time <= fr.EndDate.Value).ToList(); } else if (!string.IsNullOrWhiteSpace(fr.DeviceId)) { var found = await pumpObservationRepository.FindByDeviceIdAsync(fr.DeviceId, fr.StartDate, fr.EndDate); all = found.ToList(); } else { all = []; } var count = all.Count; var pageData = all .OrderByDescending(o => o.Time) .Skip((page - 1) * size) .Take(size) .ToList(); return new PaginationResponse(pageData, page, size, count); } // Inserción manual public async Task InsertPumpObservation(PumpObservation obs) { logger.LogDebug("Insert {obs}", obs); obs.Id = ObjectId.GenerateNewId(); if (obs.Time == DateTime.MinValue) obs.Time = DateTime.UtcNow; var obsMapped = await MapPumpObservation(obs); if (obsMapped != null) { await pumpObservationRepository.InsertAsync(obsMapped); await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, obsMapped); if (!_sendPumpsZero && obsMapped.Number == 0) return; // Tras inserción manual, actualizar y emitir snapshots var state = await UpdatePumpState(obsMapped); var activeAlarms = await alarmStateRepo.FindAllActiveByDeviceAsync(obsMapped.DeviceId!); await SendSnapshotsBroadcast(state, activeAlarms, obsMapped.PatientId, null); await DoRetentionActions(obsMapped); } } private async Task DoRetentionActions(PumpObservation obs) { var result = await configPumpsService.RetentionActions(obs); if (result is not { RetentionPolicyValue: not null }) return; switch (result.RetentionPolicy) { case RetentionPolicy.DeleteOlderDays: { var removed = await pumpObservationRepository.DeleteOlderThanDaysAsync( result.RetentionPolicyValue.Value); logger.LogInformation( "Retention DeleteOlderDays: {removed} deleted (>{days} days)", removed, result.RetentionPolicyValue); break; } case RetentionPolicy.DeleteOlderNumber: { var removed = await pumpObservationRepository.DeleteKeepLastNAsync( result.RetentionPolicyValue.Value); logger.LogInformation( "Retention DeleteOlderNumber: {removed} deleted (keeping {max})", removed, result.RetentionPolicyValue); break; } case RetentionPolicy.NoDelete: case RetentionPolicy.DeleteOlderSeconds: default: logger.LogWarning("Unknown retention policy: {policy}", result.RetentionPolicy); break; } } // BROADCAST SNAPSHOTS: PumpState + PumpAlarmState (activos) private async Task SendSnapshotsBroadcast( PumpState state, IEnumerable activeAlarms, ObjectId? patientId, ApiRequest? req) { var subscribers = new List(); if (patientId != null) { var patient = await patientService.FindById(patientId.Value); if (patient != null) { subscribers = subscribersService.GetSubscribers() .Where(s => !s.Locations.IsNullOrEmpty() && s.Locations.Any(c => c.UnitName == patient.Location.UnitName && c.Bed == patient.Location.Bed && c.Room == patient.Location.Room)) .ToList(); } } else if (req?.Location != null) { var loc = req.Location; subscribers = subscribersService.GetSubscribers() .Where(s => !s.Locations.IsNullOrEmpty() && s.Locations.Any(c => c.UnitName == loc.UnitName && c.Bed == loc.Bed && c.Room == loc.Room)) .ToList(); } if (subscribers.Count == 0) return; // Enviar PumpState foreach (var sub in subscribers) await clientMessageService.SendAsync(sub.Id, OperationType.Pump, state); // Enviar todas las PumpAlarmState activas foreach (var alarm in activeAlarms) { foreach (var sub in subscribers) await clientMessageService.SendAsync(sub.Id, OperationType.PumpAlarm, alarm); } } private static void UpdatePatientFromRequest(ApiRequest req, PumpObservation obs, ObjectId? foundPatientId) { if (obs.PatientId == null && foundPatientId != null) obs.PatientId = foundPatientId; if (obs.PatientId != null || string.IsNullOrWhiteSpace(req.PatientId)) return; if (ObjectId.TryParse(req.PatientId, out var parsed)) obs.PatientId = parsed; } } }