using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.AuditLog.Redaction;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;

namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;

/// <summary>
/// Central-only direct-write implementation of <see cref="ICentralAuditWriter"/>.
/// Wraps <see cref="IAuditLogRepository.InsertIfNotExistsAsync"/> 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).
/// </summary>
/// <remarks>
/// <para>
/// <b>Best-effort contract.</b> 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).
/// </para>
/// <para>
/// <b>Scope-per-call resolution.</b> <see cref="IAuditLogRepository"/> is a SCOPED
/// EF Core service (registered by <c>ZB.MOM.WW.ScadaBridge.ConfigurationDatabase</c>). 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
/// <see cref="IServiceScope"/> per <see cref="WriteAsync"/> invocation, mirroring
/// the per-message scope pattern used by <c>AuditLogIngestActor</c> and
/// <c>NotificationOutboxActor</c>.
/// </para>
/// <para>
/// <b>Idempotency.</b> Persistence is via <c>InsertIfNotExistsAsync</c>, so a
/// double-emitted event (same <see cref="AuditEvent.EventId"/>) is a silent
/// no-op — the writer is safe to call from any number of dispatch paths.
/// </para>
/// </remarks>
public sealed class CentralAuditWriter : ICentralAuditWriter
{
    private readonly IServiceProvider _services;
    private readonly ILogger<CentralAuditWriter> _logger;
    private readonly IAuditRedactor _redactor;
    private readonly ICentralAuditWriteFailureCounter _failureCounter;
    private readonly INodeIdentityProvider? _nodeIdentity;

    /// <summary>
    /// The central direct-write path used by the
    /// NotificationOutboxActor dispatch and the Inbound API middleware also
    /// needs to truncate + redact before the row hits MS SQL. The filter is
    /// optional so test composition roots that don't pass one keep
    /// working (they only ever write small payloads); production DI registers
    /// the real filter via <see cref="ServiceCollectionExtensions.AddAuditLog"/>.
    /// Adds the optional
    /// <see cref="ICentralAuditWriteFailureCounter"/> so a swallowed repository
    /// throw bumps the central health surface's
    /// <c>CentralAuditWriteFailures</c> counter. Defaults to a NoOp so test
    /// composition roots that don't wire the counter keep their current
    /// behaviour. SourceNode-stamping — adds the optional
    /// <see cref="INodeIdentityProvider"/> so central-origin rows (Notification
    /// Outbox dispatch, Inbound API) carry the writing central node's
    /// identifier when the caller hasn't already supplied one. Optional /
    /// defaulting-to-null so test composition roots that don't pass a
    /// provider keep working — the caller-wins discipline means an absent
    /// provider simply leaves SourceNode at whatever the caller set (often
    /// null, which is the legacy behaviour).
    /// </summary>
    /// <param name="services">Service provider used to open a per-call scope for the scoped repository.</param>
    /// <param name="logger">Logger for swallowed write-failure diagnostics.</param>
    /// <param name="redactor">Optional canonical redactor for truncation and redaction; defaults to the always-safe default.</param>
    /// <param name="failureCounter">Optional counter incremented on swallowed repository failures; defaults to a no-op.</param>
    /// <param name="nodeIdentity">Optional node identity provider for stamping <c>SourceNode</c> on central-origin rows.</param>
    public CentralAuditWriter(
        IServiceProvider services,
        ILogger<CentralAuditWriter> logger,
        IAuditRedactor? redactor = null,
        ICentralAuditWriteFailureCounter? failureCounter = null,
        INodeIdentityProvider? nodeIdentity = null)
    {
        _services = services ?? throw new ArgumentNullException(nameof(services));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        // Never default to null — over-redact instead.
        // Wired via the canonical IAuditRedactor seam.
        // SafeDefaultAuditRedactor applies HTTP header redaction with
        // hard-coded sensitive defaults so a composition root that omits the
        // real redactor still scrubs Authorization / X-Api-Key / Cookie /
        // Set-Cookie before persistence.
        _redactor = redactor ?? SafeDefaultAuditRedactor.Instance;
        _failureCounter = failureCounter ?? new NoOpCentralAuditWriteFailureCounter();
        _nodeIdentity = nodeIdentity;
    }

    /// <inheritdoc />
    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
        {
            // Redact BEFORE stamping IngestedAtUtc + handing to the repo. The
            // redactor contract is "never throws". _redactor is
            // now non-null (SafeDefaultAuditRedactor fallback) so header
            // redaction always runs even in composition roots that omit the
            // real redactor.
            var filtered = _redactor.Apply(evt);

            // SourceNode-stamping: caller-provided value wins
            // (supports any future direct-write callsite that already has its
            // own node id); otherwise stamp from the local
            // INodeIdentityProvider, when one is wired. Production DI on
            // central nodes always supplies the provider; legacy test
            // composition roots that don't pass it leave SourceNode at
            // whatever the caller set (often null), preserving back-compat.
            if (filtered.SourceNode is null && _nodeIdentity?.NodeName is { } nodeName)
            {
                filtered = filtered with { SourceNode = nodeName };
            }

            await using var scope = _services.CreateAsyncScope();
            var repo = scope.ServiceProvider.GetRequiredService<IAuditLogRepository>();
            // IngestedAtUtc is a DetailsJson field on the
            // canonical record, so stamp it via the projection helper.
            var stamped = AuditRowProjection.WithIngestedAtUtc(filtered, DateTime.UtcNow);
            await repo.InsertIfNotExistsAsync(stamped, ct).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            // Audit failure NEVER aborts the user-facing action — swallow and log.
            // Also surface the failure on the central health
            // counter so a sustained audit-write outage is visible on the
            // health dashboard rather than disappearing into the log file.
            try
            {
                _failureCounter.Increment();
            }
            catch
            {
                // Counter must NEVER throw — defence in depth. Even if a
                // misbehaving custom counter does, swallowing here keeps the
                // best-effort contract intact.
            }
            // Log the input event's identifying fields. EventId + Action are
            // immutable across the redact+stamp chain — the `with` clones above
            // touch only SourceNode and DetailsJson — so referencing `evt` here
            // is intentional and equivalent to the stamped record for
            // diagnostics. Action = "{Channel}.{Kind}" carries the kind; the
            // canonical Outcome carries the coarse status (fine-grained Status
            // lives in DetailsJson).
            _logger.LogWarning(
                ex,
                "CentralAuditWriter failed for EventId {EventId} (Action={Action}, Outcome={Outcome})",
                evt.EventId, evt.Action, evt.Outcome);
        }
    }
}
