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

namespace adas_core.Infrastructure.Repositories;

/// <summary>
/// Repository for managing camera entities in the MongoDB database. This repository provides methods to create, retrieve, update, and search for cameras based on various criteria. 
/// It also includes error handling and logging for debugging purposes.
/// </summary>
public class CameraRepository : MongoRepository<Camera>, ICameraRepository
{
    private readonly ApiSettings _apiSettings;

    /// <summary>
    /// Initializes a new instance of the CameraRepository class with the specified MongoDB database and API settings.
    /// The constructor sets up the repository to interact with the "Cameras" collection in the database and allows for configuration through the provided API settings.
    /// </summary>
    /// <param name="database">The MongoDB database instance.</param>
    /// <param name="apiSettings">The API settings containing configuration for the repository.</param>
    public CameraRepository(IMongoDatabase database, IOptions<ApiSettings> apiSettings) : base(database)
    {
        _apiSettings = apiSettings.Value;
    }

    /// <summary>
    /// Gets the name of the MongoDB collection that this repository interacts with. In this case, it returns the collection name for cameras as specified in the API settings.
    /// </summary>
    /// <returns>The name of the MongoDB collection for cameras.</returns>
    public override string GetCollectionName()
    {
       return _apiSettings.Cameras;
    }

    /// <summary>
    /// Retrieves a camera entity from the MongoDB database based on its unique identifier. The method takes an ObjectId as a parameter and returns the corresponding Camera object if found, or null if no matching camera is found. 
    /// It also includes error handling to log any exceptions that occur during the retrieval process.
    /// </summary>
    /// <param name="cameraId">The unique identifier of the camera to retrieve.</param>
    /// <returns>The Camera object if found; otherwise, null.</returns>
    public async Task<Camera?> GetById(ObjectId cameraId)
    {
        try
        {
            var filter = Builders<Camera>.Filter.Eq(x => x.Id, cameraId);
            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}", cameraId, e);
            return null;
        }
    }

    /// <summary>
    /// Retrieves a camera entity from the MongoDB database based on its name. The method takes a string parameter representing the name of the camera and returns the corresponding Camera object if found, or null if no matching camera is found.
    /// </summary>
    /// <param name="name">The name of the camera to retrieve.</param>
    /// <returns>The Camera object if found; otherwise, null.</returns>
    public async Task<Camera?> GetByName(string name)
    {
        var filterBuilder = Builders<Camera>.Filter;

        var filter = filterBuilder.Eq(r => r.Name, name);

        return await Collection.Find(filter).FirstOrDefaultAsync();
    }

    /// <summary>
    /// Retrieves a list of camera entities from the MongoDB database based on a list of unique identifiers. 
    /// The method takes a list of ObjectId values representing the camera IDs and returns a list of Camera objects that match any of the provided IDs.
    /// </summary>
    /// <param name="configurationRelayList">The list of camera IDs to retrieve.</param>
    /// <returns>A list of Camera objects that match the provided IDs.</returns>
    public List<Camera> GetCameraInList(List<ObjectId> configurationRelayList)
    {
        var filterBuilder = Builders<Camera>.Filter;

        var filter = filterBuilder.And(
            filterBuilder.In(r => r.Id, configurationRelayList));

        return Collection.Find(filter).ToList();
    }

    /// <summary>
    /// Retrieves a paginated list of camera entities from the MongoDB database based on the provided pagination filter. 
    /// The method takes a PaginationFilter object as a parameter, which contains information about the page number, page size, and any additional filtering criteria.
    /// </summary>
    /// <param name="filter">The pagination filter containing page number, page size, and any additional filtering criteria.</param>
    /// <returns>A paginated list of Camera objects.</returns>
    /// <exception cref="BadRequestException">Thrown when the provided filter is invalid or contains invalid data.</exception>
    public IFindFluent<Camera, Camera> GetPaginatedCameras(PaginationFilter filter)
    {
        var filterBuilder = Builders<Camera>.Filter;
        var sort = Builders<Camera>.Sort.Ascending("name");
        var filters = new List<FilterDefinition<Camera>>();

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

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

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

            var safeInput = Regex.Escape(textFilter);

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

        return CreateFindFluent(filters, sort);
    }


    /// <summary>
    /// Inserts a new camera entity into the MongoDB database. The method takes a Camera object as a parameter and attempts to insert it into the collection.
    /// </summary>
    /// <param name="camera">The Camera object to insert into the database.</param>
    /// <returns>The inserted Camera object if successful; otherwise, null.</returns>
    public async Task<Camera?> InsertOneCamera(Camera camera)
    {
        try
        {
            await Collection.InsertOneAsync(camera);
            return await GetById(camera.Id);
        }
        catch (Exception ex)
        {
            Log.Error("Error inserting camera: {camera}. Exception: {ex}",
                JsonConvert.SerializeObject(camera, Formatting.Indented), ex);

            return null;
        }
    }

    /// <summary>
    /// Updates an existing camera entity in the MongoDB database based on its unique identifier. The method takes an ObjectId representing the camera ID and a Camera object containing the updated information.
    /// </summary>
    /// <param name="objectId">The unique identifier of the camera to update.</param>
    /// <param name="camera">The Camera object containing the updated information.</param>
    /// <returns>The updated Camera object if successful; otherwise, null.</returns>
    public async Task<Camera?> UpdateCameraAsync(ObjectId objectId, Camera camera)
    {
        var filter = Builders<Camera>.Filter.Eq("_id", objectId);
        var update = Builders<Camera>.Update
            .Set(c => c.Streams, camera.Streams)
            .Set(c => c.Name, camera.Name)
            .Set(c => c.Username, camera.Username)
            .Set(c => c.Password, camera.Password)
            .Set(c => c.Driver, camera.Driver)
            .Set(c => c.Ip, camera.Ip)
            .Set(c => c.Streams, camera.Streams)
            .Set(c => c.Ptz, camera.Ptz);
        return await Collection.FindOneAndUpdateAsync(filter, update,
            new FindOneAndUpdateOptions<Camera, Camera> { ReturnDocument = ReturnDocument.After });
    }

    /// <summary>
    /// Searches for camera entities in the MongoDB database based on a text string that matches the camera's name. 
    /// The method takes a string parameter representing the text to search for and returns a list of Camera objects whose names match the search criteria.
    /// </summary>
    /// <param name="textToSearch">The text string to search for in the camera names.</param>
    /// <returns>A list of Camera objects whose names match the search criteria.</returns>
    /// <exception cref="BadRequestException">Thrown when the search text is too long.</exception>
    public async Task<List<Camera>> GetSearchByNameCameras(string textToSearch)
    {
        if (string.IsNullOrWhiteSpace(textToSearch))
            return [];

        if (textToSearch.Length > 100)
            throw new BadRequestException("Search text too long");

        var safeInput = Regex.Escape(textToSearch);

        var filter = Builders<Camera>.Filter.Regex(
            c => c.Name,
            new BsonRegularExpression(safeInput, "i")
        );

        return await Collection.Find(filter).ToListAsync();
    }

    /// <summary>
    /// Creates an IFindFluent object for querying the MongoDB collection based on a list of filter definitions and a sort definition.
    /// </summary>
    /// <param name="filters">A list of filter definitions to apply to the query.</param>
    /// <param name="sort">A sort definition to apply to the query results.</param>
    /// <returns>An IFindFluent object for further query customization or execution.</returns>
    private IFindFluent<Camera, Camera> CreateFindFluent(List<FilterDefinition<Camera>> filters, SortDefinition<Camera> sort)
    {
        var combinedFilter = filters.Any()
            ? Builders<Camera>.Filter.And(filters)
            : Builders<Camera>.Filter.Empty; 
        return Collection.Find(combinedFilter).Sort(sort);
    }
}