using adas_core.Application.Repositories.Interfaces; using adas_core.Domain.Enums; using adas_core.Domain.Models; using adas_core.Domain.Models.AppSettings; using adas_core.Domain.Models.DTO; using adas_core.Domain.Models.Filter; using adas_core.Domain.Models.MongoModels; using Microsoft.Extensions.Options; using MongoDB.Bson; using MongoDB.Driver; using Serilog; using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; namespace adas_core.Infrastructure.Repositories; /// /// Represents a MongoDB-backed repository for entities, providing the persistence operations defined by the interface. /// public class PointOfCareRepository : MongoRepository, IPointOfCareRepository { private readonly ApiSettings _apiSettings; public PointOfCareRepository(IOptions? apiSettings, IMongoDatabase database) : base(database) { if (apiSettings != null) _apiSettings = apiSettings.Value; else throw new ArgumentNullException(nameof(apiSettings)); } /// /// Retrieves the collection name used for Locations operations from the API settings configuration. /// /// The configured Locations collection name retrieved from the API settings. public override string GetCollectionName() { return _apiSettings.Locations; } /// /// Asynchronously inserts a single record, adding error logging around the base insertion behavior. /// If the base insert operation fails, the exception is written to the console and logged, then rethrown to preserve the original failure. /// /// The entity to insert. public override async Task InsertOneAsync(PointOfCare pointOfCare) { try { await base.InsertOneAsync(pointOfCare); } catch (Exception e) { Console.WriteLine(e); Log.Error("Exception trying to insert pointOfCare: {pointOfCare}. Exception {e}", pointOfCare, e); throw; } } /// /// Deletes the entity matching the specified from the underlying collection. /// Any exception encountered during the delete operation is logged and rethrown to the caller. /// /// The unique identifier of the entity to remove. public async Task Delete(ObjectId id) { try { var filter = Builders.Filter.Eq(x => x.Id, id); await Collection.DeleteOneAsync(filter, null); } catch (Exception e) { Log.Error("Exception trying to delete pointOfCare: {id}. Exception {e}", id, e); throw; } } /// /// Deletes all records associated with the specified unit identifier. /// Returns true if the deletion completes successfully, or false if an exception is caught and logged. /// /// The identifier of the unit whose related records should be removed. /// A task that resolves to true when the records are deleted, or false when an error occurs during the operation. public async Task DeleteManyByUnitId(ObjectId unitId) { try { var filter = Builders.Filter.Where(p => p.UnitId == unitId); await Collection.DeleteManyAsync(filter); return true; } catch (Exception e) { Log.Error(e.Message); return false; } } /// /// Updates an existing PointOfCare entity asynchronously in the data store. /// /// The PointOfCare entity to update, identified by its Id. public async Task Update(PointOfCare pointOfCare) { try { await UpdateOneAsync(pointOfCare.Id, pointOfCare); } catch (Exception e) { Log.Error("Exception trying to update pointOfCare: {pointOfCare}. Exception {e}", pointOfCare, e); throw; } } /// /// Updates the unit name and unit identifier of a PointOfCare document identified by the specified id. /// /// The ObjectId of the PointOfCare document to update. /// The Unit object whose Name and Id values will be set on the matching document. public async Task UpdateUnitId(ObjectId id, Unit unit) { var filterBuilder = Builders.Filter; var filter = filterBuilder.Eq(p => p.Id, id); var update = Builders.Update .Set(p => p.UnitName, unit.Name) .Set(p => p.UnitId, unit.Id); await Collection.UpdateOneAsync(filter, update); } /// /// Updates the relay configuration of a Point of Care by replacing its relay identifier list /// with the identifiers extracted from the provided relay configuration entries. /// /// The unique identifier of the Point of Care whose relay configuration will be updated. /// The collection of relays whose identifiers will be stored as the new relay configuration. public async Task UpdateRelayConfig(ObjectId pocId, List relayConfig) { var filterBuilder = Builders.Filter; var filter = filterBuilder.Eq(p => p.Id, pocId); var relayIds = relayConfig.Select(r => r.Id).ToList(); var update = Builders.Update .Set(p => p.Configuration!.RelayIdList, relayIds); await Collection.UpdateOneAsync(filter, update); } /// /// Updates the relay configuration of a specific point of care by replacing its associated relay identifier list. /// /// The identifier of the point of care whose relay configuration will be updated. /// The new list of relay identifiers to assign to the point of care's configuration. public async Task UpdateRelayConfig(ObjectId pocId, List relayConfig) { var filterBuilder = Builders.Filter; var filter = filterBuilder.Eq(p => p.Id, pocId); var update = Builders.Update .Set(p => p.Configuration!.RelayIdList, relayConfig); await Collection.UpdateOneAsync(filter, update); } /// /// Updates the of an existing entity identified by the specified , setting only the configuration field via a partial update. /// /// The unique identifier of the entity to update. /// The new configuration values to apply to the entity. public async Task UpdateConfiguration(ObjectId id, PointOfCareConfiguration configuration) { var filterBuilder = Builders.Filter; var filter = filterBuilder.Eq(p => p.Id, id); var update = Builders.Update .Set(p => p.Configuration, configuration); await Collection.UpdateOneAsync(filter, update); } /// /// Retrieves a entity by its unique identifier from the underlying collection. /// Returns null if no matching entity is found or if an error occurs during the lookup. /// /// The unique of the point of care to retrieve. /// A if a matching record is found; otherwise, null. public async Task FindById(ObjectId id) { try { var filter = Builders.Filter.Eq(p => p.Id, id); return await Collection.Find(filter).FirstOrDefaultAsync(); } catch (Exception ex) { Log.Error("Error searching pointOfCare by id: {id}. Exception: {ex}", id, ex); return null; } } /// /// Retrieves the collection of entries associated with the specified unit and status. When is true, virtual entries such as Pushed, Unknown, Deleted, NoBed, Cancelled, Recovered, and Moved are excluded from the results. If an error occurs during the lookup, an empty collection is returned and the exception is logged. /// /// The identifier of the unit whose point of care entries will be searched. /// The point of care status used to filter the results. /// When true, filters out entries whose bed matches one of the known values; otherwise virtual entries are included. /// A task that resolves to an containing the matching entries, or an empty collection if the query fails. public async Task> FindByUnitAndStatus(ObjectId unitId, StatusEnum.PointOfCare status, bool excludeVirtual = false) { try { var filterBuilder = Builders.Filter; var filter = filterBuilder.And( filterBuilder.Eq(p => p.UnitId, unitId), filterBuilder.Eq(p => p.Status, status) ); if (excludeVirtual) filter = filterBuilder.And( filter, filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Pushed.ToString()), filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Unknown.ToString()), filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Deleted.ToString()), filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.NoBed.ToString()), filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Cancelled.ToString()), filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Recovered.ToString()), filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Moved.ToString()) ); var result = await Collection.FindAsync(filter); return result.ToEnumerable(); } catch (Exception ex) { Log.Error("Error searching pointOfCares by Unit: {unit} and status: {}. Exception: {ex}", unitId.ToString(), status.ToString(), ex); return new List(); } } public async Task FindByBedAndUnitId(string? bed, ObjectId unitId) { try { if (string.IsNullOrEmpty(bed)) return null; var filterBuilder = Builders.Filter; List> filters = [ filterBuilder.Eq(p => p.Bed, bed), filterBuilder.Eq(p => p.UnitId, unitId) // Combina todos los filtros en uno solo usando el operador & si hay más de un filtro, de lo contrario, usa un filtro vacío. ]; // Combina todos los filtros en uno solo usando el operador & si hay más de un filtro, de lo contrario, usa un filtro vacío. var combinedFilter = filters.Count > 0 ? filters.Aggregate((current, next) => current & next) : filterBuilder.Empty; var result = await Collection.Find(combinedFilter).Limit(1).FirstOrDefaultAsync(); return result; } catch (Exception ex) { Log.Error("Error searching pointOfCares by patient location unitId: {unitId}, bed: {bed}. Exception: {ex}", unitId, bed, ex); return null; } } /// /// Retrieves all points of care associated with the specified unit. If an error occurs during the search, the exception is logged and null is returned. /// /// The identifier of the unit whose points of care should be retrieved. /// A task that yields a collection of instances matching the given unit, or null if the search fails. public async Task?> FindAllByUnitId(ObjectId unit) { try { var filter = Builders.Filter.Eq(p => p.UnitId, unit); var result = await Collection.FindAsync(filter); return result.ToEnumerable(); } catch (Exception ex) { Log.Error("Error searching pointOfCares by Unit: {unit}. Exception: {ex}", unit, ex); return null; } } /// /// Asynchronously retrieves all entities that match the specified room. /// Returns if an error occurs while querying the collection. /// /// The room identifier used to filter the point-of-care records. /// A task that represents the asynchronous operation. The task result contains a collection of matching entries, or if the search fails. public async Task?> FindByRoom(string room) { try { var filter = Builders.Filter.Eq(p => p.Room, room); var result = await Collection.FindAsync(filter); return result.ToEnumerable(); } catch (Exception ex) { Log.Error("Error searching pointOfCares by room: {room}. Exception: {ex}", room, ex); return null; } } /// /// Asynchronously finds all records matching the specified bed. /// Returns null if an error occurs during the search. /// /// The bed identifier used to filter the point of care records. /// A task containing an enumerable collection of matching records, or null if an error occurs. public async Task?> FindByBed(string bed) { try { var filter = Builders.Filter.Eq(p => p.Bed, bed); var result = await Collection.FindAsync(filter); return result.ToEnumerable(); } catch (Exception ex) { Log.Error("Error searching pointOfCares by bed: {bed}. Exception: {ex}", bed, ex); return null; } } /// /// Finds a list of documents matching the specified filter, optionally applying a projection to shape the returned fields. /// When a projection is provided, only the projected fields are returned; otherwise, the full documents are returned. /// /// The MongoDB filter definition used to match the desired documents. /// An optional projection definition to limit or transform the fields returned in each document. When null, the full documents are returned. /// A task that represents the asynchronous operation, containing a list of documents that match the filter. public async Task> FindByFilter(FilterDefinition filter, ProjectionDefinition? projection = null) { if (projection != null) return await Collection.Find(filter).Project(projection).ToListAsync(); return await Collection.Find(filter).ToListAsync(); } /// /// Asynchronously counts the number of documents associated with the specified unit, excluding entries whose bed is a virtual or non-representative value (Pushed, Unknown, Deleted, NoBed, Cancelled, UnitData, Recovered, or Moved). /// /// The identifier of the unit whose point-of-care entries should be counted. /// A that resolves to the number of matching point-of-care documents for the given unit. public async Task CountByUnitId(ObjectId unitId) { try { var filterBuilder = Builders.Filter; var excludedBeds = new[] { VirtualPointOfCare.Pushed.ToString(), VirtualPointOfCare.Unknown.ToString(), VirtualPointOfCare.Deleted.ToString(), VirtualPointOfCare.NoBed.ToString(), VirtualPointOfCare.Cancelled.ToString(), VirtualPointOfCare.UnitData.ToString(), VirtualPointOfCare.Recovered.ToString(), VirtualPointOfCare.Moved.ToString() }; var filter = filterBuilder.And( filterBuilder.Eq(p => p.UnitId, unitId), filterBuilder.Nin(p => p.Bed, excludedBeds) ); return await Collection.CountDocumentsAsync(filter); } catch (Exception e) { Log.Logger.Error(e.Message); throw; } } public async Task CountVirtualsByUnitId(ObjectId unitId) { try { var filterBuilder = Builders.Filter; // Lista de estados que NO quieres contar var excludedBeds = new[] { VirtualPointOfCare.Pushed.ToString(), VirtualPointOfCare.Unknown.ToString(), VirtualPointOfCare.Deleted.ToString(), VirtualPointOfCare.NoBed.ToString(), VirtualPointOfCare.Cancelled.ToString(), VirtualPointOfCare.Recovered.ToString(), VirtualPointOfCare.UnitData.ToString(), VirtualPointOfCare.Moved.ToString() }; // Filtramos por UnitId Y que el Bed NO esté en la lista de excluidos var filter = filterBuilder.And( filterBuilder.Eq(p => p.UnitId, unitId), filterBuilder.In(p => p.Bed, excludedBeds) ); return await Collection.CountDocumentsAsync(filter); } catch (Exception e) { Log.Logger.Error(e.Message); throw; } } /// /// Retrieves the configuration identified by the specified , projecting only the identifier and configuration fields. /// Returns null when no matching point of care is found. /// /// The unique identifier of the point of care whose configuration is being requested. /// A containing the matching with its configuration, or null if no record exists for the given identifier. public async Task GetPoCConfiguration(ObjectId pocId) { var filter = Builders.Filter.Eq(p => p.Id, pocId); var projection = Builders.Projection .Include(p => p.Id) .Include(p => p.Configuration); return await Collection.Find(filter).Project(projection).FirstOrDefaultAsync(); } /// /// Retrieves all records from the underlying data store using an empty filter. /// /// A containing a of items, or null when no results are returned. public async Task?> GetAll() { var filter = Builders.Filter.Empty; return await Collection.Find(filter).ToListAsync(); } /// /// Retrieves a by its identifier, enriching the result with the full configuration details by performing lookups against the light beacons, cameras, and relays collections. /// Returns null when no matching point of care is found. /// /// The unique identifier of the point of care to retrieve. /// A task that represents the asynchronous operation, containing the matching with its resolved beacon, camera, and relay configuration, or null if not found. public async Task FindByIdAllConfig(ObjectId id) { return await Collection.Aggregate() .Match(c=>c.Id == id) .Lookup(_apiSettings.LightBeacons, "configuration.beaconIdList", "_id", "beacons") .Lookup(_apiSettings.Cameras, "configuration.cameraIdList", "_id", "cameras") .Lookup(_apiSettings.Relays, "configuration.relayIdList", "_id", "relays") .Project(new BsonDocument { { "_id", 1 }, { "room", 1 }, { "bed", 1 }, { "hall", 1 }, { "unitId", 1 }, { "status", 1 }, { "admissionId", 1 }, { "configuration", new BsonDocument { { "beaconList", "$beacons" }, { "cameraList", "$cameras" }, { "relayList", "$relays" }, { "beaconIdList", "$configuration.beaconIdList" }, { "cameraIdList", "$configuration.cameraIdList" }, { "relayIdList", "$configuration.relayIdList" }, { "type", "$configuration.type" }, { "id", "$configuration.id" } } } }).FirstOrDefaultAsync(); } /// /// Obtiene de forma asíncrona todos los identificadores únicos de cámaras que están /// vinculados a algún PointOfCare. /// /// /// Se retorna un para optimizar la búsqueda de pertenencia (Contains) en el servicio. /// Mientras que una requiere un tiempo de búsqueda lineal $O(n)$, el HashSet utiliza /// una tabla hash que permite verificar si una cámara está en uso en tiempo constante $O(1)$. /// Esto es crítico para mantener el rendimiento al comparar los IDs de la página actual /// contra el total de cámaras en uso, independientemente del volumen de datos. /// /// Un conjunto hash con los de las cámaras en uso. public async Task> FindAllIdCamerasInUse() { var distinctIds = await Collection .DistinctAsync("configuration.cameraIdList", Builders.Filter.Empty); var list = await distinctIds.ToListAsync(); return new HashSet(list); } /// /// Retrieves all distinct beacon identifiers currently referenced by any point-of-care configuration. /// /// A set containing the unique values of beacons in use across all point-of-care records. public async Task> FindAllIdBeaconsInUse() { var distinctIds = await Collection .DistinctAsync("configuration.beaconIdList", Builders.Filter.Empty); var list = await distinctIds.ToListAsync(); return new HashSet(list); } /// /// Retrieves all points of care associated with the specified unit, including their related beacons, cameras, and relays resolved through MongoDB lookups and projected to the PointOfCare model. /// /// The identifier of the unit used to filter the points of care. /// A task that returns a collection of PointOfCare with their associated device lists populated, or null if an error occurs during the query. public async Task?> FindAllByUnitIdWithDevices(ObjectId unitId) { try { var pipeline = Collection.Aggregate() // 1. Filtramos primero por UnitId (muy importante para rendimiento) .Match(p => p.UnitId == unitId) // 2. Realizamos los Lookups usando las colecciones desde settings .Lookup(_apiSettings.LightBeacons, "configuration.beaconIdList", "_id", "beacons") .Lookup(_apiSettings.Cameras, "configuration.cameraIdList", "_id", "cameras") .Lookup(_apiSettings.Relays, "configuration.relayIdList", "_id", "relays") // 3. Proyectamos para que coincida exactamente con tu modelo C# .Project(new BsonDocument { { "_id", 1 }, { "room", 1 }, { "bed", 1 }, { "hall", 1 }, { "unitId", 1 }, { "status", 1 }, { "admissionId", 1 }, { "configuration", new BsonDocument { // Mapeamos los arrays temporales a las propiedades de la clase { "beaconList", "$beacons" }, { "cameraList", "$cameras" }, { "relayList", "$relays" }, { "beaconIdList", "$configuration.beaconIdList" }, { "cameraIdList", "$configuration.cameraIdList" }, { "relayIdList", "$configuration.relayIdList" }, { "type", "$configuration.type" }, { "id", "$configuration.id" } } } }); return await pipeline.ToListAsync(); } catch (Exception ex) { Log.Error("Error searching pointOfCares by Unit: {unit}. Exception: {ex}", unitId, ex); return null; } } /// /// Retrieves the set of distinct relay identifiers currently referenced by any configuration in the collection. /// /// A of values containing all unique relay IDs found across the configuration.relayIdList field of every document. public async Task> FindAllIdRelaysInUse() { var distinctIds = await Collection .DistinctAsync("configuration.relayIdList", Builders.Filter.Empty); var list = await distinctIds.ToListAsync(); return new HashSet(list); } /// /// Retrieves all point-of-care configurations, joining each configuration with its associated light beacons, cameras, and relays through MongoDB lookups and projecting the combined result into a flattened structure. /// /// A task that resolves to a list of objects enriched with the corresponding beacon, camera, and relay details, or null when no configurations are found. public async Task?> GetAllConfigs() { var pipeline = Collection.Aggregate() .Lookup( _apiSettings.LightBeacons, "configuration.beaconIdList", "_id", "beacons" ) .Lookup( _apiSettings.Cameras, "configuration.cameraIdList", "_id", "cameras" ) .Lookup( _apiSettings.Relays, "configuration.relayIdList", "_id", "relays" ) .Project(new BsonDocument { { "_id", 1 }, { "room", 1 }, { "bed", 1 }, { "hall", 1 }, { "unitId", 1 }, { "status", 1 }, { "admissionId", 1 }, { "configuration", new BsonDocument { { "beaconList", "$beacons" }, { "cameraList", "$cameras" }, { "relayList", "$relays" }, { "type", "$configuration.type" }, { "id", "$configuration.id" } } } }); return await pipeline.ToListAsync(); } /// /// Retrieves all point of care location records from the collection, projecting only the essential location fields (Id, UnitId, Room, and Bed). /// Uses an empty filter to return every record and returns a nullable list that may be null if no results are found. /// /// A task that represents the asynchronous operation, containing a list of objects with the projected fields, or null if no matching records exist. public async Task?> GetAllLocationInfo() { var filter = Builders.Filter.Empty; var projection = Builders.Projection .Include(p => p.Id) .Include(p => p.UnitId) .Include(p => p.Room) .Include(p => p.Bed); var result = await Collection.Find(filter).Project(projection).ToListAsync(); return result; } /// /// Retrieves a paginated, filtered query of records, supporting optional filtering by unit (by id or name) and status, sorted by id descending. /// /// The pagination and filtering criteria. If FilteredRequest is null, no additional filters are applied. /// An representing the resulting MongoDB query with the applied filters and sort. public IFindFluent GetPaginatedPoCs(PaginationFilter filter) { var filterBuilder = Builders.Filter; var sort = Builders.Sort.Descending("_id"); var filters = new List>(); if (filter.FilteredRequest == null) return CreateFindFluent(filters, sort); // Verifica UnitName contiene el valor de FilteredRequest.Text if (!string.IsNullOrEmpty(filter.FilteredRequest?.UnitId) && ObjectId.TryParse(filter.FilteredRequest.UnitId, out var unitId)) filters.Add(filterBuilder.Eq(d => d.UnitId, unitId)); else if (!string.IsNullOrEmpty(filter.FilteredRequest?.UnitName)) filters.Add(filterBuilder.Eq(d => d.Unit!.Name, filter.FilteredRequest?.UnitName)); if (filter.FilteredRequest?.PointOfCareStatus != null) { var statusFilter = filter.FilteredRequest.PointOfCareStatus; filters.Add(filterBuilder.Eq(p => p.Status, statusFilter)); } return CreateFindFluent(filters, sort); } //Deprecated PatientLocation by UnitName public async Task FindByPatientLocation(PatientLocation patientLocation) { try { var filterBuilder = Builders.Filter; List> filters = []; if (!string.IsNullOrEmpty(patientLocation.UnitName)) filters.Add(filterBuilder.Eq(p => p.UnitName, patientLocation.UnitName)); if (!string.IsNullOrEmpty(patientLocation.Bed)) filters.Add(filterBuilder.Eq(p => p.Bed, patientLocation.Bed)); if (!string.IsNullOrEmpty(patientLocation.Room)) filters.Add(filterBuilder.Eq(p => p.Room, patientLocation.Room)); // Combina todos los filtros en uno solo usando el operador & si hay más de un filtro, de lo contrario, usa un filtro vacío. var combinedFilter = filters.Count > 0 ? filters.Aggregate((current, next) => current & next) : filterBuilder.Empty; var result = await Collection.Find(combinedFilter).Limit(1).FirstOrDefaultAsync(); return result; } catch (Exception ex) { Log.Error("Error searching pointOfCares by patient location: {bed}. Exception: {ex}", patientLocation, ex); return null; } } /// /// Creates MongoDB indexes for the collection on the unitId, room, and bed fields, using background creation and non-unique constraints, to optimize query performance. /// public override async Task CreateIndexes() { var options = new CreateIndexOptions { Background = true, Unique = false }; var indexes = new List> { new("{ unitId: 1 }", options), new("{ room: 1 }", options), new("{ bed: 1 }", options) }; await MongoUtils.EnsureIndexes(Collection, indexes); } /// /// Updates the status of an existing point-of-care record identified by its unique identifier. /// /// The unique identifier of the point-of-care record to update. /// The new status value to apply to the point-of-care record. public async Task UpdateStatus(ObjectId id, StatusEnum.PointOfCare status) { var filterBuilder = Builders.Filter; var filter = filterBuilder.Eq(p => p.Id, id); var update = Builders.Update .Set(p => p.Status, status); await Collection.UpdateOneAsync(filter, update); } /// /// Builds a queryable for by combining the provided filters with an AND and applying the given sort. When no filters are supplied, an empty filter is used so that all documents are returned. /// /// The list of filter definitions to be combined; an empty list results in no filtering. /// The sort definition applied to the resulting query. /// An representing the filtered and sorted query. private IFindFluent CreateFindFluent(List> filters, SortDefinition sort) { var combinedFilter = filters.Any() ? Builders.Filter.And(filters) : Builders.Filter.Empty; // Filtra todo si no hay filtros return Collection.Find(combinedFilter).Sort(sort); } }