using Akka.Actor;
using Akka.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
using ZB.MOM.WW.OtOpcUa.Core.VirtualTags;
using ZB.MOM.WW.OtOpcUa.OpcUaServer;
using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
using ZB.MOM.WW.OtOpcUa.Runtime.Health;
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
using ZB.MOM.WW.OtOpcUa.Runtime.OpcUa;
using ZB.MOM.WW.OtOpcUa.Runtime.VirtualTags;

namespace ZB.MOM.WW.OtOpcUa.Runtime;

public static class ServiceCollectionExtensions
{
    public const string DriverRole = "driver";

    public const string DriverHostActorName = "driver-host";
    public const string DbHealthProbeActorName = "db-health";
    public const string HistorianAdapterActorName = "historian-adapter";
    public const string DependencyMuxActorName = "dependency-mux";
    public const string OpcUaPublishActorName = "opcua-publish";
    public const string PeerProbeSupervisorName = "peer-probe-supervisor";
    public const string ContinuousHistorizationRecorderActorName = "continuous-historization-recorder";

    /// <summary>
    /// Registers shared runtime services. Currently binds <see cref="IAlarmHistorianSink"/>
    /// to <see cref="NullAlarmHistorianSink"/> as the default; production deployments
    /// override this with <c>SqliteStoreAndForwardSink</c> wrapping the HistorianGateway alarm writer.
    /// Call this BEFORE <c>AddAkka</c>.
    /// </summary>
    /// <param name="services">The service collection to register with.</param>
    /// <returns>The same <paramref name="services"/> instance for chaining.</returns>
    public static IServiceCollection AddOtOpcUaRuntime(this IServiceCollection services)
    {
        services.TryAddSingleton<IAlarmHistorianSink>(NullAlarmHistorianSink.Instance);
        services.TryAddSingleton<IHistorianDataSource>(NullHistorianDataSource.Instance);
        // Historian tag provisioning. Null default (no-op) so the AddressSpaceApplier resolves a real
        // IHistorianProvisioning only when AddHistorianProvisioning registers the gateway-backed one
        // (ServerHistorian:Enabled). TryAddSingleton so the gateway registration wins last.
        services.TryAddSingleton<IHistorianProvisioning>(NullHistorianProvisioning.Instance);
        // VirtualTag historization sink. Null default — the durable AVEVA sink is infra-gated (there is
        // no live-data historian write RPC). TryAddSingleton so a deployment that bound a real
        // IHistoryWriter earlier wins.
        services.TryAddSingleton<IHistoryWriter>(NullHistoryWriter.Instance);
        services.TryAddSingleton<IDriverFactory>(NullDriverFactory.Instance);
        services.TryAddSingleton<IOpcUaAddressSpaceSink>(NullOpcUaAddressSpaceSink.Instance);
        services.TryAddSingleton<IServiceLevelPublisher>(NullServiceLevelPublisher.Instance);
        services.TryAddSingleton<IDriverHealthPublisher, AkkaDriverHealthPublisher>();
        return services;
    }

    /// <summary>
    ///     Config-gated durable alarm-historian sink. When the <c>AlarmHistorian</c> section has
    ///     <c>Enabled=true</c>, registers a <see cref="SqliteStoreAndForwardSink"/> (draining via the
    ///     <paramref name="writerFactory"/>-supplied writer) as the <see cref="IAlarmHistorianSink"/>,
    ///     overriding the <see cref="NullAlarmHistorianSink"/> default. Otherwise a no-op (Null stays).
    ///     The writer is injected so the durable downstream (the HistorianGateway alarm writer) can be
    ///     supplied by the Host, which is the only project that references it.
    /// </summary>
    /// <param name="services">The service collection to register with.</param>
    /// <param name="configuration">The configuration carrying the <c>AlarmHistorian</c> section.</param>
    /// <param name="writerFactory">
    ///     Factory the Host supplies to build the concrete <see cref="IAlarmHistorianWriter"/>
    ///     (the HistorianGateway alarm writer) from the bound options + the resolving provider.
    /// </param>
    /// <returns>The same <paramref name="services"/> instance for chaining.</returns>
    public static IServiceCollection AddAlarmHistorian(
        this IServiceCollection services,
        IConfiguration configuration,
        Func<AlarmHistorianOptions, IServiceProvider, IAlarmHistorianWriter> writerFactory)
    {
        var opts = configuration.GetSection(AlarmHistorianOptions.SectionName).Get<AlarmHistorianOptions>();
        if (opts is not { Enabled: true }) return services; // leave the Null default from AddOtOpcUaRuntime

        foreach (var warning in opts.Validate())
            Serilog.Log.Logger.ForContext<SqliteStoreAndForwardSink>().Warning("Historian config: {HistorianConfigWarning}", warning);

        services.AddSingleton<IAlarmHistorianSink>(sp =>
        {
            // SqliteStoreAndForwardSink takes a Serilog ILogger (not Microsoft.Extensions.Logging).
            // Resolve it off the host's configured static logger so the drain worker's WARN/INFO
            // lines land in the same sinks as the rest of the process.
            var sink = new SqliteStoreAndForwardSink(
                opts.DatabasePath,
                writerFactory(opts, sp),
                Serilog.Log.Logger.ForContext<SqliteStoreAndForwardSink>(),
                batchSize: opts.BatchSize,
                capacity: opts.Capacity,
                deadLetterRetention: TimeSpan.FromDays(opts.DeadLetterRetentionDays),
                maxAttempts: opts.MaxAttempts);
            sink.StartDrainLoop(TimeSpan.FromSeconds(opts.DrainIntervalSeconds));
            return sink;
        });
        return services;
    }

    /// <summary>
    ///     Config-gated server-side HistoryRead backend. When the <c>ServerHistorian</c> section has
    ///     <c>Enabled=true</c>, registers the <paramref name="dataSourceFactory"/>-supplied
    ///     <see cref="IHistorianDataSource"/> (the read-only HistorianGateway-backed data source) overriding
    ///     the <see cref="NullHistorianDataSource"/> default from <see cref="AddOtOpcUaRuntime"/>. Otherwise
    ///     a no-op (the Null default stays and the node manager's HistoryRead returns
    ///     <c>GoodNoData</c>-empty). The data source is injected so the gateway-backed client can be supplied
    ///     by the Host, which is the only project that references the driver.
    /// </summary>
    /// <param name="services">The service collection to register with.</param>
    /// <param name="configuration">The configuration carrying the <c>ServerHistorian</c> section.</param>
    /// <param name="dataSourceFactory">
    ///     Factory the Host supplies to build the concrete read <see cref="IHistorianDataSource"/>
    ///     (the gateway-backed data source) from the bound options + the resolving provider.
    /// </param>
    /// <returns>The same <paramref name="services"/> instance for chaining.</returns>
    public static IServiceCollection AddServerHistorian(
        this IServiceCollection services,
        IConfiguration configuration,
        Func<ServerHistorianOptions, IServiceProvider, IHistorianDataSource> dataSourceFactory)
    {
        var opts = configuration.GetSection(ServerHistorianOptions.SectionName).Get<ServerHistorianOptions>();
        if (opts is not { Enabled: true }) return services; // leave the Null default from AddOtOpcUaRuntime

        foreach (var warning in opts.Validate())
            Serilog.Log.Logger.ForContext<IHistorianDataSource>().Warning("ServerHistorian config: {ServerHistorianConfigWarning}", warning);

        // Last-registration-wins over the TryAddSingleton Null default seeded by AddOtOpcUaRuntime.
        services.AddSingleton<IHistorianDataSource>(sp => dataSourceFactory(opts, sp));
        return services;
    }

    /// <summary>
    ///     Config-gated historian tag provisioning. When the <c>ServerHistorian</c> section has
    ///     <c>Enabled=true</c>, registers the <paramref name="provisioningFactory"/>-supplied
    ///     <see cref="IHistorianProvisioning"/> (the gateway-backed <c>GatewayTagProvisioner</c> that calls
    ///     the gateway's <c>EnsureTags</c>) overriding the <see cref="NullHistorianProvisioning"/> default from
    ///     <see cref="AddOtOpcUaRuntime"/>. Otherwise a no-op (the Null default stays and deploying historized
    ///     tags provisions nothing). The provisioner is consumed by the <c>AddressSpaceApplier</c>, which fires
    ///     a non-blocking <see cref="IHistorianProvisioning.EnsureTagsAsync"/> for added historized tags on
    ///     every deploy. Gated on the <b>same</b> <c>ServerHistorian:Enabled</c> flag as the read path
    ///     (<see cref="AddServerHistorian"/>) — provisioning targets the same single gateway. The provisioner
    ///     is injected so the gateway-backed client can be supplied by the Host, which is the only project that
    ///     references the driver.
    /// </summary>
    /// <param name="services">The service collection to register with.</param>
    /// <param name="configuration">The configuration carrying the <c>ServerHistorian</c> section.</param>
    /// <param name="provisioningFactory">
    ///     Factory the Host supplies to build the concrete <see cref="IHistorianProvisioning"/>
    ///     (the gateway-backed provisioner) from the bound options + the resolving provider.
    /// </param>
    /// <returns>The same <paramref name="services"/> instance for chaining.</returns>
    public static IServiceCollection AddHistorianProvisioning(
        this IServiceCollection services,
        IConfiguration configuration,
        Func<ServerHistorianOptions, IServiceProvider, IHistorianProvisioning> provisioningFactory)
    {
        var opts = configuration.GetSection(ServerHistorianOptions.SectionName).Get<ServerHistorianOptions>();
        if (opts is not { Enabled: true }) return services; // leave the Null default from AddOtOpcUaRuntime

        // Last-registration-wins over the TryAddSingleton Null default seeded by AddOtOpcUaRuntime.
        services.AddSingleton<IHistorianProvisioning>(sp => provisioningFactory(opts, sp));
        return services;
    }

    /// <summary>
    /// Spawns the per-node driver-role actors on the host's <see cref="ActorSystem"/>:
    /// <see cref="DriverHostActor"/> (one per node), <see cref="DbHealthProbeActor"/>
    /// (consumed by the health endpoint + redundancy calc), and
    /// <see cref="HistorianAdapterActor"/> wrapping the registered <see cref="IAlarmHistorianSink"/>.
    ///
    /// Mirror of <c>WithOtOpcUaControlPlaneSingletons</c> for the driver role. Both must
    /// be registered on the same <see cref="AkkaConfigurationBuilder"/> as the cluster
    /// bootstrap so the actors share the host's ActorSystem.
    ///
    /// Wire from the fused Host's Program.cs when the node carries the <c>driver</c> role:
    /// <code>
    /// services.AddOtOpcUaRuntime();
    /// services.AddAkka("otopcua", (ab, sp) => { ab.WithOtOpcUaClusterBootstrap(sp); if (hasDriver) ab.WithOtOpcUaRuntimeActors(); });
    /// </code>
    /// </summary>
    /// <param name="builder">The Akka configuration builder.</param>
    /// <returns>The same <paramref name="builder"/> instance for chaining.</returns>
    public static AkkaConfigurationBuilder WithOtOpcUaRuntimeActors(this AkkaConfigurationBuilder builder)
    {
        // Production cluster HOCON (akka.conf) carries this dispatcher block, but consumers that
        // bootstrap their own HOCON (e.g. ServiceCollectionExtensionsTests) wouldn't pick it up
        // — OpcUaPublishActor.Props pins itself to opcua-synchronized-dispatcher and Akka throws
        // ConfigurationException if it doesn't exist. Prepend a fallback so the runtime extension
        // is self-contained.
        builder.AddHocon(@"
            opcua-synchronized-dispatcher {
                type = ""PinnedDispatcher""
                executor = ""thread-pool-executor""
                throughput = 1
            }
        ", HoconAddMode.Prepend);

        builder.WithActors((system, registry, resolver) =>
        {
            var dbFactory = resolver.GetService<IDbContextFactory<OtOpcUaConfigDbContext>>();
            var roleInfo = resolver.GetService<IClusterRoleInfo>();
            // Fallback to Null* if AddOtOpcUaRuntime wasn't called (e.g., test harnesses).
            var historianSink = resolver.GetService<IAlarmHistorianSink>() ?? NullAlarmHistorianSink.Instance;
            var driverFactory = resolver.GetService<IDriverFactory>() ?? NullDriverFactory.Instance;
            // Phase 6.1 resilience-invoker factory — bound by the fused Host (which references Core +
            // Polly); pass-through on nodes/harnesses that didn't register it. Resolved through the
            // Core.Abstractions seam so this Polly-free assembly stays that way.
            var invokerFactory = resolver.GetService<IDriverCapabilityInvokerFactory>()
                ?? NullDriverCapabilityInvokerFactory.Instance;
            var addressSpaceSink = resolver.GetService<IOpcUaAddressSpaceSink>() ?? NullOpcUaAddressSpaceSink.Instance;
            var serviceLevel = resolver.GetService<IServiceLevelPublisher>() ?? NullServiceLevelPublisher.Instance;
            var loggerFactory = resolver.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
            var healthPublisher = resolver.GetService<IDriverHealthPublisher>() ?? NullDriverHealthPublisher.Instance;
            // Root script logger backs the ScriptedAlarm host's engine + script logging. Registered in
            // Host DI inside the hasDriver block; may be absent in some role configs / test harnesses,
            // in which case the DriverHostActor gracefully skips spawning the ScriptedAlarm host.
            var scriptRootLogger = resolver.GetService<ScriptRootLogger>();
            // Production evaluator is the Host's RoslynVirtualTagEvaluator (registered as
            // IVirtualTagEvaluator); fall back to the null evaluator for test harnesses that don't
            // register one (VirtualTagActor children then evaluate to nothing).
            var virtualTagEvaluator = resolver.GetService<IVirtualTagEvaluator>();
            if (virtualTagEvaluator is null)
            {
                loggerFactory.CreateLogger("ZB.MOM.WW.OtOpcUa.Runtime.ServiceCollectionExtensions")
                    .LogWarning("IVirtualTagEvaluator not registered; Equipment VirtualTags will evaluate to NoChange (no live values). Expected only in test harnesses — driver-role nodes should register RoslynVirtualTagEvaluator.");
                virtualTagEvaluator = NullVirtualTagEvaluator.Instance;
            }

            // VirtualTag historization sink threaded to the spawned VirtualTagHostActor. Null default
            // (durable AVEVA sink is infra-gated); a deployment binding a real IHistoryWriter overrides.
            var historyWriter = resolver.GetService<IHistoryWriter>() ?? NullHistoryWriter.Instance;

            var dbHealth = system.ActorOf(
                DbHealthProbeActor.Props(dbFactory),
                DbHealthProbeActorName);
            registry.Register<DbHealthProbeActorKey>(dbHealth);

            // Dependency mux must be spawned before DriverHostActor so the host can forward
            // AttributeValuePublished into it from the very first driver spawn.
            var mux = system.ActorOf(DependencyMuxActor.Props(), DependencyMuxActorName);
            registry.Register<DependencyMuxActorKey>(mux);

            // Continuous-historization recorder — gated on ContinuousHistorization:Enabled AND the
            // gateway-backed IHistorianValueWriter + the durable IHistorizationOutbox being registered
            // (the Host registers both ONLY when historization is enabled and the ServerHistorian gateway
            // is configured). The recorder taps the dependency mux's value fan-out, so it is spawned after
            // (and fed) the same `mux` ref the DriverHostActor uses. It is spawned BEFORE the applier so
            // the applier's historized-ref subscription sink can wrap this recorder's IActorRef and feed it
            // the add/remove delta of historized refs on every deploy (closing the T18 ref-feed gap).
            IActorRef? continuousRecorder = null;
            var continuousOptions = resolver.GetService<ContinuousHistorizationOptions>();
            if (continuousOptions is { Enabled: true })
            {
                var valueWriter = resolver.GetService<IHistorianValueWriter>();
                var outbox = resolver.GetService<IHistorizationOutbox>();
                if (valueWriter is not null && outbox is not null)
                {
                    // Initial ref set is EMPTY: the deployed address space (and thus the historized-ref
                    // set) is built later at deploy time, not here. The applier's per-deploy add/remove
                    // feed populates the recorder's interest from that point on.
                    continuousRecorder = system.ActorOf(
                        ContinuousHistorizationRecorder.Props(
                            dependencyMux: mux,
                            writer: valueWriter,
                            outbox: outbox,
                            historizedRefs: Array.Empty<string>(),
                            drainBatchSize: continuousOptions.DrainBatchSize,
                            drainInterval: TimeSpan.FromSeconds(continuousOptions.DrainIntervalSeconds),
                            minBackoff: TimeSpan.FromSeconds(continuousOptions.MinBackoffSeconds),
                            maxBackoff: TimeSpan.FromSeconds(continuousOptions.MaxBackoffSeconds)),
                        ContinuousHistorizationRecorderActorName);
                    registry.Register<ContinuousHistorizationRecorderKey>(continuousRecorder);
                }
                else
                {
                    loggerFactory.CreateLogger("ZB.MOM.WW.OtOpcUa.Runtime.ServiceCollectionExtensions")
                        .LogWarning("ContinuousHistorization is enabled but IHistorianValueWriter and/or IHistorizationOutbox are not registered; the recorder will not be spawned. Expected only in misconfigured deployments or test harnesses.");
                }
            }

            // Historized-ref subscription sink fed by the applier on every deploy. When the recorder was
            // spawned, an adapter wraps its IActorRef (a non-blocking Tell of the add/remove delta);
            // otherwise the Null no-op sink, so the applier behaves identically when historization is off.
            IHistorizedTagSubscriptionSink historizedSubscriptions = continuousRecorder is not null
                ? new ActorHistorizedTagSubscriptionSink(continuousRecorder)
                : NullHistorizedTagSubscriptionSink.Instance;

            // Historian tag provisioner fed to the applier so deploying historized tags auto-ensures
            // them in the historian (EnsureTags). The Host registers the gateway-backed provisioner via
            // AddHistorianProvisioning when ServerHistorian:Enabled; otherwise this resolves the no-op
            // NullHistorianProvisioning default seeded by AddOtOpcUaRuntime (so the applier's hook is inert).
            var provisioning = resolver.GetService<IHistorianProvisioning>();

            // OPC UA publish actor — pinned dispatcher, owns the address-space side of the
            // pipeline. AddressSpaceApplier is constructed here so the actor + applier share the
            // same sink reference (when DeferredAddressSpaceSink swaps later, both see it).
            var applier = new AddressSpaceApplier(
                addressSpaceSink,
                loggerFactory.CreateLogger<AddressSpaceApplier>(),
                provisioning: provisioning,
                historizedSubscriptions: historizedSubscriptions);
            var publishActor = system.ActorOf(
                OpcUaPublishActor.Props(
                    sink: addressSpaceSink,
                    serviceLevel: serviceLevel,
                    localNode: roleInfo.LocalNode,
                    dbFactory: dbFactory,
                    applier: applier,
                    dbHealthProbe: dbHealth),
                OpcUaPublishActorName);
            registry.Register<OpcUaPublishActorKey>(publishActor);

            // Per-node peer-probe supervisor — keeps one OPC UA TCP probe per OTHER non-Detached
            // driver node, so every node is continuously probed by all its peers. OpcUaPublishActor
            // consumes the resulting probe verdicts to learn this node's own reachability.
            var peerProbes = system.ActorOf(
                PeerProbeSupervisor.Props(roleInfo.LocalNode),
                PeerProbeSupervisorName);
            registry.Register<PeerProbeSupervisorKey>(peerProbes);

            var driverHost = system.ActorOf(
                DriverHostActor.Props(dbFactory, roleInfo.LocalNode, coordinator: null,
                    driverFactory: driverFactory, localRoles: roleInfo.LocalRoles,
                    dependencyMux: mux,
                    opcUaPublishActor: publishActor,
                    healthPublisher: healthPublisher,
                    virtualTagEvaluator: virtualTagEvaluator,
                    historyWriter: historyWriter,
                    loggerFactory: loggerFactory,
                    scriptRootLogger: scriptRootLogger,
                    invokerFactory: invokerFactory),
                DriverHostActorName);
            registry.Register<DriverHostActorKey>(driverHost);

            var historian = system.ActorOf(
                HistorianAdapterActor.Props(historianSink, roleInfo.LocalNode),
                HistorianAdapterActorName);
            registry.Register<HistorianAdapterActorKey>(historian);
        });

        return builder;
    }
}

/// <summary>Marker key types used by <c>Akka.Hosting</c> to resolve runtime actors from the registry.</summary>
public sealed class DriverHostActorKey { }
public sealed class DbHealthProbeActorKey { }
public sealed class HistorianAdapterActorKey { }
public sealed class DependencyMuxActorKey { }
public sealed class OpcUaPublishActorKey { }

/// <summary>Marker key for the per-node ContinuousHistorizationRecorder (spawned only when
/// <c>ContinuousHistorization:Enabled=true</c> and the gateway value-writer + outbox are registered).</summary>
public sealed class ContinuousHistorizationRecorderKey { }

/// <summary>Marker key for the per-node PeerProbeSupervisor.</summary>
public sealed class PeerProbeSupervisorKey { }
