using System.Collections.Concurrent;
using Akka.Actor;
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.Communication.Actors;
using ZB.MOM.WW.ScadaBridge.Communication.Grpc;

namespace ZB.MOM.WW.ScadaBridge.Communication;

/// <summary>
/// Manages debug stream sessions by creating DebugStreamBridgeActors that persist
/// as subscribers on the site side. Both the Blazor debug view and the SignalR hub
/// use this service to start/stop streams.
/// <para>
/// <b>Consumer keepalive.</b> This service is the session registry, and holding a session
/// here IS what "a consumer is attached" means: the Blazor debug view and the SignalR hub
/// both release their session (<see cref="StopStream"/>) on dispose/disconnect. A single
/// shared timer therefore Tells <see cref="DebugStreamConsumerAlive"/> to every registered
/// bridge actor, which is the only thing that renews each actor's orphan window. A bridge
/// actor that outlives its registration — the leak the orphan net exists for — stops
/// receiving keepalives and self-terminates. The keepalive is independent of stream traffic
/// by design: neither a chatty nor a silent site can influence the orphan decision.
/// </para>
/// </summary>
public class DebugStreamService : IDisposable
{
    /// <summary>
    /// Cadence of the shared consumer keepalive. Comfortably shorter than
    /// <see cref="DebugStreamBridgeActor.ConsumerIdleTimeout"/> so a few missed ticks (GC
    /// pause, thread-pool starvation) can never orphan a live session. Settable for tests.
    /// </summary>
    internal static TimeSpan KeepaliveInterval { get; set; } = TimeSpan.FromSeconds(30);

    private readonly CommunicationService _communicationService;
    private readonly IServiceProvider _serviceProvider;
    private readonly SiteStreamGrpcClientFactory _grpcClientFactory;
    private readonly ILogger<DebugStreamService> _logger;
    private readonly ConcurrentDictionary<string, IActorRef> _sessions = new();
    private readonly object _keepaliveLock = new();
    private Timer? _keepaliveTimer;
    private bool _disposed;
    private ActorSystem? _actorSystem;

    /// <summary>
    /// Initializes a new instance of the <see cref="DebugStreamService"/> class.
    /// </summary>
    /// <param name="communicationService">The communication service.</param>
    /// <param name="serviceProvider">The service provider for dependency resolution.</param>
    /// <param name="grpcClientFactory">The gRPC client factory for creating site stream clients.</param>
    /// <param name="logger">The logger instance.</param>
    public DebugStreamService(
        CommunicationService communicationService,
        IServiceProvider serviceProvider,
        SiteStreamGrpcClientFactory grpcClientFactory,
        ILogger<DebugStreamService> logger)
    {
        _communicationService = communicationService;
        _serviceProvider = serviceProvider;
        _grpcClientFactory = grpcClientFactory;
        _logger = logger;
    }

    /// <summary>
    /// Sets the ActorSystem reference. Called during actor system startup (from AkkaHostedService).
    /// </summary>
    /// <param name="actorSystem">The actor system to use for creating bridge actors.</param>
    public void SetActorSystem(ActorSystem actorSystem)
    {
        _actorSystem = actorSystem;
    }

    /// <summary>
    /// Starts a debug stream session. Returns the initial snapshot.
    /// Ongoing events are delivered via the onEvent callback.
    /// The onTerminated callback fires if the stream is killed (site disconnect, timeout).
    /// </summary>
    /// <param name="instanceId">The instance ID to stream debug information for.</param>
    /// <param name="onEvent">Callback invoked for each event received from the stream.</param>
    /// <param name="onTerminated">Callback invoked when the stream terminates.</param>
    /// <param name="ct">A cancellation token that can be used to cancel the operation.</param>
    /// <returns>A debug stream session with the initial snapshot.</returns>
    public async Task<DebugStreamSession> StartStreamAsync(
        int instanceId,
        Action<object> onEvent,
        Action onTerminated,
        CancellationToken ct = default)
    {
        var system = _actorSystem
            ?? throw new InvalidOperationException("DebugStreamService not initialized. ActorSystem not set.");

        // Resolve instance → unique name + site
        string instanceUniqueName;
        string siteIdentifier;
        string grpcNodeAAddress;
        string grpcNodeBAddress;

        using (var scope = _serviceProvider.CreateScope())
        {
            var instanceRepo = scope.ServiceProvider.GetRequiredService<ITemplateEngineRepository>();
            var instance = await instanceRepo.GetInstanceByIdAsync(instanceId)
                ?? throw new InvalidOperationException($"Instance {instanceId} not found.");

            var siteRepo = scope.ServiceProvider.GetRequiredService<ISiteRepository>();
            var site = await siteRepo.GetSiteByIdAsync(instance.SiteId)
                ?? throw new InvalidOperationException($"Site {instance.SiteId} not found.");

            instanceUniqueName = instance.UniqueName;
            siteIdentifier = site.SiteIdentifier;
            grpcNodeAAddress = site.GrpcNodeAAddress
                ?? throw new InvalidOperationException($"Site {siteIdentifier} has no GrpcNodeAAddress configured.");
            grpcNodeBAddress = site.GrpcNodeBAddress
                ?? throw new InvalidOperationException($"Site {siteIdentifier} has no GrpcNodeBAddress configured.");
        }

        var sessionId = Guid.NewGuid().ToString("N");

        // Capture the initial snapshot via a TaskCompletionSource
        var snapshotTcs = new TaskCompletionSource<DebugViewSnapshot>(TaskCreationOptions.RunContinuationsAsynchronously);

        Action<object> onEventWrapper = evt =>
        {
            if (evt is DebugViewSnapshot snapshot && !snapshotTcs.Task.IsCompleted)
            {
                snapshotTcs.TrySetResult(snapshot);
            }
            else
            {
                onEvent(evt);
            }
        };

        Action onTerminatedWrapper = () =>
        {
            _sessions.TryRemove(sessionId, out _);
            snapshotTcs.TrySetException(new InvalidOperationException("Debug stream terminated before snapshot received."));
            onTerminated();
        };

        // Create the bridge actor — use type-based Props to avoid expression tree limitations with closures
        var commActor = _communicationService.GetCommunicationActor();

        var props = Props.Create(typeof(DebugStreamBridgeActor),
            siteIdentifier,
            instanceUniqueName,
            sessionId,
            commActor,
            onEventWrapper,
            onTerminatedWrapper,
            _grpcClientFactory,
            grpcNodeAAddress,
            grpcNodeBAddress);

        var bridgeActor = system.ActorOf(props, $"debug-stream-{sessionId}");

        _sessions[sessionId] = bridgeActor;
        EnsureKeepaliveTimer();

        // Wait for the initial snapshot (with timeout)
        using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
        timeoutCts.CancelAfter(TimeSpan.FromSeconds(30));

        DebugViewSnapshot snapshot;
        try
        {
            snapshot = await snapshotTcs.Task.WaitAsync(timeoutCts.Token);
        }
        catch (Exception ex)
        {
            // Any failure before the snapshot arrives — the 30s timeout, or the stream
            // terminating early (site disconnect / gRPC failure, surfaced by
            // onTerminatedWrapper as an InvalidOperationException) — must deterministically
            // tear down the bridge actor and its site-side subscription. Use the local
            // actor reference: a racing onTerminatedWrapper may already have removed the
            // session, which would make StopStream a no-op. StopDebugStream is idempotent
            // (the actor may already be stopping itself).
            _sessions.TryRemove(sessionId, out _);
            bridgeActor.Tell(new StopDebugStream());

            if (ex is OperationCanceledException)
                throw new TimeoutException(
                    $"Timed out waiting for debug snapshot from {instanceUniqueName} on site {siteIdentifier}.");

            throw new InvalidOperationException(
                $"Debug stream for {instanceUniqueName} on site {siteIdentifier} terminated before a snapshot was received.",
                ex);
        }

        _logger.LogInformation("Debug stream {SessionId} started for {Instance} on site {Site}",
            sessionId, instanceUniqueName, siteIdentifier);

        return new DebugStreamSession(sessionId, snapshot);
    }

    /// <summary>
    /// Stops an active debug stream session.
    /// </summary>
    /// <param name="sessionId">The session ID of the debug stream to stop.</param>
    public void StopStream(string sessionId)
    {
        if (_sessions.TryRemove(sessionId, out var bridgeActor))
        {
            bridgeActor.Tell(new StopDebugStream());
            _logger.LogInformation("Debug stream {SessionId} stopped", sessionId);
        }
    }

    /// <summary>
    /// Sends one keepalive round to every attached session. Exposed (internal) so tests can
    /// drive the keepalive deterministically instead of waiting on the timer.
    /// </summary>
    internal void SendConsumerKeepalives()
    {
        foreach (var session in _sessions)
        {
            session.Value.Tell(new DebugStreamConsumerAlive());
        }
    }

    /// <summary>
    /// Starts the shared keepalive timer on first use. One timer for the whole service (not
    /// one per session): the payload is a Tell per attached session, so a single periodic
    /// callback is the cheapest shape that covers every consumer surface.
    /// </summary>
    private void EnsureKeepaliveTimer()
    {
        if (_keepaliveTimer is not null) return;
        lock (_keepaliveLock)
        {
            if (_keepaliveTimer is not null || _disposed) return;
            _keepaliveTimer = new Timer(
                _ =>
                {
                    try
                    {
                        SendConsumerKeepalives();
                    }
                    catch (Exception ex)
                    {
                        // A keepalive round must never take the timer down — a thrown Tell
                        // would silently stop renewing EVERY session's orphan window.
                        _logger.LogWarning(ex, "Debug stream consumer keepalive round failed");
                    }
                },
                null,
                KeepaliveInterval,
                KeepaliveInterval);
        }
    }

    /// <summary>Stops the keepalive timer.</summary>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    /// <summary>Stops the keepalive timer.</summary>
    /// <param name="disposing">True when called from <see cref="Dispose()"/>.</param>
    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;
        lock (_keepaliveLock)
        {
            if (_disposed) return;
            _disposed = true;
            if (disposing)
            {
                _keepaliveTimer?.Dispose();
                _keepaliveTimer = null;
            }
        }
    }
}

public record DebugStreamSession(string SessionId, DebugViewSnapshot InitialSnapshot);
