using Akka.Actor;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.Replication;
using ZB.MOM.WW.Secrets.DependencyInjection;
using ZB.MOM.WW.Secrets.Sqlite;

namespace ZB.MOM.WW.Secrets.Replicator.AkkaDotNet.DependencyInjection;

/// <summary>
/// Dependency-injection entry point for peer-to-peer secret replication over an Akka.NET cluster.
/// </summary>
public static class AkkaSecretsServiceCollectionExtensions
{
    /// <summary>
    /// Registers secret replication over the cluster: each node keeps its own local store, local
    /// writes broadcast to peers, and a periodic manifest exchange repairs any divergence in both
    /// directions.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Choose this transport when a node must keep resolving secrets while <b>partitioned</b> from
    /// the rest of the cluster — reads never leave the node, so an isolated site keeps running on its
    /// last-known-good state and re-converges when the partition heals. If every node can always
    /// reach a shared database, the SQL-Server package's shared-store mode is simpler and has no
    /// distributed failure modes at all; prefer it unless partition tolerance is a stated requirement.
    /// </para>
    /// <para>
    /// <b>Requirements.</b> The application must (1) register its <see cref="ActorSystem"/> in DI
    /// before the first secret resolve, and (2) give every node the <b>same KEK</b> — rows carry
    /// ciphertext only, so a node with a different master key fails closed on a <c>kek_id</c>
    /// mismatch. Merging <see cref="AkkaSecretsReplication.SerializationConfig"/> into the actor
    /// system's configuration is recommended.
    /// </para>
    /// <para>
    /// <b>KEK rotation does not replicate.</b> A re-wrap deliberately leaves the revision untouched
    /// so it stays invisible to last-writer-wins; run <c>rewrap-all</c> against every node's local
    /// store.
    /// </para>
    /// </remarks>
    /// <param name="services">The service collection to add to.</param>
    /// <param name="config">The application configuration.</param>
    /// <param name="secretsSectionPath">Configuration section holding the core secrets options.</param>
    /// <param name="replicationSectionPath">Configuration section holding the replication options.</param>
    /// <returns>The same <paramref name="services"/> instance, for chaining.</returns>
    public static IServiceCollection AddZbSecretsAkkaReplication(
        this IServiceCollection services,
        IConfiguration config,
        string secretsSectionPath = "Secrets",
        string replicationSectionPath = "Secrets:Replication")
    {
        ArgumentNullException.ThrowIfNull(services);
        ArgumentNullException.ThrowIfNull(config);
        ArgumentException.ThrowIfNullOrWhiteSpace(secretsSectionPath);
        ArgumentException.ThrowIfNullOrWhiteSpace(replicationSectionPath);

        IConfigurationSection section = config.GetSection(replicationSectionPath);
        services.Configure<AkkaSecretsReplicationOptions>(section);

        // Validate eagerly so a bad interval fails at startup, not on the first announce tick.
        var eager = new AkkaSecretsReplicationOptions();
        section.Bind(eager);
        eager.Validate();

        // The actor is created lazily on first use rather than at registration: the ActorSystem is
        // typically registered by the application AFTER this call, and a cluster node may also not be
        // ready to join at the moment the container is built.
        //
        // The invalidator MUST stay deferred (scadaproj#1): it is the resolver, the resolver reads
        // ISecretStore, and ISecretStore is the ReplicatingSecretStore decorator registered below —
        // resolving it here closes a singleton cycle the container cannot detect through factory
        // lambdas, and every hosted process then deadlocks at startup inside the container's
        // resolution locks. DeferredSecretCacheInvalidator waits until the first eviction, which
        // can only happen after the graph has finished resolving.
        services.TryAddSingleton<SecretReplicationActorProvider>(sp => new SecretReplicationActorProvider(
            sp.GetRequiredService<ActorSystem>(),
            sp.GetRequiredService<SqliteSecretStore>(),
            new DeferredSecretCacheInvalidator(sp.GetService<ISecretCacheInvalidator>),
            sp.GetRequiredService<IOptions<AkkaSecretsReplicationOptions>>().Value));

        // MUST be registered BEFORE AddZbSecrets. That call does
        // TryAddSingleton<ISecretReplicator, NoOpSecretReplicator>(), so if it ran first it would
        // win and the TryAdd below would be silently discarded — leaving replication bound to the
        // no-op sink, publishing nothing, with no actor ever created and no error anywhere.
        // Shipped that way in 0.2.0; fixed in 0.2.1. The SqlServer package always had this order.
        services.TryAddSingleton<ISecretReplicator>(sp =>
            new AkkaSecretReplicator(sp.GetRequiredService<SecretReplicationActorProvider>().ActorRef));

        // The local store + migrator come from AddZbSecrets (SQLite by default).
        services.AddZbSecrets(config, secretsSectionPath);

        // Decorate the local store so every write publishes. Resolved from the concrete SQLite store,
        // not ISecretStore — asking the container for ISecretStore here would resolve this very
        // decorator and recurse forever.
        services.AddSingleton<ISecretStore>(sp => new ReplicatingSecretStore(
            sp.GetRequiredService<SqliteSecretStore>(),
            sp.GetRequiredService<ISecretReplicator>(),
            sp.GetRequiredService<ILogger<ReplicatingSecretStore>>()));

        return services;
    }
}

/// <summary>
/// Creates this node's <see cref="SecretReplicationActor"/> on first access and hands out its ref.
/// </summary>
/// <remarks>
/// The indirection exists because the actor must be created lazily — the application usually
/// registers its <see cref="ActorSystem"/> after <c>AddZbSecretsAkkaReplication</c>, so eagerly
/// resolving one at registration time would fail. Creation is guarded by
/// <see cref="Lazy{T}"/> with <see cref="LazyThreadSafetyMode.ExecutionAndPublication"/> so
/// concurrent first writes cannot spawn two actors on the same node.
/// </remarks>
/// <param name="system">The application's actor system.</param>
/// <param name="store">The node's local secret store — the undecorated one.</param>
/// <param name="cacheInvalidator">Resolver-cache seam.</param>
/// <param name="options">Replication options.</param>
public sealed class SecretReplicationActorProvider(
    ActorSystem system,
    ISecretStore store,
    ISecretCacheInvalidator? cacheInvalidator,
    AkkaSecretsReplicationOptions options)
{
    private readonly Lazy<IActorRef> _actorRef = new(
        () => system.ActorOf(
            SecretReplicationActor.Props(store, cacheInvalidator, options.AnnounceInterval),
            options.ActorName),
        LazyThreadSafetyMode.ExecutionAndPublication);

    /// <summary>This node's replication actor, created on first access.</summary>
    public IActorRef ActorRef => _actorRef.Value;
}
