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.Masters;
using adas_core.Domain.Models.MongoModels;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using Serilog;
using System.Text.RegularExpressions;
using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils;

namespace adas_core.Infrastructure.Repositories;

public class PatientRepository : MongoRepository<Patient>, IPatientRepository
{
    private readonly ApiSettings _apiSettings;

    public PatientRepository(IOptions<ApiSettings> apiSettings, IMongoDatabase database) : base(database)
    {
        if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings));
        _apiSettings = apiSettings.Value;
    } //For testing        

    // public PatientRepository(IOptions<ApiSettings> apiSettings, IOptions<DatabaseSettings> dbSetting) :
    //     base(dbSetting)
    // {
    //     if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings));
    //     _apiSettings = apiSettings.Value;
    // }

    public override string GetCollectionName()
    {
        return _apiSettings.Patients ?? "patients";
    }

    public async Task<Patient?> FindById(ObjectId id)
    {
        try
        {
            var filter = Builders<Patient>.Filter.Eq(p => p.Id, id);
            var result = await Collection.FindAsync(filter);

            return await result.FirstOrDefaultAsync();
        }
        catch (Exception ex)
        {
            Log.Error("Error searching patient by id: {id}. Exception: {ex}", id, ex);
            return null;
        }
    }

    public async Task<Patient?> FindByPointOfCareId(ObjectId id)
    {
        try
        {
            var filter = Builders<Patient>.Filter.Eq(p => p.PointOfCareId, id);
            var result = await Collection.FindAsync(filter);

            return await result.FirstOrDefaultAsync();
        }
        catch (Exception ex)
        {
            Log.Error("Error searching patient by id: {id}. Exception: {ex}", id, ex);
            return null;
        }
    }

    public async Task<Patient> FindByUnitAndPocId(ObjectId unit, ObjectId pointOfCare)
    {
        try
        {
            var filter = Builders<Patient>.Filter.And(
                Builders<Patient>.Filter.Eq(p => p.PointOfCareId, pointOfCare),
                Builders<Patient>.Filter.Eq(p => p.UnitId, unit)
            );
            var result = await Collection.FindAsync(filter);

            return await result.FirstOrDefaultAsync();
        }
        catch (Exception ex)
        {
            Log.Error("Error searching patient by unitId: {unitid} pointOfCareid: {id}. Exception: {ex}", unit,
                pointOfCare, ex);
            return null!;
        }
    }

    //Deprecated
    public async Task<Patient?> FindByLocation(PatientLocation? location)
    {
        if (location == null)
            return null;
        // Primero encontrar la unidad

        var filterBuilder = Builders<Patient>.Filter;
        var filter = filterBuilder.Empty;

        if (!string.IsNullOrEmpty(location.UnitName) && !string.IsNullOrEmpty(location.Bed))
            filter = filterBuilder.And(
                filterBuilder.Eq(p => p.UnitString, location.UnitName),
                filterBuilder.Eq(p => p.Bed, location.Bed)
            );
        else if (!string.IsNullOrEmpty(location.Bed)) filter = filterBuilder.Eq(p => p.Bed, location.Bed);

        var result = await Collection.Find(filter).Limit(1).FirstOrDefaultAsync();

        return result;
    }


    public override async Task InsertOneAsync(Patient patient)
    {
        try
        {
            patient.CreationDate = DateTime.UtcNow;
            await base.InsertOneAsync(patient);
        }
        catch (Exception e)
        {
            Console.WriteLine(e);

            var patientAux = await FindByLocation(patient.Location);
            if (patientAux != null && patientAux.PatientNumber == patient.PatientNumber)
            {
                Log.Warning("Exception trying to insert an existing patient: {patient}. Exception {e}", patient, e);
            }
            else
            {
                Log.Error("Exception trying to insert patient: {patient}. Exception {e}", patient, e);
                throw;
            }
        }
    }

    public async Task Update(Patient patient)
    {
        patient.UpdateDate = DateTime.UtcNow;
        await UpdateOneAsync(patient.Id, patient);
    }

    public async Task Delete(ObjectId id)
    {
        var filter = Builders<Patient>.Filter.Eq(x => x.Id, id);
        await Collection.DeleteOneAsync(filter, null);
    }

    public async Task UpdateLocation(ObjectId id, PatientLocation location)
    {
        var patient = await FindById(id);
        if (patient == null)
        {
            Log.Warning("Patient not found for updating location: {id}", id);
            return;
        }

        patient.UpdateDate = DateTime.UtcNow;
        patient.Location = location;
        await UpdateOneAsync(patient.Id, patient);
        /*var filterBuilder = Builders<Patient>.Filter;
        var filter = filterBuilder.Eq(p => p.Id, id);

        var update = Builders<Patient>.Update
            .Set(p => p.Bed, location?.Bed)
            .Set(p => p.UnitString, location?.UnitName)
            .Set(p => p.UpdateDate, DateTime.UtcNow)
            .AddToSet(p => p.HistoricalLocations, new KeyValuePair<string, PatientLocation>(DateTime.UtcNow.ToString("o"), location));

        var result = await Collection.UpdateOneAsync(filter, update);

        Log.Debug("Update location result: {result}", result);*/
    }

    public async Task UpdateLocation(ObjectId id, ObjectId location)
    {
        var patient = await FindById(id);
        if (patient == null)
        {
            Log.Warning("Patient not found for updating location: {id}", id);
            return;
        }

        patient.UpdateDate = DateTime.UtcNow;
        patient.PointOfCareId = location;
        await UpdateOneAsync(patient.Id, patient);
    }

    public async Task UpdateAttendingDoctor(ObjectId id, Person attendingDoctor)
    {
        var update = Builders<Patient>.Update
            .Set(p => p.AttendingDoctor, attendingDoctor)
            .Set(p => p.UpdateDate, DateTime.UtcNow);

        await Collection.UpdateOneAsync(p => p.Id == id, update);
    }

    public async Task UpdatePatientData(ObjectId id, string patientNumber, Person data, bool updatePatientNumber = true)
    {
        var filterBuilder = Builders<Patient>.Filter;
        var updateBuilder = Builders<Patient>.Update.Set(p => p.Person, data).Set(p => p.UpdateDate, DateTime.UtcNow);

        var filter = filterBuilder.Eq(p => p.Id, id);
        var update = updateBuilder;

        if (updatePatientNumber) update = update.Set(p => p.PatientNumber, patientNumber);

        await Collection.UpdateOneAsync(filter, update);
    }


    public async Task<Patient?> FindByPatientNumber(string patientNumber)
    {
        if (string.IsNullOrWhiteSpace(patientNumber)) return null;

        //Último paciente admitido
        var patient = await Collection.Find(Builders<Patient>.Filter.And(
                Builders<Patient>.Filter.Eq(p => p.DisTime, null),
                Builders<Patient>.Filter.Eq(p => p.PatientNumber, patientNumber)
            ))
            .Sort(Builders<Patient>.Sort.Descending(p => p.AdmTime))
            .Limit(1)
            .FirstOrDefaultAsync();


        //Último con fecha de alta más reciente
        patient ??= await Collection.Find(Builders<Patient>.Filter.And(
                Builders<Patient>.Filter.Ne(p => p.DisTime, null),
                Builders<Patient>.Filter.Eq(p => p.PatientNumber, patientNumber)
            ))
            .Sort(Builders<Patient>.Sort.Descending(p => p.DisTime))
            .Limit(1)
            .FirstOrDefaultAsync();

        return patient;
    }

    public async Task<Patient?> SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(patientNumber)) return null;

            // Paciente ubicado en un PoC pero en diferente unidad
            var patient = await Collection.Find(Builders<Patient>.Filter.And(
                Builders<Patient>.Filter.Eq(p => p.PatientNumber, patientNumber),
                Builders<Patient>.Filter.Ne(p => p.UnitId, unitId)
            )).ToListAsync();
            // Si hay mas de una coincidencia devolvemos null por que puede no estar completo el patientNumber
            return patient.Count > 1 ? null : patient.FirstOrDefault();
        }
        catch (Exception e)
        {
            Log.Error(
                "Error Search By Patient Number And Distinct Unit on patient repository patientNumber: {patientNumber}, unitId: {unitId}, Excepción: {e}",
                patientNumber, unitId, e.Message);
            return null;
        }
    }

    public async Task<List<Patient>> FindAllPatientWithFinishedProcedures(int archiveProcedureEndDateAfterMinutes)
    {
        var currentDateTime = DateTime.UtcNow;

        // Filtro para documentos que contienen al menos un procedimiento con opción "procedure"
        var filter = Builders<Patient>.Filter.ElemMatch(
            x => x.Procedures,
            procedure =>
                //procedure.OptionType == "procedure" &&
                procedure.EndDate.HasValue
        );

        // Ejecutar la consulta inicial y traer los documentos
        var patientsWithProcedures = await Collection.Find(filter).ToListAsync();

        // Aplicar el filtro adicional en memoria
        var patientsWithFinishedProcedures = patientsWithProcedures.Where(patient =>
            patient.Procedures != null &&
            patient.Procedures.Any(procedure =>
                procedure is
                {
                    //OptionType: "procedure",
                    EndDate: not null
                } &&
                procedure.EndDate.Value.AddMinutes(archiveProcedureEndDateAfterMinutes) < currentDateTime
            )
        ).ToList();

        return patientsWithFinishedProcedures;
    }

    public async Task<List<Patient>> FindAllPatientWithFinishedTests(int archiveTestEndDateAfterMinutes)
    {
        var currentDateTime = DateTime.UtcNow;

        // Filtro para documentos que contienen al menos un procedimiento con opción "procedure"
        var filter = Builders<Patient>.Filter.ElemMatch(
            x => x.Tests,
            procedure => //procedure.OptionType == "test" &&
                procedure.EndDate.HasValue
        );

        // Ejecutar la consulta inicial y traer los documentos
        var patientsWithTests = await Collection.Find(filter).ToListAsync();

        // Aplicar el filtro adicional en memoria
        var patientsWithFinishedTests = patientsWithTests.Where(patient =>
            patient.Tests != null &&
            patient.Tests.Any(procedure =>
                procedure is
                {
                    //OptionType: "test",
                    EndDate: not null
                } &&
                procedure.EndDate.Value.AddMinutes(archiveTestEndDateAfterMinutes) < currentDateTime
            )
        ).ToList();

        return patientsWithFinishedTests;
    }

    public async Task<List<Patient>> FindAllPatientWithFinishedTreatment(int archiveTreatmentEndDateAfterMinutes)
    {
        var currentDateTime = DateTime.UtcNow;

        // Filtro para documentos que contienen al menos un procedimiento con opción "procedure"
        var filter = Builders<Patient>.Filter.ElemMatch(
            x => x.Treatment,
            treatment => treatment.EndDate.HasValue
        );

        // Ejecutar la consulta inicial y traer los documentos
        var patientsWithTreatment = await Collection.Find(filter).ToListAsync();

        // Aplicar el filtro adicional en memoria
        var patientsWithFinishedTreatments = patientsWithTreatment.Where(patient =>
            patient.Treatment != null &&
            patient.Treatment.Any(treatment =>
                treatment.EndDate.HasValue &&
                treatment.EndDate.Value.AddMinutes(archiveTreatmentEndDateAfterMinutes) < currentDateTime
            )
        ).ToList();

        return patientsWithFinishedTreatments;
    }

    public async Task<List<Patient>> UpdateMasterListOption(List<ObjectId> unitIds, UpdateOptionMasterListDto opt,
        string typeName)
    {
        var isParsed = Enum.TryParse<MasterListType>(typeName, out var parsedTypeName);
        if (isParsed)
        {
            // Tener en cuenta los datos auxiliares ya que pueden ser texto libre o asignarse el establecido en la lista
            // originAux / diagnosisAux modificar en caso de ser el mismo que el padre
            // Notificar a todos los fronts con el nuevo valor de cada paciente por el id de los poc's afectados(?) 
            var filterUnit = Builders<Patient>.Filter.In("unitId", unitIds);
            switch (parsedTypeName)
            {
                case MasterListType.DiagnosisList:

                    var diagnosisFilter = Builders<Patient>.Filter.Eq(
                        "diagnosis.name", opt.OldOption?.Name
                    );
                    var filterUpdateDiagnosis = Builders<Patient>.Filter.And(filterUnit, diagnosisFilter);
                    var updateDiagnosis = Builders<Patient>.Update
                        .Set("diagnosis.name", opt.UpdatedOption?.Name)
                        .Set("diagnosis.description", opt.UpdatedOption?.Description);
                    await Collection.UpdateManyAsync(filterUpdateDiagnosis, updateDiagnosis);

                    await Collection.UpdateManyAsync(Builders<Patient>.Filter.And(filterUnit,
                        Builders<Patient>.Filter.Eq(
                            "diagnosisAux", opt.OldOption?.Name
                        )), Builders<Patient>.Update
                        .Set("diagnosisAux", opt.UpdatedOption?.Name));

                    // Filtro para devolver los documentos actualizados (OR entre diagnosis.name y diagnosisAux)
                    var diagnosisFilterToReturn =
                        Builders<Patient>.Filter.Eq("diagnosis.name", opt.UpdatedOption?.Name);
                    var diagnosisAuxFilterToReturn =
                        Builders<Patient>.Filter.Eq("diagnosisAux", opt.UpdatedOption?.Name);
                    var filterToReturn = Builders<Patient>.Filter.And(
                        filterUnit,
                        Builders<Patient>.Filter.Or(diagnosisFilterToReturn, diagnosisAuxFilterToReturn)
                    );

                    // Devolver los documentos actualizados
                    var updatedDocuments = await Collection.Find(filterToReturn).ToListAsync();
                    return updatedDocuments;
                case MasterListType.DoctorList:
                    // Filtro para encontrar el elemento en el array `doctors` que coincida con el nombre
                    var doctorFilter = Builders<Patient>.Filter.ElemMatch(
                        "doctors",
                        Builders<Patient>.Filter.Eq("name", opt.OldOption?.Name)
                    );
                    var filterUpdateDoctor = Builders<Patient>.Filter.And(filterUnit, doctorFilter);

                    // Actualizar todos los elementos de la lista `doctors` que coincidan
                    var updateDoctor = Builders<Patient>.Update.Set(
                        "doctors.$[nameElem].name", opt.UpdatedOption?.Name
                    );

                    // Definir filtros únicos para cada campo utilizado
                    var arrayFilters = new List<ArrayFilterDefinition>
                    {
                        new BsonDocumentArrayFilterDefinition<BsonDocument>(
                            new BsonDocument("nameElem.name", opt.OldOption?.Name))
                    };

                    // Agregar filtro para `optionType` solo si es necesario
                    if (!string.IsNullOrEmpty(opt.OldOption?.OptionType))
                    {
                        updateDoctor = updateDoctor.Set(
                            "doctors.$[typeElem].optionType", opt.UpdatedOption?.OptionType
                        );

                        arrayFilters.Add(
                            new BsonDocumentArrayFilterDefinition<BsonDocument>(
                                new BsonDocument("typeElem.optionType", opt.OldOption.OptionType)));
                    }

                    var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters };

                    // Ejecutar la actualización
                    await Collection.UpdateManyAsync(filterUpdateDoctor, updateDoctor, updateOptions);

                    // Devolver los documentos actualizados
                    var doctorFilterToReturn = Builders<Patient>.Filter.ElemMatch(
                        "doctors",
                        Builders<Patient>.Filter.Eq("name", opt.UpdatedOption?.Name)
                    );

                    var filterToReturnDoctor = Builders<Patient>.Filter.And(filterUnit, doctorFilterToReturn);

                    var updatedDoctorDocuments = await Collection.Find(filterToReturnDoctor).ToListAsync();

                    return updatedDoctorDocuments;
                case MasterListType.InsulationList:
                    break;
                case MasterListType.OriginList:
                    var originFilter = Builders<Patient>.Filter.Eq(
                        "origin.name", opt.OldOption?.Name
                    );
                    var filterUpdateOrigin = Builders<Patient>.Filter.And(filterUnit, originFilter);
                    var updateOrigin = Builders<Patient>.Update
                        .Set("origin.name", opt.UpdatedOption?.Name);
                    await Collection.UpdateManyAsync(filterUpdateOrigin, updateOrigin);

                    await Collection.UpdateManyAsync(Builders<Patient>.Filter.And(filterUnit,
                        Builders<Patient>.Filter.Eq(
                            "originAux", opt.OldOption?.Name
                        )), Builders<Patient>.Update
                        .Set("originAux", opt.UpdatedOption?.Name));

                    // Filtro para devolver los documentos actualizados (OR entre diagnosis.name y diagnosisAux)
                    var originFilterToReturn = Builders<Patient>.Filter.Eq("origin.name", opt.UpdatedOption?.Name);
                    var originAuxFilterToReturn = Builders<Patient>.Filter.Eq("originAux", opt.UpdatedOption?.Name);
                    var originfilterToReturn = Builders<Patient>.Filter.And(
                        filterUnit,
                        Builders<Patient>.Filter.Or(originFilterToReturn, originAuxFilterToReturn)
                    );

                    // Devolver los documentos actualizados
                    var updatedDocumentsorigin = await Collection.Find(originfilterToReturn).ToListAsync();
                    return updatedDocumentsorigin;

                case MasterListType.DoctorTypeList:
                case MasterListType.AllergyList:
                case MasterListType.DestinationList:
                case MasterListType.ProcedureList:
                case MasterListType.ServiceList:
                case MasterListType.TreatmentList:
                case MasterListType.AltableOptionList:
                case MasterListType.DischargeStatusList:
                case MasterListType.InternalDestinationList:
                case MasterListType.LanguageBarrierList:
                case MasterListType.MobilityOptionList:
                case MasterListType.PassiveSittingList:
                case MasterListType.PatientStatusList:
                case MasterListType.TherapeuticCeilingList:
                case MasterListType.VisitOptionList:
                case MasterListType.AccessControlList:
                    break;
            }
        }

        return [];
    }

    public async Task<List<Patient>> GetPatientsByUnitIds(List<ObjectId> unitIds, string typeName)
    {
        var isParsed = Enum.TryParse<MasterListType>(typeName, out _);
        if (!isParsed) return [];

        var filterUnit = Builders<Patient>.Filter.In("unitId", unitIds);
        return await Collection.Find(filterUnit).ToListAsync();
    }

    public async Task<long> CountByUnitId(ObjectId unitId)
    {
        try
        {
            var filter = Builders<Patient>.Filter.Eq(p => p.UnitId, unitId);
            return await Collection.CountDocumentsAsync(filter);
        }
        catch (Exception e)
        {
            Log.Logger.Error(e.Message);
            return 0;
        }
    }


    public async Task<IEnumerable<Patient>> DeleteMasterListOption(List<ObjectId> unitIds, OptionList opt,
        string typeName)
    {
        var isParsed = Enum.TryParse<MasterListType>(typeName, out var parsedTypeName);
        if (isParsed)
        {
            // Tener en cuenta los datos auxiliares ya que pueden ser texto libre o asignarse el establecido en la lista
            // originAux / diagnosisAux modificar en caso de ser el mismo que el padre
            // Notificar a todos los fronts con el nuevo valor de cada paciente por el id de los poc's afectados(?) 
            var filterUnit = Builders<Patient>.Filter.In("unitId", unitIds);
            switch (parsedTypeName)
            {
                case MasterListType.DiagnosisList:

                    // Filtro para devolver los documentos actualizados (OR entre diagnosis.name y diagnosisAux)
                    var diagnosisFilterToReturn = Builders<Patient>.Filter.Eq("diagnosis.name", opt.Name);
                    var diagnosisAuxFilterToReturn = Builders<Patient>.Filter.Eq("diagnosisAux", opt.Name);
                    var filterToReturn = Builders<Patient>.Filter.And(
                        filterUnit,
                        Builders<Patient>.Filter.Or(diagnosisFilterToReturn, diagnosisAuxFilterToReturn)
                    );

                    // Devolver los documentos actualizados
                    var updatedDocuments = await Collection.Find(filterToReturn).ToListAsync();

                    var diagnosisFilter = Builders<Patient>.Filter.Eq(
                        "diagnosis.name", opt.Name
                    );
                    var filterUpdateDiagnosis = Builders<Patient>.Filter.And(filterUnit, diagnosisFilter);
                    var updateDiagnosis = Builders<Patient>.Update
                        .Set(x => x.Diagnosis, null);
                    await Collection.UpdateManyAsync(filterUpdateDiagnosis, updateDiagnosis);

                    await Collection.UpdateManyAsync(Builders<Patient>.Filter.And(filterUnit,
                        Builders<Patient>.Filter.Eq(
                            "diagnosisAux", opt.Name
                        )), Builders<Patient>.Update
                        .Set(x => x.DiagnosisAux, null));


                    return updatedDocuments;
                case MasterListType.DoctorList:
                    // Devolvemos los datos
                    var doctorFilterToReturn = Builders<Patient>.Filter.ElemMatch(
                        "doctors",
                        Builders<Patient>.Filter.Eq("_id", opt.Id)
                    );
                    var filterToReturnDoctor = Builders<Patient>.Filter.And(filterUnit, doctorFilterToReturn);
                    var updatedDoctorDocuments = await Collection.Find(filterToReturnDoctor).ToListAsync();

                    // Filtro para encontrar el elemento en el array `doctors` que coincida con el nombre
                    var doctorFilter = Builders<Patient>.Filter.ElemMatch(
                        "doctors",
                        Builders<Patient>.Filter.Eq("_id", opt.Id)
                    );
                    var filterUpdateDoctor = Builders<Patient>.Filter.And(filterUnit, doctorFilter);

                    // Actualización del campo `name` en el array `doctors`
                    var updateDoctor = Builders<Patient>.Update.PullFilter(
                        "doctors", Builders<BsonDocument>.Filter.And(
                            Builders<BsonDocument>.Filter.Eq("_id", opt.Id)
                        ));

                    // Ejecutar la actualización
                    await Collection.UpdateManyAsync(filterUpdateDoctor, updateDoctor);

                    return updatedDoctorDocuments;
                case MasterListType.InsulationList:
                    break;
                case MasterListType.OriginList:
                    // Filtro para devolver los documentos actualizados (OR entre diagnosis.name y diagnosisAux)
                    var originFilterToReturn = Builders<Patient>.Filter.Eq("origin.name", opt.Name);
                    var originAuxFilterToReturn = Builders<Patient>.Filter.Eq("originAux", opt.Name);
                    var filterOriginToReturn = Builders<Patient>.Filter.And(
                        filterUnit,
                        Builders<Patient>.Filter.Or(originFilterToReturn, originAuxFilterToReturn)
                    );

                    // Devolver los documentos actualizados
                    var updatedDocumentsOrigin = await Collection.Find(filterOriginToReturn).ToListAsync();

                    var originFilter = Builders<Patient>.Filter.Eq(
                        "origin.name", opt.Name
                    );
                    var filterUpdateOrigin = Builders<Patient>.Filter.And(filterUnit, originFilter);
                    var updateOrigin = Builders<Patient>.Update
                        .Set(x => x.Diagnosis, null);
                    await Collection.UpdateManyAsync(filterUpdateOrigin, updateOrigin);

                    await Collection.UpdateManyAsync(Builders<Patient>.Filter.And(filterUnit,
                        Builders<Patient>.Filter.Eq(
                            "originAux", opt.Name
                        )), Builders<Patient>.Update
                        .Set(x => x.OriginAux, null));


                    return updatedDocumentsOrigin;

                case MasterListType.DoctorTypeList:
                case MasterListType.AllergyList:
                case MasterListType.DestinationList:
                case MasterListType.ProcedureList:
                case MasterListType.ServiceList:
                case MasterListType.TreatmentList:
                case MasterListType.AltableOptionList:
                case MasterListType.DischargeStatusList:
                case MasterListType.InternalDestinationList:
                case MasterListType.LanguageBarrierList:
                case MasterListType.MobilityOptionList:
                case MasterListType.PassiveSittingList:
                case MasterListType.PatientStatusList:
                case MasterListType.TherapeuticCeilingList:
                case MasterListType.VisitOptionList:
                case MasterListType.AccessControlList:
                    break;
            }
        }

        return new List<Patient>();
    }

    public async Task<Patient?> FindByPatientId(string patientId)
    {
        if (string.IsNullOrWhiteSpace(patientId)) return null;

        //Último paciente admitido
        var patient = await Collection.Find(p => !p.DisTime.HasValue && patientId == p.PatientId)
            .Sort(Builders<Patient>.Sort.Descending(p => p.AdmTime)).Limit(1).FirstOrDefaultAsync();

        //Último con fecha de alta más reciente
        patient ??= await Collection.Find(p => p.DisTime.HasValue && patientId == p.PatientId)
            .Sort(Builders<Patient>.Sort.Descending(p => p.DisTime)).Limit(1).FirstOrDefaultAsync();

        return patient;
    }

    public async Task<Patient?> FindByPatientId(ObjectId patientId)
    {
        //Último paciente admitido
        var patient = await Collection.Find(p => !p.DisTime.HasValue && patientId == p.Id)
            .Sort(Builders<Patient>.Sort.Descending(p => p.AdmTime)).Limit(1).FirstOrDefaultAsync();

        //Último con fecha de alta más reciente
        patient ??= await Collection.Find(p => p.DisTime.HasValue && patientId == p.Id)
            .Sort(Builders<Patient>.Sort.Descending(p => p.DisTime)).Limit(1).FirstOrDefaultAsync();

        return patient;
    }

    public async Task<List<Patient>> FindAll()
    {
        return (await Collection.FindAsync(Builders<Patient>.Filter.Empty)).ToList();
        //return (await Collection.FindAsync(_ => true)).ToList();
    }

    public async Task<List<Patient>> FindByPointOfCare(string pointOfCare)
    {
        var filterBuilder = Builders<Patient>.Filter;
        var filter = filterBuilder.Eq(pa => pa.UnitString, pointOfCare);

        var result = await Collection.FindAsync(filter);

        return await result.ToListAsync();
    }

    public async Task<List<Patient>> FindByPointOfCare(ObjectId pointOfCare)
    {
        var filterBuilder = Builders<Patient>.Filter;
        var filter = filterBuilder.Eq(pa => pa.PointOfCareId, pointOfCare);

        var result = await Collection.FindAsync(filter);

        return await result.ToListAsync();
    }

    public async Task<List<Patient>> FindInActivePoC()
    {
        var virtualPointOfCareValues = Enum.GetValues(typeof(VirtualPointOfCare)).Cast<VirtualPointOfCare>()
            .Select(p => p.ToString())
            .ToList();

        //var filterBuilder = Builders<Patient>.Filter;

        // Define el pipeline de agregación
        var pipeline = new[]
        {
            new BsonDocument("$lookup", new BsonDocument
            {
                { "from", "pointOfCares" }, // Colección de PointOfCare
                { "localField", "pointOfCareId" },
                { "foreignField", "_id" },
                { "as", "pointOfCareInfo" }
            }),
            new BsonDocument("$unwind", "$pointOfCareInfo"),
            new BsonDocument("$match", new BsonDocument
            {
                { "pointOfCareInfo.bed", new BsonDocument("$nin", new BsonArray(virtualPointOfCareValues)) }
            })
        };

        var result = await Collection.Aggregate<Patient>(pipeline).ToListAsync();
        return result;
    }


    public async Task<List<Patient>> FindInInactivePoC()
    {
        var virtualPointOfCareValues = Enum.GetValues(typeof(VirtualPointOfCare)).Cast<VirtualPointOfCare>()
            .Select(p => p.ToString())
            .ToList();

        //var filterBuilder = Builders<Patient>.Filter;

        // Define el pipeline de agregación
        var pipeline = new[]
        {
            new BsonDocument("$lookup", new BsonDocument
            {
                { "from", "pointOfCares" }, // Colección de PointOfCare
                { "localField", "pointOfCareId" },
                { "foreignField", "_id" },
                { "as", "pointOfCareInfo" }
            }),
            new BsonDocument("$unwind", "$pointOfCareInfo"),
            new BsonDocument("$match", new BsonDocument
            {
                { "pointOfCareInfo.bed", new BsonDocument("$in", new BsonArray(virtualPointOfCareValues)) }
            })
        };

        var result = await Collection.Aggregate<Patient>(pipeline).ToListAsync();
        return result;
    }

    public IFindFluent<Patient, Patient> GetPaginatedPatients(PaginationFilter filter)
    {
        var filterBuilder = Builders<Patient>.Filter;
        var sort = Builders<Patient>.Sort.Descending("admTime");
        var filters = new List<FilterDefinition<Patient>>();

        if (filter.FilteredRequest != null)
        {
            AddDefaultFilters(filters, filter, filterBuilder);
            // return CreateFindFluent(filters, sort);
        }

        var requestFilter = filter.FilteredRequest;

        if(requestFilter == null) return CreateFindFluent(filters, sort);

        AddTimeFilters(requestFilter, filters, filterBuilder);

        if (!string.IsNullOrEmpty(filter.FilteredRequest?.Text))
        {
            var textFilter = filter.FilteredRequest.Text;
            var textFilterEscaped = Regex.Escape(textFilter);

            var orFilters = new List<FilterDefinition<Patient>>
            {
                filterBuilder.Regex(p => p.Person!.FirstName, new BsonRegularExpression(textFilterEscaped, "i")),
                filterBuilder.Regex(p => p.Person!.SecondName, new BsonRegularExpression(textFilterEscaped, "i")),
                filterBuilder.Regex(p => p.Person!.LastName, new BsonRegularExpression(textFilterEscaped, "i")),
                filterBuilder.Regex(p => p.PatientNumber, new BsonRegularExpression(textFilterEscaped, "i"))
            };

            if (ObjectId.TryParse(textFilter, out var id)) orFilters.Add(filterBuilder.Eq("_id", id));

            filters.Add(filterBuilder.Or(orFilters));
        }

        if (filter.FilteredRequest?.UnitId != null && ObjectId.TryParse(filter.FilteredRequest.UnitId, out var unitId))
            filters.Add(filterBuilder.Eq("unitId", unitId));
        if (filter.FilteredRequest?.PocId != null && ObjectId.TryParse(filter.FilteredRequest.PocId, out var pocId))
            filters.Add(filterBuilder.Eq("pointOfCareId", pocId));
        return CreateFindFluent(filters, sort);
    }


    public async Task<List<Patient>> FindPatientsNotUpdatedSince(DateTime date)
    {
        var filter = Builders<Patient>.Filter.Or(
            Builders<Patient>.Filter.Lt(p => p.UpdateDate, date),
            Builders<Patient>.Filter.Eq(p => p.UpdateDate, null)
        );
        return await Collection.Find(filter).ToListAsync();

        //return await Collection.Find(p => p.UpdateDate < date).ToListAsync();
    }

    public async Task<List<Patient>> FindDischargedPatients()
    {
        try
        {
            var filter = Builders<Patient>.Filter.Ne(p => p.DisTime, null);
            var cursor = await Collection.FindAsync(filter);
            var patients = await cursor.ToListAsync();

            return patients;
        }
        catch (Exception ex)
        {
            Log.Error("Error finding discharged patients {exMessage}", ex.Message);
            return [];
        }
    }

    public async Task<Patient?> UpdateOne(Patient updatedPatient)
    {
        var filter = Builders<Patient>.Filter.Eq("_id", updatedPatient.Id);

        var update = Builders<Patient>.Update
            .Set(p => p.PatientNumber, updatedPatient.PatientNumber)
            .Set(p => p.Person, updatedPatient.Person)
            .Set(p => p.UpdateDate, DateTime.UtcNow);

        return await Collection.FindOneAndUpdateAsync(filter, update,
            new FindOneAndUpdateOptions<Patient, Patient> { ReturnDocument = ReturnDocument.After });
    }

    public async Task<Patient?> UpdatePatientIncomingData(ObjectId patientId, Patient person)
    {
        var filter = Builders<Patient>.Filter.Eq("_id", patientId);
        var update = Builders<Patient>.Update
            .Set(p => p.OriginAux, person.OriginAux)
            .Set(p => p.DiagnosisAux, person.DiagnosisAux)
            .Set(p => p.Diagnosis, person.Diagnosis)
            .Set(p => p.UpdateDate, DateTime.UtcNow)
            .Set(p => p.AdmTime, person.AdmTime)
            .Set(p => p.Origin, person.Origin);

        return await Collection.FindOneAndUpdateAsync(filter, update,
            new FindOneAndUpdateOptions<Patient, Patient> { ReturnDocument = ReturnDocument.After });
    }

    public async Task<Patient?> UpdatePatientDemographicData(ObjectId patientId, Patient person)
    {
        var filter = Builders<Patient>.Filter.Eq("_id", patientId);
        var update = Builders<Patient>.Update
            .Set(p => p.UpdateDate, DateTime.UtcNow)
            .Set(p => p.Allergies, person.Allergies)
            .Set(p => p.LanguageBarrier, person.LanguageBarrier)
            .Set(p => p.DiagnosisAux, person.DiagnosisAux)
            .Set(p => p.Diagnosis, person.Diagnosis)
            .Set(p => p.Person, person.Person);


        return await Collection.FindOneAndUpdateAsync(filter, update,
            new FindOneAndUpdateOptions<Patient, Patient> { ReturnDocument = ReturnDocument.After });
    }


    public override async Task CreateIndexes()
    {
        var options = new CreateIndexOptions<Patient> { Background = true, Unique = false };
        var optionsUq = new CreateIndexOptions<Patient>
        {
            Background = true,
            Unique = true
            //PartialFilterExpression = Builders<Patient>.Filter.Exists(p => p.PointOfCareId) &
            //                          Builders<Patient>.Filter.Exists(p => p.UnitId)
        };
        var indexes = new List<CreateIndexModel<Patient>>
        {
            new("{ patientNumber: 1 }", optionsUq),
            new("{ admTime: 1 }", options)
            //new("{ pointOfCareId: 1, unitId: 1 }", optionsUq),
            //new("{ pointOfCareId: 1 }", optionsUq)
        };

        await MongoUtils.EnsureIndexes(Collection, indexes);
    }

    private static void AddTimeFilters(FilteredRequest requestFilter, List<FilterDefinition<Patient>> filters,
        FilterDefinitionBuilder<Patient> filterBuilder)
    {
        // Filter by admission time
        if (requestFilter.StartAdmTime.HasValue)
            filters.Add(filterBuilder.Gte(p => p.AdmTime, requestFilter.StartAdmTime.Value));
        if (requestFilter.EndAdmTime.HasValue)
            filters.Add(filterBuilder.Lte(p => p.AdmTime, requestFilter.EndAdmTime.Value));

        // Filter by discharge time
        if (requestFilter.StartDischargeTime.HasValue)
            filters.Add(filterBuilder.Gte(p => p.DisTime, requestFilter.StartDischargeTime.Value));
        if (requestFilter.EndDischargeTime.HasValue)
            filters.Add(filterBuilder.Lte(p => p.DisTime, requestFilter.EndDischargeTime.Value));

        // Filter by last observation time
        if (requestFilter.StartLastObsTime.HasValue)
            filters.Add(filterBuilder.Gte(p => p.LastObservationDate, requestFilter.StartLastObsTime.Value));
        if (requestFilter.EndLastObsTime.HasValue)
            filters.Add(filterBuilder.Lte(p => p.LastObservationDate, requestFilter.EndLastObsTime.Value));

        // Filter by creation date
        if (requestFilter.StartCreationDateTime.HasValue)
            filters.Add(filterBuilder.Gte(p => p.CreationDate, requestFilter.StartCreationDateTime.Value));
        if (requestFilter.EndCreationDateTime.HasValue)
            filters.Add(filterBuilder.Lte(p => p.CreationDate, requestFilter.EndCreationDateTime.Value));

        // Filter by update date
        if (requestFilter.StartUpDateTime.HasValue)
            filters.Add(filterBuilder.Gte(p => p.UpdateDate, requestFilter.StartUpDateTime.Value));
        if (requestFilter.EndUpDateTime.HasValue)
            filters.Add(filterBuilder.Lte(p => p.UpdateDate, requestFilter.EndUpDateTime.Value));

        // Filter by birth date (with Person null check)
        if (requestFilter.StartBirthDate.HasValue || requestFilter.EndBirthDate.HasValue)
        {
            // Check if Person is not null
            filters.Add(filterBuilder.Exists(p => p.Person));

            // Apply birth date filters only if Person is not null
            if (requestFilter.StartBirthDate.HasValue)
                filters.Add(filterBuilder.Gte(p => p.Person!.BirthDate, requestFilter.StartBirthDate.Value));
            if (requestFilter.EndBirthDate.HasValue)
                filters.Add(filterBuilder.Lte(p => p.Person!.BirthDate, requestFilter.EndBirthDate.Value));
        }
    }


    public async Task<List<Patient>> FindAllByPointOfCareId(ObjectId pointOfCare)
    {
        var filterBuilder = Builders<Patient>.Filter;
        var filter = filterBuilder.Eq(pa => pa.PointOfCareId, pointOfCare);

        var result = await Collection.FindAsync(filter);

        return await result.ToListAsync();
    }

    private void AddDefaultFilters(List<FilterDefinition<Patient>> filters, PaginationFilter filter,
        FilterDefinitionBuilder<Patient> filterBuilder)
    {
        // Verifica si PatientId tiene valor
        if (filter.FilteredRequest?.PatientId != null)
        {
            if(ObjectId.TryParse(filter.FilteredRequest.PatientId, out var patientId))
                filters.Add(
                    filterBuilder.Eq(p => p.Id, patientId)
                );
        }

        // Verifica si PatientNumber tiene valor
        else if (filter.FilteredRequest?.PatientNumber != null)
            filters.Add(
                filterBuilder.Eq(p => p.PatientNumber, filter.FilteredRequest.PatientNumber)
            );
    }

    private IFindFluent<Patient, Patient> CreateFindFluent(List<FilterDefinition<Patient>> filters,
        SortDefinition<Patient> sort)
    {
        var combinedFilter = filters.Any()
            ? Builders<Patient>.Filter.And(filters)
            : Builders<Patient>.Filter.Empty;

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