using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Interfaces.Services;
namespace ScadaLink.AuditLog.Central;
///
/// Central-only direct-write implementation of .
/// Wraps as a best-effort
/// audit emission path for components that originate audit events ON the central
/// node (Notification Outbox dispatch, Inbound API) — NOT for site telemetry
/// ingest (that path is the SiteAudit → AuditLogIngestActor batched flow).
///
///
///
/// Best-effort contract. Audit-write failures NEVER abort the user-facing
/// action (alog.md §13). The writer catches every exception thrown by repository
/// resolution or the insert call, logs at warning, and returns successfully.
/// Callers may still wrap the call in their own try/catch (defensive — the writer
/// is supposed to swallow).
///
///
/// Scope-per-call resolution. is a SCOPED
/// EF Core service (registered by ScadaLink.ConfigurationDatabase). The
/// writer itself is registered as a singleton (so all callers share one instance),
/// so it cannot hold a scope across calls — it opens a fresh
/// per invocation, mirroring
/// the per-message scope pattern used by AuditLogIngestActor and
/// NotificationOutboxActor.
///
///
/// Idempotency. Persistence is via InsertIfNotExistsAsync, so a
/// double-emitted event (same ) is a silent
/// no-op — the writer is safe to call from any number of dispatch paths.
///
///
public sealed class CentralAuditWriter : ICentralAuditWriter
{
private readonly IServiceProvider _services;
private readonly ILogger _logger;
public CentralAuditWriter(IServiceProvider services, ILogger logger)
{
_services = services ?? throw new ArgumentNullException(nameof(services));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
///
/// Persists into the central AuditLog table
/// idempotently on . Stamps
/// from the central-side clock.
/// Internal failures are logged and swallowed — never thrown.
///
public async Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
{
if (evt is null)
{
// Defensive — a null event is a programming bug at the caller and
// produces no meaningful audit row. Log and return.
_logger.LogWarning("CentralAuditWriter.WriteAsync received null event; ignoring.");
return;
}
try
{
await using var scope = _services.CreateAsyncScope();
var repo = scope.ServiceProvider.GetRequiredService();
var stamped = evt with { IngestedAtUtc = DateTime.UtcNow };
await repo.InsertIfNotExistsAsync(stamped, ct).ConfigureAwait(false);
}
catch (Exception ex)
{
// Audit failure NEVER aborts the user-facing action — swallow and log.
_logger.LogWarning(
ex,
"CentralAuditWriter failed for EventId {EventId} (Kind={Kind}, Status={Status})",
evt.EventId, evt.Kind, evt.Status);
}
}
}