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

namespace adas_core.Infrastructure.Repositories;

public class PointOfCareRepository : MongoRepository<PointOfCare>, IPointOfCareRepository
{
    private readonly ApiSettings _apiSettings;

    public PointOfCareRepository(IOptions<ApiSettings>? apiSettings, IMongoDatabase database) : base(database)
    {
        if (apiSettings != null)
            _apiSettings = apiSettings.Value;
        else
            throw new ArgumentNullException(nameof(apiSettings));
    }

   

    public override string GetCollectionName()
    {
        return _apiSettings.Locations;
    }

    public override async Task InsertOneAsync(PointOfCare pointOfCare)
    {
        try
        {
            await base.InsertOneAsync(pointOfCare);
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            Log.Error("Exception trying to insert pointOfCare: {pointOfCare}. Exception {e}", pointOfCare, e);
            throw;
        }
    }

    public async Task Delete(ObjectId id)
    {
        try
        {
            var filter = Builders<PointOfCare>.Filter.Eq(x => x.Id, id);
            await Collection.DeleteOneAsync(filter, null);
        }
        catch (Exception e)
        {
            Log.Error("Exception trying to delete pointOfCare: {id}. Exception {e}", id, e);
            throw;
        }
    }

    public async Task<bool> DeleteManyByUnitId(ObjectId unitId)
    {
        try
        {
            var filter = Builders<PointOfCare>.Filter.Where(p => p.UnitId == unitId);
            await Collection.DeleteManyAsync(filter);

            return true;
        }
        catch (Exception e)
        {
            Log.Error(e.Message);
            return false;
        }
    }

    public async Task Update(PointOfCare pointOfCare)
    {
        try
        {
            await UpdateOneAsync(pointOfCare.Id, pointOfCare);
        }
        catch (Exception e)
        {
            Log.Error("Exception trying to update pointOfCare: {pointOfCare}. Exception {e}", pointOfCare, e);
            throw;
        }
    }

    public async Task UpdateUnitId(ObjectId id, Unit unit)
    {
        var filterBuilder = Builders<PointOfCare>.Filter;
        var filter = filterBuilder.Eq(p => p.Id, id);

        var update = Builders<PointOfCare>.Update
            .Set(p => p.UnitName, unit.Name)
            .Set(p => p.UnitId, unit.Id);

        await Collection.UpdateOneAsync(filter, update);
    }

    public async Task UpdateRelayConfig(ObjectId pocId, List<Relay> relayConfig)
    {
        var filterBuilder = Builders<PointOfCare>.Filter;
        var filter = filterBuilder.Eq(p => p.Id, pocId);
        var relayIds = relayConfig.Select(r => r.Id).ToList();
        var update = Builders<PointOfCare>.Update
            .Set(p => p.Configuration!.RelayIdList, relayIds);

        await Collection.UpdateOneAsync(filter, update);
    }
    public async Task UpdateRelayConfig(ObjectId pocId, List<ObjectId> relayConfig)
    {
        var filterBuilder = Builders<PointOfCare>.Filter;
        var filter = filterBuilder.Eq(p => p.Id, pocId);
        var update = Builders<PointOfCare>.Update
            .Set(p => p.Configuration!.RelayIdList, relayConfig);

        await Collection.UpdateOneAsync(filter, update);
    }

    public async Task UpdateConfiguration(ObjectId id, PointOfCareConfiguration configuration)
    {
        var filterBuilder = Builders<PointOfCare>.Filter;
        var filter = filterBuilder.Eq(p => p.Id, id);

        var update = Builders<PointOfCare>.Update
            .Set(p => p.Configuration, configuration);

        await Collection.UpdateOneAsync(filter, update);
    }

    public async Task<PointOfCare?> FindById(ObjectId id)
    {
        try
        {
            var filter = Builders<PointOfCare>.Filter.Eq(p => p.Id, id);
            return await Collection.Find(filter).FirstOrDefaultAsync();
        }
        catch (Exception ex)
        {
            Log.Error("Error searching pointOfCare by id: {id}. Exception: {ex}", id, ex);
            return null;
        }
    }


    public async Task<IEnumerable<PointOfCare>> FindByUnitAndStatus(ObjectId unitId, StatusEnum.PointOfCare status,
        bool excludeVirtual = false)
    {
        try
        {
            var filterBuilder = Builders<PointOfCare>.Filter;

            var filter = filterBuilder.And(
                filterBuilder.Eq(p => p.UnitId, unitId),
                filterBuilder.Eq(p => p.Status, status)
            );
            if (excludeVirtual)
                filter = filterBuilder.And(
                    filter,
                    filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Pushed.ToString()),
                    filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Unknown.ToString()),
                    filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Deleted.ToString()),
                    filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.NoBed.ToString()),
                    filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Cancelled.ToString()),
                    filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Recovered.ToString()),
                    filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Moved.ToString())
                );
            var result = await Collection.FindAsync(filter);

            return result.ToEnumerable();
        }
        catch (Exception ex)
        {
            Log.Error("Error searching pointOfCares by Unit: {unit} and status: {}. Exception: {ex}", unitId.ToString(),
                status.ToString(), ex);
            return new List<PointOfCare>();
        }
    }

    public async Task<PointOfCare?> FindByBedAndUnitId(string? bed, ObjectId unitId)
    {
        try
        {
            if (string.IsNullOrEmpty(bed)) return null;
            var filterBuilder = Builders<PointOfCare>.Filter;
            List<FilterDefinition<PointOfCare>> filters =
            [
                filterBuilder.Eq(p => p.Bed, bed),
                filterBuilder.Eq(p => p.UnitId, unitId)
                // Combina todos los filtros en uno solo usando el operador & si hay más de un filtro, de lo contrario, usa un filtro vacío.
            ];

            // Combina todos los filtros en uno solo usando el operador & si hay más de un filtro, de lo contrario, usa un filtro vacío.
            var combinedFilter = filters.Count > 0
                ? filters.Aggregate((current, next) => current & next)
                : filterBuilder.Empty;

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

            return result;
        }
        catch (Exception ex)
        {
            Log.Error("Error searching pointOfCares by patient location unitId: {unitId}, bed: {bed}. Exception: {ex}",
                unitId, bed, ex);
            return null;
        }
    }

    public async Task<IEnumerable<PointOfCare>?> FindAllByUnitId(ObjectId unit)
    {
        try
        {
            var filter = Builders<PointOfCare>.Filter.Eq(p => p.UnitId, unit);
            var result = await Collection.FindAsync(filter);

            return result.ToEnumerable();
        }
        catch (Exception ex)
        {
            Log.Error("Error searching pointOfCares by Unit: {unit}. Exception: {ex}", unit, ex);
            return null;
        }
    }

    public async Task<IEnumerable<PointOfCare>?> FindByRoom(string room)
    {
        try
        {
            var filter = Builders<PointOfCare>.Filter.Eq(p => p.Room, room);
            var result = await Collection.FindAsync(filter);

            return result.ToEnumerable();
        }
        catch (Exception ex)
        {
            Log.Error("Error searching pointOfCares by room: {room}. Exception: {ex}", room, ex);
            return null;
        }
    }

    public async Task<IEnumerable<PointOfCare>?> FindByBed(string bed)
    {
        try
        {
            var filter = Builders<PointOfCare>.Filter.Eq(p => p.Bed, bed);
            var result = await Collection.FindAsync(filter);

            return result.ToEnumerable();
        }
        catch (Exception ex)
        {
            Log.Error("Error searching pointOfCares by bed: {bed}. Exception: {ex}", bed, ex);
            return null;
        }
    }

    public async Task<List<PointOfCare>> FindByFilter(FilterDefinition<PointOfCare> filter,
        ProjectionDefinition<PointOfCare>? projection = null)
    {
        if (projection != null)
            return await Collection.Find(filter).Project<PointOfCare>(projection).ToListAsync();
        return await Collection.Find(filter).ToListAsync();
    }

    public async Task<long> CountByUnitId(ObjectId unitId)
    {
        try
        {
            var filterBuilder = Builders<PointOfCare>.Filter;
            var excludedBeds = new[]
            {
                VirtualPointOfCare.Pushed.ToString(),
                VirtualPointOfCare.Unknown.ToString(),
                VirtualPointOfCare.Deleted.ToString(),
                VirtualPointOfCare.NoBed.ToString(),
                VirtualPointOfCare.Cancelled.ToString(),
                VirtualPointOfCare.UnitData.ToString(),
                VirtualPointOfCare.Recovered.ToString(),
                VirtualPointOfCare.Moved.ToString()
            };
            var filter = filterBuilder.And(
                filterBuilder.Eq(p => p.UnitId, unitId),
                filterBuilder.Nin(p => p.Bed, excludedBeds)
            );
            
            return await Collection.CountDocumentsAsync(filter);
        }
        catch (Exception e)
        {
            Log.Logger.Error(e.Message);
            throw;
        }
    }
    public async Task<long> CountVirtualsByUnitId(ObjectId unitId)
    {
        try
        {
            var filterBuilder = Builders<PointOfCare>.Filter;

            // Lista de estados que NO quieres contar
            var excludedBeds = new[]
            {
                VirtualPointOfCare.Pushed.ToString(),
                VirtualPointOfCare.Unknown.ToString(),
                VirtualPointOfCare.Deleted.ToString(),
                VirtualPointOfCare.NoBed.ToString(),
                VirtualPointOfCare.Cancelled.ToString(),
                VirtualPointOfCare.Recovered.ToString(),
                VirtualPointOfCare.UnitData.ToString(),
                VirtualPointOfCare.Moved.ToString()
            };

            // Filtramos por UnitId Y que el Bed NO esté en la lista de excluidos
            var filter = filterBuilder.And(
                filterBuilder.Eq(p => p.UnitId, unitId),
                filterBuilder.In(p => p.Bed, excludedBeds)
            );
        
            return await Collection.CountDocumentsAsync(filter);
        }
        catch (Exception e)
        {
            Log.Logger.Error(e.Message);
            throw;
        }
    }
    public async Task<PointOfCare?> GetPoCConfiguration(ObjectId pocId)
    {
        var filter = Builders<PointOfCare>.Filter.Eq(p => p.Id, pocId);

        var projection = Builders<PointOfCare>.Projection
            .Include(p => p.Id)
            .Include(p => p.Configuration);

        return await Collection.Find(filter).Project<PointOfCare>(projection).FirstOrDefaultAsync();
    }

    public async Task<List<PointOfCare>?> GetAll()
    {
        var filter = Builders<PointOfCare>.Filter.Empty;
        return await Collection.Find(filter).ToListAsync();
    }
    public async Task<PointOfCare?> FindByIdAllConfig(ObjectId id)
    {
        return await Collection.Aggregate()
            .Match(c=>c.Id == id)
            .Lookup(_apiSettings.LightBeacons, "configuration.beaconIdList", "_id", "beacons")
            .Lookup(_apiSettings.Cameras, "configuration.cameraIdList", "_id", "cameras")
            .Lookup(_apiSettings.Relays, "configuration.relayIdList", "_id", "relays")
            .Project<PointOfCare>(new BsonDocument
            {
                { "_id", 1 },
                { "room", 1 },
                { "bed", 1 },
                { "hall", 1 },
                { "unitId", 1 },
                { "status", 1 },
                { "admissionId", 1 },
                { "configuration", new BsonDocument
                    {
                        { "beaconList", "$beacons" }, 
                        { "cameraList", "$cameras" },
                        { "relayList", "$relays" },
                        { "beaconIdList", "$configuration.beaconIdList" },
                        { "cameraIdList", "$configuration.cameraIdList" },
                        { "relayIdList", "$configuration.relayIdList" },
                        { "type", "$configuration.type" },
                        { "id", "$configuration.id" }
                    }
                }
            }).FirstOrDefaultAsync();
    }

    /// <summary>
    /// Obtiene de forma asíncrona todos los identificadores únicos de cámaras que están 
    /// vinculados a algún PointOfCare.
    /// </summary>
    /// <remarks>
    /// Se retorna un <see cref="HashSet{ObjectId}"/> para optimizar la búsqueda de pertenencia (Contains) en el servicio.
    /// Mientras que una <see cref="List{T}"/> requiere un tiempo de búsqueda lineal $O(n)$, el HashSet utiliza 
    /// una tabla hash que permite verificar si una cámara está en uso en tiempo constante $O(1)$.
    /// Esto es crítico para mantener el rendimiento al comparar los IDs de la página actual 
    /// contra el total de cámaras en uso, independientemente del volumen de datos.
    /// </remarks>
    /// <returns>Un conjunto hash con los <see cref="ObjectId"/> de las cámaras en uso.</returns>
    public async Task<HashSet<ObjectId>> FindAllIdCamerasInUse()
    {
        var distinctIds = await Collection
            .DistinctAsync<ObjectId>("configuration.cameraIdList", Builders<PointOfCare>.Filter.Empty);

        var list = await distinctIds.ToListAsync();
        return new HashSet<ObjectId>(list);
    }
    public async Task<HashSet<ObjectId>> FindAllIdBeaconsInUse()
    {
        var distinctIds = await Collection
            .DistinctAsync<ObjectId>("configuration.beaconIdList", Builders<PointOfCare>.Filter.Empty);

        var list = await distinctIds.ToListAsync();
        return new HashSet<ObjectId>(list);
    }

    public async Task<IEnumerable<PointOfCare>?> FindAllByUnitIdWithDevices(ObjectId unitId)
    {
        try
        {
            var pipeline = Collection.Aggregate()
                // 1. Filtramos primero por UnitId (muy importante para rendimiento)
                .Match(p => p.UnitId == unitId)
            
                // 2. Realizamos los Lookups usando las colecciones desde settings
                .Lookup(_apiSettings.LightBeacons, "configuration.beaconIdList", "_id", "beacons")
                .Lookup(_apiSettings.Cameras, "configuration.cameraIdList", "_id", "cameras")
                .Lookup(_apiSettings.Relays, "configuration.relayIdList", "_id", "relays")
            
                // 3. Proyectamos para que coincida exactamente con tu modelo C#
                .Project<PointOfCare>(new BsonDocument
                {
                    { "_id", 1 },
                    { "room", 1 },
                    { "bed", 1 },
                    { "hall", 1 },
                    { "unitId", 1 },
                    { "status", 1 },
                    { "admissionId", 1 },
                    { "configuration", new BsonDocument
                        {
                            // Mapeamos los arrays temporales a las propiedades de la clase
                            { "beaconList", "$beacons" }, 
                            { "cameraList", "$cameras" },
                            { "relayList", "$relays" },
                            { "beaconIdList", "$configuration.beaconIdList" },
                            { "cameraIdList", "$configuration.cameraIdList" },
                            { "relayIdList", "$configuration.relayIdList" },
                            { "type", "$configuration.type" },
                            { "id", "$configuration.id" }
                        }
                    }
                });

            return await pipeline.ToListAsync();
        }
        catch (Exception ex)
        {
            Log.Error("Error searching pointOfCares by Unit: {unit}. Exception: {ex}", unitId, ex);
            return null;
        }
    }

    public async Task<HashSet<ObjectId>> FindAllIdRelaysInUse()
    {
        var distinctIds = await Collection
            .DistinctAsync<ObjectId>("configuration.relayIdList", Builders<PointOfCare>.Filter.Empty);

        var list = await distinctIds.ToListAsync();
        return new HashSet<ObjectId>(list);
    }

    public async Task<List<PointOfCare>?> GetAllConfigs()
    {
        var pipeline = Collection.Aggregate()
            .Lookup(
                _apiSettings.LightBeacons,
                "configuration.beaconIdList",
                "_id",
                "beacons"
            )
            .Lookup(
                _apiSettings.Cameras,
                "configuration.cameraIdList",
                "_id",
                "cameras"
            )
            .Lookup(
                _apiSettings.Relays,
                "configuration.relayIdList",
                "_id",
                "relays"
            )
            .Project<PointOfCare>(new BsonDocument
            {
                { "_id", 1 },
                { "room", 1 },
                { "bed", 1 },
                { "hall", 1 },
                { "unitId", 1 },
                { "status", 1 },
                { "admissionId", 1 },

                { "configuration", new BsonDocument
                    {
                        { "beaconList", "$beacons" },
                        { "cameraList", "$cameras" },
                        { "relayList", "$relays" },
                        { "type", "$configuration.type" },
                        { "id", "$configuration.id" }
                    }
                }
            });

        return await pipeline.ToListAsync();
    }

    public async Task<List<PointOfCare>?> GetAllLocationInfo()
    {
        var filter = Builders<PointOfCare>.Filter.Empty;

        var projection = Builders<PointOfCare>.Projection
            .Include(p => p.Id)
            .Include(p => p.UnitId)
            .Include(p => p.Room)
            .Include(p => p.Bed);

        var result = await Collection.Find(filter).Project<PointOfCare>(projection).ToListAsync();

        return result;
    }

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

        if (filter.FilteredRequest == null) return CreateFindFluent(filters, sort);

        // Verifica UnitName contiene el valor de FilteredRequest.Text
        if (!string.IsNullOrEmpty(filter.FilteredRequest?.UnitId) &&
            ObjectId.TryParse(filter.FilteredRequest.UnitId, out var unitId))
            filters.Add(filterBuilder.Eq(d => d.UnitId, unitId));
        else if (!string.IsNullOrEmpty(filter.FilteredRequest?.UnitName))
            filters.Add(filterBuilder.Eq(d => d.Unit!.Name, filter.FilteredRequest?.UnitName));

        if (filter.FilteredRequest?.PointOfCareStatus != null)
        {
            var statusFilter = filter.FilteredRequest.PointOfCareStatus;

            filters.Add(filterBuilder.Eq(p => p.Status, statusFilter));
        }

        return CreateFindFluent(filters, sort);
    }

    //Deprecated PatientLocation by UnitName
    public async Task<PointOfCare?> FindByPatientLocation(PatientLocation patientLocation)
    {
        try
        {
            var filterBuilder = Builders<PointOfCare>.Filter;
            List<FilterDefinition<PointOfCare>> filters = [];

            if (!string.IsNullOrEmpty(patientLocation.UnitName))
                filters.Add(filterBuilder.Eq(p => p.UnitName, patientLocation.UnitName));

            if (!string.IsNullOrEmpty(patientLocation.Bed))
                filters.Add(filterBuilder.Eq(p => p.Bed, patientLocation.Bed));

            if (!string.IsNullOrEmpty(patientLocation.Room))
                filters.Add(filterBuilder.Eq(p => p.Room, patientLocation.Room));

            // Combina todos los filtros en uno solo usando el operador & si hay más de un filtro, de lo contrario, usa un filtro vacío.
            var combinedFilter = filters.Count > 0
                ? filters.Aggregate((current, next) => current & next)
                : filterBuilder.Empty;

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

            return result;
        }
        catch (Exception ex)
        {
            Log.Error("Error searching pointOfCares by patient location: {bed}. Exception: {ex}", patientLocation, ex);
            return null;
        }
    }

    public override async Task CreateIndexes()
    {
        var options = new CreateIndexOptions { Background = true, Unique = false };
        var indexes = new List<CreateIndexModel<PointOfCare>>
        {
            new("{ unitId: 1 }", options),
            new("{ room: 1 }", options),
            new("{ bed: 1 }", options)
        };

        await MongoUtils.EnsureIndexes(Collection, indexes);
    }

    public async Task UpdateStatus(ObjectId id, StatusEnum.PointOfCare status)
    {
        var filterBuilder = Builders<PointOfCare>.Filter;
        var filter = filterBuilder.Eq(p => p.Id, id);

        var update = Builders<PointOfCare>.Update
            .Set(p => p.Status, status);

        await Collection.UpdateOneAsync(filter, update);
    }

    private IFindFluent<PointOfCare, PointOfCare> CreateFindFluent(List<FilterDefinition<PointOfCare>> filters,
        SortDefinition<PointOfCare> sort)
    {
        var combinedFilter = filters.Any()
            ? Builders<PointOfCare>.Filter.And(filters)
            : Builders<PointOfCare>.Filter.Empty; // Filtra todo si no hay filtros
        return Collection.Find(combinedFilter).Sort(sort);
    }
}