using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Hosting;
using ZB.MOM.WW.OtOpcUa.Core.Resilience;
using ZB.MOM.WW.Secrets.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Host.Drivers;

// Probe type aliases — keep the using list concise.
using ModbusProbe    = Driver.Modbus.ModbusDriverProbe;
using AbCipProbe     = Driver.AbCip.AbCipDriverProbe;
using AbLegacyProbe  = Driver.AbLegacy.AbLegacyDriverProbe;
using S7Probe        = Driver.S7.S7DriverProbe;
using TwinCATProbe   = Driver.TwinCAT.TwinCATDriverProbe;
using FocasProbe     = Driver.FOCAS.FocasDriverProbe;
using OpcUaProbe     = Driver.OpcUaClient.OpcUaClientDriverProbe;
using GalaxyProbe    = Driver.Galaxy.GalaxyDriverProbe;
using CalculationProbe = Driver.Calculation.CalculationDriverProbe;
using SqlProbe       = Driver.Sql.SqlDriverProbe;
using MqttProbe      = Driver.Mqtt.MqttDriverProbe;
using MTConnectProbe = Driver.MTConnect.MTConnectDriverProbe;

/// <summary>
///     Wires every cross-platform driver assembly's <c>Register(registry, loggerFactory)</c>
///     extension into a single <see cref="DriverFactoryRegistry"/> singleton and binds the
///     v2 <see cref="IDriverFactory"/> abstraction to a <see cref="DriverFactoryRegistryAdapter"/>
///     over it. Replaces the F7 seam's <c>NullDriverFactory</c> default so deploys actually
///     materialise real <see cref="IDriver"/> instances on driver-role nodes.
///
///     The factory registry is skipped on admin-only nodes — they never run drivers, so it doesn't
///     need to exist (Program.cs guards via the <c>hasDriver</c> flag). The driver <em>probe</em>
///     set is the exception: it backs the AdminUI Test Connect button and so must also be wired on
///     admin nodes — see <see cref="AddOtOpcUaDriverProbes"/>.
/// </summary>
public static class DriverFactoryBootstrap
{
    /// <summary>
    ///     Register the cross-platform driver factories + bind <see cref="IDriverFactory"/>.
    ///     Must be called BEFORE <c>services.AddAkka</c> so the runtime extension can resolve
    ///     <see cref="IDriverFactory"/> from DI when spawning <c>DriverHostActor</c>.
    /// </summary>
    /// <param name="services">The service collection to register driver factories with.</param>
    /// <returns>The same service collection, for chaining.</returns>
    public static IServiceCollection AddOtOpcUaDriverFactories(this IServiceCollection services)
    {
        services.AddSingleton<DriverFactoryRegistry>(sp =>
        {
            var registry = new DriverFactoryRegistry();
            var loggerFactory = sp.GetService<ILoggerFactory>();
            // The calc driver needs the root script logger so a script failure fans out onto the
            // script-logs topic; resolve it here (null on nodes without the script pipeline wired).
            var scriptRoot = sp.GetService<ZB.MOM.WW.OtOpcUa.Core.Scripting.ScriptRootLogger>();
            // The Galaxy driver resolves its Gateway.ApiKeySecretRef secret: arm through the
            // shared ISecretResolver (registered unconditionally by AddZbSecrets on every node).
            var secretResolver = sp.GetRequiredService<ISecretResolver>();
            Register(registry, loggerFactory, secretResolver, scriptRoot);
            return registry;
        });
        services.AddSingleton<IDriverFactory>(sp =>
            new DriverFactoryRegistryAdapter(sp.GetRequiredService<DriverFactoryRegistry>()));

        // Phase 6.1 resilience infrastructure — the process-singleton tracker + Polly pipeline builder,
        // and the per-driver-instance invoker factory the DriverHostActor uses to wrap every capability
        // call. Registered here (driver nodes) because the factory needs DriverFactoryRegistry.GetTier;
        // admin-only nodes skip this and the runtime falls back to the pass-through factory.
        services.AddSingleton<DriverResilienceStatusTracker>();
        services.AddSingleton<DriverResiliencePipelineBuilder>(sp =>
            new DriverResiliencePipelineBuilder(
                timeProvider: null, // TimeProvider.System
                statusTracker: sp.GetRequiredService<DriverResilienceStatusTracker>(),
                logger: sp.GetService<ILoggerFactory>()?.CreateLogger("ZB.MOM.WW.OtOpcUa.Core.Resilience.DriverResiliencePipelineBuilder")));
        services.AddSingleton<IDriverCapabilityInvokerFactory>(sp =>
        {
            var registry = sp.GetRequiredService<DriverFactoryRegistry>();
            return new DriverCapabilityInvokerFactory(
                sp.GetRequiredService<DriverResiliencePipelineBuilder>(),
                sp.GetRequiredService<DriverResilienceStatusTracker>(),
                registry.GetTier,
                logger: sp.GetService<ILoggerFactory>()?.CreateLogger("ZB.MOM.WW.OtOpcUa.Core.Resilience.DriverCapabilityInvokerFactory"));
        });

        // The tracker's operator-facing reader: a periodic DPS publisher that mirrors each snapshot to the
        // AdminUI resilience panel (R2-10 — closes the "tracker fed by everything, read by nothing" gap).
        // Lives on driver nodes exactly where the tracker exists. The lazy Func<ActorSystem> is idempotent
        // with the one Program.cs registers for the alarm-command router.
        services.TryAddSingleton<Func<Akka.Actor.ActorSystem>>(sp => () => sp.GetRequiredService<Akka.Actor.ActorSystem>());
        services.AddHostedService<DriverResilienceStatusPublisherService>();

        // Driver nodes also carry the probe set so a fused admin,driver node has it; the admin-only
        // case is covered by Program.cs calling AddOtOpcUaDriverProbes() in the hasAdmin block.
        services.AddOtOpcUaDriverProbes();

        return services;
    }

    /// <summary>
    ///     Register one <see cref="IDriverProbe"/> per driver type. These back the AdminUI's
    ///     "Test Connect" button: the <c>admin-operations</c> cluster singleton resolves
    ///     <see cref="IEnumerable{T}"/> of <see cref="IDriverProbe"/> and dispatches by DriverType.
    ///     <para>
    ///     That singleton is role-pinned to <c>admin</c>, so this MUST be wired on admin nodes —
    ///     including admin-only nodes that lack the <c>driver</c> role (e.g. the MAIN cluster's
    ///     admin-a/admin-b). Probes are lightweight (cheap connect, no persistent state) and don't
    ///     need the driver-factory registry, so they register independently of
    ///     <see cref="AddOtOpcUaDriverFactories"/>.
    ///     </para>
    ///     <para>
    ///     Uses <c>TryAddEnumerable</c> so a fused admin,driver node — which reaches this from both
    ///     the driver-factory path and the admin path — registers each probe exactly once. A
    ///     duplicate would make the singleton's <c>ToDictionary(p =&gt; p.DriverType)</c> throw.
    ///     </para>
    /// </summary>
    /// <param name="services">The service collection to register driver probes with.</param>
    /// <returns>The same service collection, for chaining.</returns>
    public static IServiceCollection AddOtOpcUaDriverProbes(this IServiceCollection services)
    {
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, ModbusProbe>());
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, AbCipProbe>());
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, AbLegacyProbe>());
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, S7Probe>());
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, TwinCATProbe>());
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, FocasProbe>());
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, OpcUaProbe>());
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, GalaxyProbe>());
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, CalculationProbe>());
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, SqlProbe>());
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, MqttProbe>());
        services.TryAddEnumerable(ServiceDescriptor.Singleton<IDriverProbe, MTConnectProbe>());

        return services;
    }

    /// <summary>
    ///     Invoke every cross-platform driver's <c>Register</c> extension. New driver assemblies
    ///     get added here — one line per type. ShouldStub() in <c>DriverInstanceActor</c> still
    ///     handles platform/role-dependent stubbing (e.g. Galaxy on macOS), so registering a
    ///     factory here doesn't mean it always runs in production.
    /// </summary>
    private static void Register(
        DriverFactoryRegistry registry,
        ILoggerFactory? loggerFactory,
        ISecretResolver secretResolver,
        ZB.MOM.WW.OtOpcUa.Core.Scripting.ScriptRootLogger? scriptRoot = null)
    {
        Driver.AbCip.AbCipDriverFactoryExtensions.Register(registry);
        Driver.AbLegacy.AbLegacyDriverFactoryExtensions.Register(registry, loggerFactory);
        Driver.Calculation.CalculationDriverFactoryExtensions.Register(registry, loggerFactory, scriptRoot);
        Driver.FOCAS.FocasDriverFactoryExtensions.Register(registry);
        Driver.Galaxy.GalaxyDriverFactoryExtensions.Register(registry, secretResolver, loggerFactory);
        Driver.Modbus.ModbusDriverFactoryExtensions.Register(registry, loggerFactory);
        Driver.Mqtt.MqttDriverFactoryExtensions.Register(registry, loggerFactory);
        // Tier A (the Register default): fully managed — HttpClient + System.Xml.Linq, no native SDK,
        // no COM. Tier C is the only tier that arms process-level recycle (MemoryRecycle hard-breach /
        // ScheduledRecycleScheduler), which would be wrong here: the driver is in-process and killing
        // it kills every OPC UA session. The long-lived /sample stream does not argue for a slower
        // tier either — SubscribeAsync returns synchronously after starting the pump on a background
        // task, so the Tier A 5s Subscribe budget never covers the stream's lifetime.
        Driver.MTConnect.MTConnectDriverFactoryExtensions.Register(registry, loggerFactory);
        Driver.OpcUaClient.OpcUaClientDriverFactoryExtensions.Register(registry, loggerFactory, secretResolver);
        Driver.S7.S7DriverFactoryExtensions.Register(registry);
        Driver.Sql.SqlDriverFactoryExtensions.Register(registry, loggerFactory);
        Driver.TwinCAT.TwinCATDriverFactoryExtensions.Register(registry);
    }
}
