using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Server.Alarms;
using ZB.MOM.WW.MxGateway.Server.Configuration;

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

/// <summary>
/// Background service that subscribes to
/// <see cref="IGatewayAlarmService.StreamAsync"/> (no filter) and re-broadcasts
/// every <see cref="AlarmFeedMessage"/> to every <see cref="AlarmsHub"/>
/// client. The hub itself is session-less; clients filter / route messages
/// in the browser.
/// </summary>
/// <remarks>
/// When <c>MxGateway:Dashboard:ShowTagValues</c> is false (the default), the
/// alarm value fields are stripped from a redacted copy before the message
/// reaches any browser client — the same rule
/// <see cref="DashboardEventBroadcaster"/> applies to the events-hub mirror, so
/// the two SignalR seams cannot disagree about whether values leave the gateway.
/// The source message is never mutated: it fans out from one feed to the gRPC
/// <c>StreamAlarms</c> subscribers and the alarms page as well, and none of
/// those audiences is subject to this dashboard-display flag.
/// </remarks>
/// <param name="alarmService">The gateway's central alarm feed.</param>
/// <param name="hubContext">Hub context used to broadcast to the alarms group.</param>
/// <param name="options">Gateway options supplying <c>Dashboard:ShowTagValues</c>.</param>
/// <param name="logger">Logger for best-effort broadcast failures.</param>
public sealed class AlarmsHubPublisher(
    IGatewayAlarmService alarmService,
    IHubContext<AlarmsHub> hubContext,
    IOptions<GatewayOptions> options,
    ILogger<AlarmsHubPublisher> logger) : BackgroundService
{
    private readonly bool _showTagValues = options.Value.Dashboard.ShowTagValues;

    /// <inheritdoc />
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Loop forever — when StreamAsync completes (monitor restart, etc.)
        // reopen the subscription. The hosted-service lifetime ends only
        // when the host stops.
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await foreach (AlarmFeedMessage message in alarmService
                    .StreamAsync(alarmFilterPrefix: null, stoppingToken)
                    .ConfigureAwait(false))
                {
                    if (stoppingToken.IsCancellationRequested)
                    {
                        break;
                    }

                    try
                    {
                        AlarmFeedMessage outbound = _showTagValues ? message : RedactValues(message);
                        await hubContext.Clients
                            .Group(AlarmsHub.AllAlarmsGroup)
                            .SendAsync(AlarmsHub.AlarmMessage, outbound, stoppingToken)
                            .ConfigureAwait(false);
                    }
                    catch (Exception ex) when (ex is not OperationCanceledException)
                    {
                        logger.LogWarning(ex, "Alarm broadcast failed; continuing.");
                    }
                }
            }
            catch (OperationCanceledException)
            {
                return;
            }
            catch (Exception ex)
            {
                logger.LogWarning(ex, "Alarm subscription faulted; reconnecting in 5s.");
                try
                {
                    await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken).ConfigureAwait(false);
                }
                catch (OperationCanceledException)
                {
                    return;
                }
            }
        }
    }

    /// <summary>
    /// Produces a copy of <paramref name="source"/> with the alarm value fields
    /// cleared, leaving every other field — reference, severity, state, category,
    /// operator, timestamps — intact so the alarms page still renders the row.
    /// </summary>
    /// <param name="source">The message as the alarm feed produced it.</param>
    /// <returns>
    /// A redacted deep clone for the two value-bearing payload arms; the source
    /// instance itself for every other arm, which carries no value to strip.
    /// New payload arms therefore pass through unchanged by default — the switch
    /// names only the arms that have something to redact, so adding a valueless
    /// arm to the contract needs no change here.
    /// </returns>
    private static AlarmFeedMessage RedactValues(AlarmFeedMessage source)
    {
        switch (source.PayloadCase)
        {
            case AlarmFeedMessage.PayloadOneofCase.Transition:
            {
                AlarmFeedMessage redacted = source.Clone();
                redacted.Transition.CurrentValue = null;
                redacted.Transition.LimitValue = null;
                return redacted;
            }

            case AlarmFeedMessage.PayloadOneofCase.ActiveAlarm:
            {
                AlarmFeedMessage redacted = source.Clone();
                redacted.ActiveAlarm.CurrentValue = null;
                redacted.ActiveAlarm.LimitValue = null;
                return redacted;
            }

            default:
                return source;
        }
    }
}
