using adas_core.Application.Repositories.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.Filter;
using adas_core.Domain.Models.MongoModels;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using System.Text.RegularExpressions;

namespace adas_core.Infrastructure.Repositories;

/// <summary>
/// Represents a MongoDB-backed repository for managing <see cref="User"/> entities.
/// </summary>
/// <remarks>
/// Inherits core data access functionality from <see cref="MongoRepository{User}"/> and implements the <see cref="IUserRepository"/> contract.
/// </remarks>
public class UserRepository : MongoRepository<User>, IUserRepository
{
    private readonly ApiSettings _apiSettings;

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

    /// <summary>
    /// Retrieves the API collection name for users from the configured API settings.
    /// </summary>
    /// <returns>The configured name of the users collection.</returns>
    public override string GetCollectionName()
        {
            return _apiSettings.Users;
        }

    /// <summary>
    /// Retrieves a user from the collection whose username and password match the provided credentials.
    /// Returns the first matching user, or <c>null</c> when no user with the given username and password combination is found.
    /// </summary>
    /// <param name="username">The username to look up in the collection.</param>
    /// <param name="password">The password that must match the stored value for the user to be returned.</param>
    /// <returns>A <see cref="User"/> instance when a matching record is found; otherwise, <c>null</c>.</returns>
    public async Task<User?> GetUser(string username, string password)
        {
            var filter = Builders<User>
                .Filter.Eq(p => p.UserName, username) & Builders<User>
                .Filter.Eq(p => p.Password, password);
            var result = await Collection.FindAsync(filter);
            return await result.FirstOrDefaultAsync();
        }

    /// <summary>
    /// Retrieves a user from the collection by their unique identifier, returning <c>null</c> when no matching document is found.
    /// </summary>
    /// <param name="id">The <see cref="ObjectId"/> of the user to look up.</param>
    /// <returns>A <see cref="User"/> instance if a matching document exists; otherwise, <c>null</c>.</returns>
    public async Task<User?> GetById(ObjectId id)
        {
            var filter = Builders<User>
                .Filter.Eq(p => p.Id, id);
            var result = await Collection.FindAsync(filter);
            return await result.FirstOrDefaultAsync();
        }

    /// <summary>
    /// Retrieves a user from the data store by their unique username.
    /// Returns <c>null</c> when no user matches the provided username.
    /// </summary>
    /// <param name="name">The username used to look up the user.</param>
    /// <returns>A <see cref="User"/> instance if a match is found; otherwise, <c>null</c>.</returns>
    public async Task<User?> GetByUserName(string name)
        {
            var filter = Builders<User>
                .Filter.Eq(p => p.UserName, name);
            var result = await Collection.FindAsync(filter);
            return await result.FirstOrDefaultAsync();
        }

    /// <summary>
    /// Retrieves a user by their username along with their associated authorizations, using a MongoDB aggregation pipeline that joins the user with the authorizations collection and resolves the user's role (Admin role is preserved, otherwise mapped to "Some"). Returns <c>null</c> when no user matches the provided username.
    /// </summary>
    /// <param name="name">The username used to match the user document in the collection.</param>
    /// <returns>A <see cref="Task{TResult}"/> that yields the matching <see cref="User"/> with its authorizations and resolved role, or <c>null</c> if no user is found.</returns>
    public async Task<User?> GetByUserAndAuthoritesName(string name)
        {
            var matchStage = new BsonDocument("$match", new BsonDocument("userName", name));
            var lookupStage = new BsonDocument("$lookup", new BsonDocument
            {
                { "from", "authorizations" },
                { "localField", "_id" },
                { "foreignField", "userId" },
                { "as", "Authorization" }
            });
    
            var projectStage = new BsonDocument("$project", new BsonDocument
            {
                { "_id", 1 },
                { "userName", 1 },
                { "email", 1 },
                { "name", 1 },
                { "Authorization", "$Authorization" },
                {
                    "rol", new BsonDocument("$cond", new BsonArray
                    {
                        new BsonDocument("$eq", new BsonArray { "$Authorization.rol", "Admin" }),
                        "$rol",
                        "Some"
                    })
                }
            });
    
            var pipeline = new[]
            {
                matchStage,
                lookupStage,
                projectStage
            };
    
            var options = new AggregateOptions { AllowDiskUse = true };
            var result = await Collection.AggregateAsync<User>(pipeline, options);
            var bsonResult = await result.FirstOrDefaultAsync();
    
            return bsonResult;
        }

    /// <summary>
    /// Retrieves a user from the data store whose name exactly matches the specified value, returning null if no matching user exists.
    /// </summary>
    /// <param name="name">The name of the user to look up.</param>
    /// <returns>A <see cref="User"/> instance if a match is found; otherwise, null.</returns>
    public async Task<User?> GetByName(string name)
        {
            var filter = Builders<User>
                .Filter.Eq(p => p.Name, name);
            var result = await Collection.FindAsync(filter);
            return await result.FirstOrDefaultAsync();
        }

    /// <summary>
    /// Updates an existing user in the data store with the provided information, optionally including the password.
    /// When <paramref name="updatePass"/> is <c>true</c>, the user's password is also persisted; otherwise, it is left unchanged.
    /// </summary>
    /// <param name="user">The user entity whose fields will be applied to the existing record, identified by <see cref="User.Id"/>.</param>
    /// <param name="updatePass">If <c>true</c>, the password field is included in the update; if <c>false</c>, the password is not modified.</param>
    /// <returns>The updated <see cref="User"/> retrieved after the update, or <c>null</c> if no matching user was found.</returns>
    public async Task<User?> UpdateUser(User user, bool updatePass)
        {
            var filter = Builders<User>
                .Filter.Eq(p => p.Id, user.Id);
            var update = Builders<User>.Update
                .Set(u => u.UserName, user.UserName)
                .Set(u => u.Name, user.Name)
                .Set(u => u.Email, user.Email)
                .Set(u => u.LockExpirationDate, user.LockExpirationDate)
                .Set(u => u.LastLogin, user.LastLogin)
                .Set(u => u.IsEnabled, user.IsEnabled);
    
            if (updatePass) update = update.Set(u => u.Password, user.Password);
    
            await Collection.UpdateOneAsync(filter, update);
            var result = await Collection.Find(filter).FirstOrDefaultAsync();
            return result;
        }

    /// <summary>
    /// Retrieves a paginated and filterable queryable collection of users based on the supplied pagination and filter criteria. Applies a case-insensitive text search across the user's Name, Email, and UserName fields when a text filter is provided, and conditionally combines user status and user type filters; when no filter payload is supplied, returns the base sorted find fluent without applying additional filters.
    /// </summary>
    /// <param name="filteredRequest">The pagination filter containing paging details and the optional filter payload (text, user status, and user type) used to build the MongoDB query.</param>
    /// <returns>An <see cref="IFindFluent{TDocument, TProjection}"/> of <see cref="User"/> sorted ascending by user name and shaped by the assembled filter definitions.</returns>
    public IFindFluent<User, User> GetPaginatedUsers(PaginationFilter filteredRequest)
        {
            // Crear variable con la clase que construye los filtros que necesitamos
            var filterBuilder = Builders<User>.Filter;
            var sort = Builders<User>.Sort.Ascending("userName");
            // Crear una lista de filtros que pueden venir de tu servicio
            var filters = new List<FilterDefinition<User>>();
            if (filteredRequest.FilteredRequest == null) return CreateFindFluent(filters, sort);
            var textFilter = filteredRequest.FilteredRequest?.Text;
            if (!string.IsNullOrEmpty(textFilter))
            {
                var textFilterEscaped = Regex.Escape(textFilter);
                var orFilters = new List<FilterDefinition<User>>
                {
                    filterBuilder.Regex(p => p.Name, new BsonRegularExpression(textFilterEscaped, "i")),
                    filterBuilder.Regex(p => p.Email, new BsonRegularExpression(textFilterEscaped, "i")),
                    filterBuilder.Regex(p => p.UserName, new BsonRegularExpression(textFilterEscaped, "i"))
                };
                filters.Add(filterBuilder.Or(orFilters));
            }
    
            var userStatus = filteredRequest.FilteredRequest?.UserStatus;
            filters.Add(filterBuilder.And(GetUserStatusFilter(userStatus)));
    
    
            if (Enum.TryParse(filteredRequest.FilteredRequest?.UserType, out UserEnum.Type userType))
            {
                GetUserTypeFilter(userType);
                filters.Add(filterBuilder.And(GetUserTypeFilter(userType)));
            }
    
            return CreateFindFluent(filters, sort);
        }


    /// <summary>
    /// Retrieves the system user identified by the username "System", creating and inserting a new one with default credentials if no existing user is found.
    /// </summary>
    /// <returns>The existing system user if found; otherwise, the newly created and inserted system user.</returns>
    public async Task<User> GetOrCreateSystemUser()
        {
            var user = await GetByUserName("System");
            if (user == null)
            {
                var userToInsert = new User
                {
                    UserName = "System",
                    Name = "System",
                    Password = "$2a$12$crWa3EN1izcZBXNc81RzmOlfaYW2TPr2NdDQEWI7RzLTnA0Dd68WG"
                };
                await Collection.InsertOneAsync(userToInsert);
                return userToInsert;
            }
    
            return user;
        }

    /// <summary>
    /// Inserts the initial load of data by ensuring that the system user exists, creating one if necessary.
    /// </summary>
    public sealed override async Task InsertInitialLoad()
        {
            await GetOrCreateSystemUser();
        }

    /// <summary>
    /// Creates a fluent find query for users by combining the provided filters with an AND operation
    /// and applying the specified sort definition. When no filters are supplied, an empty filter is used
    /// as a fallback, which matches all documents.
    /// </summary>
    /// <param name="filters">The list of filter definitions to be combined with logical AND.</param>
    /// <param name="sort">The sort definition to apply to the query results.</param>
    /// <returns>A fluent find query for the <see cref="User"/> collection with the combined filter and sort applied.</returns>
    private IFindFluent<User, User> CreateFindFluent(List<FilterDefinition<User>> filters, SortDefinition<User> sort)
        {
            var combinedFilter = filters.Any()
                ? Builders<User>.Filter.And(filters)
                : Builders<User>.Filter.Empty; // Filtra todo si no hay filtros
    
            return Collection.Find(combinedFilter).Sort(sort);
        }

    /// <summary>
    /// Builds a list of MongoDB filter definitions for users based on the specified user type.
    /// Only Local and Ldap user types produce filters; any other value or null results in an empty filter list.
    /// </summary>
    /// <param name="type">The user type to filter by. If null or not one of the handled values, no filter is added.</param>
    /// <returns>A list of FilterDefinition objects matching the specified user type, or an empty list if no specific type was matched.</returns>
    private List<FilterDefinition<User>> GetUserTypeFilter(UserEnum.Type? type)
        {
            var filters = new List<FilterDefinition<User>>();
            var filterBuilder = Builders<User>.Filter;
    
            switch (type)
            {
                case UserEnum.Type.Local:
                    filters.Add(filterBuilder.Eq(u => u.Type, UserEnum.Type.Local));
                    break;
                case UserEnum.Type.Ldap:
                    filters.Add(filterBuilder.Eq(u => u.Type, UserEnum.Type.Ldap));
                    break;
            }
    
            return filters;
        }

    /// <summary>
    /// Builds a list of MongoDB filters that match users according to the requested status, handling enabled (including accounts where the enabled flag is missing), enabled and unlocked, enabled and locked, and disabled cases.
    /// </summary>
    /// <param name="status">The user status used to select which filter combination is applied; a <c>null</c> or unhandled value yields an empty filter list.</param>
    /// <returns>A list of <see cref="FilterDefinition{TDocument}"/> filters that should be combined to query <see cref="User"/> documents for the specified status.</returns>
    private List<FilterDefinition<User>> GetUserStatusFilter(StatusEnum.User? status)
        {
            var filters = new List<FilterDefinition<User>>();
            var filterBuilder = Builders<User>.Filter;
            switch (status)
            {
                case StatusEnum.User.Enabled:
                    filters.Add(filterBuilder.Or(
                        filterBuilder.Eq(u => u.IsEnabled, true),
                        filterBuilder.Exists(u => u.IsEnabled, false)
                    ));
                    break;
                case StatusEnum.User.EnabledUnlocked:
                    filters.Add(filterBuilder.Or(
                        filterBuilder.Eq(u => u.IsEnabled, true),
                        filterBuilder.Exists(u => u.IsEnabled, false)
                    ));
                    filters.Add(filterBuilder.Ne(u => u.LockExpirationDate, null));
                    break;
                case StatusEnum.User.EnabledLocked:
                    filters.Add(filterBuilder.Or(
                        filterBuilder.Eq(u => u.IsEnabled, true),
                        filterBuilder.Exists(u => u.IsEnabled, false)
                    ));
                    filters.Add(filterBuilder.Eq(u => u.LockExpirationDate, null));
                    break;
                case StatusEnum.User.Disabled:
                    filters.Add(filterBuilder.Eq(u => u.IsEnabled, false));
                    break;
            }
    
            return filters;
        }
}