using Microsoft.AspNetCore.SignalR;

namespace Mbproxy.Admin;

/// <summary>
/// SignalR hub backing the live admin dashboard. Two subscription scopes:
/// <list type="bullet">
///   <item><see cref="SubscribeFleet"/> — the fleet dashboard (<c>GET /</c>) joins the
///         <see cref="FleetGroup"/> group and receives a <c>"fleet"</c> message every
///         push tick.</item>
///   <item><see cref="SubscribePlc"/> — a connection-detail page (<c>GET /plc/{name}</c>)
///         joins <see cref="PlcGroup"/> and receives a <c>"plc"</c> message. It also
///         registers the calling tab with the <see cref="PlcSubscriptionTracker"/> so
///         the PLC's tag-value capture is armed while the page is open.</item>
/// </list>
///
/// <para>The hub itself is transient (one instance per call). Cross-call state — which
/// tabs view which PLCs — lives in the singleton <see cref="PlcSubscriptionTracker"/>.
/// The hub deliberately does <b>not</b> arm or disarm captures: a SignalR reconnect
/// gives the connection a new <c>ConnectionId</c>, and arming off that lifetime leaks.
/// Instead the hub only mutates the tracker (keyed on a stable client <c>tabId</c>) and
/// <see cref="StatusBroadcaster"/> reconciles capture arm state from the tracker each
/// push cycle. The actual pushes are issued by the broadcaster, not the hub.</para>
/// </summary>
internal sealed class StatusHub : Hub
{
    /// <summary>SignalR group name for fleet-dashboard subscribers.</summary>
    public const string FleetGroup = "fleet";

    /// <summary>SignalR group name for a single PLC's detail-page subscribers.</summary>
    public static string PlcGroup(string plcName) => "plc:" + plcName;

    private readonly PlcSubscriptionTracker _tracker;

    public StatusHub(PlcSubscriptionTracker tracker) => _tracker = tracker;

    /// <summary>Subscribes the calling connection to fleet-wide status pushes.</summary>
    public Task SubscribeFleet()
        => Groups.AddToGroupAsync(Context.ConnectionId, FleetGroup);

    /// <summary>
    /// Subscribes the calling connection to one PLC's detail pushes. <paramref name="tabId"/>
    /// is a stable per-page-load identifier supplied by the client so a transport
    /// reconnect (which changes <c>ConnectionId</c>) is recognised as the same viewer.
    /// </summary>
    public async Task SubscribePlc(string plcName, string tabId)
    {
        // Register with the tracker first (synchronous, lock-guarded) so this connection's
        // own OnDisconnectedAsync — which SignalR dispatches only after this invocation's
        // Task completes — always observes a consistent state. Capture arming is NOT done
        // here; StatusBroadcaster reconciles it each cycle from the tracker.
        _tracker.SubscribePlc(Context.ConnectionId, tabId, plcName);
        await Groups.AddToGroupAsync(Context.ConnectionId, PlcGroup(plcName)).ConfigureAwait(false);
    }

    /// <summary>
    /// On disconnect, releases the connection from its tab. If it was the tab's last
    /// connection the tab's PLC subscriptions are dropped; the broadcaster disarms the
    /// now-unviewed captures on its next cycle.
    /// </summary>
    public override Task OnDisconnectedAsync(Exception? exception)
    {
        _tracker.RemoveConnection(Context.ConnectionId);
        return base.OnDisconnectedAsync(exception);
    }
}
