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>
/// Server-042: <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.
/// </remarks>
public sealed class DashboardSnapshotPublisher : BackgroundService
{
    private static readonly TimeSpan DefaultReconnectDelay = TimeSpan.FromSeconds(5);

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

    public DashboardSnapshotPublisher(
        IDashboardSnapshotService snapshotService,
        IHubContext<DashboardSnapshotHub> hubContext,
        ILogger<DashboardSnapshotPublisher> logger)
        : this(snapshotService, hubContext, logger, DefaultReconnectDelay)
    {
    }

    // Internal hook for the Server-042 regression test: tests inject a
    // very short reconnect delay so the assertion doesn't wait the full
    // 5s. Production wiring always uses the 5s default via the public ctor.
    internal DashboardSnapshotPublisher(
        IDashboardSnapshotService snapshotService,
        IHubContext<DashboardSnapshotHub> hubContext,
        ILogger<DashboardSnapshotPublisher> logger,
        TimeSpan reconnectDelay)
    {
        _snapshotService = snapshotService;
        _hubContext = hubContext;
        _logger = logger;
        _reconnectDelay = reconnectDelay;
    }

    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
            {
                await foreach (DashboardSnapshot snapshot in _snapshotService
                    .WatchSnapshotsAsync(stoppingToken)
                    .ConfigureAwait(false))
                {
                    if (stoppingToken.IsCancellationRequested)
                    {
                        break;
                    }

                    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;
                }
            }
        }
    }
}
