using Microsoft.AspNetCore.SignalR;

namespace ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs;

/// <summary>
/// Background service that subscribes to <see cref="IDashboardSnapshotService.WatchSnapshotsAsync"/>
/// and broadcasts every snapshot it produces to every connected
/// <see cref="DashboardSnapshotHub"/> client. There is one publisher per
/// gateway process; clients listen via the hub.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="ExecuteAsync"/> wraps the snapshot subscription in
/// a reconnect loop with a configurable retry delay (5s by default,
/// mirroring <see cref="AlarmsHubPublisher"/>). A transient failure inside
/// <see cref="IDashboardSnapshotService.WatchSnapshotsAsync"/> — e.g. a
/// one-time logger-init failure or a transient SQL error from the Galaxy
/// summary projection — would otherwise end the BackgroundService with no
/// reconnect, taking the dashboard offline until process restart.
/// </para>
/// <para>
/// The loop is idle-gated on <see cref="DashboardSnapshotHubConnectionCounter"/>.
/// Each snapshot costs a session-registry snapshot and sort, a metrics snapshot
/// that copies dictionaries under the global metrics lock, and (periodically) a
/// SQLite read of the API key table — work with no consumer when no dashboard is
/// connected. While the count is zero the publisher does not advance the snapshot
/// enumerator at all, so the producing iterator stays suspended and builds nothing.
/// </para>
/// </remarks>
public sealed class DashboardSnapshotPublisher : BackgroundService
{
    private static readonly TimeSpan DefaultReconnectDelay = TimeSpan.FromSeconds(5);
    private static readonly TimeSpan DefaultIdlePollInterval = TimeSpan.FromSeconds(1);

    private readonly IDashboardSnapshotService _snapshotService;
    private readonly IHubContext<DashboardSnapshotHub> _hubContext;
    private readonly DashboardSnapshotHubConnectionCounter _connectionCounter;
    private readonly ILogger<DashboardSnapshotPublisher> _logger;
    private readonly TimeSpan _reconnectDelay;
    private readonly TimeSpan _idlePollInterval;

    /// <summary>Initializes a new instance of the DashboardSnapshotPublisher class.</summary>
    /// <param name="snapshotService">The snapshot service to subscribe to.</param>
    /// <param name="hubContext">The SignalR hub context for broadcasting.</param>
    /// <param name="connectionCounter">Live snapshot hub connection count used to idle-gate the tick.</param>
    /// <param name="logger">The logger instance.</param>
    public DashboardSnapshotPublisher(
        IDashboardSnapshotService snapshotService,
        IHubContext<DashboardSnapshotHub> hubContext,
        DashboardSnapshotHubConnectionCounter connectionCounter,
        ILogger<DashboardSnapshotPublisher> logger)
        : this(snapshotService, hubContext, connectionCounter, logger, DefaultReconnectDelay, DefaultIdlePollInterval)
    {
    }

    /// <summary>Initializes a new instance of the DashboardSnapshotPublisher class with custom cadences.</summary>
    /// <remarks>
    /// Internal hook for testing: tests inject a very short reconnect delay so assertions
    /// don't wait the full 5s, and a short idle poll so the resume-from-idle path is fast.
    /// </remarks>
    /// <param name="snapshotService">The snapshot service to subscribe to.</param>
    /// <param name="hubContext">The SignalR hub context for broadcasting.</param>
    /// <param name="connectionCounter">Live snapshot hub connection count used to idle-gate the tick.</param>
    /// <param name="logger">The logger instance.</param>
    /// <param name="reconnectDelay">The delay before reconnecting after a subscription failure.</param>
    /// <param name="idlePollInterval">How often the idle publisher re-checks for a connected viewer.</param>
    internal DashboardSnapshotPublisher(
        IDashboardSnapshotService snapshotService,
        IHubContext<DashboardSnapshotHub> hubContext,
        DashboardSnapshotHubConnectionCounter connectionCounter,
        ILogger<DashboardSnapshotPublisher> logger,
        TimeSpan reconnectDelay,
        TimeSpan idlePollInterval)
    {
        _snapshotService = snapshotService;
        _hubContext = hubContext;
        _connectionCounter = connectionCounter;
        _logger = logger;
        _reconnectDelay = reconnectDelay;
        _idlePollInterval = idlePollInterval;
    }

    /// <inheritdoc />
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Loop forever — when WatchSnapshotsAsync completes or throws, reopen
        // the subscription after a short delay. The hosted-service lifetime
        // ends only when the host stops. Mirrors AlarmsHubPublisher.
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                // Enumerated by hand rather than with await foreach: the snapshot is
                // built inside the producer's MoveNextAsync, so not calling MoveNextAsync
                // is what makes the idle gate skip the build and not just the broadcast.
                await using IAsyncEnumerator<DashboardSnapshot> snapshots = _snapshotService
                    .WatchSnapshotsAsync(stoppingToken)
                    .GetAsyncEnumerator(stoppingToken);

                while (!stoppingToken.IsCancellationRequested)
                {
                    if (_connectionCounter.Count == 0)
                    {
                        // Nobody is watching: leave the producer suspended and re-check
                        // shortly. The first viewer to connect resumes the tick, and is
                        // seeded directly by the hub's OnConnectedAsync meanwhile.
                        await Task.Delay(_idlePollInterval, stoppingToken).ConfigureAwait(false);
                        continue;
                    }

                    if (!await snapshots.MoveNextAsync().ConfigureAwait(false))
                    {
                        break;
                    }

                    DashboardSnapshot snapshot = snapshots.Current;

                    try
                    {
                        await _hubContext.Clients
                            .All
                            .SendAsync(DashboardSnapshotHub.SnapshotMessage, snapshot, stoppingToken)
                            .ConfigureAwait(false);
                    }
                    catch (Exception ex) when (ex is not OperationCanceledException)
                    {
                        _logger.LogWarning(ex, "Snapshot broadcast failed; will retry on the next snapshot tick.");
                    }
                }
            }
            catch (OperationCanceledException)
            {
                return;
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Snapshot subscription faulted; reconnecting in {DelaySeconds:F1}s.", _reconnectDelay.TotalSeconds);
                try
                {
                    await Task.Delay(_reconnectDelay, stoppingToken).ConfigureAwait(false);
                }
                catch (OperationCanceledException)
                {
                    return;
                }
            }
        }
    }
}
