using adas_core.Application.Exceptions;
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.Filter;
using adas_core.Domain.Models.GroupedObservations;
using adas_core.Domain.Models.MongoModels;
using adas_core.Domain.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using System.Diagnostics;
using System.Text.RegularExpressions;
using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils;

namespace adas_core.Infrastructure.Repositories;

/// <summary>
/// Repository implementation for managing <see cref="PatientObservation"/> entities in MongoDB.
/// Provides specialized query, aggregation, retention, and expiration operations for patient clinical observations.
/// </summary>
public class ObservationRepository : MongoRepository<PatientObservation>, IObservationRepository
{
    private readonly ApiSettings _apiSettings;
    private readonly ILogger<ObservationRepository> _logger;


    /// <summary>
    /// Initializes a new instance of the <see cref="ObservationRepository"/> class.
    /// </summary>
    /// <param name="apiSettings">The application API settings containing configuration values, including the collection name. Must not be <see langword="null"/>.</param>
    /// <param name="database">The MongoDB database instance used to access the collection.</param>
    /// <param name="logger">The logger used to record diagnostic and error information.</param>
    /// <exception cref="ArgumentNullException">Thrown when <paramref name="apiSettings"/> is <see langword="null"/>.</exception>
    public ObservationRepository(
        IOptions<ApiSettings>? apiSettings,
        IMongoDatabase database,
        ILogger<ObservationRepository> logger) : base(database)
    {
        if (apiSettings != null)
        {
            _logger = logger;
            _apiSettings = apiSettings.Value;
        }
        else
        {
            throw new ArgumentNullException(nameof(apiSettings));
        }
    } //For testing


    /// <summary>
    /// Gets the name of the MongoDB collection used to store patient observations.
    /// Falls back to the default "patients_observations" collection name when not configured in the API settings.
    /// </summary>
    /// <returns>The collection name retrieved from the API settings, or "patients_observations" if not configured.</returns>
    public override string GetCollectionName()
    {
        return _apiSettings.PatientsObservations ?? "patients_observations";
    }

    /// <summary>
    /// Asynchronously finds the most recent observations of a specific type (by coding system and code) for a patient.
    /// Results are sorted by time in descending order and limited to <paramref name="num"/> entries.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient.</param>
    /// <param name="codingSystem">The coding system used (for example, LOINC, SNOMED).</param>
    /// <param name="code">The code identifying the observation type within the coding system.</param>
    /// <param name="num">The maximum number of recent observations to return. Defaults to 2.</param>
    /// <returns>An <see cref="IEnumerable{PatientObservation}"/> containing the matching observations ordered from newest to oldest.</returns>
    public async Task<IEnumerable<PatientObservation>> FindLastObservations(ObjectId patientId, string codingSystem,
        string code, int num = 2)
    {
        var filter = Builders<PatientObservation>.Filter.And(
            Builders<PatientObservation>.Filter.Eq(ob => ob.PatientId, patientId),
            Builders<PatientObservation>.Filter.Eq(ob => ob.CodingSystem, codingSystem),
            Builders<PatientObservation>.Filter.Eq(ob => ob.Code, code)
        );

        var result = await Collection.FindAsync(
            filter,
            new FindOptions<PatientObservation>
            { Sort = Builders<PatientObservation>.Sort.Descending("time"), Limit = num }
        );

        return result.ToEnumerable();
    }

    /// <summary>
    /// Asynchronously finds the most recent observations of a specific coding system for a patient.
    /// Results are sorted by time in descending order and limited to <paramref name="num"/> entries.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient.</param>
    /// <param name="codingSystem">The coding system to filter observations by.</param>
    /// <param name="num">The maximum number of recent observations to return. Defaults to 10.</param>
    /// <returns>A <see cref="List{PatientObservation}"/> containing the matching observations ordered from newest to oldest.</returns>
    public async Task<List<PatientObservation>> FindLastObservationsByCodingSystem(ObjectId patientId,
        string codingSystem, int num = 10)
    {
        var filter = Builders<PatientObservation>.Filter.And(
            Builders<PatientObservation>.Filter.Eq(ob => ob.PatientId, patientId),
            Builders<PatientObservation>.Filter.Eq(ob => ob.CodingSystem, codingSystem)
        );

        var result = await Collection.FindAsync(
            filter,
            new FindOptions<PatientObservation>
            { Sort = Builders<PatientObservation>.Sort.Descending("time"), Limit = num }
        );

        return await result.ToListAsync();
    }

    /// <summary>
    /// Asynchronously retrieves the most recent observations for a patient, optionally restricted to a list of observation names.
    /// If <paramref name="filterObservations"/> is <see langword="null"/>, all distinct observation names for the patient are used.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient.</param>
    /// <param name="num">The maximum number of observations to return per observation name.</param>
    /// <param name="filterObservations">Optional list of observation names to restrict the query to. When <see langword="null"/>, all distinct names are discovered.</param>
    /// <returns>A <see cref="List{PatientObservation}"/> containing the aggregated latest observations across all matching names.</returns>
    public async Task<List<PatientObservation>> AggregatedPatientLastObservations(ObjectId patientId, int num,
        List<string>? filterObservations = null)
    {
        filterObservations = await AggregatePatientObservations(patientId, filterObservations);
        var results = new List<PatientObservation>();
        foreach (var obs in filterObservations)
        {
            var builder = Builders<PatientObservation>.Filter;
            var filter = builder.And(
                builder.Eq(o => o.PatientId, patientId),
                builder.Eq(o => o.Name, obs)
            );

            var cursor = await Collection.FindAsync(filter,
                new FindOptions<PatientObservation>
                {
                    Sort = Builders<PatientObservation>.Sort.Descending("time").Descending("id"),
                    Limit = num
                });

            results.AddRange(cursor.ToEnumerable());
        }

        return results;
    }


    /// <summary>
    /// Asynchronously retrieves the most recent observations for a patient that occurred on or before a given date,
    /// optionally restricted to a list of observation names.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient.</param>
    /// <param name="num">The maximum number of observations to return per observation name.</param>
    /// <param name="lastDate">The inclusive upper bound (UTC) for the observation <c>time</c> field.</param>
    /// <param name="filterObservations">Optional list of observation names to restrict the query to. When <see langword="null"/>, all distinct names are discovered.</param>
    /// <returns>A <see cref="List{PatientObservation}"/> containing the matching observations.</returns>
    public async Task<List<PatientObservation>> AggregatedPatientLastObservationsByLastDate(ObjectId patientId, int num,
        DateTime lastDate, List<string>? filterObservations = null)
    {
        filterObservations = await AggregatePatientObservations(patientId, filterObservations);
        var results = new List<PatientObservation>();

        foreach (var obs in filterObservations)
        {
            var filter = Builders<PatientObservation>.Filter.And(
                Builders<PatientObservation>.Filter.Eq(o => o.PatientId, patientId),
                Builders<PatientObservation>.Filter.Eq(o => o.Name, obs),
                Builders<PatientObservation>.Filter.Lte(o => o.Time, lastDate)
            );

            var cursor = await Collection.FindAsync(
                filter,
                new FindOptions<PatientObservation>
                { Sort = Builders<PatientObservation>.Sort.Descending("time").Descending("_id"), Limit = num }
            );

            results.AddRange(await cursor.ToListAsync());
        }

        return results;
    }


    /// <summary>
    /// Asynchronously retrieves the most recent observations for a patient, with per-field limits and an optional "expired" flag filter.
    /// When a <see cref="Field"/> specifies <see cref="Field.OnlyExpired"/> as <see langword="true"/>, only expired observations are returned.
    /// When <paramref name="filterObservations"/> is <see langword="null"/>, all observations for the patient are returned.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient.</param>
    /// <param name="filterObservations">Optional list of <see cref="Field"/> descriptors defining the names, limits, and expiration filter. When <see langword="null"/>, all observations for the patient are returned.</param>
    /// <returns>
    /// A <see cref="List{PatientObservation}"/> containing the matching observations.
    /// Returns an empty list when an exception occurs while querying the database.
    /// </returns>
    public async Task<List<PatientObservation>> AggregatedPatientLastObservationsByField(ObjectId patientId,
        List<Field>? filterObservations)
    {
        try
        {
            var results = new List<PatientObservation>();
            IAsyncCursor<PatientObservation>? cursor;
            var builder = Builders<PatientObservation>.Filter;

            if (filterObservations != null)
            {
                foreach (var obs in filterObservations)
                {
                    FilterDefinition<PatientObservation> filter;

                    if (obs is { OnlyExpired: true, Name: not null })
                        filter = builder.And(
                            builder.Eq(o => o.PatientId, patientId),
                            builder.Eq(o => o.Name, obs.Name),
                            builder.Eq(o => o.Expired, obs.OnlyExpired)
                        );
                    else
                        filter = builder.And(
                            builder.Eq(o => o.PatientId, patientId),
                            builder.Eq(o => o.Name, obs.Name)
                        );

                    cursor = await Collection.FindAsync(
                        filter,
                        new FindOptions<PatientObservation>
                        {
                            Sort = Builders<PatientObservation>.Sort.Descending("time").Descending("id"),
                            Limit = obs.Last
                        });

                    results.AddRange(cursor.ToEnumerable());
                }
            }
            else
            {
                var filter = builder.Eq(o => o.PatientId, patientId);

                cursor = await Collection.FindAsync(
                    filter,
                    new FindOptions<PatientObservation>
                    { Sort = Builders<PatientObservation>.Sort.Descending("time").Descending("id") });

                results.AddRange(cursor.ToEnumerable());
            }

            return results;
        }
        catch (Exception ex)
        {
            _logger.LogError("Error aggregated patient last observations by field {exMessage}", ex.Message);
            return [];
        }
    }


    /// <summary>
    /// Asynchronously executes an aggregation pipeline that groups a patient's observations into time buckets
    /// (second / minute / hour / day / shift / times) and computes per-bucket results such as first, last, min, max, sum, average, or count.
    /// Supports a "since last observation" mode and a configurable look-back window based on the regularity.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient.</param>
    /// <param name="groupedField">A <see cref="GroupedField"/> descriptor containing the observation name(s), regularity, look-back window, and the result computations to perform.</param>
    /// <returns>
    /// A <see cref="List{BsonDocument}"/> with one document per time bucket.
    /// Returns an empty list when an exception occurs while executing the pipeline.
    /// </returns>
    public async Task<List<BsonDocument>> AggregatedPatientGroupedObservations(ObjectId patientId,
        GroupedField groupedField)
    {
        try
        {
            DateTime? lastObsTime = null;
            if (groupedField.Since == GroupedObservationEnum.Since.Last && !string.IsNullOrEmpty(groupedField.Name))
            {
                var lastObsList =
                    await AggregatedPatientLastObservations(patientId, 1, [groupedField.Name]);
                var lastObs = lastObsList.FirstOrDefault();
                if (lastObs != null)
                {
                    lastObsTime = lastObs.Time;
                    lastObsTime = DateTime.SpecifyKind(lastObsTime.Value, DateTimeKind.Utc);
                }
            }


            var fields = new BsonArray();

            var match = new BsonDocument { { "patientid", patientId } };
            if (groupedField.GetNames().Count > 1)
            {
                groupedField.GetNames().ForEach(name => { fields.Add(new BsonDocument { { "name", name } }); });
                match.Add("$or", fields);
            }
            else
            {
                match.Add("name", groupedField.Name);
            }

            var projectDate = new BsonDocument
            {
                { "y", new BsonDocument { { "$year", "$time" } } },
                { "M", new BsonDocument { { "$month", "$time" } } },
                { "d", new BsonDocument { { "$dayOfMonth", "$time" } } }
            };
            var projectTimeFromParts = new BsonDocument
            {
                { "year", "$_id.year" },
                { "month", "$_id.month" },
                { "day", "$_id.day" }
            };
            var idDocument = new BsonDocument { { "year", "$y" }, { "month", "$M" }, { "day", "$d" } };
            var fromDate = DateTime.MinValue;
            if (groupedField.Regularity == GroupedObservationEnum.Regularity.Day)
            {
                if (groupedField.Since == GroupedObservationEnum.Since.Last && lastObsTime.HasValue)
                    fromDate = lastObsTime.Value.AddDays(-1 * groupedField.Max);
                else
                    fromDate = DateTime.UtcNow.AddDays(-1 * groupedField.Max);
            }

            if (groupedField.Regularity is GroupedObservationEnum.Regularity.Hour
                or GroupedObservationEnum.Regularity.Minute or GroupedObservationEnum.Regularity.Second
                or GroupedObservationEnum.Regularity.Times or GroupedObservationEnum.Regularity.Shift)
            {
                projectDate.Add("h", new BsonDocument { { "$hour", "$time" } });
                idDocument.Add("hour", "$h");
                projectTimeFromParts.Add("hour", "$_id.hour");
                //fromDate = DateTime.UtcNow.AddHours(-1 * groupedField.max);
                if (groupedField.Since == GroupedObservationEnum.Since.Last && lastObsTime.HasValue)
                    fromDate = new DateTime(lastObsTime.Value.Year, lastObsTime.Value.Month, lastObsTime.Value.Day,
                        lastObsTime.Value.AddHours(1).Hour, 00, 00).AddHours(-1 * groupedField.Max);
                else
                    fromDate = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, DateTime.UtcNow.Day,
                        DateTime.UtcNow.Hour, 00, 00).AddHours(-1 * groupedField.Max);
            }

            if (groupedField.Regularity == GroupedObservationEnum.Regularity.Minute ||
                groupedField.Regularity == GroupedObservationEnum.Regularity.Second ||
                groupedField.Regularity == GroupedObservationEnum.Regularity.Times
               //No queremos los minutos cuando pedimos por turno, en principio. Revisar si en algún caso necesitamos los minutos, quitado de momento por problemas
               // a la hora de devolver el last.
               //|| groupedField.regularity == Regularity.Shift
               )
            {
                projectDate.Add("m", new BsonDocument { { "$minute", "$time" } });
                idDocument.Add("minute", "$m");
                projectTimeFromParts.Add("minute", "$_id.minute");

                if (groupedField.Regularity != GroupedObservationEnum.Regularity.Shift)
                {
                    if (groupedField.Since == GroupedObservationEnum.Since.Last && lastObsTime.HasValue)
                        fromDate = lastObsTime.Value.AddMinutes(-1 * groupedField.Max);
                    else
                        fromDate = DateTime.UtcNow.AddMinutes(-1 * groupedField.Max);
                }
            }

            if (groupedField.Regularity == GroupedObservationEnum.Regularity.Second ||
                groupedField.Regularity == GroupedObservationEnum.Regularity.Times)
            {
                projectDate.Add("s", new BsonDocument { { "$second", "$time" } });
                idDocument.Add("second", "$s");
                projectTimeFromParts.Add("second", "$_id.second");
                if (groupedField.Since == GroupedObservationEnum.Since.Last && lastObsTime.HasValue)
                    fromDate = lastObsTime.Value.AddSeconds(-1 * groupedField.Max);
                else
                    fromDate = DateTime.UtcNow.AddSeconds(-1 * groupedField.Max);
            }

            if (groupedField.Regularity == GroupedObservationEnum.Regularity.Second ||
                groupedField.Regularity == GroupedObservationEnum.Regularity.Times)
            {
                projectDate.Add("ms", new BsonDocument { { "$millisecond", "$time" } });
                idDocument.Add("millisecond ", "$ms");
                projectTimeFromParts.Add("millisecond", "$_id.millisecond");
            }

            projectDate.Add("name", "$name");
            projectDate.Add("min", "$min");
            projectDate.Add("max", "$max");
            projectDate.Add("time", "$time");
            projectDate.Add("value", "$value");
            idDocument.Add("name", "$name");
            //No queremos meter el mínimo y el maximo en la agrupación.
            //idDocument.Add("min", "$min");
            //idDocument.Add("max", "$max");
            fromDate = DateTime.SpecifyKind(fromDate, DateTimeKind.Utc);
            if (groupedField.Max > 0)
            {
                //Add filter lte for ICCA future observations like hour balance.
                if (groupedField.Since == GroupedObservationEnum.Since.Last && lastObsTime.HasValue)
                    match.Add("time", new BsonDocument("$gte", fromDate).Add("$lte", lastObsTime.Value));
                else
                    match.Add("time", new BsonDocument("$gte", fromDate).Add("$lte", DateTime.UtcNow));
            }


            var group = new BsonDocument
            {
                { "_id", idDocument }
            };
            if (groupedField.Result.Count == 0 || groupedField.Result.Contains(GroupedObservationEnum.Result.First))
                group.Add("first",
                    new BsonDocument
                    {
                        {
                            "$first",
                            new BsonDocument
                                { { "time", "$time" }, { "value", "$value" }, { "min", "$min" }, { "max", "$max" } }
                        }
                    });

            if (groupedField.Result.Contains(GroupedObservationEnum.Result.Last))
                group.Add("last",
                    new BsonDocument
                    {
                        {
                            "$last",
                            new BsonDocument
                                { { "time", "$time" }, { "value", "$value" }, { "min", "$min" }, { "max", "$max" } }
                        }
                    });

            if (groupedField.Result.Contains(GroupedObservationEnum.Result.LastFilled))
                group.Add("lastfilled",
                    new BsonDocument
                    {
                        {
                            "$last",
                            new BsonDocument
                                { { "time", "$time" }, { "value", "$value" }, { "min", "$min" }, { "max", "$max" } }
                        }
                    });

            if (groupedField.Result.Contains(GroupedObservationEnum.Result.Min))
                group.Add("min",
                    new BsonDocument
                    {
                        {
                            "$min",
                            new BsonDocument
                                { { "value", "$value" }, { "time", "$time" }, { "min", "$min" }, { "max", "$max" } }
                        }
                    });

            if (groupedField.Result.Contains(GroupedObservationEnum.Result.Max))
                group.Add("max",
                    new BsonDocument
                    {
                        {
                            "$max",
                            new BsonDocument
                            {
                                { "value", "$value" }, { "time", "$time" }, { "min", "$min" }, { "max", "$max" }
                            }
                        }
                    });

            if (groupedField.Result.Contains(GroupedObservationEnum.Result.Sum))
                group.Add("sum", new BsonDocument { { "$sum", "$value" } });

            if (groupedField.Result.Contains(GroupedObservationEnum.Result.Average))
                group.Add("average", new BsonDocument { { "$avg", "$value" } });

            if (groupedField.Result.Contains(GroupedObservationEnum.Result.Count))
                group.Add("count", new BsonDocument { { "$sum", 1 } });

            if (groupedField.Result.Contains(GroupedObservationEnum.Result.HalfHour))
                group.Add("all",
                    new BsonDocument
                    {
                        {
                            "$push",
                            new BsonDocument
                            {
                                { "value", "$value" }, { "time", "$time" }, { "min", "$min" }, { "max", "$max" }
                            }
                        }
                    });
            //group.Add("halfHour", new BsonDocument { { "$unset", new BsonDocument { { "time", "$time" }, { "value", "$value" }, { "min", "$min" }, { "max", "$max" } } } });
            var pipeline = new List<BsonDocument>
            {
                new()
                {
                    {
                        "$match", match
                    }
                },
                new()
                {
                    {
                        "$sort", new BsonDocument { { "time", 1 } }
                    }
                },
                new()
                {
                    {
                        "$project", projectDate
                    }
                },
                new()
                {
                    {
                        "$group", group
                    }
                },
                new()
                {
                    {
                        "$addFields",
                        new BsonDocument
                        {
                            { "time", new BsonDocument { { "$dateFromParts", projectTimeFromParts } } },
                            { "isFilled", false }
                        }
                    }
                },
                new()
                {
                    {
                        "$sort", new BsonDocument { { "time", 1 } }
                    }
                }
            };
            if (groupedField is { Regularity: GroupedObservationEnum.Regularity.Times, Max: > 0 })
                pipeline.Add(new BsonDocument("$limit", groupedField.Max));
            //System.Diagnostics.Debug.WriteLine("AggregatedPatientGroupedObservations query: \n" + pipeline.ToJson());
            _logger.LogDebug("Executing AggregatedPatientGroupedObservations query: {pipeline}:", pipeline.ToJson());
            var resultsCursor =
                await Collection.AggregateAsync<BsonDocument>(pipeline, new AggregateOptions { AllowDiskUse = true });
            var results = resultsCursor.ToList();

            return results;
        }
        catch (Exception ex)
        {
            _logger.LogError("Error agregated patient grouped observations {exMessage}", ex.Message);
            return [];
        }
    }


    /// <summary>
    /// Asynchronously inserts a single patient observation, automatically retrying with a new <see cref="ObjectId"/>
    /// when a MongoDB duplicate-key error is encountered. The retry strategy is bounded by an internal maximum.
    /// </summary>
    /// <param name="patientObservation">The <see cref="PatientObservation"/> to insert. If a duplicate-key error occurs, a new identifier is generated and the insert is retried.</param>
    /// <returns>A <see cref="Task"/> representing the asynchronous insert operation.</returns>
    /// <exception cref="MongoWriteException">Rethrown after the maximum number of retries has been reached when a duplicate-key error keeps occurring.</exception>
    /// <exception cref="Exception">Rethrown when an unexpected error occurs during the insert operation.</exception>
    public new async Task InsertOneAsync(PatientObservation patientObservation)
    {
        const int maxRetries = 2; // Número máximo de reintentos
        var retryCount = 0;

        while (retryCount < maxRetries)
            try
            {
                await Collection.InsertOneAsync(patientObservation);
                return;
            }
            catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
            {
                retryCount++;
                _logger.LogWarning(
                    "Duplicate key error encountered. Retrying with new ObjectId. Attempt {attempt} of {maxRetries}",
                    retryCount, maxRetries);

                if (retryCount < maxRetries) continue;

                _logger.LogError(
                    "Maximum retry attempts reached. Could not insert document due to duplicate key error.");
                throw; // Relanzar la excepción después de alcanzar el número máximo de reintentos
            }
            catch (Exception ex)
            {
                _logger.LogError("Error inserting patient observation: {exMessage}", ex.Message);
                throw;
            }
    }

    /// <summary>
    /// Asynchronously deletes all observations associated with the specified patient.
    /// </summary>
    /// <param name="id">The <see cref="ObjectId"/> of the patient whose observations should be removed.</param>
    /// <returns>A <see cref="Task"/> representing the asynchronous delete operation.</returns>
    public async Task DeleteByPatientId(ObjectId id)
    {
        var filter = Builders<PatientObservation>.Filter.Eq(po => po.PatientId, id);
        await Collection.DeleteManyAsync(filter);
    }

    /// <summary>
    /// Asynchronously returns a cursor over all observations for a given patient, using a server-side batch size of 100.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient.</param>
    /// <returns>
    /// An <see cref="IAsyncCursor{PatientObservation}"/> that can be enumerated to retrieve the patient's observations.
    /// </returns>
    public async Task<IAsyncCursor<PatientObservation>> FindByPatientIdAsync(ObjectId patientId)
    {
        var filter = Builders<PatientObservation>.Filter.Eq(ob => ob.PatientId, patientId);
        return await Collection.FindAsync(filter, new FindOptions<PatientObservation> { BatchSize = 100 });
    }

    /// <summary>
    /// Asynchronously returns a cursor over all observations for a patient that match a given coding system and observation name.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient.</param>
    /// <param name="codingSystem">The coding system to filter by.</param>
    /// <param name="name">The observation name to filter by.</param>
    /// <returns>
    /// An <see cref="IAsyncCursor{PatientObservation}"/> containing the matching observations,
    /// retrieved with a server-side batch size of 100.
    /// </returns>
    public async Task<IAsyncCursor<PatientObservation>> FindByPatientIdAndCodingSystemAsync(ObjectId patientId,
        string codingSystem, string name)
    {
        var filter =
            Builders<PatientObservation>.Filter.And(
                Builders<PatientObservation>.Filter.Eq(ob => ob.PatientId, patientId),
                Builders<PatientObservation>.Filter.Eq(ob => ob.CodingSystem, codingSystem),
                Builders<PatientObservation>.Filter.Eq(ob => ob.Name, name)
            );

        return await Collection.FindAsync(filter, new FindOptions<PatientObservation> { BatchSize = 100 });
    }


    /// <summary>
    /// Asynchronously deletes a single observation by its identifier. This method hides the base
    /// <c>DeleteAsync(ObjectId)</c> defined on <see cref="MongoRepository{T}"/> because the base returns the deleted document,
    /// while this implementation is fire-and-forget.
    /// </summary>
    /// <param name="id">The <see cref="ObjectId"/> of the observation to delete.</param>
    /// <returns>A <see cref="Task"/> representing the asynchronous delete operation.</returns>
    public new async Task DeleteAsync(ObjectId id)
    {
        var filter = Builders<PatientObservation>.Filter.Eq(obs => obs.Id, id);
        await Collection.DeleteOneAsync(filter);
    }


    /// <summary>
    /// Deletes all observations with the specified name that are older than the configured number of days.
    /// Returns the documents that were deleted for downstream processing.
    /// </summary>
    /// <param name="name">The observation name to filter by.</param>
    /// <param name="retentionPolicyValue">The retention window expressed in days. Observations older than <c>UtcNow - retentionPolicyValue</c> days are removed.</param>
    /// <returns>A <see cref="List{PatientObservation}"/> containing the documents that were deleted.</returns>
    public async Task<List<PatientObservation>> DeleteOlderDaysAsync(string name, int retentionPolicyValue)
    {
        var dateLimit = DateTime.UtcNow.AddDays(-1 * retentionPolicyValue);
        var filter = Builders<PatientObservation>.Filter.And(
            Builders<PatientObservation>.Filter.Eq(obs => obs.Name, name),
            Builders<PatientObservation>.Filter.Lt(obs => obs.Time, dateLimit)
        );
        var documentsToDelete = await Collection.Find(filter).ToListAsync();

        await Collection.DeleteManyAsync(filter);

        return documentsToDelete;
    }


    /// <summary>
    /// Deletes all observations with the specified name that are older than the configured number of seconds.
    /// Returns the documents that were deleted for downstream processing.
    /// </summary>
    /// <param name="name">The observation name to filter by.</param>
    /// <param name="retentionPolicyValue">The retention window expressed in seconds. Observations older than <c>UtcNow - retentionPolicyValue</c> seconds are removed.</param>
    /// <returns>A <see cref="List{PatientObservation}"/> containing the documents that were deleted.</returns>
    public async Task<List<PatientObservation>> DeleteOlderSecondsAsync(string name, int retentionPolicyValue)
    {
        var dateLimit = DateTime.UtcNow.AddSeconds(-1 * retentionPolicyValue);
        var filter = Builders<PatientObservation>.Filter.And(
            Builders<PatientObservation>.Filter.Eq(obs => obs.Name, name),
            Builders<PatientObservation>.Filter.Lt(obs => obs.Time, dateLimit)
        );
        var documentsToDelete = await Collection.Find(filter).ToListAsync();

        await Collection.DeleteManyAsync(filter);

        return documentsToDelete;
    }


    /// <summary>
    /// Keeps only the <paramref name="retentionPolicyValue"/> most recent observations for the specified name,
    /// deleting all older ones. Returns the documents that were deleted for downstream processing.
    /// </summary>
    /// <param name="name">The observation name to apply the retention policy to.</param>
    /// <param name="retentionPolicyValue">The maximum number of observations to retain. The remainder are deleted.</param>
    /// <returns>A <see cref="List{PatientObservation}"/> containing the documents that were deleted. Returns an empty list when nothing had to be deleted.</returns>
    public async Task<List<PatientObservation>> DeleteOlderNumberAsync(string name, int retentionPolicyValue)
    {
        var builder = Builders<PatientObservation>.Filter;
        var idsToDelete = await Collection.Find(obs => obs.Name == name)
            .Project(obs => obs.Id)
            .Sort("{time: -1}")
            .Skip(retentionPolicyValue)
            .ToListAsync();

        List<PatientObservation> documentsToDelete = [];

        if (idsToDelete == null || !idsToDelete.Any()) return documentsToDelete;


        var idFilter = builder.In("_id", idsToDelete);
        documentsToDelete = await Collection.Find(idFilter).ToListAsync();

        await Collection.DeleteManyAsync(idFilter);

        return documentsToDelete;
    }


    /// <summary>
    /// Asynchronously checks whether an observation exists for a given patient with the specified <c>SystemId</c>.
    /// Only the document identifier is projected, making the query lightweight.
    /// </summary>
    /// <param name="patientid">The unique identifier of the patient.</param>
    /// <param name="systemId">The external system identifier to look up.</param>
    /// <returns><see langword="true"/> if a matching observation exists; otherwise, <see langword="false"/>.</returns>
    public async Task<bool> ExistBySystemId(ObjectId patientid, string systemId)
    {
        return await Collection.Find(Builders<PatientObservation>.Filter.And(
                Builders<PatientObservation>.Filter.Eq(obs => obs.PatientId, patientid),
                Builders<PatientObservation>.Filter.Eq(obs => obs.SystemId, systemId)
            ))
            .Project("{ _id: true }")
            .AnyAsync();
    }


    /// <summary>
    /// Asynchronously finds the most recent observation for a patient with the given name that occurred strictly before the specified date.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient.</param>
    /// <param name="name">The observation name to search for. Can be <see langword="null"/>.</param>
    /// <param name="date">The upper-bound (exclusive) observation <c>time</c>.</param>
    /// <returns>
    /// A <see cref="Task{PatientObservation}"/> representing the asynchronous operation.
    /// The task result contains the most recent matching observation, or <see langword="null"/> if no observation matches.
    /// </returns>
    public async Task<PatientObservation?> FindLastObservationBeforeDate(ObjectId patientId, string? name,
        DateTime date)
    {
        var builder = Builders<PatientObservation>.Filter;
        var filter = builder.And(
            builder.Eq(obs => obs.PatientId, patientId),
            builder.Eq(obs => obs.Name, name),
            builder.Lt(obs => obs.Time, date)
        );

        var result = await Collection.FindAsync(filter,
            new FindOptions<PatientObservation>
            { Sort = Builders<PatientObservation>.Sort.Descending("time").Descending("_id") });

        return await result.FirstOrDefaultAsync();
    }

    /// <summary>
    /// Asynchronously retrieves all observations for a patient with a given name whose <c>time</c> exactly matches the provided value.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient.</param>
    /// <param name="name">The observation name to match. Can be <see langword="null"/>.</param>
    /// <param name="date">The exact <c>time</c> value to match.</param>
    /// <returns>
    /// A <see cref="Task{List{PatientObservation}}"/> representing the asynchronous operation.
    /// The task result contains a list of matching observations, which may be empty.
    /// </returns>
    public async Task<List<PatientObservation>?> FindAnyWithSameDate(ObjectId patientId, string? name, DateTime date)
    {
        var builder = Builders<PatientObservation>.Filter;
        var filter = builder.Eq(obs => obs.PatientId, patientId)
                     & builder.Eq(obs => obs.Name, name)
                     & builder.Eq(obs => obs.Time, date);

        var result = await Collection.FindAsync(filter);
        return result.ToList();
    }


    /// <summary>
    /// Asynchronously retrieves all observations for a patient whose <c>time</c> is strictly before the specified date.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient.</param>
    /// <param name="date">The upper-bound (exclusive) observation <c>time</c>.</param>
    /// <returns>
    /// A <see cref="List{PatientObservation}"/> containing the matching observations, which may be empty.
    /// </returns>
    public async Task<List<PatientObservation>> FindAnyBeforeDate(ObjectId patientId, DateTime date)
    {
        var filterBuilder = Builders<PatientObservation>.Filter;
        var patientIdFilter = filterBuilder.Eq(obs => obs.PatientId, patientId);
        var dateFilter = filterBuilder.Lt(obs => obs.Time, date);

        var filter = filterBuilder.And(patientIdFilter, dateFilter);

        var result = await Collection.FindAsync(filter);
        return result.ToList();
    }

    /// <summary>
    /// Asynchronously retrieves, for a given patient and observation name, the latest occurrence of each distinct value.
    /// Optionally restricts the result to observations not older than <paramref name="expires"/> seconds.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient.</param>
    /// <param name="name">The observation name to search for.</param>
    /// <param name="expires">
    /// Optional expiration window expressed in seconds. When provided, only observations newer than
    /// <c>UtcNow - expires</c> seconds are considered.
    /// </param>
    /// <returns>
    /// A <see cref="List{PatientObservation}"/> containing the most recent observation for each distinct value.
    /// </returns>
    public async Task<List<PatientObservation>> FindLatestUniqueValuesByName(ObjectId patientId, string name,
        int? expires)
    {
        // Crear el filtro base
        var matchFilter = new BsonDocument
        {
            { "patientid", patientId },
            { "name", name }
        };

        // Agregar filtro de expiración si expires no es null
        if (expires.HasValue)
        {
            var expirationDate = DateTime.UtcNow.AddSeconds(-expires.Value);
            matchFilter.Add("time", new BsonDocument("$gte", expirationDate));
        }

        var pipeline = new[]
        {
            // Filtrar por PatientId, name y (opcionalmente) tiempo no expirado
            new BsonDocument("$match", matchFilter),

            // Ordenar por tiempo en orden descendente
            new BsonDocument("$sort", new BsonDocument("time", -1)),

            // Agrupar por el campo `value` (valores únicos)
            new BsonDocument("$group", new BsonDocument
            {
                { "_id", "$value" },
                { "LatestObservation", new BsonDocument("$first", "$$ROOT") }
            }),

            // Proyectar solo los campos originales
            new BsonDocument("$replaceRoot", new BsonDocument("newRoot", "$LatestObservation"))
        };

        var result = await Collection.AggregateAsync<PatientObservation>(pipeline);
        return result.ToList();
    }


    /// <summary>
    /// Asynchronously retrieves the currently active intravenous-line observations for a patient,
    /// grouping by the <c>Location</c> and <c>Type</c> of the <see cref="PatientIntravenousLinesValue"/>.
    /// Within each group, only the most recent observation (ordered by <c>time</c> and <c>id</c>) is returned.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient.</param>
    /// <returns>
    /// A <see cref="List{PatientObservation}"/> containing one observation per unique (Location, Type) combination.
    /// </returns>
    public async Task<List<PatientObservation?>> AggregatedPatientActiveIntravenousLinesObservations(ObjectId patientId)
    {
        var results = new List<PatientObservation>();

        var cursor = await Collection.FindAsync(
            o => o.PatientId == patientId && o.Name == "IntravenousLinesObs",
            new FindOptions<PatientObservation>
            { Sort = Builders<PatientObservation>.Sort.Descending("time").Descending("id") });

        results.AddRange(cursor.ToEnumerable());


        //Group by tpye and location, cant be in same location two of same type.
        return results
            .GroupBy(obs => new
            {
                ((PatientIntravenousLinesValue)obs.Value).Location,
                ((PatientIntravenousLinesValue)obs.Value).Type
            })
            .Select(g =>
                g.OrderBy(t => t.Time).ThenBy(t => t.Id).LastOrDefault())
            .ToList();
    }


    /// <summary>
    /// Asynchronously retrieves, for every patient with observations, the timestamp of the most recent observation.
    /// Implemented as a server-side aggregation that groups by <c>patientid</c> and selects the latest <c>time</c> value.
    /// </summary>
    /// <returns>
    /// A <see cref="Dictionary{ObjectId, DateTime}"/> mapping each patient's <see cref="ObjectId"/> to the UTC timestamp of their latest observation.
    /// </returns>
    public async Task<Dictionary<ObjectId, DateTime>> FindAllLastPatientObservationTime()
    {
        var group = new BsonDocument
        {
            { "_id", "$patientid" },
            { "time", new BsonDocument { { "$last", "$time" } } }
        };

        var pipeline = new List<BsonDocument>
        {
            new()
            {
                {
                    "$sort", new BsonDocument { { "time", 1 } }
                }
            },
            new()
            {
                {
                    "$group", group
                }
            }
        };

        var cursor = await Collection.AggregateAsync<BsonDocument>(pipeline,
            new AggregateOptions { AllowDiskUse = true, BatchSize = 10 });

        var result = new Dictionary<ObjectId, DateTime>();

        while (await cursor.MoveNextAsync())
            cursor.Current
                .Where(obs => obs.GetValue("_id").BsonType != BsonType.Null)
                .ToList()
                .ForEach(obs =>
                {
                    if (obs.Get("_id") != null)
                        result.Add(obs.Get("_id")?.AsObjectId ?? new ObjectId(),
                            obs.Get("time")?.ToUniversalTime() ?? DateTime.MinValue);
                }
                );
        return result;
    }

    /// <summary>
    /// Asynchronously finds an observation by its unique identifier.
    /// </summary>
    /// <param name="id">The <see cref="ObjectId"/> of the observation to retrieve.</param>
    /// <returns>
    /// A <see cref="Task{PatientObservation}"/> representing the asynchronous operation.
    /// The task result contains the <see cref="PatientObservation"/> if found; otherwise, <see langword="null"/>.
    /// </returns>
    public async Task<PatientObservation?> FindById(ObjectId id)
    {
        var filter = Builders<PatientObservation>.Filter.Eq(o => o.Id, id);

        var result = await Collection.FindAsync(filter);

        return await result.FirstOrDefaultAsync();
    }

    /// <summary>
    /// Asynchronously retrieves all observations for a patient by the patient's identifier.
    /// </summary>
    /// <param name="id">The <see cref="ObjectId"/> of the patient whose observations should be retrieved.</param>
    /// <returns>
    /// A <see cref="Task{List{PatientObservation}}"/> representing the asynchronous operation.
    /// The task result contains a list of observations, which may be empty.
    /// </returns>
    public async Task<List<PatientObservation>?> FindByPatientId(ObjectId id)
    {
        var filter = Builders<PatientObservation>.Filter.Eq(o => o.PatientId, id);

        var result = await Collection.FindAsync(filter);

        return await result.ToListAsync();
    }


    /// <summary>
    /// Asynchronously updates a single observation by replacing its document with the provided instance.
    /// </summary>
    /// <param name="observation">The <see cref="PatientObservation"/> whose <see cref="ObjectId"/> identifies the document to update.</param>
    /// <returns>A <see cref="Task"/> representing the asynchronous update operation.</returns>
    public async Task Update(PatientObservation observation)
    {
        await UpdateOneAsync(observation.Id, observation);
    }

    /// <summary>
    /// Asynchronously updates all documents in the collection where the specified field equals <paramref name="oldId"/>,
    /// setting that field to the new <paramref name="id"/>. Thin wrapper around the protected helper on the base repository.
    /// </summary>
    /// <param name="nameId">The name of the field to match and update.</param>
    /// <param name="id">The new <see cref="ObjectId"/> value to assign.</param>
    /// <param name="oldId">The existing <see cref="ObjectId"/> value to replace.</param>
    /// <returns>A <see cref="Task"/> representing the asynchronous update operation.</returns>
    public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId)
    {
        await UpdateManyObjectIdAsync(nameId, id, oldId);
    }

    /// <summary>
    /// Asynchronously marks the supplied list of observations as expired by setting their <c>Expired</c> flag to <see langword="true"/>.
    /// </summary>
    /// <param name="expiredObservations">The list of <see cref="PatientObservation"/> instances to mark as expired. The set of identifiers is used to build the update filter.</param>
    /// <returns>A <see cref="Task"/> representing the asynchronous update operation.</returns>
    public async Task UpdateExpiredObservations(List<PatientObservation> expiredObservations)
    {
        var filter = Builders<PatientObservation>.Filter
            .In(x => x.Id, expiredObservations.Select(c => c.Id));
        //var filter = Builders<PatientObservation>.Filter.In("Id", expiredObservations);
        var update = Builders<PatientObservation>.Update.Set("Expired", true);
        _ = await Collection.UpdateManyAsync(filter, update);
    }

    /// <summary>
    /// Asynchronously applies the given update definition to a set of observations identified by their identifiers.
    /// </summary>
    /// <param name="patientObservations">The observations whose identifiers form the target set of the update.</param>
    /// <param name="update">The <see cref="UpdateDefinition{PatientObservation}"/> describing the changes to apply to each matching document.</param>
    /// <returns>A <see cref="Task"/> representing the asynchronous update operation.</returns>
    public async Task UpdateMany(IEnumerable<PatientObservation> patientObservations,
        UpdateDefinition<PatientObservation> update)
    {
        var filter = Builders<PatientObservation>.Filter.In(x => x.Id, patientObservations.Select(x => x.Id));

        await Collection.UpdateManyAsync(filter, update);
    }

    /// <summary>
    /// Asynchronously returns every observation stored in the collection.
    /// </summary>
    /// <returns>
    /// An <see cref="IEnumerable{PatientObservation}"/> containing all observations.
    /// </returns>
    public async Task<IEnumerable<PatientObservation>> FindAll()
    {
        var result = await Collection.FindAsync(_ => true);

        return result.ToEnumerable();
    }

    /// <summary>
    /// Asynchronously marks observations as expired when their <c>time</c> is older than the configured expiration
    /// window defined by the supplied <see cref="ConfigObservation"/> entries. Only observations that are not
    /// already marked as expired are updated. Errors are logged and swallowed.
    /// </summary>
    /// <param name="configObservationsToExpire">
    /// A list of <see cref="ConfigObservation"/> entries describing the observation names and their expiration windows
    /// (in minutes). Observations whose <c>time</c> is older than <c>DateTime.Now - expires</c> minutes are marked as expired.
    /// </param>
    /// <returns>A <see cref="Task"/> representing the asynchronous update operation.</returns>
    public async Task ExpireExpiredObservations(
        List<ConfigObservation> configObservationsToExpire)
    {
        try
        {
            var builder = Builders<PatientObservation>.Filter;
            var filterObs = builder.Empty;
            configObservationsToExpire.ForEach(ob =>
            {
                var minsToAdd = ob.Expires.HasValue ? ob.Expires * -1 : 0;
                if (filterObs == builder.Empty)
                    filterObs = builder.Where(o =>
                        o.Name == ob.Name && o.Time < DateTime.Now.AddMinutes(minsToAdd.Value));
                else
                    filterObs |= builder.Where(o =>
                        o.Name == ob.Name && o.Time < DateTime.Now.AddMinutes(minsToAdd.Value));
            });
            var filterNotExpired = Builders<PatientObservation>.Filter.Where(o => !o.Expired);
            var combineFilter = filterObs & filterNotExpired;
            /*var documentSerializer = BsonSerializer.SerializerRegistry.GetSerializer<PatientObservation>();
            var renderedFilter = combineFilter.Render(documentSerializer, BsonSerializer.SerializerRegistry);
            Debug.WriteLine($"Result : {renderedFilter}");*/

            var updateDefinition = Builders<PatientObservation>.Update.Set(o => o.Expired, true);
            await Collection.UpdateManyAsync(combineFilter, updateDefinition);
        }
        catch (Exception ex)
        {
            _logger.LogError("exception expiring observations expired: {exMessage} ", ex);
        }
    }


    /// <summary>
    /// Asynchronously retrieves observations that are not marked as expired, optionally restricted to a list of observation names.
    /// Observations with a <see langword="null"/> name are excluded from the result.
    /// </summary>
    /// <param name="filterObservations">
    /// Optional list of observation names to match. When <see langword="null"/>, all non-null named observations are considered
    /// (still subject to the not-expired filter).
    /// </param>
    /// <returns>
    /// An <see cref="IEnumerable{PatientObservation}"/> containing the matching non-expired observations.
    /// </returns>
    public async Task<IEnumerable<PatientObservation>> FindNotExpired(List<string?>? filterObservations)
    {
        var filterBuilder = Builders<PatientObservation>.Filter;
        var filters = new List<FilterDefinition<PatientObservation>>();

        if (filterObservations != null)
        {
            filters.Add(filterBuilder.Not(filterBuilder.Eq(o => o.Name, null)));
            filters.Add(filterBuilder.In(o => o.Name, filterObservations));
        }

        filters.Add(filterBuilder.Ne(o => o.Expired, true));

        var filter = filterBuilder.And(filters);

        var result = await Collection.FindAsync(filter);

        return await result.ToListAsync();
    }


    /// <summary>
    /// Asynchronously finds observations whose name matches the given pattern (case-insensitive substring/regex),
    /// optionally restricted to those with a <c>time</c> greater than <paramref name="fromDate"/>.
    /// </summary>
    /// <param name="name">The regex pattern to match against the observation name. Cannot be null, empty, whitespace, or longer than 100 characters.</param>
    /// <param name="fromDate">Optional inclusive lower bound on the observation <c>time</c>.</param>
    /// <returns>
    /// An <see cref="IEnumerable{PatientObservation}"/> containing the matching observations.
    /// </returns>
    /// <exception cref="BadRequestException">
    /// Thrown when <paramref name="name"/> is null, empty, or whitespace,
    /// or when its length exceeds 100 characters.
    /// </exception>
    public async Task<IEnumerable<PatientObservation>> FindByName(string name, DateTime? fromDate = null)
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat);

        if (name.Length > 100)
            throw new BadRequestException("Name too long");

        var safeName = Regex.Escape(name);

        var filterBuilder = Builders<PatientObservation>.Filter;

        var nameFilter = filterBuilder.And(
            filterBuilder.Ne(o => o.Name, null),
            filterBuilder.Regex(o => o.Name, new BsonRegularExpression(safeName, "i"))
        );

        var dateFilter = fromDate.HasValue
            ? filterBuilder.Gt(o => o.Time, fromDate.Value)
            : filterBuilder.Empty;

        var filter = filterBuilder.And(nameFilter, dateFilter);

        var result = await Collection.FindAsync(filter);

        return await result.ToListAsync();
    }

    /// <summary>
    /// Builds a paginated, sorted, and filtered query over observations.
    /// Results are sorted by <c>time</c> in descending order. When a <see cref="PaginationFilter.FilteredRequest"/>
    /// is provided, observations can be filtered by name list and patient identifier. A mandatory time window
    /// (defaulting to <c>DateTime.MinValue</c> / <c>DateTime.MaxValue</c>) is always applied.
    /// </summary>
    /// <param name="filter">The <see cref="PaginationFilter"/> containing pagination and filter criteria.</param>
    /// <returns>
    /// An <see cref="IFindFluent{PatientObservation, PatientObservation}"/> instance that can be used to further refine and execute the query.
    /// </returns>
    public IFindFluent<PatientObservation, PatientObservation> GetPaginatedObservations(PaginationFilter filter)
    {
        // Crear variable con la clase que construye los filtros 
        var filterBuilder = Builders<PatientObservation>.Filter;
        // Crear una lista de filtros 
        var filters = new List<FilterDefinition<PatientObservation>>();
        // Ordenar los resultados por "time" en orden descendente
        var sort = Builders<PatientObservation>.Sort.Descending("time");
        if (filter.FilteredRequest != null)
        {
            var requestFilter = filter.FilteredRequest;
            if (!requestFilter.Observations.IsNullOrEmpty())
                filters.Add(filterBuilder.In(o => o.Name, requestFilter.Observations));

            if (requestFilter.PatientId != null)
            {
                var parsed = ObjectId.TryParse(requestFilter.PatientId, out var patientObjectId);
                if (parsed) filters.Add(filterBuilder.Eq(o => o.PatientId, patientObjectId));
            }
        }

        // Añadir los filtros por defecto en caso de llegar la lista de filtros vacia, si no la consulta a base de datos falla
        filters.Add(filterBuilder.Gt(p => p.Time, filter.FilteredRequest?.StartDate ?? DateTime.MinValue));
        filters.Add(filterBuilder.Lt(p => p.Time, filter.FilteredRequest?.EndDate ?? DateTime.MaxValue));

        // Combinar los filtros en una consulta compuesta con operador "$and"
        var combinedFilter = Builders<PatientObservation>.Filter.And(filters);

        return Collection
            .Find(combinedFilter)
            .Sort(sort);
    }

    /// <summary>
    /// Asynchronously retrieves the most recent observations for a patient with a given name,
    /// optionally bounded to a recent time window and an explicit maximum count.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient.</param>
    /// <param name="name">The observation name to filter by.</param>
    /// <param name="endAfter">
    /// Optional look-back window in seconds. When provided, only observations whose <c>time</c> is greater than or equal to
    /// <c>UtcNow - endAfter</c> are returned.
    /// </param>
    /// <param name="num">Optional maximum number of observations to return. <see langword="null"/> means no limit.</param>
    /// <returns>
    /// An <see cref="IEnumerable{PatientObservation}"/> containing the matching observations ordered from newest to oldest.
    /// </returns>
    public async Task<IEnumerable<PatientObservation>> FindLastNotExpiredObservatonsByPatient(ObjectId patientId,
        string name, int? endAfter = null, int? num = null)
    {
        var sort = Builders<PatientObservation>.Sort.Descending("time");

        var filterBuilder = Builders<PatientObservation>.Filter;

        if (endAfter.HasValue)
        {
            var now = DateTime.UtcNow;

            var thresholdTime = now.AddSeconds(-endAfter.Value); // Calcular la fecha límite

            var dateFilter =
                filterBuilder.Gte(o => o.Time, thresholdTime); // con fecha mayor que la fecha límite calculada

            filterBuilder.And(dateFilter);
        }


        var patientFilter = filterBuilder.Eq(o => o.PatientId, patientId);

        var nameFilter = filterBuilder.Eq(o => o.Name, name);

        var filters = filterBuilder.And(patientFilter, nameFilter);

        var result = await Collection.FindAsync(
            filters,
            new FindOptions<PatientObservation> { Sort = sort, Limit = num }
        );

        return result.ToEnumerable();
    }

    /// <summary>
    /// Creates the indexes required by the observations collection to support the repository's query patterns.
    /// Indexes are created in the background and are non-unique. If an error occurs, it is logged and rethrown.
    /// </summary>
    /// <returns>A <see cref="Task"/> representing the asynchronous index creation operation.</returns>
    /// <exception cref="Exception">Rethrown when an error occurs while creating the indexes.</exception>
    public override async Task CreateIndexes()
    {
        try
        {
            var options = new CreateIndexOptions { Background = true, Unique = false };
            var indexes = new List<CreateIndexModel<PatientObservation>>
            {
                new("{ patientid: 1 }", options),
                new("{ patientid: 1, name: 1, codingSystem: 1 }", options),
                new("{ patientid: 1, name: 1 }", options),
                new("{ name: 1, time: -1 }", options),
                new("{ patientid: 1, systemId: 1 }", options),
                new("{ name: 1 }", options),
                new("{ patientid: 1, name: 1 , time: -1, id: -1}", options),
                new("{ patientid: 1, name: 1 , time: 1}", options)
            };
            await MongoUtils.EnsureIndexes(Collection, indexes);
        }
        catch (Exception e)
        {
            _logger.LogError(
                "error creating indexes for observation collection {eMessage} TRACE: {eStackTrace}", e.Message,
                e.StackTrace);
            throw;
        }
    }


    /// <summary>
    /// Determines the list of observation names to use when running a per-name aggregation for a patient.
    /// If <paramref name="filterObservations"/> is null or empty, the distinct observation names currently
    /// stored for the patient are discovered via a server-side <c>$group</c> aggregation.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient.</param>
    /// <param name="filterObservations">
    /// Optional list of observation names to use. When null or empty, the method discovers the patient's
    /// distinct observation names automatically.
    /// </param>
    /// <returns>
    /// A <see cref="List{String}"/> containing the observation names to aggregate over.
    /// Returns an empty list if no observations exist for the patient.
    /// </returns>
    private async Task<List<string>> AggregatePatientObservations(ObjectId patientId,
        List<string>? 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("AggregatedPatientLastObservations distinct obs: \n" +
                            distinctPipeline.ToJson());
            var resultList = await Collection.AggregateAsync<BsonDocument>(distinctPipeline,
                new AggregateOptions { AllowDiskUse = true });
            var result = resultList.ToList().FirstOrDefault();

            if (result != null)
                filterObservations = result.GetValue("obs").AsBsonArray.Select(it => it.AsString)
                    .ToList();
        }

        return filterObservations ?? [];
    }
}