using Akka.Actor;
using Akka.Event;
using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;

namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Audit;

/// <summary>
/// Cluster-singleton actor that batches <see cref="AuditEvent"/> messages from the cluster
/// and bulk-inserts them into <c>ConfigAuditLog</c>. Flush triggers:
/// - Buffer reaches <see cref="FlushBatchSize"/> events.
/// - <see cref="FlushInterval"/> elapses with a non-empty buffer.
/// - <c>PreRestart</c> / <c>PostStop</c> (supervisor swap or coordinated shutdown).
///
/// Dedup is two-layer: in-buffer (the <see cref="Dictionary{TKey, TValue}"/> below collapses
/// duplicate EventIds before flush) and at the database via the filtered unique index
/// <c>UX_ConfigAuditLog_EventId</c> (cross-restart safety — a retry of an already-flushed
/// batch hits the constraint and we drop the duplicate insert without losing the rest of
/// the batch).
///
/// Implements the shared <see cref="IAuditWriter"/> seam: <see cref="WriteAsync"/> is a
/// best-effort, never-throwing entry point that simply <c>Tell</c>s this actor and returns
/// a completed task, so non-Akka callers can emit canonical audit events through the same
/// batching/dedup pipeline as in-cluster <c>Tell</c> traffic.
/// </summary>
public sealed class AuditWriterActor : ReceiveActor, IWithTimers, IAuditWriter
{
    public const int FlushBatchSize = 500;
    public static readonly TimeSpan FlushInterval = TimeSpan.FromSeconds(5);

    private readonly IDbContextFactory<OtOpcUaConfigDbContext> _dbFactory;
    private readonly ILoggingAdapter _log = Context.GetLogger();
    private readonly Dictionary<Guid, AuditEvent> _buffer = new();

    /// <summary>Gets or sets the timer scheduler for the actor.</summary>
    public ITimerScheduler Timers { get; set; } = null!;

    /// <summary>Creates a Props factory for the AuditWriterActor.</summary>
    /// <param name="dbFactory">The database context factory for creating ConfigDb connections.</param>
    /// <returns>The <see cref="Akka.Actor.Props"/> used to create an <see cref="AuditWriterActor"/>.</returns>
    public static Props Props(IDbContextFactory<OtOpcUaConfigDbContext> dbFactory) =>
        Akka.Actor.Props.Create(() => new AuditWriterActor(dbFactory));

    /// <summary>Initializes a new instance of the AuditWriterActor class.</summary>
    /// <param name="dbFactory">The database context factory for creating ConfigDb connections.</param>
    public AuditWriterActor(IDbContextFactory<OtOpcUaConfigDbContext> dbFactory)
    {
        _dbFactory = dbFactory;
        Receive<AuditEvent>(HandleEvent);
        Receive<Flush>(_ => FlushBuffer());
    }

    /// <inheritdoc />
    protected override void PreStart()
    {
        Timers.StartPeriodicTimer("flush", Flush.Instance, FlushInterval);
    }

    /// <summary>
    /// <see cref="IAuditWriter"/> seam. Best-effort and never throws: routes the event onto this
    /// actor's mailbox via <c>Tell</c> (thread-safe from any caller) so it flows through the same
    /// batching + dedup pipeline as in-cluster traffic, then returns immediately. The actual
    /// persistence happens asynchronously on the next flush; a write failure there is logged and
    /// the batch dropped (per the best-effort audit contract).
    /// </summary>
    /// <param name="evt">The canonical audit event to persist.</param>
    /// <param name="ct">Unused — enqueue is synchronous and non-blocking.</param>
    /// <returns>A completed task.</returns>
    public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
    {
        // Akka Tell is safe to call from any thread and never throws to the caller.
        Self.Tell(evt);
        return Task.CompletedTask;
    }

    private void HandleEvent(AuditEvent evt)
    {
        // In-buffer dedup. Last write wins on duplicate EventId within the batch — events
        // with the same EventId are by contract identical, so this is a no-op.
        _buffer[evt.EventId] = evt;
        if (_buffer.Count >= FlushBatchSize) FlushBuffer();
    }

    private void FlushBuffer()
    {
        if (_buffer.Count == 0) return;

        var snapshot = _buffer.Values.ToList();
        _buffer.Clear();

        try
        {
            using var db = _dbFactory.CreateDbContext();
            foreach (var evt in snapshot)
            {
                db.ConfigAuditLogs.Add(new ConfigAuditLog
                {
                    Timestamp = evt.OccurredAtUtc.UtcDateTime,
                    Principal = evt.Actor,
                    EventType = $"{evt.Category}:{evt.Action}",
                    NodeId = evt.SourceNode,
                    DetailsJson = evt.DetailsJson,
                    EventId = evt.EventId,
                    CorrelationId = evt.CorrelationId,
                    Outcome = evt.Outcome.ToString(),
                });
            }
            db.SaveChanges();
            _log.Debug("AuditWriter flushed {Count} events", snapshot.Count);
        }
        catch (Exception ex)
        {
            _log.Error(ex, "AuditWriter flush failed; {Count} events dropped", snapshot.Count);
        }
    }

    /// <inheritdoc />
    protected override void PreRestart(Exception reason, object message)
    {
        FlushBuffer();
        base.PreRestart(reason, message);
    }

    /// <inheritdoc />
    protected override void PostStop()
    {
        FlushBuffer();
        base.PostStop();
    }

    public sealed class Flush
    {
        public static readonly Flush Instance = new();
        private Flush() { }
    }
}
