using adas_core.Application.Exceptions;
using adas_core.Application.Repositories.Interfaces;
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.Filter;
using adas_core.Domain.Models.Responses;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
namespace adas_core.Application.Services;
///
/// Implements and provides the application service responsible for medicine-related operations, coordinating and .
///
///
/// Configured via , instrumented with , supplied with the current HTTP context through , and audited by .
///
///
public class MedicineService(
IMedicineRepository medicineRepository,
ITreatmentService treatmentService,
IOptions apiSettings,
ILogger logger,
IHttpContextAccessor httpContextAccessor,
ILocalAuditService auditService)
: IMedicineService
{
private readonly List _notesIndicatingMedication = apiSettings.Value.NotesIndicatingMedication ?? [];
///
/// Retrieves all medicines from the repository asynchronously.
///
/// A task representing the asynchronous operation, containing a list of all entities.
public async Task> GetAll()
{
return await medicineRepository.GetAll();
}
///
/// Retrieves a medicine from the repository by its unique code, returning null if no matching medicine is found.
///
/// The unique code identifier of the medicine to retrieve.
/// A instance if a medicine with the specified code exists; otherwise, null.
public async Task GetByCode(string code)
{
return await medicineRepository.GetMedicine(code);
}
///
/// Retrieves a list of medicines that match the provided codes or notes by delegating to the medicine repository.
///
/// A list of strings representing the codes or notes used to search for matching medicines.
/// A task that represents the asynchronous operation, containing the list of objects that match the specified codes or notes.
public async Task> GetByCodeOrNote(List codeNote)
{
return await medicineRepository.GetMedicineByCodeOrNote(codeNote);
}
///
/// Retrieves a from the repository by its name.
/// Returns null when no matching medicine is found.
///
/// The name of the medicine to look up.
/// A if a match is found; otherwise, null.
public async Task GetByName(string name)
{
return await medicineRepository.GetMedicineByName(name);
}
///
/// Retrieves the medicines associated with a collection of patient treatments, resolving each treatment's requested give codes against the medicine repository. Falls back to parental nutrition calculation when no medicine is found, and to note-based detection when notes indicate medication; medicines of type "Nutrition" returned by the repository are excluded except when produced through the parental nutrition path.
///
/// The patient treatments to resolve medicines for. Null entries are skipped.
/// An enumerable of medicines resolved from the supplied treatments, aggregating types, codes, groups, and notes when a treatment specifies a RequestedGiveTreatment name.
public async Task> GetMedicinesOfTreatments(IEnumerable treatments)
{
var totalMedicines = new List();
try
{
foreach (var treatment in treatments)
{
if (treatment == null)
continue;
var medicines = new List();
foreach (var code in treatment.RequestedGiveCodes)
{
var medicineList = await medicineRepository.GetMedicineByCodeOrNote([code.Identifier]);
var medicine = medicineList.FirstOrDefault();
//Cant retrieve from medicine list check for parental nutrition and if is not and any note indicate that is medication it is added
//to collection
if (medicine == null)
{
var parentalNutritionMedicine = await CalculateParentalNutritionMedicine(treatment);
if (parentalNutritionMedicine != null)
{
medicines.Add(parentalNutritionMedicine);
continue;
}
var isMedicine = treatment.Notes.Any(n => _notesIndicatingMedication.Contains(n.Comment));
if (isMedicine)
medicines.Add(new Medicine
{
Codes = [code.Identifier],
Name = code.Text
});
}
else if (!medicine.Type.Contains("Nutrition"))
{
medicines.Add(medicine);
}
}
if (!string.IsNullOrEmpty(treatment.RequestedGiveTreatment))
try
{
medicines =
[
new Medicine
{
Name = treatment.RequestedGiveTreatment,
Type = medicines.FindAll(t => t.Type.Any())
.Select(m => m.Type.Aggregate((x, y) => x + "," + y)).Distinct()
.ToList(),
Codes = medicines.FindAll(t => t.Codes.Any())
.Select(m => m.Codes.Aggregate((x, y) => x + "," + y)).Distinct()
.ToList(),
Group = medicines.FindAll(t => t.Group.Any())
.Select(m => m.Group.Aggregate((x, y) => x + "," + y)).Distinct()
.ToList(),
Notes = medicines.FindAll(t => t.Notes.Any())
.Select(m => m.Notes.Aggregate((x, y) => x + "," + y)).Distinct()
.ToList()
}
];
}
catch (Exception ex)
{
logger.LogError("Error Getting Medicines from Treatment. {medicines} . Exception {ex}",
string.Join(",", medicines), ex);
}
totalMedicines.AddRange(medicines);
}
}
catch (Exception e)
{
logger.LogError("Error get Medicines Of Treatments: {eMessage} {eStackTrace}", e.Message, e.StackTrace);
}
return totalMedicines.AsEnumerable();
}
///
/// Retrieves all active medicines associated with a patient by first resolving their active treatments and then collecting the medicines prescribed within them.
///
/// The unique identifier of the patient whose active medicines are being queried.
/// A task that represents the asynchronous operation, containing an enumerable collection of instances linked to the patient's active treatments.
public async Task> GetActiveMedicinesByPatient(ObjectId patientId)
{
var activeTreatments = await treatmentService.GetActiveTreatmentsByPatient(patientId);
return await GetMedicinesOfTreatments(activeTreatments);
}
///
/// Retrieves a paginated collection of medicines based on the specified pagination filter, including the total document count for the current query.
///
/// The pagination filter containing the page number and page size used to determine which subset of medicines to return.
/// A task that represents the asynchronous operation. The task result contains a with the medicines for the requested page, the current page number, the page size, and the total count of medicines matching the query.
public async Task> GetPaginatedMedicines(PaginationFilter filter)
{
var result = medicineRepository.GetPaginatedMedicines(filter);
var count = await result.CountDocumentsAsync();
var data = await result.Skip((filter.PageNumber - 1) * filter.PageSize)
.Limit(filter.PageSize)
.ToCursorAsync();
var dataList = await data.ToListAsync();
return new PaginationResponse(dataList, filter.PageNumber, filter.PageSize, count);
}
///
/// Retrieves a medicine by its unique identifier from the repository.
/// Throws a when no medicine is found matching the provided identifier.
///
/// The unique identifier of the medicine to retrieve.
/// The entity if found; otherwise, the method throws an exception.
/// Thrown when no medicine is found for the specified .
public async Task GetMedicineById(ObjectId medicineId)
{
return await medicineRepository.GetMedicineById(medicineId) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
///
/// Creates a new medicine record in the repository and records an audit log entry for the operation. Throws a conflict exception when the repository fails to produce a result.
///
/// The medicine entity to create.
/// The newly created entity.
/// Thrown when the repository returns a null result, indicating the creation failed.
public async Task PostMedicine(Medicine medicine)
{
var result = await medicineRepository.PostMedicine(medicine) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, result);
return result;
}
///
/// Updates an existing medicine record and records the change in the audit log.
///
/// The medicine entity containing the updated information.
/// The updated , or null if the update did not produce a result.
public async Task UpdateMedicine(Medicine medicine)
{
var oldMedicine = GetMedicineById(medicine.Id);
var newMedicine = await medicineRepository.UpdateMedicine(medicine);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldMedicine, newMedicine);
return newMedicine;
}
///
/// Deletes a medicine identified by its unique identifier and records the operation in the audit log.
/// Captures the existing medicine state prior to deletion to preserve audit trail details.
///
/// The unique identifier of the medicine to delete.
public async Task DeleteMedicineById(ObjectId medicineId)
{
var oldMedicine = GetMedicineById(medicineId);
await medicineRepository.DeleteMedicineById(medicineId);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldMedicine, null);
}
///
/// Asynchronously retrieves all distinct medicine types from the repository. Returns an empty list if an exception occurs during the retrieval process.
///
/// A task representing the asynchronous operation, containing a list of distinct medicine type strings, or an empty list if an error occurs.
public async Task> GetAllTypes()
{
try
{
var bsonDocuments = await medicineRepository.GetDistinctFieldDataQuery("type").ToListAsync();
var result = bsonDocuments.Select(doc => doc["type"].AsString).ToList();
return result;
}
catch (Exception)
{
return [];
}
}
///
/// Asynchronously retrieves all distinct "group" values from the medicine repository and returns them as a list of strings.
/// Returns an empty list if an error occurs while fetching or converting the data.
///
/// A task that represents the asynchronous operation, containing a list of distinct group strings, or an empty list if an exception is encountered.
public async Task> GetAllGroups()
{
try
{
var bsonDocuments = await medicineRepository.GetDistinctFieldDataQuery("group").ToListAsync();
var result = bsonDocuments.Select(doc => doc["group"].AsString).ToList();
return result;
}
catch (Exception)
{
return [];
}
}
///
/// Retrieves all distinct medicine names from the repository asynchronously, returning an empty list if any error occurs during the data retrieval or mapping process.
///
/// A task that represents the asynchronous operation. The task result contains a list of medicine names, or an empty list if the operation fails.
public async Task> GetAllNames()
{
try
{
var bsonDocuments = await medicineRepository.GetDistinctFieldDataQuery("name").ToListAsync();
var result = bsonDocuments.Select(doc => doc["name"].AsString).ToList();
return result;
}
catch (Exception)
{
return [];
}
}
///
/// Asynchronously retrieves all distinct code values from the medicine repository.
/// Returns an empty list if an exception occurs during retrieval.
///
/// A task representing the asynchronous operation, containing a list of code strings, or an empty list if an error occurs.
public async Task> GetAllCodes()
{
try
{
var bsonDocuments = await medicineRepository.GetDistinctFieldDataQuery("codes").ToListAsync();
var result = bsonDocuments.Select(doc => doc["codes"].AsString).ToList();
return result;
}
catch (Exception)
{
return [];
}
}
//Exclusive for H12O, not in calculatedObservations of H12O to dont repeat code in multiple places.
///
/// Calculates a representing the parental nutrition for a patient treatment, returning null when the treatment does not contain a note with the "NPT" comment.
/// The medicine name is taken from the first note with the "formularybaseformulation" comment type, defaulting to "UNKNOWN" when absent, and its type is set to ParenteralNutritionLipids when a neonatal lipids note is present, otherwise ParenteralNutrition.
///
/// The patient treatment whose notes are used to derive the parental nutrition medicine.
/// A task containing the calculated , or null when no "NPT" note is found or when an error is logged during processing.
private Task CalculateParentalNutritionMedicine(PatientTreatment treatment)
{
Medicine? medicine = null;
try
{
if (treatment is { Notes: not null } && treatment.Notes.FirstOrDefault(n => n.Comment == "NPT") != null)
{
medicine = new Medicine
{
Name = treatment.Notes.FirstOrDefault(n => n.CommentType == "formularybaseformulation")?.Comment ??
"UNKNOWN"
};
if (treatment.Notes is { Count: > 0 }) medicine.Notes = treatment.Notes.Select(t => t.Comment).ToList();
if (treatment.Notes.FirstOrDefault(n =>
n.Comment is "LÍPIDOS NEONATALES AL 20%" or "LÍPIDOS NEONATALES AL 20% CON...") != null)
medicine.Type = [MedicineEnum.Types.ParenteralNutritionLipids.ToString()];
else
medicine.Type = [MedicineEnum.Types.ParenteralNutrition.ToString()];
}
}
catch (Exception e)
{
logger.LogError("Error calculate Parental Nutrition Medicine: {eMessage} {eStackTrace}", e.Message,
e.StackTrace);
}
return Task.FromResult(medicine);
}
}