using Akka.Actor;
using Akka.Hosting;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.OtOpcUa.AdminUI.Telemetry;
using ZB.MOM.WW.OtOpcUa.Cluster;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.ControlPlane.Telemetry;

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;

public static class HubServiceCollectionExtensions
{
    public const string FleetStatusSignalRBridgeName = "fleet-status-signalr-bridge";
    public const string AlertSignalRBridgeName = "alert-signalr-bridge";
    public const string ScriptLogSignalRBridgeName = "script-log-signalr-bridge";
    public const string DriverStatusSignalRBridgeName = "driver-status-signalr-bridge";
    public const string DriverResilienceStatusBridgeName = "driver-resilience-status-bridge";
    public const string TelemetryDialSupervisorName = "telemetry-dial-supervisor";

    /// <summary>
    ///     Registers the in-process live-push services the AdminUI's Blazor Server panels read
    ///     directly (instead of self-connecting a SignalR <c>HubConnection</c>, which fails behind a
    ///     reverse proxy — see <see cref="IInProcessBroadcaster{T}"/>):
    ///     <list type="bullet">
    ///       <item><see cref="IDriverStatusSnapshotStore"/> — last-value snapshot per driver.</item>
    ///       <item><see cref="IInProcessBroadcaster{T}"/> — append-stream fan-out (alarm
    ///       transitions, script-log lines). Registered as an open generic so each closed type
    ///       resolves to its own singleton shared by the bridge actor and the consuming component.</item>
    ///     </list>
    /// </summary>
    /// <param name="services">The service collection.</param>
    /// <returns>The same service collection, for chaining.</returns>
    public static IServiceCollection AddOtOpcUaDriverStatusServices(this IServiceCollection services)
    {
        services.AddSingleton<IDriverStatusSnapshotStore, InMemoryDriverStatusSnapshotStore>();
        services.AddSingleton<IDriverResilienceStatusStore, InMemoryDriverResilienceStatusStore>();
        services.AddSingleton(typeof(IInProcessBroadcaster<>), typeof(InProcessBroadcaster<>));
        return services;
    }

    /// <summary>
    /// Spawns the SignalR bridge actors that forward DPS messages to browser-facing SignalR
    /// hubs: <c>fleet-status</c> → <see cref="FleetStatusHub"/>, <c>alerts</c> →
    /// <see cref="AlertHub"/>, <c>script-logs</c> → <see cref="ScriptLogHub"/>.
    ///
    /// Call inside the admin-role configurator on the shared <see cref="AkkaConfigurationBuilder"/>:
    /// <code>
    /// if (hasAdmin)
    /// {
    ///     ab.WithOtOpcUaControlPlaneSingletons();
    ///     ab.WithOtOpcUaSignalRBridges();
    /// }
    /// </code>
    /// </summary>
    /// <param name="builder">The Akka configuration builder.</param>
    /// <returns>The same builder, for chaining.</returns>
    public static AkkaConfigurationBuilder WithOtOpcUaSignalRBridges(this AkkaConfigurationBuilder builder)
    {
        builder.WithActors((system, registry, resolver) =>
        {
            // Fleet-status always stays on DPS in both modes (deferred / out of Phase 5 scope) — it is
            // never gated by the telemetry dark switch.
            var fleetHub = resolver.GetService<IHubContext<FleetStatusHub>>();
            var fleetBridge = system.ActorOf(FleetStatusSignalRBridge.Props(fleetHub), FleetStatusSignalRBridgeName);
            registry.Register<FleetStatusSignalRBridgeKey>(fleetBridge);

            // The four telemetry sinks are registered identically in both modes; only the upstream that
            // feeds them swaps (DPS bridges vs. the gRPC dial supervisor). Resolve them once, above the
            // branch, so both paths feed the SAME singletons the Blazor panels read.
            var alertBroadcaster = resolver.GetService<IInProcessBroadcaster<Commons.Messages.Alerts.AlarmTransitionEvent>>();
            var scriptLogBroadcaster = resolver.GetService<IInProcessBroadcaster<Commons.Messages.Logging.ScriptLogEntry>>();
            var driverStatusStore = resolver.GetService<IDriverStatusSnapshotStore>();
            var resilienceStore = resolver.GetService<IDriverResilienceStatusStore>();

            // Phase 5 dark switch. Absent options ⇒ Dps (today's behaviour); case-insensitive.
            var telemetryMode = resolver.GetService<IOptions<TelemetryDialOptions>>()?.Value.Mode
                ?? TelemetryDialOptions.ModeDps;

            if (string.Equals(telemetryMode, TelemetryDialOptions.ModeGrpc, StringComparison.OrdinalIgnoreCase))
            {
                // Grpc: central dials each enabled node's dedicated telemetry stream, feeding the SAME
                // four sinks the DPS bridges feed. No DPS telemetry bridges are spawned.
                var options = resolver.GetService<IOptions<TelemetryDialOptions>>()!.Value;
                var dbFactory = resolver.GetService<IDbContextFactory<OtOpcUaConfigDbContext>>();
                var loggerFactory = resolver.GetService<ILoggerFactory>();

                var nodeSource = TelemetryNodeSource.Create(
                    dbFactory!, loggerFactory!.CreateLogger(typeof(TelemetryNodeSource).FullName!));
                var dialLoop = TelemetryNodeSource.CreateDialLoop(
                    options.ApiKey, loggerFactory.CreateLogger<TelemetryStreamClient>());

                var supervisor = system.ActorOf(
                    TelemetryDialSupervisor.Props(
                        nodeSource,
                        dialLoop,
                        alertBroadcaster,
                        scriptLogBroadcaster,
                        driverStatusStore,
                        resilienceStore,
                        options),
                    TelemetryDialSupervisorName);
                registry.Register<TelemetryDialSupervisorKey>(supervisor);
            }
            else
            {
                // Dps (default): the four DPS bridges subscribe their mesh-wide topics and feed the sinks.
                var alertHub = resolver.GetService<IHubContext<AlertHub>>();
                var alertBridge = system.ActorOf(AlertSignalRBridge.Props(alertHub, alertBroadcaster), AlertSignalRBridgeName);
                registry.Register<AlertSignalRBridgeKey>(alertBridge);

                var scriptLogHub = resolver.GetService<IHubContext<ScriptLogHub>>();
                var scriptLogBridge = system.ActorOf(ScriptLogSignalRBridge.Props(scriptLogHub, scriptLogBroadcaster), ScriptLogSignalRBridgeName);
                registry.Register<ScriptLogSignalRBridgeKey>(scriptLogBridge);

                var driverStatusHub = resolver.GetService<IHubContext<DriverStatusHub>>();
                var driverStatusBridge = system.ActorOf(DriverStatusSignalRBridge.Props(driverStatusHub, driverStatusStore), DriverStatusSignalRBridgeName);
                registry.Register<DriverStatusSignalRBridgeKey>(driverStatusBridge);

                // Resilience-status bridge: DPS topic -> in-process store (no SignalR hub — the panel reads
                // the store directly, and resilience has no browser-JS consumer).
                var resilienceBridge = system.ActorOf(DriverResilienceStatusBridge.Props(resilienceStore), DriverResilienceStatusBridgeName);
                registry.Register<DriverResilienceStatusBridgeKey>(resilienceBridge);
            }
        });
        return builder;
    }
}

/// <summary>Marker keys for <see cref="ActorRegistry"/> lookup of the SignalR bridge actors.</summary>
public sealed class FleetStatusSignalRBridgeKey { }
public sealed class AlertSignalRBridgeKey { }
public sealed class ScriptLogSignalRBridgeKey { }
public sealed class DriverStatusSignalRBridgeKey { }
public sealed class DriverResilienceStatusBridgeKey { }

/// <summary>Marker key for <see cref="ActorRegistry"/> lookup of the Grpc-mode telemetry dial supervisor.</summary>
public sealed class TelemetryDialSupervisorKey { }
