using System.Text.Json;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport;

namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Services;

public class AuditService : IAuditService
{
    private readonly ScadaBridgeDbContext _context;
    private readonly IAuditCorrelationContext _correlationContext;

    /// <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
    };

    /// <summary>
    /// Initializes the audit service with the EF Core context and correlation context.
    /// </summary>
    /// <param name="context">The EF Core database context used to stage audit entries.</param>
    /// <param name="correlationContext">Provides the active bundle import id for audit row stamping.</param>
    public AuditService(ScadaBridgeDbContext context, IAuditCorrelationContext correlationContext)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
        _correlationContext = correlationContext ?? throw new ArgumentNullException(nameof(correlationContext));
    }

    /// <inheritdoc />
    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,
            // Stamp the active bundle import id (if any) so audit rows emitted during a
            // bundle import are attributable to that import session. Null in the normal
            // interactive code path.
            BundleImportId = _correlationContext.BundleImportId
        };

        // 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
            });
        }
    }
}
