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 Newtonsoft.Json;
using Serilog;
using System.Text.RegularExpressions;

namespace adas_core.Infrastructure.Repositories;

/// <summary>
/// Represents a repository for managing <see cref="Relay"/> entities, providing MongoDB-backed data access through the <see cref="MongoRepository{Relay}"/> base class and exposing the contract defined by <see cref="IRelayRepository"/>.
/// </summary>
/// <typeparam name="Relay">The type of the entity managed by the repository.</typeparam>
/// <remarks>This class combines a concrete MongoDB repository implementation with a domain-specific interface, enabling standardized persistence operations for relay entities.</remarks>
public class RelayRepository : MongoRepository<Relay>, IRelayRepository
{
    private readonly ApiSettings _apiSettings;
    
  

    public RelayRepository(IMongoDatabase database, IOptions<ApiSettings> apiSettings) : base(database)
    {
        _apiSettings = apiSettings.Value;
    }

    /// <summary>
    /// Retrieves the collection name for relays from the API settings configuration.
    /// </summary>
    /// <returns>The configured relays collection name as specified in the API settings.</returns>
    public override string GetCollectionName()
        {
            return _apiSettings.Relays;
        }

    /// <summary>
    /// Retrieves a <see cref="Relay"/> from the collection by its identifier, returning the first match or <c>null</c> when no document is found or an error occurs while querying.
    /// </summary>
    /// <param name="relayId">The unique identifier of the relay to look up.</param>
    /// <returns>The matching <see cref="Relay"/>, or <c>null</c> if no document matches the identifier or the query fails.</returns>
    public async Task<Relay?> GetById(ObjectId relayId)
        {
            try
            {
                var filter = Builders<Relay>.Filter.Eq(x => x.Id, relayId);
                var result = await Collection.FindAsync(filter, null);
                return result.FirstOrDefault();
            }
            catch (Exception e)
            {
                Log.Error("Exception trying to get relay by id: {id}. Exception {e}", relayId, e);
                return null;
            }
        }

    /// <summary>
    /// Retrieves a list of relays from the collection whose identifiers are contained in the provided configuration list and whose type matches the specified value.
    /// </summary>
    /// <param name="configurationRelayList">The collection of relay identifiers used to filter relays by their Id field.</param>
    /// <param name="type">The relay type used as an equality filter on the Type field.</param>
    /// <returns>A list of <see cref="Relay"/> instances matching both the identifier and type filters; an empty list is returned when no relays match.</returns>
    public List<Relay> GetRelayByTypeInList(List<ObjectId> configurationRelayList, RelayEnum.Type type)
        {
            var filterBuilder = Builders<Relay>.Filter;
    
            var filter = filterBuilder.And(
                filterBuilder.In(r => r.Id, configurationRelayList),
                filterBuilder.Eq(r => r.Type, type)
            );
    
            return Collection.Find(filter).ToList();
        }

    /// <summary>
    /// Retrieves the <see cref="Relay"/> entries whose identifiers are contained in the supplied list, returning all matches as a list.
    /// </summary>
    /// <param name="configurationRelayList">The collection of <see cref="ObjectId"/> values used to match relays by their <c>Id</c> field.</param>
    /// <returns>A <see cref="List{Relay}"/> containing the relays whose identifiers are found in <paramref name="configurationRelayList"/>; an empty list is returned when no matching relays exist.</returns>
    public List<Relay> GetRelayInList(List<ObjectId> configurationRelayList)
        {
            var filterBuilder = Builders<Relay>.Filter;
    
            var filter = filterBuilder.And(
                filterBuilder.In(r => r.Id, configurationRelayList));
    
            return Collection.Find(filter).ToList();
        }

    /// <summary>
    /// Retrieves a paginated, sortable set of relays, optionally filtered by a case-insensitive text match on the relay name.
    /// When the filter's text is null or empty, no text-based criteria are applied and the result is returned with the default ascending sort by relay name.
    /// </summary>
    /// <param name="filter">The pagination criteria containing the optional text filter applied against the relay name.</param>
    /// <returns>A fluent find query for <see cref="Relay"/> entities, ordered by relay name in ascending order, ready for further pagination.</returns>
    public IFindFluent<Relay, Relay> GetPaginatedRelays(PaginationFilter filter)
        {
            var filterBuilder = Builders<Relay>.Filter;
            var sort = Builders<Relay>.Sort.Ascending("relayName");
            var filters = new List<FilterDefinition<Relay>>();
    
            if (filter.FilteredRequest == null) return CreateFindFluent(filters, sort);
    
            if (!string.IsNullOrEmpty(filter.FilteredRequest?.Text))
            {
                var textFilter = filter.FilteredRequest.Text;
                var textFilterEscaped = Regex.Escape(textFilter);
    
                filters.Add(
                    filterBuilder.Or(
                        filterBuilder.Regex(p => p.RelayName,
                            new BsonRegularExpression(textFilterEscaped, "i"))
                    )
                );
            }
    
            return CreateFindFluent(filters, sort);
        }

    /// <summary>
    /// Asynchronously inserts a new relay into the collection and returns the persisted entity fetched by its identifier.
    /// On failure, the exception is logged and the method returns <c>null</c> instead of propagating the error.
    /// </summary>
    /// <param name="request">The relay entity to insert into the collection.</param>
    /// <returns>The inserted relay retrieved by its identifier, or <c>null</c> if the insertion fails.</returns>
    public async Task<Relay?> InsertOneRelayAsync(Relay request)
        {
            try
            {
                await Collection.InsertOneAsync(request);
                return await GetById(request.Id);
            }
            catch (Exception ex)
            {
                Log.Error("Error inserting relay: {relay}. Exception: {ex}",
                    JsonConvert.SerializeObject(request, Formatting.Indented), ex);
    
                return null;
            }
        }

    /// <summary>
    /// Updates an existing relay document identified by the specified object identifier with the provided relay data and returns the updated entity.
    /// </summary>
    /// <param name="objectId">The unique identifier of the relay document to update in the collection.</param>
    /// <param name="relay">The relay instance containing the new values to apply to the existing document.</param>
    /// <returns>A task that represents the asynchronous operation, containing the updated <see cref="Relay"/> after the update, or <c>null</c> if no matching document was found.</returns>
    public async Task<Relay?> UpdateRelayAsync(ObjectId objectId, Relay relay)
        {
            var filter = Builders<Relay>.Filter.Eq("_id", objectId);
            var update = Builders<Relay>.Update
                .Set(c => c.Mode, relay.Mode)
                .Set(c => c.RelayNumber, relay.RelayNumber)
                .Set(c => c.Username, relay.Username)
                .Set(c => c.Password, relay.Password)
                .Set(c => c.Driver, relay.Driver)
                .Set(c => c.Ip, relay.Ip)
                .Set(c => c.Port, relay.Port)
                .Set(c => c.RelayName, relay.RelayName);
            return await Collection.FindOneAndUpdateAsync(filter, update,
                new FindOneAndUpdateOptions<Relay, Relay> { ReturnDocument = ReturnDocument.After });
        }

    /// <summary>
    /// Retrieves a <see cref="Relay"/> from the collection whose <c>RelayName</c> matches the specified name.
    /// Returns <c>null</c> when no relay with the given name exists in the collection.
    /// </summary>
    /// <param name="requestRelayName">The name of the relay to look up. May be <c>null</c>, in which case the query matches relays with a null name.</param>
    /// <returns>A <see cref="Task{TResult}"/> containing the matching <see cref="Relay"/>, or <c>null</c> if no relay with the specified name is found.</returns>
    public async Task<Relay?> GetByName(string? requestRelayName)
        {
            var filterBuilder = Builders<Relay>.Filter;
    
            var filter = filterBuilder.Eq(r => r.RelayName, requestRelayName);
    
            return await Collection.Find(filter).FirstOrDefaultAsync();
        }

    /// <summary>
    /// Creates a fluent find query for the Relay collection by combining the provided filters with a logical AND, or applying an empty filter when no filters are supplied, and then applying the given sort definition.
    /// </summary>
    /// <param name="filters">The list of filter definitions to combine; when empty, an empty filter is used to match all documents.</param>
    /// <param name="sort">The sort definition applied to the query results.</param>
    /// <returns>An <see cref="IFindFluent{TDocument, TProjection}"/> representing the filtered and sorted Relay query.</returns>
    private IFindFluent<Relay, Relay> CreateFindFluent(List<FilterDefinition<Relay>> filters, SortDefinition<Relay> sort)
        {
            var combinedFilter = filters.Any()
                ? Builders<Relay>.Filter.And(filters)
                : Builders<Relay>.Filter.Empty; 
            return Collection.Find(combinedFilter).Sort(sort);
        }
}