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;
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);
}
}
}