using Akka.Actor;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Messages.Audit;

namespace ScadaLink.AuditLog.Central;

/// <summary>
/// Central-side singleton (per Bundle E wiring) that ingests batches of
/// <see cref="AuditEvent"/> rows pushed from sites via the
/// <c>IngestAuditEvents</c> gRPC RPC. Each row is stamped with the central-side
/// <see cref="AuditEvent.IngestedAtUtc"/> and inserted idempotently via
/// <see cref="IAuditLogRepository.InsertIfNotExistsAsync"/> — duplicates are
/// silently swallowed (first-write-wins per Bundle A's hardening).
/// </summary>
/// <remarks>
/// <para>
/// Idempotency is the contract: a row that already exists at central counts
/// as "accepted" for the purposes of the reply, because the storage state is
/// consistent and the site is free to flip its local row to <c>Forwarded</c>.
/// </para>
/// <para>
/// Per Bundle D's brief, audit-write failures must NEVER abort the user-facing
/// action. The actor wraps each repository call in its own try/catch so a
/// single bad row cannot cause the rest of the batch to be lost; the actor's
/// <see cref="SupervisorStrategy"/> uses <c>Resume</c> so a thrown exception
/// inside <c>ReceiveAsync</c> does not restart the actor (which would also
/// reset any in-flight state).
/// </para>
/// <para>
/// Two constructors exist for a deliberate reason: Bundle D's tests inject a
/// concrete <see cref="IAuditLogRepository"/> against a per-test MSSQL fixture
/// (the only way to verify the IngestedAtUtc stamp + duplicate-key idempotency
/// end to end), while Bundle E's host wiring registers the actor as a cluster
/// singleton and must therefore resolve the repository — which is a scoped EF
/// Core service — from a fresh DI scope per message. Mirroring the Notification
/// Outbox actor's pattern.
/// </para>
/// </remarks>
public class AuditLogIngestActor : ReceiveActor
{
    private readonly IServiceProvider? _serviceProvider;
    private readonly IAuditLogRepository? _injectedRepository;
    private readonly ILogger<AuditLogIngestActor> _logger;

    /// <summary>
    /// Test-mode constructor — injects a concrete repository instance whose
    /// lifetime exceeds the test, so the actor reuses the same instance across
    /// every message. Used by Bundle D's MSSQL-backed TestKit fixture.
    /// </summary>
    public AuditLogIngestActor(
        IAuditLogRepository repository,
        ILogger<AuditLogIngestActor> logger)
    {
        ArgumentNullException.ThrowIfNull(repository);
        ArgumentNullException.ThrowIfNull(logger);

        _injectedRepository = repository;
        _logger = logger;

        ReceiveAsync<IngestAuditEventsCommand>(OnIngestAsync);
    }

    /// <summary>
    /// Production constructor — resolves <see cref="IAuditLogRepository"/> from
    /// a fresh DI scope per message because the repository is a scoped EF Core
    /// service registered by <c>AddConfigurationDatabase</c>. The actor itself
    /// is a long-lived cluster singleton, so it cannot hold a scope across
    /// messages.
    /// </summary>
    public AuditLogIngestActor(
        IServiceProvider serviceProvider,
        ILogger<AuditLogIngestActor> logger)
    {
        ArgumentNullException.ThrowIfNull(serviceProvider);
        ArgumentNullException.ThrowIfNull(logger);

        _serviceProvider = serviceProvider;
        _logger = logger;

        ReceiveAsync<IngestAuditEventsCommand>(OnIngestAsync);
    }

    /// <summary>
    /// Audit-write failures are best-effort by design (see alog.md §13): a
    /// thrown exception in the ingest pipeline must not crash the actor.
    /// Resume keeps the actor's state intact so the next batch is processed
    /// against the same repository instance.
    /// </summary>
    protected override SupervisorStrategy SupervisorStrategy()
    {
        return new OneForOneStrategy(maxNrOfRetries: 0, withinTimeRange: TimeSpan.Zero, decider:
            Akka.Actor.SupervisorStrategy.DefaultDecider);
    }

    private async Task OnIngestAsync(IngestAuditEventsCommand cmd)
    {
        // Sender is captured before the first await — Akka resets Sender
        // between message dispatches, so a post-await Tell would go to
        // DeadLetters.
        var replyTo = Sender;
        var nowUtc = DateTime.UtcNow;
        var accepted = new List<Guid>(cmd.Events.Count);

        // Resolve the repository for the whole batch — one DbContext per
        // message, mirroring NotificationOutboxActor. The injected-repository
        // mode (Bundle D tests) skips the scope entirely.
        IServiceScope? scope = null;
        IAuditLogRepository repository;
        if (_injectedRepository is not null)
        {
            repository = _injectedRepository;
        }
        else
        {
            scope = _serviceProvider!.CreateScope();
            repository = scope.ServiceProvider.GetRequiredService<IAuditLogRepository>();
        }

        try
        {
            foreach (var evt in cmd.Events)
            {
                try
                {
                    // Stamp IngestedAtUtc here, not at the site. Bundle A's
                    // repository hardening already swallows duplicate-key races,
                    // so the same id arriving twice (site retry, reconciliation)
                    // is a silent no-op.
                    var ingested = evt with { IngestedAtUtc = nowUtc };
                    await repository.InsertIfNotExistsAsync(ingested).ConfigureAwait(false);
                    accepted.Add(evt.EventId);
                }
                catch (Exception ex)
                {
                    // Per-row catch — one bad row never sinks the whole batch.
                    // The row stays Pending at the site; the next drain retries.
                    _logger.LogError(ex,
                        "Failed to persist audit event {EventId} during batch ingest; row will be retried by the site.",
                        evt.EventId);
                }
            }
        }
        finally
        {
            scope?.Dispose();
        }

        replyTo.Tell(new IngestAuditEventsReply(accepted));
    }
}
