using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.LocalDb;
using ZB.MOM.WW.LocalDb.Replication;
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
using ZB.MOM.WW.OtOpcUa.Runtime.DeploymentCache;
using ZB.MOM.WW.OtOpcUa.Runtime.ScriptedAlarms;

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

/// <summary>
///     Single registration point for the node-local LocalDb subsystem: the embedded SQLite store
///     that caches the deployed-configuration artifact, plus the optional gRPC replication that
///     mirrors it to the node's redundant pair peer.
/// </summary>
/// <remarks>
///     <para>
///         Exists as a named extension rather than inline <c>AddZbLocalDb</c> calls in
///         <c>Program.cs</c> so the wiring is covered by a DI resolution test — <c>Program.cs</c> is
///         top-level statements and cannot be exercised directly, which is exactly how a
///         "registered but never resolvable" defect ships unnoticed. This family has shipped that
///         defect three times (Secrets 0.2.0, Secrets 0.2.2, ScadaBridge #22).
///     </para>
///     <para>
///         <b>Driver-role nodes only.</b> Admin-only nodes have no deployed configuration to cache,
///         and registering here would impose the <c>LocalDb:Path</c>-required-or-no-boot constraint
///         on them for nothing.
///     </para>
///     <para>
///         <b>Storage is unconditional; replication is default-OFF.</b> A node with no
///         <c>PeerAddress</c> and no <c>SyncListenPort</c> is simply a fast local SQLite file —
///         <c>AddZbLocalDbReplication</c> registers the engine but it stays idle. Replication is
///         opt-in per pair because there is no production distribution story for the sync
///         <c>ApiKey</c> yet (the same open question the Secrets KEK has).
///     </para>
/// </remarks>
public static class LocalDbRegistration
{
    /// <summary>Configuration section holding <c>LocalDbOptions</c>.</summary>
    public const string LocalDbSectionPath = "LocalDb";

    /// <summary>Configuration section holding <c>ReplicationOptions</c>.</summary>
    public const string ReplicationSectionPath = "LocalDb:Replication";

    /// <summary>
    ///     Configuration key carrying the dedicated h2c sync listener's port. Zero or absent means
    ///     no listener is bound and the passive sync endpoint is not mapped.
    /// </summary>
    public const string SyncListenPortKey = "LocalDb:SyncListenPort";

    /// <summary>
    ///     The port the dedicated cleartext-HTTP/2 sync listener should bind, or <c>0</c> when the
    ///     node should not listen at all. Defaults to <c>0</c> — absent configuration must mean
    ///     "off".
    /// </summary>
    /// <param name="configuration">The application configuration.</param>
    /// <returns>The configured sync port, or <c>0</c>.</returns>
    public static int SyncListenPort(IConfiguration configuration)
    {
        ArgumentNullException.ThrowIfNull(configuration);
        return configuration.GetValue(SyncListenPortKey, defaultValue: 0);
    }

    /// <summary>
    ///     Registers the local database (with the deployment-cache schema applied and registered for
    ///     replication) and the replication engine.
    /// </summary>
    /// <remarks>
    ///     <para>
    ///         <see cref="LocalDbSetup.OnReady"/> runs inside <c>AddZbLocalDb</c>'s singleton factory,
    ///         before the first caller receives the <c>ILocalDb</c> — so every consumer is guaranteed
    ///         to see the tables created and their capture triggers installed.
    ///     </para>
    ///     <para>
    ///         <c>AddZbLocalDbReplication</c> is called unconditionally rather than behind an
    ///         <c>Enabled</c> flag because the library already models "off" as "no
    ///         <c>PeerAddress</c>": the initiator background service idles and the passive endpoint
    ///         is only reachable if <c>Program.cs</c> maps it, which it does only when
    ///         <see cref="SyncListenPortKey"/> is set. Adding a second gate on top would give two
    ///         ways to express the same thing and a way for them to disagree.
    ///     </para>
    /// </remarks>
    /// <param name="services">The service collection to add to.</param>
    /// <param name="configuration">The application configuration.</param>
    /// <returns>The same <paramref name="services"/> instance, for chaining.</returns>
    public static IServiceCollection AddOtOpcUaLocalDb(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        ArgumentNullException.ThrowIfNull(services);
        ArgumentNullException.ThrowIfNull(configuration);

        services.AddZbLocalDb(configuration, db => LocalDbSetup.OnReady(db, configuration));
        services.AddZbLocalDbReplication(configuration);

        // The consumer-facing seam. WithOtOpcUaRuntimeActors resolves this optionally and threads it
        // into DriverHostActor; registering the store without this leaves the cache tables present,
        // replicating, and never written to.
        services.AddSingleton<IDeploymentArtifactCache, LocalDbDeploymentArtifactCache>();

        // Per-cluster mesh Phase 4: scripted-alarm condition state served from the replicated LocalDb
        // store instead of the ConfigDb-backed EF store, so a driver-only node (no ConfigDb) keeps its
        // Part 9 condition state and mirrors it to its pair peer. Driver-role only, alongside the cache —
        // admin-only nodes never register LocalDb. WithOtOpcUaRuntimeActors resolves it optionally and
        // threads it into DriverHostActor's ScriptedAlarm engine. Singleton to match ILocalDb + the cache.
        services.AddSingleton<IAlarmStateStore, LocalDbAlarmConditionStateStore>();

        return services;
    }
}
