using Newtonsoft.Json;

namespace audit_logs.Services;

using System.Security.Claims;
using System.Text.RegularExpressions;
using audit_logs.Models;
using audit_logs.Models.AppSettings;
using audit_logs.Models.DTO;
using audit_logs.Services.Interfaces;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using audit_logs.Repositories;
using MongoDB.Driver;
using audit_logs.Utils;
using audit.Model;
using audit_logs.Repositories.Interfaces;
using Serilog;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

public class AuditService : IAuditService
{
    private readonly IAuditRecordRepository _auditLogRepository;

    public AuditService(IAuditRecordRepository auditLogRepository)
    {
        _auditLogRepository = auditLogRepository;
    }

    public AuditService(IMongoDatabase database)
    {
        _auditLogRepository = new AuditRecordRepository(database);
    }

    public AuditService(IOptions<MongoDbSettingsAudit> dbSettings)
    {
        _auditLogRepository = new AuditRecordRepository(dbSettings);
    }

    public IAuditRecordRepository AuditLogRepository => _auditLogRepository;

    /// <summary>
    /// Records a detailed audit log entry.
    /// </summary>
    /// <param name="auditRecord">The data required to create an audit record.</param>
    public async Task RecordAuditAsync(AuditRecord auditRecord)
    {
        if (auditRecord == null) throw new ArgumentNullException(nameof(auditRecord));
        if (string.IsNullOrWhiteSpace(auditRecord.EntityType))
            throw new ArgumentNullException(nameof(auditRecord.EntityType));
        if (string.IsNullOrWhiteSpace(auditRecord.RecordId))
            throw new ArgumentNullException(nameof(auditRecord.RecordId));
        if (string.IsNullOrWhiteSpace(auditRecord.UserId))
            throw new ArgumentNullException(nameof(auditRecord.UserId));
        if (string.IsNullOrWhiteSpace(auditRecord.ActionType))
            throw new ArgumentNullException(nameof(auditRecord.ActionType));
        if (auditRecord.Changes == null) throw new ArgumentNullException(nameof(auditRecord.Changes));
        if (string.IsNullOrWhiteSpace(auditRecord.UserIpAddress))
            throw new ArgumentNullException(nameof(auditRecord.UserIpAddress));
        
        await _auditLogRepository.InsertOneAsync(auditRecord);
    }
    
    public async Task<T> DeepCopyAsync<T>(T source)
    {
        return await Task.Run(() =>
        {
            var serialized = JsonConvert.SerializeObject(source);
            var deserialized = JsonConvert.DeserializeObject<T>(serialized);
            if (deserialized == null)
                throw new InvalidOperationException("Deserialization resulted in null");
            return deserialized;
        });
    }

    #region Documentacioòn para el metodo CreateAuditLogAsync

    /// <summary>
    /// Creates an audit log record based on the provided original and modified data.
    /// This method generates an <see cref="AuditRecord"/> object to encapsulate all necessary details for the audit log entry.
    /// </summary>
    /// <param name="user">
    /// The <see cref="ClaimsPrincipal"/> representing the user performing the action. 
    /// This parameter is required and is used to populate the <c>UserId</c> and optionally other user-related fields.
    /// </param>
    /// <param name="dataOriginal">
    /// The original object before the change. This parameter can be null for new records (when <c>ActionType</c> is "create").
    /// </param>
    /// <param name="dataModified">
    /// The modified object after the change. This parameter can be null for deleted records (when <c>ActionType</c> is "delete").
    /// </param>
    /// <param name="reason">
    /// The reason for the change. This parameter is optional; if null or empty, it defaults to "Not specified".
    /// </param>
    /// <exception cref="ArgumentNullException">
    /// Thrown if <paramref name="user"/> is null.
    /// </exception>
    /// <remarks>
    /// This method constructs and populates an <see cref="AuditRecord"/> object that contains the following fields:
    /// <list type="bullet">
    ///   <item>
    ///     <description><c>EntityType</c>: The type of entity being audited (e.g., "Patient").</description>
    ///   </item>
    ///   <item>
    ///     <description><c>RecordId</c>: The unique identifier of the record being modified within the entity (e.g. patient.id, nhc,etc) <c>EntityType</c>.</description>
    ///   </item>
    ///   <item>
    ///     <description><c>UserId</c>: The ID of the user who performed the action that triggered the audit entry.</description>
    ///   </item>
    ///   <item>
    ///     <description><c>ActionType</c>: The type of action performed, which can be one of the following:
    ///       <list type="bullet">
    ///         <item><description><c>"create"</c>: Indicates a new record was created.</description></item>
    ///         <item><description><c>"update"</c>: Indicates an existing record was updated.</description></item>
    ///         <item><description><c>"delete"</c>: Indicates a record was removed.</description></item>
    ///       </list>
    ///     </description>
    ///   </item>
    ///   <item>
    ///     <description><c>Reason</c>: (Optional) A description or justification for the action being logged.</description>
    ///   </item>
    ///   <item>
    ///     <description><c>ActionTime</c>: The timestamp of when the action occurred, represented in UTC.</description>
    ///   </item>
    ///   <item>
    ///     <description><c>Changes</c>: A list detailing the differences between the original and modified data. Each change includes:
    ///       <list type="bullet">
    ///         <item><description><c>Field</c>: The name of the field that was changed.</description></item>
    ///         <item><description><c>OldValue</c>: The value of the field before the change.</description></item>
    ///         <item><description><c>NewValue</c>: The value of the field after the change.</description></item>
    ///         <item><description><c>ValueType</c>: The data type of the field (e.g., "string", "int", "bool").</description></item>
    ///       </list>
    ///     </description>
    ///   </item>
    ///   <item>
    ///     <description><c>UserIpAddress</c>: (Optional) The IP address of the user performing the action. Useful for tracking location-based information.</description>
    ///   </item>
    /// </list>
    /// Once the <see cref="AuditRecord"/> is constructed, it is asynchronously saved using <c>RecordAuditAsync</c>.
    /// </remarks>
    /// <returns>
    /// A task representing the asynchronous operation of recording the audit log. The task completes when the audit record is successfully stored.
    /// </returns>
    /// <example>
    /// Here is an example of how to use this method:
    /// <code>
    /// await CreateAuditLogAsync(
    ///     user: HttpContext.User,
    ///     dataOriginal: oldData,
    ///     dataModified: newData,
    ///     reason: "Updated user details"
    /// );
    /// </code>
    /// </example>
    #endregion

    public async Task CreateAuditLogAsync(ClaimsPrincipal user, Object? dataOriginal, Object? dataModified,
        string? reason)
    {
       if (user == null) throw new ArgumentNullException(nameof(user));

        AuditRecord auditLogData = new AuditRecord();
        JsonComparer jsonComparer = new();
        
        if (dataOriginal != null)
        {
            auditLogData.EntityType =
                IsAnonymousType(dataOriginal.GetType()) ? "AnonymousType" : dataOriginal.GetType().Name;
            
            var idProperty = dataOriginal.GetType().GetProperty("Id");
            
            if (idProperty != null && idProperty.PropertyType == typeof(ObjectId))
            {
                ObjectId id = (ObjectId)idProperty.GetValue(dataOriginal);
               auditLogData.RecordId = id.ToString();
               
            }else if (idProperty != null && idProperty.PropertyType == typeof(string))
            {
                auditLogData.RecordId = idProperty.GetValue(dataOriginal).ToString();
            }
        }
        else if (dataModified != null)
        {
            auditLogData.EntityType =
                IsAnonymousType(dataModified.GetType()) ? "AnonymousType" : dataModified.GetType().Name;
            var idProperty = dataModified.GetType().GetProperty("Id");
            if (idProperty != null && idProperty.PropertyType == typeof(ObjectId))
            {
                ObjectId id = (ObjectId)idProperty.GetValue(dataModified);
                //TODO verificar si esta bien obtener el valor asi:
                auditLogData.RecordId = id.ToString();
            }else if (idProperty != null && idProperty.PropertyType == typeof(string))
            {
                auditLogData.RecordId = idProperty.GetValue(dataModified).ToString();
            }
        }

        auditLogData.ActionTime = DateTime.Now;
        auditLogData.Reason = string.IsNullOrEmpty(reason) ? "Not specified" : reason;
        auditLogData.UserIpAddress = string.IsNullOrEmpty(user.FindFirst("IpAddress")?.Value)
            ? "Not specified"
            : user.FindFirst("IpAddress")?.Value ?? "Not specified";
        auditLogData.UserId = user.FindFirst(ClaimTypes.Name)?.Value ?? "Not specified";
        // Asignar ActionType segun las condiciones
        if (dataOriginal == null || string.IsNullOrEmpty(dataOriginal.ToString()))
        {
            auditLogData.ActionType = "create";
        }
        else if (dataModified == null || string.IsNullOrEmpty(dataModified.ToString()))
        {
            auditLogData.ActionType = "delete";
        }
        else
        {
            auditLogData.ActionType = "update";
        }
       // dataOriginal="{\n  \"Id\": \"PV8\",\n  \"Items\": [\n    {\n      \"Code\": \"Temperature\",\n      \"CodingSystem\": \"SENSOR\",\n      \"OriginalName\": \"Room_Temperature\",\n      \"ParentCode\": \"T\",\n      \"ParentCodingSystem\": \"Room_Temperature\",\n      \"ParentName\": null,\n      \"Name\": \"Room_Temperature\",\n      \"Units\": \"T\",\n      \"ArrowType\": 1,\n      \"ShowArrow\": true,\n      \"ForceUnits\": false,\n      \"MinAlert\": null,\n      \"MaxAlert\": null,\n      \"MaxWarn\": null,\n      \"MinWarn\": null,\n      \"WarnColor\": null,\n      \"AlertColor\": null,\n      \"AlertValues\": null,\n      \"WarningValues\": null,\n      \"ForceWarn\": false,\n      \"ForceAlert\": false,\n      \"Alert\": null,\n      \"Expires\": null,\n      \"ShowOnExpired\": false,\n      \"Persist\": null,\n      \"ColorOnExpired\": null,\n      \"RetentionPolicy\": null,\n      \"RetentionPolicyValue\": null,\n      \"LevelCondition\": null,\n      \"UiConfiguration\": {\n        \"screenLabel\": \"screenLabel\"\n      },\n      \"Alarm\": null,\n      \"Description\": null,\n      \"RequiredValue\": null,\n      \"Preconditions\": null,\n      \"CheckObservations\": false,\n      \"CreateObservation\": null,\n      \"TimeFromMessageTime\": false\n    }\n  ]\n}"
        //dataModified = "{\n  \"Id\": \"PV8\",\n  \"Items\": [\n    {\n      \"Code\": \"Temperature\",\n      \"CodingSystem\": \"SENSOR\",\n      \"OriginalName\": \"Room_Temperature\",\n      \"ParentCode\": \"T\",\n      \"ParentCodingSystem\": \"Room_Temperature\",\n      \"ParentName\": null,\n      \"Name\": \"Room_Temperature\",\n      \"Units\": \"T\",\n      \"ArrowType\": 1,\n      \"ShowArrow\": true,\n      \"ForceUnits\": false,\n      \"MinAlert\": null,\n      \"MaxAlert\": null,\n      \"MaxWarn\": null,\n      \"MinWarn\": null,\n      \"WarnColor\": null,\n      \"AlertColor\": null,\n      \"AlertValues\": null,\n      \"WarningValues\": null,\n      \"ForceWarn\": false,\n      \"ForceAlert\": false,\n      \"Alert\": null,\n      \"Expires\": null,\n      \"ShowOnExpired\": false,\n      \"Persist\": null,\n      \"ColorOnExpired\": null,\n      \"RetentionPolicy\": null,\n      \"RetentionPolicyValue\": null,\n      \"LevelCondition\": null,\n      \"UiConfiguration\": {\n        \"screenLabel\": \"screenLabel\"\n      },\n      \"Alarm\": null,\n      \"Description\": null,\n      \"RequiredValue\": null,\n      \"Preconditions\": null,\n      \"CheckObservations\": false,\n      \"CreateObservation\": null,\n      \"TimeFromMessageTime\": false\n    },\n    {\n      \"Code\": \"88\",\n      \"CodingSystem\": \"88\",\n      \"OriginalName\": \"88\",\n      \"ParentCode\": \"88\",\n      \"ParentCodingSystem\": \"88\",\n      \"ParentName\": null,\n      \"Name\": \"88\",\n      \"Units\": \"88\",\n      \"ArrowType\": 1,\n      \"ShowArrow\": true,\n      \"ForceUnits\": false,\n      \"MinAlert\": null,\n      \"MaxAlert\": null,\n      \"MaxWarn\": null,\n      \"MinWarn\": null,\n      \"WarnColor\": null,\n      \"AlertColor\": null,\n      \"AlertValues\": null,\n      \"WarningValues\": null,\n      \"ForceWarn\": false,\n      \"ForceAlert\": false,\n      \"Alert\": null,\n      \"Expires\": null,\n      \"ShowOnExpired\": false,\n      \"Persist\": null,\n      \"ColorOnExpired\": null,\n      \"RetentionPolicy\": null,\n      \"RetentionPolicyValue\": null,\n      \"LevelCondition\": null,\n      \"UiConfiguration\": {\n        \"screenLabel\": \"88\"\n      },\n      \"Alarm\": null,\n      \"Description\": null,\n      \"RequiredValue\": null,\n      \"Preconditions\": null,\n      \"CheckObservations\": false,\n      \"CreateObservation\": null,\n      \"TimeFromMessageTime\": false\n    }\n  ]\n}";
       
        auditLogData.Changes = jsonComparer.GetDifferences(dataOriginal, dataModified);
        await RecordAuditAsync(auditLogData);
    }

    // Método auxiliar para verificar si un tipo es anonimo, solo es necesario para pruebas
    private bool IsAnonymousType(Type type)
    {
        return type.IsGenericType && type.Name.StartsWith("<>f__AnonymousType");
    }

    /// <summary>
    /// Creates and records an audit log entry based on the differences between the original and modified data provided in an <see cref="AuditLogData"/> object.
    /// </summary>
    /// <param name="auditLogData">An object containing all necessary details for the audit log entry, including:</param>
    /// <param name="auditLogData.EntityType">The type of entity being modified.</param>
    /// <param name="auditLogData.RecordId">The unique ID of the record being changed.</param>
    /// <param name="auditLogData.UserId">The ID of the user performing the action.</param>
    /// <param name="auditLogData.ActionType">The type of action performed.</param>
    /// <param name="auditLogData.Reason">The reason for the action.</param>
    /// <param name="auditLogData.ActionTime">The timestamp of the action.</param>
    /// <param name="auditLogData.OriginalJson">The original JSON data.</param>
    /// <param name="auditLogData.ModifiedJson">The modified JSON data.</param>
    /// <param name="auditLogData.UserIpAddress">The IP address of the user.</param>
    public async Task CreateAuditLogAsync(AuditLogData auditLogData)
    {
        if (auditLogData == null) throw new ArgumentNullException(nameof(auditLogData));
        AuditRecord auditRecord = new AuditRecord();
        auditRecord.EntityType = auditLogData.EntityType;
        auditRecord.RecordId = auditLogData.RecordId;
        auditRecord.UserId = auditLogData.UserId;
        auditRecord.ActionTime = auditLogData.ActionTime;
        auditRecord.ActionType = auditLogData.ActionType;
        auditRecord.Reason = auditLogData.Reason;
        auditRecord.UserIpAddress = auditLogData.UserIpAddress;
        JsonComparer jsonComparer = new();
        auditRecord.Changes = jsonComparer.GetDifferences(auditLogData.OriginalJson, auditLogData.ModifiedJson);
        await RecordAuditAsync(auditRecord);
    }

    /// <summary>
    /// Retrieves paginated audit logs.
    /// </summary>
    /// <param name="filterDto.pageNumber">The page number to retrieve (1-based).</param>
    /// <param name="filterDto.pageSize">The number of records per page.</param>
    /// <param name="filterDto.userId">filter taking into account the user who made the modification.</param>
    /// <param name="filterDto.startDate">filter by the date the modification was made.</param>
    /// <param name="filterDto.endDate">filter by the date the modification was made.</param>
    /// <param name="filterDto.recordId">filter by modified record id in the collection.</param>
    /// <returns>A paginated list of audit records.</returns>
    public async Task<PaginatedLogsResultDto<AuditRecordDto>> GetAuditLogsBySpecificFilterAsync(
        SpecificAuditLogFilterDto filterDto)
    {
        if (filterDto.pageNumber <= 0)
            throw new ArgumentException("Page number must be greater than 0.", nameof(filterDto.pageNumber));
        if (filterDto.pageSize <= 0)
            throw new ArgumentException("Page size must be greater than 0.", nameof(filterDto.pageSize));

        // Construir filtro dinámico
        var filter = BuildAuditLogFilter(filterDto.userId, filterDto.recordId, filterDto.entityType,
            filterDto.startDate, filterDto.endDate, filterDto.actionTime);

        var totalRecords = await _auditLogRepository.CountDocumentsAsync(filter);
        var totalPages = (int)Math.Ceiling((double)totalRecords / filterDto.pageSize);
        var sort = Builders<AuditRecord>.Sort.Descending(x => x.ActionTime); // Ordenar por fecha más reciente
        var records = await _auditLogRepository.GetPaginatedAsync(filterDto.pageNumber, filterDto.pageSize, filter, sort);


//        var records = await _auditLogRepository.GetPaginatedAsync(filterDto.pageNumber, filterDto.pageSize, filter);
        var recordsDto = records.Select(record => AuditRecordMapper.ToDto(record)).ToList();

        return new PaginatedLogsResultDto<AuditRecordDto>
        {
            CurrentPage = filterDto.pageNumber,
            PageSize = filterDto.pageSize,
            TotalRecords = totalRecords,
            TotalPages = totalPages,
            Records = recordsDto
        };
    }

    /// <summary>
    /// Retrieves paginated audit logs with optional full-text search across multiple fields.
    /// </summary>
    /// <param name="filtertTextOrDateDto.pageNumber">The page number to retrieve (1-based).</param>
    /// <param name="filtertTextOrDateDto.pageSize">The number of records per page.</param>
    /// <param name="filtertTextOrDateDto.text_to_search">Text to search in any field.</param>
    /// /// <param name="filtertTextOrDateDto.startDate">filter by the date the modification was made.</param>
    /// <param name="filtertTextOrDateDto.endDate">filter by the date the modification was made.</param>
    /// <returns>A paginated list of audit records.</returns>
    public async Task<PaginatedLogsResultDto<AuditRecordDto>> GetAuditLogsMatchingTextOrDateAsync(
        AuditLogTextOrDateSearchDTO filtertTextOrDateDto)
    {
        if (filtertTextOrDateDto.pageNumber <= 0)
            throw new ArgumentException("Page number must be greater than 0.", nameof(filtertTextOrDateDto.pageNumber));
        if (filtertTextOrDateDto.pageSize <= 0)
            throw new ArgumentException("Page size must be greater than 0.", nameof(filtertTextOrDateDto.pageSize));

        // Construct the text search filter if text_to_search is provided
        FilterDefinition<AuditRecord> filter;
        if (!string.IsNullOrWhiteSpace(filtertTextOrDateDto.searchText) || filtertTextOrDateDto.startDate.HasValue ||
            filtertTextOrDateDto.endDate.HasValue || filtertTextOrDateDto.actionTime.HasValue)
        {
            filter = BuildAuditLogFilterByText(filtertTextOrDateDto.searchText, filtertTextOrDateDto.startDate,
                filtertTextOrDateDto.endDate, filtertTextOrDateDto.actionTime);
        }
        else
        {
            filter = Builders<AuditRecord>.Filter.Empty;
        }

        var totalRecords = await _auditLogRepository.CountDocumentsAsync(filter);
        var totalPages = (int)Math.Ceiling((double)totalRecords / filtertTextOrDateDto.pageSize);
        var sort = Builders<AuditRecord>.Sort.Descending(x => x.ActionTime); // Ordenar por fecha más reciente
       
        var records = await _auditLogRepository.GetPaginatedAsync(filtertTextOrDateDto.pageNumber,
            filtertTextOrDateDto.pageSize, filter,sort);
        var recordsDto = records.Select(record => AuditRecordMapper.ToDto(record)).ToList();

        return new PaginatedLogsResultDto<AuditRecordDto>
        {
            CurrentPage = filtertTextOrDateDto.pageNumber,
            PageSize = filtertTextOrDateDto.pageSize,
            TotalRecords = totalRecords,
            TotalPages = totalPages,
            Records = recordsDto
        };
    }


    private static FilterDefinition<AuditRecord> BuildAuditLogFilter(
        string? userId,
        string? recordId,
        string? entityType,
        DateTime? startDate,
        DateTime? endDate,
        DateTime? actionTime)
    {
        var builder = Builders<AuditRecord>.Filter;
        var filters = new List<FilterDefinition<AuditRecord>>();

        if (!string.IsNullOrEmpty(userId))
        {
            filters.Add(builder.Eq(x => x.UserId, userId));
        }

        if (!string.IsNullOrEmpty(recordId))
        {
            filters.Add(builder.Eq(x => x.RecordId, recordId));
        }

        if (startDate.HasValue)
        {
            filters.Add(builder.Gte(x => x.ActionTime, startDate.Value));
        }

        if (endDate.HasValue)
        {
            filters.Add(builder.Lte(x => x.ActionTime, endDate.Value));
        }
        if (actionTime.HasValue)
        {
            filters.Add(builder.Eq(x => x.ActionTime, actionTime));
        }

        if (!string.IsNullOrEmpty(entityType))
        {
            filters.Add(builder.Eq(x => x.EntityType, entityType));
        }

        return filters.Count > 0 ? builder.And(filters) : builder.Empty;
    }


    private static FilterDefinition<AuditRecord> BuildAuditLogFilterByText(
        string? searchText,
        DateTime? startDate = null,
        DateTime? endDate = null,
        DateTime? actionTime = null)
    {
        var builder = Builders<AuditRecord>.Filter;
        var filters = new List<FilterDefinition<AuditRecord>>();

        // Filtro por rango de fechas
        if (startDate.HasValue && endDate.HasValue)
        {
            filters.Add(builder.And(
                builder.Gte(x => x.ActionTime, startDate.Value),
                builder.Lte(x => x.ActionTime, endDate.Value)
            ));
        }
        else if (startDate.HasValue)
        {
            filters.Add(builder.Gte(x => x.ActionTime, startDate.Value));
        }
        else if (endDate.HasValue)
        {
            filters.Add(builder.Lte(x => x.ActionTime, endDate.Value));
        }
        else if (actionTime.HasValue)
        {
            filters.Add(builder.Eq(x => x.ActionTime, actionTime));
        }

        // Filtro por texto si se proporciona
        if (!string.IsNullOrEmpty(searchText))
        {
            var regexFilter = new BsonRegularExpression(new Regex(searchText, RegexOptions.IgnoreCase));
            filters.Add(builder.Or(
                builder.Regex(x => x.UserId, regexFilter),
                builder.Regex(x => x.RecordId, regexFilter),
                builder.Regex(x => x.EntityType, regexFilter),
                builder.Regex(x => x.ActionType, regexFilter)
            ));
        }

        // Combina todos los filtros con AND
        if (filters.Any())
        {
            return builder.And(filters);
        }

        return builder.Empty; // Sin filtros, devuelve todos los documentos
    }

    
}