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 ?? [];
}
}