using System.Diagnostics; using adas_core.Application.Repositories.Interfaces; using adas_core.Domain.Models; using adas_core.Domain.Models.AppSettings; using Microsoft.Extensions.Options; using MongoDB.Bson; using MongoDB.Driver; using Serilog; namespace adas_core.Infrastructure.Repositories; /// /// Repository implementation for managing PatientObservation archive entities in MongoDB. /// Provides operations for storing and retrieving historical patient observations. /// public class ObservationArchiveRepository : MongoRepository, IObservationArchiveRepository { private readonly ApiSettings _apiSettings; /// /// Initializes a new instance of the ObservationArchiveRepository. /// /// API settings containing collection names configuration. /// The MongoDB database instance. /// Thrown when apiSettings is null. public ObservationArchiveRepository(IOptions apiSettings, IMongoDatabase database) : base(database) { if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); _apiSettings = apiSettings.Value; } //For testing /// /// Retrieves the aggregated last observations for a specific patient. /// Filters observations by name and date, returning the most recent ones. /// /// The ObjectId of the patient. /// The maximum number of observations to return per observation type. /// The cutoff date to filter observations (inclusive). /// Optional list of observation names to filter by. If null, retrieves all distinct observations. /// A list of PatientObservation entities sorted by time descending. public async Task> AggregatedPatientLastObservations(ObjectId patientId, int num, DateTime lastDate, List? filterObservations = null) { filterObservations = await AggregatePatientObservations(patientId, filterObservations); var results = new List(); foreach (var obs in filterObservations) { var filter = Builders.Filter.And( Builders.Filter.Eq(o => o.PatientId, patientId), Builders.Filter.Eq(o => o.Name, obs), Builders.Filter.Lte(o => o.Time, lastDate) ); var sort = Builders.Sort.Descending(o => o.Time); results.AddRange(Collection.Find(filter).Sort(sort).Limit(num).ToEnumerable()); } return results; } /// /// Gets the name of the collection for archived patient observations. /// /// The collection name from API settings, or default "archive_patients_observations". public override string GetCollectionName() { return _apiSettings.ArchivePatientsObservations ?? "archive_patients_observations"; } /// /// Inserts a new patient observation into the archive with retry logic for duplicate key errors. /// If a duplicate key error occurs, generates a new ObjectId and retries up to maxRetries times. /// /// The PatientObservation entity to insert. /// Throws when duplicate key error persists after max retries. /// Throws when insertion fails for reasons other than duplicate key. public new async Task InsertOneAsync(PatientObservation patientObservation) { const int maxRetries = 2; // Número máximo de reintentos var retryCount = 0; while (true) try { await Collection.InsertOneAsync(patientObservation); return; } catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { retryCount++; Log.Warning( "Duplicate key error encountered. Retrying with new ObjectId. Attempt {attempt} of {maxRetries}", retryCount, maxRetries); patientObservation.Id = new ObjectId(); Log.Information("Generated ObjectId: {objectId}", patientObservation.Id); if (retryCount >= maxRetries) { Log.Error("Maximum retry attempts reached. Could not insert document due to duplicate key error."); throw; // Re-lanzar la excepción después de alcanzar el número máximo de reintentos } } catch (Exception ex) { Log.Error("Error inserting patient observation: {exMessage}", ex.Message); throw; } } /// /// Deletes all patient observations before a specified date. /// Useful for archival cleanup operations. /// /// The cutoff date. Observations older than this date will be deleted. /// The number of deleted documents. public async Task DeleteBeforeDate(DateTime date) { var filter = Builders.Filter.Lt(po => po.Time, date); await Collection.DeleteManyAsync(filter); } /// /// Inserts a batch of patient observations using bulk write operation. /// /// An enumerable of PatientObservation entities to insert. /// The count of successfully inserted documents. public async Task InsertBatch(IEnumerable observations) { var writes = new List>(); writes.AddRange(observations.Select(d => new InsertOneModel(d))); var bulkInsert = await Collection.BulkWriteAsync(writes); return bulkInsert.InsertedCount; } /// /// Retrieves all archived observations for a specific patient. /// /// The ObjectId of the patient. /// A list of all PatientObservation entities for the patient. public async Task> FindAllFromPatient(ObjectId patientId) { var filter = Builders.Filter.Eq(p => p.PatientId, patientId); var result = await Collection.FindAsync(filter); return await result.ToListAsync(); } /// /// Aggregates distinct observation names for a patient using MongoDB aggregation pipeline. /// If filterObservations is provided, returns that list; otherwise, computes distinct observations. /// /// The ObjectId of the patient. /// Optional pre-filtered list of observation names. If null or empty, computes distinct values. /// A list of distinct observation name strings. private async Task> AggregatePatientObservations(ObjectId patientId, List? filterObservations = null) { var matchPatient = new BsonDocument { { "patientid", patientId }, { "name", new BsonDocument { { "$ne", BsonNull.Value } } } }; if (filterObservations == null || !filterObservations.Any()) { // GET DISTINCT OBSERVATIONS var distinctObs = new BsonDocument { { "$group", new BsonDocument { { "_id", "1" }, { "obs", new BsonDocument { { "$addToSet", "$name" } } } } } }; var distinctPipeline = new[] { new() { { "$match", matchPatient } }, distinctObs }; Debug.WriteLine("AggregatedArchivedPatientLastObservations distinct obs: \n" + distinctPipeline.ToJson()); var resultList = await Collection.AggregateAsync(distinctPipeline, new AggregateOptions { AllowDiskUse = true }); var result = resultList.ToList().FirstOrDefault(); if (result != null && result.Any()) filterObservations = result.GetValue("obs").AsBsonArray.Select(it => it.AsString).ToList(); } return filterObservations ?? []; } }