using System.Text.Json;
using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Interfaces.Services;

namespace ScadaLink.ConfigurationDatabase.Services;

public class AuditService : IAuditService
{
    private readonly ScadaLinkDbContext _context;

    /// <summary>
    /// Serializer options for audit <c>afterState</c> payloads. Audit writes commit in the
    /// same transaction as the change they record, so a serialization exception here would
    /// roll back the entire business operation. Reference cycles (common when an EF entity
    /// with loaded navigations is passed in) are ignored rather than thrown, and depth is
    /// bounded so a pathological graph cannot produce an unbounded payload.
    /// </summary>
    private static readonly JsonSerializerOptions AuditSerializerOptions = new()
    {
        ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.IgnoreCycles,
        MaxDepth = 32
    };

    public AuditService(ScadaLinkDbContext context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
    }

    public async Task LogAsync(
        string user,
        string action,
        string entityType,
        string entityId,
        string entityName,
        object? afterState,
        CancellationToken cancellationToken = default)
    {
        var entry = new AuditLogEntry(user, action, entityType, entityId, entityName)
        {
            Timestamp = DateTimeOffset.UtcNow,
            AfterStateJson = afterState != null
                ? SerializeAfterState(afterState)
                : null
        };

        // Add to change tracker only — caller is responsible for calling SaveChangesAsync
        // to ensure atomicity with the entity change.
        await _context.AuditLogEntries.AddAsync(entry, cancellationToken);
    }

    /// <summary>
    /// Serializes the caller-supplied after-state, tolerating arbitrary object shapes.
    /// Reference cycles are ignored via <see cref="AuditSerializerOptions"/>. If serialization
    /// still fails (e.g. <c>MaxDepth</c> exceeded), the audit entry is preserved with a
    /// diagnostic placeholder rather than throwing — a serialization failure must never
    /// roll back the business operation the audit entry is recording.
    /// </summary>
    private static string SerializeAfterState(object afterState)
    {
        try
        {
            return JsonSerializer.Serialize(afterState, AuditSerializerOptions);
        }
        catch (Exception ex) when (ex is JsonException or NotSupportedException)
        {
            return JsonSerializer.Serialize(new
            {
                AuditSerializationError = ex.Message,
                StateType = afterState.GetType().FullName
            });
        }
    }
}
