using Akka.Actor;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Audit;
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;

namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site.Telemetry;

/// <summary>
/// Production <see cref="ISiteStreamAuditClient"/> binding for site composition
/// roots: pushes audit telemetry to central via the site's
/// <c>SiteCommunicationActor</c>, which owns the transport choice (today
/// <c>GrpcCentralTransport</c>, over the central-hosted <c>CentralControlService</c>).
/// The central <c>CentralCommunicationActor</c> Asks the <c>AuditLogIngestActor</c>
/// proxy — the same command/control path notifications already use. Wired by the
/// Host for site roles; central and test composition roots keep the
/// <see cref="NoOpSiteStreamAuditClient"/> DI default (they have no
/// <c>SiteCommunicationActor</c>).
/// </summary>
/// <remarks>
/// <para>
/// <b>Throw-on-failure contract.</b> An Ask timeout or a faulted reply
/// (<see cref="Status.Failure"/>) propagates as a thrown exception out of the
/// <c>Ingest*Async</c> methods — it is NOT caught and turned into an empty ack.
/// The <see cref="SiteAuditTelemetryActor"/> drain loop treats a thrown
/// exception as transient and leaves the rows <c>Pending</c> for the next tick.
/// Swallowing the fault into an empty ack would be indistinguishable from "zero
/// rows accepted" and would silently lose the retry signal. It has been confirmed
/// that the central receiving end does not collapse an ingest fault into an empty
/// ack either, so a site-side Ask through the whole path faults cleanly on a
/// central-side timeout.
/// </para>
/// <para>
/// The batches arrive as proto DTOs (<see cref="AuditEventBatch"/> /
/// <see cref="CachedTelemetryBatch"/>) because the
/// <see cref="SiteAuditTelemetryActor"/> builds them with
/// <see cref="AuditEventDtoMapper.ToDto"/>. This client converts them back into
/// the <see cref="AuditEvent"/> / <see cref="SiteCall"/> entities the command
/// messages carry — the same DTO→entity translation the
/// <c>SiteStreamGrpcServer</c> performs for the gRPC reconciliation path.
/// </para>
/// </remarks>
public sealed class SiteCommunicationAuditClient : ISiteStreamAuditClient
{
    private readonly IActorRef _siteCommunicationActor;
    private readonly TimeSpan _askTimeout;

    /// <summary>
    /// Initializes a new instance that forwards audit telemetry to central via the site's <c>SiteCommunicationActor</c>.
    /// </summary>
    /// <param name="siteCommunicationActor">
    /// The site's <c>SiteCommunicationActor</c> — it forwards the ingest command
    /// to central over its configured <c>ICentralTransport</c> and routes the
    /// reply back to this client's Ask.
    /// </param>
    /// <param name="askTimeout">
    /// Ask timeout for the round-trip to central. On expiry the Ask throws
    /// <see cref="Akka.Actor.AskTimeoutException"/>, which the drain loop treats
    /// as transient (rows stay <c>Pending</c>).
    /// </param>
    public SiteCommunicationAuditClient(IActorRef siteCommunicationActor, TimeSpan askTimeout)
    {
        ArgumentNullException.ThrowIfNull(siteCommunicationActor);
        _siteCommunicationActor = siteCommunicationActor;
        _askTimeout = askTimeout;
    }

    /// <inheritdoc/>
    public async Task<IngestAck> IngestAuditEventsAsync(AuditEventBatch batch, CancellationToken ct)
    {
        ArgumentNullException.ThrowIfNull(batch);

        var events = new List<AuditEvent>(batch.Events.Count);
        foreach (var dto in batch.Events)
        {
            events.Add(AuditEventDtoMapper.FromDto(dto));
        }

        // Ask<T> throws AskTimeoutException on timeout and rethrows a
        // Status.Failure's inner cause — both surface as a thrown exception so
        // the drain loop keeps the rows Pending. We deliberately do NOT catch.
        var reply = await _siteCommunicationActor
            .Ask<IngestAuditEventsReply>(new IngestAuditEventsCommand(events), _askTimeout, ct)
            .ConfigureAwait(false);

        return ToAck(reply.AcceptedEventIds);
    }

    /// <inheritdoc/>
    public async Task<IngestAck> IngestCachedTelemetryAsync(CachedTelemetryBatch batch, CancellationToken ct)
    {
        ArgumentNullException.ThrowIfNull(batch);

        var entries = new List<CachedTelemetryEntry>(batch.Packets.Count);
        foreach (var packet in batch.Packets)
        {
            var audit = AuditEventDtoMapper.FromDto(packet.AuditEvent);
            var siteCall = SiteCallDtoMapper.FromDto(packet.Operational);
            entries.Add(new CachedTelemetryEntry(audit, siteCall));
        }

        // Same throw-on-failure contract as IngestAuditEventsAsync. The reply
        // type is IngestCachedTelemetryReply (the central dual-write reply),
        // distinct from IngestAuditEventsReply.
        var reply = await _siteCommunicationActor
            .Ask<IngestCachedTelemetryReply>(new IngestCachedTelemetryCommand(entries), _askTimeout, ct)
            .ConfigureAwait(false);

        return ToAck(reply.AcceptedEventIds);
    }

    private static IngestAck ToAck(IReadOnlyList<Guid> acceptedEventIds)
    {
        var ack = new IngestAck();
        foreach (var id in acceptedEventIds)
        {
            ack.AcceptedEventIds.Add(id.ToString());
        }
        return ack;
    }
}
