using adas_core.Application.Exceptions;
using adas_core.Application.Repositories.Interfaces;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.Filter;
using adas_core.Domain.Models.MongoModels;
using Microsoft.Extensions.Logging;
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;

/// <summary>
/// Repository implementation for managing Display entities in MongoDB.
/// Provides CRUD operations and specialized queries for display devices.
/// </summary>
public class DisplayRepository : MongoRepository<Display>, IDisplayRepository
{
    #region Properties

    private readonly ApiSettings _apiSettings;
    private readonly ILogger<DisplayRepository> _logger;

    #endregion

    #region Constructor

    /// <summary>
    /// Initializes a new instance of the DisplayRepository.
    /// </summary>
    /// <param name="apiSettings">API settings containing collection names configuration.</param>
    /// <param name="database">The MongoDB database instance.</param>
    /// <param name="logger">Logger for repository operations.</param>
    public DisplayRepository(
        IOptions<ApiSettings> apiSettings,
        IMongoDatabase database,
        ILogger<DisplayRepository> logger) : base(database)
    {
        _logger = logger;
        _apiSettings = apiSettings.Value;
    }

    #endregion

    #region Methods

    #region Create

    /// <summary>
    /// Creates the necessary indexes for the Display collection.
    /// Creates indexes on displayConfigId and unitId fields for improved query performance.
    /// </summary>
    public override async Task CreateIndexes()
    {
        var options = new CreateIndexOptions<Display> { Background = true, Unique = false };

        var indexes = new List<CreateIndexModel<Display>>
        {
            new("{ displayConfigId: 1 }", options),
            new("{ unitId: 1 }", options)
        };

        await MongoUtils.EnsureIndexes(Collection, indexes);
    }

    #endregion

    #region Read

    /// <summary>
    /// Gets the name of the collection for displays.
    /// </summary>
    /// <returns>The collection name from API settings.</returns>
    public override string GetCollectionName()
    {
        return _apiSettings.Displays;
    }

    /// <summary>
    /// Retrieves all displays from the database.
    /// </summary>
    /// <returns>A list of all Display entities.</returns>
    public async Task<List<Display>> GetAll()
    {
        var result = await Collection.Find(Builders<Display>.Filter.Empty).ToListAsync();
        return result;
    }

    /// <summary>
    /// Retrieves paginated displays with optional filtering.
    /// Supports filtering by text, unit ID, unit name, and display type.
    /// </summary>
    /// <param name="filter">The pagination and filtering parameters.</param>
    /// <returns>A fluent queryable for Display results.</returns>
    /// <exception cref="BadRequestException">Thrown when the text filter exceeds 100 characters.</exception>
    public IFindFluent<Display, Display> GetPaginatedDisplays(PaginationFilter filter)
    {
        var filterBuilder = Builders<Display>.Filter;
        var sort = Builders<Display>.Sort.Ascending("name");
        var filters = new List<FilterDefinition<Display>>();

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

        // Text filter seguro
        if (!string.IsNullOrWhiteSpace(filter.FilteredRequest.Text))
        {
            var textFilter = filter.FilteredRequest.Text;

            if (textFilter.Length > 100)
                throw new BadRequestException("Text filter too long");

            var escapedTextFilter = Regex.Escape(textFilter);

            filters.Add(filterBuilder.Regex(
                d => d.Name,
                new BsonRegularExpression(escapedTextFilter, "i")
            ));
        }

        // UnitId
        if (!string.IsNullOrWhiteSpace(filter.FilteredRequest.UnitId) &&
            ObjectId.TryParse(filter.FilteredRequest.UnitId, out var unitId))
        {
            filters.Add(filterBuilder.Eq(d => d.UnitId, unitId));
        }
        // UnitName fallback
        else if (!string.IsNullOrWhiteSpace(filter.FilteredRequest.UnitName))
        {
            filters.Add(filterBuilder.Eq(d => d.Unit!.Name, filter.FilteredRequest.UnitName));
        }

        // DisplayType
        if (filter.FilteredRequest.DisplayType != null)
        {
            filters.Add(filterBuilder.Eq(d => d.Type, filter.FilteredRequest.DisplayType));
        }

        return CreateFindFluent(filters, sort);
    }

    /// <summary>
    /// Creates a fluent query for paginated displays with combined filters.
    /// </summary>
    /// <param name="filters">List of filter definitions to apply.</param>
    /// <param name="sort">Sort definition for the query results.</param>
    /// <returns>A fluent queryable for Display results.</returns>
    private IFindFluent<Display, Display> CreateFindFluent(List<FilterDefinition<Display>> filters,
        SortDefinition<Display> sort)
    {
        var combinedFilter = filters.Any()
            ? Builders<Display>.Filter.And(filters)
            : Builders<Display>.Filter.Empty; // Filtra todo si no hay filtros
        return Collection.Find(combinedFilter).Sort(sort);
    }

    /// <summary>
    /// Retrieves all displays associated with a specific point of care.
    /// </summary>
    /// <param name="pointOfCare">The PointOfCare to filter by.</param>
    /// <returns>A list of Display entities associated with the point of care.</returns>
    public async Task<List<Display>> GetByPointOfCare(PointOfCare pointOfCare)
    {
        var filter = Builders<Display>.Filter.AnyEq(x => x.PointOfCareIdList, pointOfCare.Id);
        var result = await Collection.Find(filter).ToListAsync();
        return result;
    }

    /// <summary>
    /// Retrieves a display by its name.
    /// </summary>
    /// <param name="name">The name of the display to retrieve.</param>
    /// <returns>The Display if found; otherwise, null.</returns>
    public async Task<Display?> GetByName(string name)
    {
        var result = await Collection.FindAsync(Builders<Display>.Filter.Eq(p => p.Name, name));
        return await result.FirstOrDefaultAsync();
    }

    /// <summary>
    /// Retrieves a display by its ID.
    /// </summary>
    /// <param name="id">The ObjectId of the display to retrieve.</param>
    /// <returns>The Display if found; otherwise, null.</returns>
    public async Task<Display?> GetById(ObjectId id)
    {
        var result = await Collection.FindAsync(Builders<Display>.Filter.Eq(p => p.Id, id));
        return await result.FirstOrDefaultAsync();
    }

    /// <summary>
    /// Retrieves a display by its ID with the associated display configuration.
    /// Performs a MongoDB lookup to join the display with its configuration.
    /// </summary>
    /// <param name="id">The ObjectId of the display to retrieve.</param>
    /// <returns>The Display with DisplayConfig populated if found; otherwise, null.</returns>
    public async Task<Display?> GetByIdWithConfigDisplay(ObjectId id)
    {
        var pipeline = new BsonDocument[]
        {
            new("$match", new BsonDocument("_id", id)),
            new("$lookup", new BsonDocument
            {
                { "from", "config_displays" },
                { "localField", "displayConfigId" }, // Asumiendo que este es el campo que refiere a config_display
                { "foreignField", "_id" },
                { "as", "DisplayNurse" }
            }),
            new("$unwind", new BsonDocument
            {
                { "path", "$configDisplay" },
                { "preserveNullAndEmptyArrays", true }
            })
        };

        var result = await Collection.AggregateAsync<Display>(pipeline, new AggregateOptions { AllowDiskUse = true });
        return await result.FirstOrDefaultAsync();
    }

    /// <summary>
    /// Retrieves all displays associated with a specific unit.
    /// </summary>
    /// <param name="id">The ObjectId of the unit.</param>
    /// <returns>A list of Display entities associated with the unit.</returns>
    public async Task<List<Display>> GetByUnitId(ObjectId id)
    {
        var filter = Builders<Display>.Filter.Eq(p => p.UnitId, id);
        var result = await Collection.Find(filter).ToListAsync();
        return result;
    }

    /// <summary>
    /// Counts the number of displays associated with a specific unit.
    /// </summary>
    /// <param name="unitId">The ObjectId of the unit.</param>
    /// <returns>The count of displays for the unit, or 0 if an error occurs.</returns>
    /// <exception cref="Exception">Logs errors and returns 0 on failure.</exception>
    public async Task<long> CountByUnitId(ObjectId unitId)
    {
        try
        {
            return await Collection.CountDocumentsAsync(Builders<Display>.Filter.Eq(p => p.UnitId, unitId));
        }
        catch (Exception e)
        {
            _logger.LogError(e.Message);
            return 0;
        }
    }

    /// <summary>
    /// Retrieves all displays associated with a specific display configuration.
    /// </summary>
    /// <param name="id">The ObjectId of the display configuration.</param>
    /// <returns>A list of Display entities using the specified configuration.</returns>
    public async Task<List<Display>> GetByConfigId(ObjectId id)
    {
        var result = await Collection.FindAsync(Builders<Display>.Filter.Eq(p => p.DisplayConfigId, id));
        return result.ToList();
    }

    /// <summary>
    /// Retrieves all displays that use a specific card configuration through their display configuration.
    /// Performs an aggregation to join Display with DisplayConfig and filter by cardConfigId.
    /// </summary>
    /// <param name="configId">The ObjectId of the card configuration.</param>
    /// <returns>A list of Display entities using the specified card config, or empty list on error.</returns>
    /// <exception cref="Exception">Logs errors and returns empty list on failure.</exception>
    public async Task<List<Display>> GetByCardConfigId(ObjectId configId)
    {
        try
        {
            var aggregate = Collection.Aggregate()
                // 1. Unimos la colección Display con DisplayConfig
                .Lookup(
                    _apiSettings.DisplaysConfig, // Nombre de la colección externa
                    "displayConfigId", // Campo local en la colección 'Display'
                    "_id", // Campo en la colección 'DisplayConfig'
                    "displayConfig" // Nombre de la propiedad en la clase C# (debe coincidir)
                )
                // 2. Convertimos el array resultante del lookup en un objeto único
                .Unwind("displayConfig", new AggregateUnwindOptions<BsonDocument>
                {
                    PreserveNullAndEmptyArrays = false // Si no tiene config, no nos interesa
                })
                // 3. Filtramos por la propiedad interna del objeto ya "unido"
                // Nota: Usamos el nombre del campo tal cual está en el BSON (normalmente camelCase)
                .Match(Builders<BsonDocument>.Filter.Eq("displayConfig.cardConfigId", configId))

                // 4. Casteamos el resultado de vuelta a nuestra clase Display
                .As<Display>();

            return await aggregate.ToListAsync();
        }
        catch (Exception ex)
        {
            _logger.LogError("Error en GetByCardConfigId: {ex}", ex.Message);
            return [];
        }
    }

    /// <summary>
    /// Checks if a display configuration is currently in use by any displays.
    /// </summary>
    /// <param name="displayConfigId">The ObjectId of the display configuration to check.</param>
    /// <returns>The count of displays using the configuration.</returns>
    public async Task<long> IsDisplayConfigInUse(ObjectId displayConfigId)
    {
        return await Collection.CountDocumentsAsync(
            Builders<Display>.Filter.Eq(p => p.DisplayConfigId, displayConfigId));
    }

    #endregion

    #region Update

    /// <summary>
    /// Updates the point of care list for a specific display.
    /// </summary>
    /// <param name="objectId">The ObjectId of the display to update.</param>
    /// <param name="listPocObId">The new list of point of care ObjectIds.</param>
    /// <returns>The updated Display if successful; otherwise, null.</returns>
    /// <exception cref="Exception">Logs errors and returns null on failure.</exception>
    public async Task<Display?> UpdatePointOfCareList(ObjectId objectId, List<ObjectId> listPocObId)
    {
        try
        {
            var filter = Builders<Display>.Filter.Eq("Id", objectId);
            var update = Builders<Display>.Update
                .Set(c => c.PointOfCareIdList, listPocObId);

            return await Collection.FindOneAndUpdateAsync(filter, update,
                new FindOneAndUpdateOptions<Display, Display> { ReturnDocument = ReturnDocument.After });
        }
        catch (Exception e)
        {
            _logger.LogError($"Unable to update pointOfCareList from Display Exception {e}");
            return null;
        }
    }

    /// <summary>
    /// Updates the display configuration for a specific display.
    /// </summary>
    /// <param name="objectId">The ObjectId of the display to update.</param>
    /// <param name="config">The new DisplayConfig to set.</param>
    /// <returns>The updated Display if successful; otherwise, null.</returns>
    /// <exception cref="Exception">Logs errors and returns null on failure.</exception>
    public async Task<Display?> UpdateConfig(ObjectId objectId, DisplayConfig config)
    {
        try
        {
            var filter = Builders<Display>.Filter.Eq("Id", objectId);
            var update = Builders<Display>.Update
                .Set(c => c.DisplayConfig, config);

            return await Collection.FindOneAndUpdateAsync(filter, update,
                new FindOneAndUpdateOptions<Display, Display> { ReturnDocument = ReturnDocument.After });
        }
        catch (Exception e)
        {
            _logger.LogError($"Unable to update config from Display Exception {e.Message}");
            return null;
        }
    }

    /// <summary>
    /// Updates the display configuration ID reference for a specific display.
    /// </summary>
    /// <param name="objectId">The ObjectId of the display to update.</param>
    /// <param name="displayConfigId">The new display configuration ObjectId to set.</param>
    /// <returns>The updated Display if successful; otherwise, null.</returns>
    /// <exception cref="Exception">Logs errors and returns null on failure.</exception>
    public async Task<Display?> UpdateConfigId(ObjectId objectId, ObjectId displayConfigId)
    {
        try
        {
            var filter = Builders<Display>.Filter.Eq("Id", objectId);
            var update = Builders<Display>.Update
                .Set(c => c.DisplayConfigId, displayConfigId);

            return await Collection.FindOneAndUpdateAsync(filter, update,
                new FindOneAndUpdateOptions<Display, Display> { ReturnDocument = ReturnDocument.After });
        }
        catch (Exception e)
        {
            _logger.LogError($"Unable to update config from Display Exception {e.Message}");
            return null;
        }
    }

    /// <summary>
    /// Updates the display configuration preset for a specific display.
    /// </summary>
    /// <param name="objectIdDisplay">The ObjectId of the display to update.</param>
    /// <param name="objectIdConfigDisplay">The ObjectId of the display configuration preset.</param>
    /// <returns>The updated Display if successful; otherwise, null.</returns>
    /// <exception cref="Exception">Logs errors and returns null on failure.</exception>
    public async Task<Display?> UpdateConfigPreset(ObjectId objectIdDisplay, ObjectId objectIdConfigDisplay)
    {
        try
        {
            var filter = Builders<Display>.Filter.Eq("Id", objectIdDisplay);
            var update = Builders<Display>.Update
                .Set(c => c.DisplayConfigId, objectIdConfigDisplay);

            return await Collection.FindOneAndUpdateAsync(filter, update,
                new FindOneAndUpdateOptions<Display, Display> { ReturnDocument = ReturnDocument.After });
        }
        catch (Exception e)
        {
            _logger.LogError($"Unable to update config from Display Exception: {e.Message}");
            return null;
        }
    }

    /// <summary>
    /// Updates the name of a specific display.
    /// </summary>
    /// <param name="display">The Display entity to update.</param>
    /// <param name="name">The new name for the display.</param>
    /// <returns>The updated Display if successful.</returns>
    /// <exception cref="Exception">Throws an exception if MongoDB update fails.</exception>
    public async Task<Display> UpdateName(Display display, string name)
    {
        try
        {
            var filter = Builders<Display>.Filter.Eq("_id", display.Id);

            var update = Builders<Display>.Update
                .Set(d => d.Name, name);

            return await Collection.FindOneAndUpdateAsync(filter, update,
                new FindOneAndUpdateOptions<Display, Display> { ReturnDocument = ReturnDocument.After });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex.Message);
            throw;
        }
    }

    #endregion

    #region Delete

    /// <summary>
    /// Deletes all displays associated with a specific unit.
    /// </summary>
    /// <param name="unitId">The ObjectId of the unit whose displays should be deleted.</param>
    /// <returns>True if deletion was successful; otherwise, false.</returns>
    /// <exception cref="Exception">Logs errors and returns false on failure.</exception>
    public async Task<bool> DeleteManyByUnitId(ObjectId unitId)
    {
        try
        {
            var filter = Builders<Display>.Filter.Where(p => p.UnitId == unitId);
            await Collection.DeleteManyAsync(filter);

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

    #endregion

    #endregion
}