using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
using ZB.MOM.WW.ScadaBridge.Communication;
using ZB.MOM.WW.ScadaBridge.Security;

namespace ZB.MOM.WW.ScadaBridge.ManagementService;

/// <summary>
/// SignalR hub for real-time debug stream subscriptions.
/// External consumers (CLI) connect via WebSocket, authenticate with Basic Auth,
/// and receive streaming attribute value and alarm state changes.
/// </summary>
public class DebugStreamHub : Hub
{
    private const string SessionIdKey = "DebugStreamSessionId";
    private const string RolesKey = "DebugStreamRoles";
    private const string PermittedSiteIdsKey = "DebugStreamPermittedSiteIds";

    /// <summary>
    /// Pure site-scope authorization check for a debug-stream subscription.
    /// Returns true when the caller may subscribe to a debug stream for an instance
    /// belonging to <paramref name="instanceSiteId"/>.
    /// Administrator role, or an empty <paramref name="permittedSiteIds"/> (system-wide
    /// Deployer), grants access to any site; otherwise the instance's site must be
    /// in the permitted set.
    /// </summary>
    /// <param name="roles">Roles held by the connected user.</param>
    /// <param name="permittedSiteIds">Site ids the user is scoped to; empty means system-wide.</param>
    /// <param name="instanceSiteId">Site id of the instance being subscribed to.</param>
    /// <returns><see langword="true"/> if the user may subscribe to the instance; otherwise <see langword="false"/>.</returns>
    public static bool IsInstanceAccessAllowed(
        IReadOnlyCollection<string> roles,
        IReadOnlyCollection<string> permittedSiteIds,
        int instanceSiteId)
    {
        if (roles.Contains(Roles.Administrator, StringComparer.OrdinalIgnoreCase)) return true;
        if (permittedSiteIds.Count == 0) return true; // system-wide deployment
        return permittedSiteIds.Contains(instanceSiteId.ToString());
    }

    private readonly DebugStreamService _debugStreamService;
    private readonly IHubContext<DebugStreamHub> _hubContext;
    private readonly ILogger<DebugStreamHub> _logger;

    /// <summary>
    /// Initializes the hub with its required services.
    /// </summary>
    /// <param name="debugStreamService">Service that manages debug stream subscriptions.</param>
    /// <param name="hubContext">SignalR hub context for pushing events to clients.</param>
    /// <param name="logger">Logger for this hub.</param>
    public DebugStreamHub(
        DebugStreamService debugStreamService,
        IHubContext<DebugStreamHub> hubContext,
        ILogger<DebugStreamHub> logger)
    {
        _debugStreamService = debugStreamService;
        _hubContext = hubContext;
        _logger = logger;
    }

    /// <inheritdoc />
    public override async Task OnConnectedAsync()
    {
        var httpContext = Context.GetHttpContext();
        if (httpContext == null)
        {
            _logger.LogWarning("DebugStreamHub connection rejected: no HTTP context");
            Context.Abort();
            return;
        }

        // Authenticate via the SAME shared Basic→LDAP flow as POST /management and the
        // audit REST (arch-review R2 N1): ManagementAuthenticator applies the DisableLogin
        // bypass, the LoginThrottle lockout check BEFORE the bind, and RecordFailure/
        // RecordSuccess bookkeeping — so hub connection attempts can no longer be used as
        // an unthrottled LDAP-bind oracle, and hub failures feed the shared lockout.
        var outcome = await ManagementAuthenticator.AuthenticateAsync(httpContext);
        if (outcome.User is null)
        {
            _logger.LogWarning("DebugStreamHub connection rejected: authentication failed or throttled");
            Context.Abort();
            return;
        }
        var user = outcome.User;

        // Role check — Deployer role required (unchanged policy).
        if (!user.Roles.Contains(Roles.Deployer))
        {
            _logger.LogWarning("DebugStreamHub connection rejected: {Username} lacks Deployer role", user.Username);
            Context.Abort();
            return;
        }

        // Persist the resolved identity on the connection so per-instance site-scope
        // enforcement can be applied to SubscribeInstance calls. PermittedSiteIds is the
        // authenticator's normalized form (empty == system-wide), same as every other surface.
        Context.Items[RolesKey] = user.Roles;
        Context.Items[PermittedSiteIdsKey] = user.PermittedSiteIds;

        _logger.LogInformation("DebugStreamHub connection established for {Username}", user.Username);
        await base.OnConnectedAsync();
    }

    /// <summary>
    /// Subscribes to a debug stream for the specified instance.
    /// Sends the initial snapshot immediately, then streams incremental changes.
    /// </summary>
    /// <param name="instanceId">Database id of the instance to subscribe to.</param>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public async Task SubscribeInstance(int instanceId)
    {
        // Stop any existing subscription for this connection
        await UnsubscribeInstance();

        var connectionId = Context.ConnectionId;

        // Per-instance site-scope enforcement: a site-scoped Deployment user must not
        // be able to stream an instance belonging to a site outside their scope.
        var httpContext = Context.GetHttpContext();
        if (httpContext == null)
        {
            _logger.LogWarning("DebugStreamHub: {ConnectionId} subscribe rejected — no HTTP context", connectionId);
            await Clients.Caller.SendAsync("OnStreamTerminated", "Authorization context unavailable.");
            return;
        }

        var roles = Context.Items.TryGetValue(RolesKey, out var rolesObj) && rolesObj is string[] r
            ? r : Array.Empty<string>();
        var permittedSiteIds = Context.Items.TryGetValue(PermittedSiteIdsKey, out var sitesObj) && sitesObj is string[] s
            ? s : Array.Empty<string>();

        var instanceRepo = httpContext.RequestServices.GetRequiredService<ITemplateEngineRepository>();
        var instance = await instanceRepo.GetInstanceByIdAsync(instanceId);
        if (instance == null)
        {
            _logger.LogWarning("DebugStreamHub: {ConnectionId} subscribe rejected — instance {InstanceId} not found",
                connectionId, instanceId);
            await Clients.Caller.SendAsync("OnStreamTerminated", $"Instance {instanceId} not found.");
            return;
        }

        if (!IsInstanceAccessAllowed(roles, permittedSiteIds, instance.SiteId))
        {
            _logger.LogWarning(
                "DebugStreamHub: {ConnectionId} subscribe to instance {InstanceId} denied — site {SiteId} outside permitted scope",
                connectionId, instanceId, instance.SiteId);
            await Clients.Caller.SendAsync("OnStreamTerminated",
                $"Access denied: instance {instanceId} belongs to a site outside your Deployment scope.");
            return;
        }

        try
        {
            // Use IHubContext for callbacks — the hub instance is transient (disposed after method returns),
            // but IHubContext is a singleton that remains valid for the lifetime of the connection.
            var hubClients = _hubContext.Clients;
            // Capture the logger into a local: the callbacks outlive this (transient)
            // hub instance, and ILogger is a singleton, so this is safe to hold.
            var logger = _logger;

            var session = await _debugStreamService.StartStreamAsync(
                instanceId,
                onEvent: evt =>
                {
                    var send = evt switch
                    {
                        AttributeValueChanged changed =>
                            hubClients.Client(connectionId).SendAsync("OnAttributeChanged", changed),
                        AlarmStateChanged changed =>
                            hubClients.Client(connectionId).SendAsync("OnAlarmChanged", changed),
                        DebugViewSnapshot snapshot =>
                            hubClients.Client(connectionId).SendAsync("OnSnapshot", snapshot),
                        _ => Task.CompletedTask
                    };
                    // Fire-and-forget (deliberate — a slow/dead client must not block the
                    // stream), but log the push failure rather than swallowing it silently.
                    _ = send.ContinueWith(
                        t => logger.LogWarning(t.Exception?.GetBaseException(),
                            "Debug stream push to connection {ConnectionId} failed", connectionId),
                        CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
                },
                onTerminated: () =>
                {
                    _ = hubClients.Client(connectionId).SendAsync("OnStreamTerminated", "Site disconnected")
                        .ContinueWith(
                            t => logger.LogWarning(t.Exception?.GetBaseException(),
                                "Debug stream termination push to connection {ConnectionId} failed", connectionId),
                            CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
                });

            Context.Items[SessionIdKey] = session.SessionId;

            // Send the initial snapshot
            await Clients.Caller.SendAsync("OnSnapshot", session.InitialSnapshot);

            _logger.LogInformation("DebugStreamHub: {ConnectionId} subscribed to instance {InstanceId}",
                connectionId, instanceId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "DebugStreamHub: Failed to subscribe {ConnectionId} to instance {InstanceId}",
                connectionId, instanceId);
            await Clients.Caller.SendAsync("OnStreamTerminated", ex.Message);
        }
    }

    /// <summary>
    /// Unsubscribes from the current debug stream.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation.</returns>
    public Task UnsubscribeInstance()
    {
        if (Context.Items.TryGetValue(SessionIdKey, out var sessionIdObj) && sessionIdObj is string sessionId)
        {
            _debugStreamService.StopStream(sessionId);
            Context.Items.Remove(SessionIdKey);
            _logger.LogInformation("DebugStreamHub: {ConnectionId} unsubscribed", Context.ConnectionId);
        }
        return Task.CompletedTask;
    }

    /// <inheritdoc />
    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        await UnsubscribeInstance();
        await base.OnDisconnectedAsync(exception);
    }
}
