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; 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)); } public override string GetCollectionName() { return _apiSettings.Locations; } 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; } } 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; } } 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; } } 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; } } 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); } 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); } 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); } 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); } 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; } } 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; } } 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; } } 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; } } 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; } } 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(); } 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; } } 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(); } public async Task?> GetAll() { var filter = Builders.Filter.Empty; return await Collection.Find(filter).ToListAsync(); } 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); } public async Task> FindAllIdBeaconsInUse() { var distinctIds = await Collection .DistinctAsync("configuration.beaconIdList", Builders.Filter.Empty); var list = await distinctIds.ToListAsync(); return new HashSet(list); } 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; } } public async Task> FindAllIdRelaysInUse() { var distinctIds = await Collection .DistinctAsync("configuration.relayIdList", Builders.Filter.Empty); var list = await distinctIds.ToListAsync(); return new HashSet(list); } 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(); } 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; } 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; } } 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); } 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); } 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); } }