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;

/// <summary>
/// Repository implementation for managing PatientObservation archive entities in MongoDB.
/// Provides operations for storing and retrieving historical patient observations.
/// </summary>
public class ObservationArchiveRepository : MongoRepository<PatientObservation>, IObservationArchiveRepository
{
    private readonly ApiSettings _apiSettings;

    /// <summary>
    /// Initializes a new instance of the ObservationArchiveRepository.
    /// </summary>
    /// <param name="apiSettings">API settings containing collection names configuration.</param>
    /// <param name="database">The MongoDB database instance.</param>
    /// <exception cref="ArgumentNullException">Thrown when apiSettings is null.</exception>
    public ObservationArchiveRepository(IOptions<ApiSettings> apiSettings, IMongoDatabase database) : base(database)
    {
        if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings));
        _apiSettings = apiSettings.Value;
    } //For testing

    /// <summary>
    /// Retrieves the aggregated last observations for a specific patient.
    /// Filters observations by name and date, returning the most recent ones.
    /// </summary>
    /// <param name="patientId">The ObjectId of the patient.</param>
    /// <param name="num">The maximum number of observations to return per observation type.</param>
    /// <param name="lastDate">The cutoff date to filter observations (inclusive).</param>
    /// <param name="filterObservations">Optional list of observation names to filter by. If null, retrieves all distinct observations.</param>
    /// <returns>A list of PatientObservation entities sorted by time descending.</returns>
    public async Task<List<PatientObservation>> AggregatedPatientLastObservations(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 sort = Builders<PatientObservation>.Sort.Descending(o => o.Time);

            results.AddRange(Collection.Find(filter).Sort(sort).Limit(num).ToEnumerable());
        }

        return results;
    }

    /// <summary>
    /// Gets the name of the collection for archived patient observations.
    /// </summary>
    /// <returns>The collection name from API settings, or default "archive_patients_observations".</returns>
    public override string GetCollectionName()
    {
        return _apiSettings.ArchivePatientsObservations ?? "archive_patients_observations";
    }

    /// <summary>
    /// 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.
    /// </summary>
    /// <param name="patientObservation">The PatientObservation entity to insert.</param>
    /// <exception cref="MongoWriteException">Throws when duplicate key error persists after max retries.</exception>
    /// <exception cref="Exception">Throws when insertion fails for reasons other than duplicate key.</exception>
    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;
            }
    }

    /// <summary>
    /// Deletes all patient observations before a specified date.
    /// Useful for archival cleanup operations.
    /// </summary>
    /// <param name="date">The cutoff date. Observations older than this date will be deleted.</param>
    /// <returns>The number of deleted documents.</returns>
    public async Task DeleteBeforeDate(DateTime date)
    {
        var filter = Builders<PatientObservation>.Filter.Lt(po => po.Time, date);
        await Collection.DeleteManyAsync(filter);
    }

    /// <summary>
    /// Inserts a batch of patient observations using bulk write operation.
    /// </summary>
    /// <param name="observations">An enumerable of PatientObservation entities to insert.</param>
    /// <returns>The count of successfully inserted documents.</returns>
    public async Task<long> InsertBatch(IEnumerable<PatientObservation> observations)
    {
        var writes = new List<WriteModel<PatientObservation>>();
        writes.AddRange(observations.Select(d => new InsertOneModel<PatientObservation>(d)));

        var bulkInsert = await Collection.BulkWriteAsync(writes);

        return bulkInsert.InsertedCount;
    }

    /// <summary>
    /// Retrieves all archived observations for a specific patient.
    /// </summary>
    /// <param name="patientId">The ObjectId of the patient.</param>
    /// <returns>A list of all PatientObservation entities for the patient.</returns>
    public async Task<List<PatientObservation>> FindAllFromPatient(ObjectId patientId)
    {
        var filter = Builders<PatientObservation>.Filter.Eq(p => p.PatientId, patientId);
        var result = await Collection.FindAsync(filter);

        return await result.ToListAsync();
    }

    /// <summary>
    /// Aggregates distinct observation names for a patient using MongoDB aggregation pipeline.
    /// If filterObservations is provided, returns that list; otherwise, computes distinct observations.
    /// </summary>
    /// <param name="patientId">The ObjectId of the patient.</param>
    /// <param name="filterObservations">Optional pre-filtered list of observation names. If null or empty, computes distinct values.</param>
    /// <returns>A list of distinct observation name strings.</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("AggregatedArchivedPatientLastObservations distinct obs: \n" + distinctPipeline.ToJson());
            var resultList =
                await Collection.AggregateAsync<BsonDocument>(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 ?? [];
    }
}