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.MongoModels; using adas_core.Domain.Utils; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MongoDB.Bson; using MongoDB.Driver; namespace adas_core.Application.Services; /// /// Provides the implementation of the contract, /// offering diagnosis-related operations as defined by the interface. /// /// public class DiagnosisService : IDiagnosisService { private readonly ILocalAuditService _auditService; private readonly Lazy _calculatedObservations; private readonly IClientMessageService _clientMessageService; private readonly IDiagnosisArchiveRepository _diagnosisArchiveRepository; private readonly List _diagnosisCode = []; private readonly IDiagnosisRepository _diagnosisRepository; private readonly string _diagnosisSystem; private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; private readonly Lazy _patientService; private readonly ISubscribersService _subscribersService; private readonly IUnitService _unitService; /// /// Initializes a new instance of the , which provides operations for managing diagnoses and their archives. The constructor stores injected collaborators and seeds the diagnosis system identifier and configured diagnosis codes from . /// /// A that resolves an for patient lookups. /// The of providing configuration such as the diagnosis system and diagnosis codes. /// The used to read and persist diagnoses. /// The used to read and persist archived diagnoses. /// The used to record diagnostic information. /// The used to deliver messages to clients. /// The used to notify subscribers of diagnosis events. /// A that resolves an for derived observations. /// The providing access to the current HTTP context. /// The used to record local audit entries. /// The used to manage measurement units. /// public DiagnosisService( Lazy patientService, IOptions apiSettings, IDiagnosisRepository diagnosisRepository, IDiagnosisArchiveRepository diagnosisArchiveRepository, ILogger logger, IClientMessageService clientMessageService, ISubscribersService subscribersService, Lazy calculatedObservations, IHttpContextAccessor httpContextAccessor, ILocalAuditService auditService, IUnitService unitService) { _patientService = patientService; _diagnosisRepository = diagnosisRepository; _diagnosisArchiveRepository = diagnosisArchiveRepository; _logger = logger; _clientMessageService = clientMessageService; _subscribersService = subscribersService; _calculatedObservations = calculatedObservations; _unitService = unitService; _httpContextAccessor = httpContextAccessor; _auditService = auditService; _diagnosisSystem = apiSettings.Value.DiagnosisSystem ?? "CUSTOM"; if (apiSettings.Value.DiagnosisCode.Any()) _diagnosisCode = apiSettings.Value.DiagnosisCode; } // private async Task SendBroadcast(PatientDiagnosis diagnosis) // { // var patient = await _patientService.Value.FindById(diagnosis.PatientId); // if (patient == null) return; // // // var subscribers = _subscribersService.GetSubscribers().Where(s => // (s.SubscriptionType == SubscriptionType.Box && s.Box == patient.Bed && // s.Section == patient.UnitString) || // (s.SubscriptionType == SubscriptionType.Section && s.Section == patient.UnitString)).ToList(); // subscribers.ForEach(Action); // return; // // async void Action(WsSubscriber subscriber) => // await _clientMessageService.SendAsync(subscriber.Id, OperationType.Diagnosis, diagnosis); // } /// /// Archives the specified patient by delegating to the archival routine keyed by the patient's identifier. /// /// The patient to be archived. Its Id is used to locate the record to archive. /// public async Task Archive(Patient patient) { await ArchiveByPatientId(patient.Id); } /// /// Archives all diagnoses associated with the specified patient by copying them to the diagnosis archive repository and then deleting the original records. /// /// The identifier of the patient whose diagnoses will be archived. /// public async Task ArchiveByPatientId(ObjectId id) { _logger.LogDebug("Archive Diagnoses by Patient Id {id}", id); using (var cursor = await FindByPatientIdAsync(id)) { while (await cursor.MoveNextAsync()) foreach (var current in cursor.Current) await _diagnosisArchiveRepository.InsertOneAsync(current); } await DeleteByPatientId(id); } /// /// Deletes all diagnoses associated with the specified patient identifier and records an audit log entry capturing the previous state of the records. /// /// The unique identifier of the patient whose diagnoses should be deleted. /// public async Task DeleteByPatientId(ObjectId id) { _logger.LogDebug("Delete Diagnoses by Patient Id {id}", id); var oldPatient = await _diagnosisRepository.GetByPatient(id); await _diagnosisRepository.DeleteByPatientId(id); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, null); } /// /// Asynchronously retrieves the list of patient diagnoses associated with the specified patient identifier by delegating to the diagnosis repository. /// /// The unique identifier of the patient whose diagnoses are being retrieved. /// A task that represents the asynchronous operation, containing a list of records for the given patient. /// public async Task> GetByPatientId(ObjectId patientId) { var diagnosis = await _diagnosisRepository.GetByPatient(patientId); return diagnosis; } /// /// Asynchronously saves the specified API request by delegating to an overload that accepts a secondary parameter, which is passed as null. /// /// The API request to save. /// A task that represents the asynchronous save operation. /// public Task SaveRequest(ApiRequest apiRequest) { return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest, null); })); } /// /// Asynchronously saves the specified API request by delegating to the underlying save operation with a null secondary parameter. /// /// The API request instance to persist. /// A task that represents the asynchronous save operation. /// public Task SaveRequestAsync(ApiRequest apiRequest) { return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest, null); })); } /// /// Processes a diagnosis observation from an API request and persists it as a patient diagnosis. Maps SNOMED-coded observation values to diagnosis properties (description, label, code, state, category, start/end time), falling back to the current date when the observation time is missing, and skipping processing if the observations collection is null. /// /// The API request containing the observation codes, values, message time, and optional observation time used to build the diagnosis. /// The patient associated with the diagnosis, whose identifier is assigned to the new . /// A task that represents the asynchronous insertion of the resulting . /// public async Task ProcessDiagnosisObservation(ApiRequest apiRequest, Patient patient) { _logger.LogDebug("INSERT DiagnosisObservation from obsservation"); var time = apiRequest.ObservationData?.Time; if (time == null) _logger.LogWarning("Processing diagnosis without time. Set Datetime now {DateTimeNow}. ", DateTime.Now); var obs = new PatientDiagnosis { CodingSystem = _diagnosisSystem, Time = time ?? DateTime.Now, PatientId = patient.Id, MessageTime = apiRequest.MessageTime }; if (apiRequest.Observations == null) { _logger.LogError("ApiRequest Observations null. "); return; } for (var i = 0; i <= apiRequest.Observations.Count - 1; i++) { var value = apiRequest.Observations[i].Value; var strValue = value.ToString() ?? "null"; switch (apiRequest.Observations[i].Code) { case "272099008": obs.Description = strValue; break; case "1000000013": obs.Label = strValue; break; case "1000000014": obs.Code = strValue; break; case "394731006": obs.State = strValue; break; case "272125009": obs.Category = strValue; break; case "398201009": if (DateTime.TryParse(apiRequest.Observations[i].Value.ToString(), out var startTime)) obs.StartTime = startTime; break; case "397898000": if (DateTime.TryParse(apiRequest.Observations[i].Value.ToString(), out var endTime)) obs.EndTime = endTime; break; } } await InsertDiagnosis(obs); } /// /// Processes an incoming API request for diagnosis-related observations, resolving the patient from the request when not supplied. Validates that at least one of the patient number or location unit name is provided, handles ORU_R01 and ORU_R40 observation messages, and triggers diagnosis observation processing when the observation code is recognized. /// /// The API request containing the type, patient identifiers, location, and observation data to be processed. /// An optional pre-resolved patient; when null, the patient is resolved via the patient service using the request data. /// Thrown when both the patient number and the location unit name are missing from the request, or when the request type is not valid for diagnosis processing. /// public async Task SaveRequest(ApiRequest apiRequest, Patient? patient) { if (patient == null) { _logger.LogDebug("message:ApiRequest Diagnosis"); if (string.IsNullOrEmpty(apiRequest.PatientNumber) && string.IsNullOrEmpty(apiRequest.Location?.UnitName)) { _logger.LogDebug("person and PointOfCare are nulls"); throw new ApiRequestException("person and PointOfCare are nulls"); } _logger.LogDebug("patientNumber: {apiRequestpatientNumber} location: {apiRequestlocation}", apiRequest.PatientNumber, apiRequest.Location); _logger.LogDebug("RequestType: {apiRequesttype}", apiRequest.Type); patient = await _patientService.Value.FindPatientByApiRequest(apiRequest); } if (patient == null) { // NO PATIENTS OR LOCATIONS WERE FOUND _logger.LogWarning( "Observation for patient: {apiRequestpatientNumber} with location: {apiRequestlocation} not found, ignoring", apiRequest.PatientNumber, apiRequest.Location); return; } var unitConfig = await _unitService.FindById(patient.UnitId); if (!Hl7Utils.ManageAutoAdt(unitConfig, null, _logger, "ORU Diagnosis")) return; switch (apiRequest.Type) { //* ORU_R01 - Unsolicited transmission of an observation message //* ORU_R40 - Unsolicited transmission of an alert observation message case "ORU_R01": case "ORU_R40": // OBSERVATIONS if (apiRequest.Observation != null && apiRequest.Observations?.FirstOrDefault() == null) apiRequest.Observations = [apiRequest.Observation]; if (apiRequest.Observations != null) { var obrcode = apiRequest.ObservationData?.Code; if (apiRequest.ObservationData?.Value != null) apiRequest.Observations.Add(new PatientObservation { Value = apiRequest.ObservationData.Value }); if (obrcode != null && _diagnosisCode.Contains(obrcode)) _ = ProcessDiagnosisObservation(apiRequest, patient); } break; default: _logger.LogWarning("ApiRequest type {apiRequesttype} sis not valid for Diagnosis", apiRequest.Type); throw new ApiRequestException("ApiRequest type " + apiRequest.Type + " is not valid for Diagnosis"); } } /// /// Processes a list of patient diagnoses by associating each entry with the specified patient and message time, then inserting them as diagnosis observations. /// /// The list of patient diagnoses to be processed and inserted. /// The patient whose identifier is assigned to each diagnosis entry. /// The timestamp assigned to each diagnosis entry during processing. /// public async Task ProcessDiagnosis(List diagnosis, Patient patient, DateTime messageTime) { _logger.LogDebug("INSERT DiagnosisObservation from diagnosis"); foreach (var d in diagnosis) { d.PatientId = patient.Id; d.Time = messageTime; await InsertDiagnosis(d); } } /// /// Updates many diagnosis records, replacing the with the new for the specified field, by delegating to the diagnosis repository. /// /// The name of the identifier field used to locate the records to update. /// The new ObjectId to assign to the matching records. /// The existing ObjectId to be replaced in the matching records. /// public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId) { await _diagnosisRepository.UpdateManyObjectId(nameId, id, oldId); } /// /// Retrieves all diagnoses associated with the specified patient identifier from the diagnosis repository. /// /// The unique identifier of the patient whose diagnoses are being requested. /// A task that represents the asynchronous operation, containing a list of records for the given patient. /// public async Task> GetByPatient(ObjectId id) { return await _diagnosisRepository.GetByPatient(id); } /// /// Inserts a patient diagnosis into the repository after mapping it to the underlying data model, and broadcasts the stored record. /// If the diagnosis cannot be mapped (returns null), the insert and broadcast operations are skipped. /// /// The patient diagnosis to insert. /// public async Task Insert(PatientDiagnosis diagnosis) { _logger.LogDebug("Insert {diagnosis}", diagnosis); var diag = await MapDiagnosis(diagnosis); if (diag != null) { await _diagnosisRepository.InsertOneAsync(diag); await SendBroadcast(diag); } } /// /// Maps a through the calculated observations mapper to produce a transformed diagnosis instance. /// When the mapper yields a result, indicating the diagnosis is not applicable or cannot be mapped, a debug message is logged and the result is returned as-is. /// /// The patient diagnosis to be mapped. /// The mapped produced by the calculated observations mapper, or if the diagnosis was ignored. /// private async Task MapDiagnosis(PatientDiagnosis diagnosis) { var diagnosis2 = await _calculatedObservations.Value.Map(diagnosis); if (diagnosis2 == null) _logger.LogDebug("Mapping {diagnosis}: Ignored", diagnosis2); return diagnosis2; } /// /// Sends a patient diagnosis broadcast to all subscribers whose registered location matches the patient's location (unit, room, and bed). Returns early without broadcasting if the patient cannot be found. /// /// The patient diagnosis payload to broadcast to the matching subscribers. /// private async Task SendBroadcast(PatientDiagnosis diagnosis) { var patient = await _patientService.Value.FindById(diagnosis.PatientId); if (patient == null) return; var displaySubscribers = _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(); displaySubscribers.ForEach(Action); return; void Action(WsSubscriber subscriber) { _clientMessageService.SendAsync(subscriber.Id, OperationType.Diagnosis, diagnosis); } } /// /// Asynchronously retrieves all patient diagnosis records associated with the specified patient identifier by delegating to the diagnosis repository. /// /// The unique identifier of the patient whose diagnosis records are to be retrieved. /// A task that represents the asynchronous operation, containing an asynchronous cursor over the matching records. /// public Task> FindByPatientIdAsync(ObjectId patientId) { return _diagnosisRepository.FindByPatientIdAsync(patientId); } /// /// Inserts or updates a patient diagnosis, creating an audit log entry. If a diagnosis /// already exists for the same patient, code, and coding system, it is updated while /// preserving its identifier and original timestamp; otherwise a new record is inserted. /// A broadcast is dispatched asynchronously after a successful insert or update. /// /// The patient diagnosis to persist. /// public async Task InsertDiagnosis(PatientDiagnosis diagnosis) { try { _logger.LogDebug("Insert {diagnosis}", diagnosis); var diag = await MapDiagnosis(diagnosis); if (diag == null) { _logger.LogError("Mepped diagnosis is null. Diagnosis: {diagnosis}", diagnosis); } else { var dgdb = await _diagnosisRepository.FindByPatientIdAndCode(diag.PatientId, diag.Code, diag.CodingSystem); if (dgdb != null) { var auxDgdb = dgdb; diag.Id = dgdb.Id; diag.Time = dgdb.Time; diag.UpdateDate = diagnosis.Time; await _diagnosisRepository.UpdateOneAsync(diag.Id, diag); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxDgdb, dgdb); } else { await _diagnosisRepository.InsertOneAsync(diagnosis); await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, diagnosis); } _ = SendBroadcast(diagnosis); } } catch (Exception ex) { _logger.LogError("Error inserting diagnosis. Diagnosis: {diagnosis}. Exception:{ex}", diagnosis, ex); } } }