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.DTO; using adas_core.Domain.Models.Filter; using adas_core.Domain.Models.Masters; using adas_core.Domain.Models.MongoModels; using adas_core.Domain.Models.Responses; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using MongoDB.Bson; using MongoDB.Driver; using Serilog; namespace adas_core.Application.Services; public class UnitService( IUnitRepository unitRepository, Lazy patientService, ILogger logger, IMasterListServiceFactory masterListServiceFactory, ISubscribersService subscribersService, Lazy clientMessageService, IPointOfCareService pointOfCareService, IHttpContextAccessor httpContextAccessor, ILocalAuditService auditService) : IUnitService { /// /// Retrieves all units, optionally including their associated PointOfCares. /// When is true, the PointOfCares collection is populated for each unit using the point of care service; otherwise, only the unit data is returned. /// /// If true, loads and assigns the PointOfCares for each unit; if false, returns units without their PointOfCares. /// A task representing the asynchronous operation, containing the list of all units, with PointOfCares populated when requested. public async Task> GetAll(bool withPoCs = false) { var units = await unitRepository.GetAll(); if (withPoCs) foreach (var unit in units) { var pocList = await pointOfCareService.FindAllByUnitId(unit.Id); unit.PointOfCares = pocList?.ToList(); } return units; } /// /// Retrieves all units in a compact representation, mapping each unit to a containing its identifier, name, and title, with null name and title values safely replaced by empty strings. /// /// A task that represents the asynchronous operation. The task result contains a list of objects for all available units. public async Task> GetAllCompact() { var units = await unitRepository.GetAll(); var result = new List(); foreach (var unit in units) result.Add(new UnitInfoDto { Id = unit.Id, Name = unit.Name ?? string.Empty, Title = unit.Title ?? string.Empty }); return result; } /// /// Retrieves a compact representation of a unit by its identifier, returning a populated with the unit's id, name, and title. If no unit is found for the given id, the returned DTO contains a null id with empty name and title values. /// /// The unique identifier of the unit to retrieve. /// A containing the compact unit information, or a DTO with null/empty fields if the unit does not exist. public async Task GetOneCompact(ObjectId id) { var unit = await unitRepository.FindById(id); var result = new UnitInfoDto { Id = unit?.Id, Name = unit?.Name ?? string.Empty, Title = unit?.Title ?? string.Empty }; return result; } /// /// Retrieves a paginated list of units, optionally enriched with their associated Points of Care (PoCs). /// When is true, all PoCs for the returned units are loaded in a single batch call and assigned to each unit. /// /// The pagination parameters controlling the page number, page size, and total count. /// Indicates whether the response units should be populated with their related Points of Care. Defaults to false. /// A containing the requested page of units along with pagination metadata. public async Task> GetPaginatedUnits(PaginationFilter filter, bool withPoCs = false) { var result = unitRepository.GetPaginatedUnits(filter); var count = await result.CountDocumentsAsync(); var data = await result.Skip((filter.PageNumber - 1) * filter.PageSize) .Limit(filter.PageSize) .ToListAsync(); if (withPoCs && data.Any()) { var unitIds = data.Select(u => u.Id).ToList(); var allPocsForUnits = await pointOfCareService.FindAllByUnitIds(unitIds); foreach (var unit in data) unit.PointOfCares = allPocsForUnits.Where(poc => poc.UnitId == unit.Id).ToList(); } return new PaginationResponse(data, filter.PageNumber, filter.PageSize, count); } // public Task GetByPointOfCare(PointOfCare pointOfCare) // { // return _unitRepository.FindByPointOfCare(pointOfCare); // } /// /// Retrieves a by its identifier, optionally hydrating its related master lists (e.g., allergy, diagnosis, origin, doctor, procedure, service, treatment, visit option, access control, language barrier, passive sitting, generic lists) for the specified locale, and optionally including its associated Points of Care. /// /// The unique identifier of the unit to retrieve. /// The locale used to resolve localized values for the related master lists; can be . /// When (default), populates every available related master list referenced by the unit using the given locale; when , only the base unit is returned. /// When , also loads and assigns the Points of Care associated with the unit; when (default), the Points of Care collection is not populated. /// A that yields the requested with its optional related lists and Points of Care, or when no unit matches the identifier. /// Thrown when no unit is found for the supplied . public async Task GetInfo(ObjectId id, LocaleEnum? dataLocale, bool fillLists = true, bool withPoCs = false) { var unit = await Get(id.ToString()) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); //if (unit == null )// || unit.PointOfCares == null tiene sentido? //{ // _logger.LogError("Section not found: {id}", id); // return null; //} if (fillLists) { if (unit.AltableOptionListId.HasValue) unit.AltableOptionList = await masterListServiceFactory.GetMasterListById(MasterListType.AltableOptionList, unit.AltableOptionListId.Value, dataLocale) as AltableOptionList; if (unit.AllergyListId.HasValue) unit.AllergyList = await masterListServiceFactory.GetMasterListById(MasterListType.AllergyList, unit.AllergyListId.Value, dataLocale) as AllergyList; if (unit.DestinationListId.HasValue) unit.DestinationList = await masterListServiceFactory.GetMasterListById(MasterListType.DestinationList, unit.DestinationListId.Value, dataLocale) as DestinationList; if (unit.InternalDestinationListId.HasValue) unit.InternalDestinationList = await masterListServiceFactory.GetMasterListById(MasterListType.InternalDestinationList, unit.InternalDestinationListId.Value, dataLocale) as InternalDestinationList; if (unit.DiagnosisListId.HasValue) unit.DiagnosisList = await masterListServiceFactory.GetMasterListById(MasterListType.DiagnosisList, unit.DiagnosisListId.Value, dataLocale) as DiagnosisList; if (unit.DischargeStatusListId.HasValue) unit.DischargeStatusList = await masterListServiceFactory.GetMasterListById(MasterListType.DischargeStatusList, unit.DischargeStatusListId.Value, dataLocale) as DischargeStatusList; if (unit.DoctorListId.HasValue) unit.DoctorList = await masterListServiceFactory.GetMasterListById(MasterListType.DoctorList, unit.DoctorListId.Value, dataLocale) as DoctorList; if (unit.DoctorTypeListId.HasValue) unit.DoctorTypeList = await masterListServiceFactory.GetMasterListById(MasterListType.DoctorTypeList, unit.DoctorTypeListId.Value, dataLocale) as DoctorTypeList; if (unit.InsulationListId.HasValue) unit.InsulationList = await masterListServiceFactory.GetMasterListById(MasterListType.InsulationList, unit.InsulationListId.Value, dataLocale) as InsulationList; if (unit.MobilityOptionListId.HasValue) unit.MobilityOptionList = await masterListServiceFactory.GetMasterListById(MasterListType.MobilityOptionList, unit.MobilityOptionListId.Value, dataLocale) as MobilityOptionList; if (unit.OriginListId.HasValue) unit.OriginList = await masterListServiceFactory.GetMasterListById(MasterListType.OriginList, unit.OriginListId.Value, dataLocale) as OriginList; if (unit.PatientStatusListId.HasValue) unit.PatientStatusList = await masterListServiceFactory.GetMasterListById(MasterListType.PatientStatusList, unit.PatientStatusListId.Value, dataLocale) as PatientStatusList; if (unit.ProcedureListId.HasValue) unit.ProcedureList = await masterListServiceFactory.GetMasterListById(MasterListType.ProcedureList, unit.ProcedureListId.Value, dataLocale) as ProcedureList; if (unit.TestListId.HasValue) unit.TestList = await masterListServiceFactory.GetMasterListById(MasterListType.TestList, unit.TestListId.Value, dataLocale) as TestList; if (unit.ServiceListId.HasValue) unit.ServiceList = await masterListServiceFactory.GetMasterListById(MasterListType.ServiceList, unit.ServiceListId.Value, dataLocale) as ServiceList; if (unit.TherapeuticCeilingListId.HasValue) unit.TherapeuticCeilingList = await masterListServiceFactory.GetMasterListById(MasterListType.TherapeuticCeilingList, unit.TherapeuticCeilingListId.Value, dataLocale) as TherapeuticCeilingList; if (unit.TreatmentListId.HasValue) unit.TreatmentList = await masterListServiceFactory.GetMasterListById(MasterListType.TreatmentList, unit.TreatmentListId.Value, dataLocale) as TreatmentList; if (unit.VisitOptionListId.HasValue) unit.VisitOptionList = await masterListServiceFactory.GetMasterListById(MasterListType.VisitOptionList, unit.VisitOptionListId.Value, dataLocale) as VisitOptionList; if (unit.AccessControlListId.HasValue) unit.AccessControlList = await masterListServiceFactory.GetMasterListById(MasterListType.AccessControlList, unit.AccessControlListId.Value, dataLocale) as AccessControlList; if (unit.LanguageBarrierListId.HasValue) unit.LanguageBarrierList = await masterListServiceFactory.GetMasterListById(MasterListType.LanguageBarrierList, unit.LanguageBarrierListId.Value, dataLocale) as LanguageBarrierList; if (unit.PassiveSittingListId.HasValue) unit.PassiveSittingList = await masterListServiceFactory.GetMasterListById(MasterListType.PassiveSittingList, unit.PassiveSittingListId.Value, dataLocale) as PassiveSittingList; if (unit.GenericListId.HasValue) unit.GenericList = await masterListServiceFactory.GetMasterListById(MasterListType.GenericList, unit.GenericListId.Value, dataLocale) as GenericList; } if (withPoCs) { var pocList = await pointOfCareService.FindAllByUnitId(unit.Id); unit.PointOfCares = pocList?.ToList(); } return unit; } /// /// Retrieves a unit by its identifier and, when requested, enriches it with its associated points of care (including their devices). /// /// The identifier of the unit to look up. /// When true, loads and assigns the unit's points of care to the result. /// Flag intended to control device inclusion alongside the points of care. /// The matching instance. /// Thrown when no unit exists for the supplied . public async Task GetInfo(ObjectId id, bool withPoCs = true, bool withDevices = true) { var unit = await Get(id.ToString()) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); if (withPoCs) { var pocList = await pointOfCareService.FindAllByUnitIdWithDevices(unit.Id); unit.PointOfCares = pocList?.ToList(); } return unit; } /// /// Retrieves a unit by its name from the repository. If no matching unit is found, a not-found exception is thrown. /// /// The name of the unit to search for. /// The unit matching the specified name. /// Thrown when no unit exists with the provided name. public async Task GetByName(string itemUnitName) { return await unitRepository.FindByName(itemUnitName) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); } /// /// Finds the associated with a given patient by resolving the patient record and returning its linked unit. /// Returns the unit only when the patient exists and has a non-null UnitId; otherwise, throws a not-found exception. /// /// The identifier of the patient whose associated unit should be retrieved. /// A containing the associated if found, or null when no matching unit exists. /// Thrown when the patient has no associated UnitId (i.e., the resource is missing). public async Task FindByPatientId(ObjectId patientId) { //var sections = await GetAll(); var patient = await patientService.Value.FindById(patientId); // if (patient != null && !string.IsNullOrEmpty(patient.Bed) && patient.IsInActivePoC()) // { // // Deberia ser una lista? revisar como gestionar varias unidades con eel mismo pointOfCare // return sections.FirstOrDefault(s => s.PointOfCares.Any(c=>c.Bed==patient.Bed && c.UnitName == patient.UnitString)); // } if (patient is { UnitId: not null }) return await FindById(patient.UnitId); throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); } /// /// Asynchronously retrieves a collection of entities associated with the specified master list identifier and type. /// If an exception occurs during the lookup, the error is logged and null is returned. /// /// The identifier of the master list used to find the associated units. /// The type of the master list used to filter the units. /// A task that represents the asynchronous operation, containing a collection of entities if found, or null if an error occurs. public async Task?> FindUnitsByMasterListId(ObjectId masterListId, MasterListType masterListType) { try { var result = await unitRepository.FindByMasterListId(masterListId, masterListType); return result; } catch (Exception ex) { Log.Error("Exception finding by MasterId{id} section exception:{e} ", masterListId, ex.Message); return null; } } /// /// Asynchronously counts the number of units associated with the specified master list identifier and master list type by delegating to the unit repository. /// /// The unique identifier of the master list whose units should be counted. /// The type of the master list used to filter the units to be counted. /// A representing the asynchronous operation, containing the total number of units that match the given master list identifier and type. public async Task CountUnitsByMasterListId(ObjectId masterListId, MasterListType masterListType) { return await unitRepository.CountUnitsByMasterListId(masterListId, masterListType); } /// /// Retrieves a collection of units associated with the specified master list identifier. /// On failure, logs the exception and returns an empty list as a fallback. /// /// The identifier of the master list whose units should be retrieved. /// A task that yields the collection of items matching the master list identifier, or an empty list if an error occurs. public async Task> FindUnitsByMasterListId(ObjectId masterListId) { try { var result = await unitRepository.FindByMasterListId(masterListId); return result; } catch (Exception ex) { Log.Error("Exception finding by MasterId{id} section exception:{e} ", masterListId, ex.Message); return new List(); } } /// /// Updates the master list information of an existing unit identified by the provided identifier. /// Throws a not found exception if the unit does not exist and a conflict exception if the update operation fails. /// /// The data transfer object containing the unit identifier and the updated master list information. /// The updated if the operation succeeds; otherwise, null when the underlying update returns no result. /// Thrown when no unit is found matching the identifier specified in . /// Thrown when the update operation performed by the repository fails to produce a result. public async Task UpdateUnitMasterList(UpdateUnitIdListDto updateUnitListDto) { var unit = await FindById(updateUnitListDto.UnitId) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); var updatedUnit = await unitRepository.UpdateUnitMasterList(updateUnitListDto) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, unit, updatedUnit); return updatedUnit; } /// /// Updates the configuration of an existing unit and records an audit log entry capturing the previous and resulting state. /// /// The parsed identifier of the unit whose configuration should be updated. /// The new configuration values to apply to the unit. /// A task that resolves to true when the configuration was successfully updated; otherwise, false. /// Thrown when the unit cannot be found either before or after the update operation. /// Thrown when the underlying update operation fails to persist the new configuration. public async Task UpdateConfiguration(ObjectId unitIdParsed, UnitConfiguration unitConfiguration) { var oldConfig = await FindById(unitIdParsed) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); var result = await unitRepository.UpdateConfiguration(unitIdParsed, unitConfiguration); var newConfig = await FindById(unitIdParsed) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); if (!result) throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldConfig, newConfig); return result; } /// /// Retrieves a unit by its identifier. Returns null when the provided identifier is null; otherwise, delegates the lookup to the unit repository. /// /// The identifier of the unit to find. /// The unit matching the specified identifier, or null if the identifier is null. public async Task FindById(ObjectId? id) { if (id == null) return null; return await unitRepository.FindById(id); } /// /// Retrieves a by its name, returning when the provided name is null or empty. /// Otherwise, delegates the lookup to the unit repository. /// /// The name of the unit to look up. Can be or empty. /// A containing the matching , or if no name was provided. public async Task FindByName(string? name) { if (string.IsNullOrEmpty(name)) return null; return await unitRepository.FindByName(name); } /// /// Finds a unit by its name, or alternatively by a point of care bed identifier when no name is provided. /// If the name is supplied, the unit is looked up directly; otherwise, the point of care is resolved from the bed and the associated unit is returned. /// Returns null when neither a name nor a matching point of care is available, or when no unit is found. /// /// The name of the unit to search for. Takes precedence over when provided. /// The point of care bed identifier used as a fallback to locate the unit when is not supplied. /// A task containing the matching , or null if no unit can be resolved from the given inputs. public async Task FindByUnitNameOrPocName(string? name, string? pocName) { if (!string.IsNullOrEmpty(name)) return await unitRepository.FindByName(name); if (!string.IsNullOrEmpty(pocName)) { var poc = await pointOfCareService.FindByBed(pocName); if (poc != null) return await FindById(poc.FirstOrDefault()?.UnitId); } return null; } // public async Task FindByPointOfCare(PointOfCare pointOfCare) // { // return await _unitRepository.FindByPointOfCare(pointOfCare); // } /// /// Inserts a new unit into the repository and records an audit log entry for the operation using the current HTTP context user. Throws a when the repository fails to create the unit. /// /// The unit entity to be inserted. /// The newly created unit returned by the repository. /// Thrown when the repository returns null, indicating that the unit could not be created. public async Task InsertOne(Unit unit) { var newUnit = await unitRepository.InsertOneUnit(unit) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed); await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, newUnit); return newUnit; } /// /// Updates an existing in the repository, creating an audit log entry and broadcasting the change to interested parties. /// /// The unit containing the updated information, including the identifier of the existing unit to modify. /// The updated if the operation succeeded; null if the repository could not persist the update. /// Thrown when no unit exists with the specified identifier. public async Task UpdateUnit(Unit unit) { var oldUnit = await FindById(unit.Id) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); var newUnit = await unitRepository.UpdateUnit(unit); if (newUnit == null) return null; await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldUnit, newUnit); SendUnitBroadcast(newUnit, OperationType.UpdateUnit); return newUnit; } /// /// Updates the name and title of an existing unit, records the change in the audit log, and broadcasts the update to subscribed clients. /// /// The unique identifier of the unit to update. /// The new name to assign to the unit. /// The new title to assign to the unit. /// Optional configuration observer identifier associated with the update. /// The updated unit when the operation succeeds; otherwise, null. /// Thrown when no unit is found for the provided identifier. /// Thrown when the underlying unit update operation fails. public async Task UpdateUnitInfo(ObjectId unitId, string name, string title, string? configObsId = null) { var oldUnit = await FindById(unitId) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); var newUnit = await unitRepository.UpdateUnitInfo(unitId, name, title) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldUnit, newUnit); if (newUnit != null) SendUnitBroadcast(newUnit, OperationType.UpdateUnit); return newUnit; } /// /// Deletes a unit from the repository by its identifier and records an audit log entry for the operation. /// Throws a when the underlying delete operation does not complete successfully. /// /// The unit entity to delete, identified by its Id. /// A task that resolves to true when the unit is successfully deleted. /// Thrown when the delete operation fails (returns null). public async Task DeleteUnitById(Unit unit) { //if (unit is not { Status: null }) throw new ConflictException(ErrorMessage.Conflict_ResourceInUse); _ = await unitRepository.DeleteAsync(unit.Id) ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, unit, null); return true; } /// /// Retrieves a by attempting multiple lookup strategies: first by ObjectId, then by title, and finally by name. /// /// The identifier used to locate the unit. It can be an ObjectId, a title, or a name. /// The matching if found; otherwise, null. public async Task Get(string id) { Unit? section = null; if (ObjectId.TryParse(id, out var oid)) section = await FindById(oid); var sections = await GetAll(); section ??= sections.FirstOrDefault(s => s.Title == id); section ??= sections.FirstOrDefault(s => s.Name == id); return section; } /// /// Retrieves a list of entries that contain a point of care matching the specified , based on bed and unit name criteria. /// /// The providing the Bed and UnitName values used to filter the results. /// A task containing a list of entries whose point of care matches the given location, or null if an error occurs while retrieving the data. public async Task?> FindByLocation(PatientLocation location) { try { var sections = await GetAll(); // Cambiar por comparacion con Location? return sections.Where(section => section.PointOfCares != null && section.PointOfCares.Any(c => c.Bed == location.Bed && c.UnitName == location.UnitName)).ToList(); } catch (Exception e) { Log.Error("Exception finding by location section exception:{e} location: {location} ", e.Message, location); return null; } } /// /// Sends an asynchronous broadcast message about a unit operation to all subscribers whose location IDs match the points of care associated with the given unit. If no points of care are found for the unit, the method returns without sending any messages. Any errors encountered during the broadcast are logged without rethrowing. /// /// The unit whose operation is being broadcast. /// The type of operation performed on the unit, sent as part of the broadcast message. private async void SendUnitBroadcast(Unit unit, OperationType operation) { try { var locations = new List(); var pocs = await pointOfCareService.FindAllByUnitId(unit.Id); if (pocs == null) return; locations.AddRange(pocs.Select(c => c.Id)); var subscribers = subscribersService.GetSubscribers() .Where(s => s.LocationIds.Any(id => locations.Contains(id))) .ToList(); foreach (var subscriber in subscribers) _ = clientMessageService.Value.SendAsync(subscriber.Id, operation, unit); } catch (Exception e) { logger.LogError( "Error sending Unit Broadcast {unit} with operation type {operationToString()} message: {eMessage}", unit, operation.ToString(), e.Message); } } }