using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.Secrets.Abstractions;
using ZB.MOM.WW.Secrets.Crypto;
using ZB.MOM.WW.Secrets.MasterKey;
using ZB.MOM.WW.Secrets.Sqlite;

namespace ZB.MOM.WW.Secrets.DependencyInjection;

/// <summary>
/// Dependency-injection helpers that wire the ZB.MOM.WW secrets subsystem — master-key provider,
/// envelope cipher, SQLite store, resolver, no-op replicator, and the startup migration hosted
/// service — from configuration with a single call.
/// </summary>
public static class SecretsServiceCollectionExtensions
{
    /// <summary>
    /// Registers the secrets subsystem: binds <see cref="SecretsOptions"/> from the configuration
    /// section at <paramref name="sectionPath"/> and wires the master-key provider, envelope cipher,
    /// SQLite-backed store, the caching resolver, the single-node no-op replicator, and a hosted
    /// service that runs the schema migration on startup (when enabled).
    /// </summary>
    /// <param name="services">The service collection to add to.</param>
    /// <param name="config">The application configuration.</param>
    /// <param name="sectionPath">Path of the configuration section holding the secrets options.</param>
    /// <returns>The same <paramref name="services"/> instance, for chaining.</returns>
    public static IServiceCollection AddZbSecrets(
        this IServiceCollection services,
        IConfiguration config,
        string sectionPath)
    {
        ArgumentNullException.ThrowIfNull(services);
        ArgumentNullException.ThrowIfNull(config);
        ArgumentException.ThrowIfNullOrWhiteSpace(sectionPath);

        services.Configure<SecretsOptions>(config.GetSection(sectionPath));

        // One connection factory targets the configured SQLite path. Singleton: it is stateless
        // aside from the path and opens a fresh connection per operation.
        services.TryAddSingleton(sp =>
            new SecretsSqliteConnectionFactory(
                sp.GetRequiredService<IOptions<SecretsOptions>>().Value.SqlitePath));

        services.TryAddSingleton<IMasterKeyProvider>(sp =>
            MasterKeyProviderFactory.Create(
                sp.GetRequiredService<IOptions<SecretsOptions>>().Value.MasterKey));

        services.TryAddSingleton<ISecretCipher>(sp =>
            new AesGcmEnvelopeCipher(sp.GetRequiredService<IMasterKeyProvider>()));

        // Registered BOTH concretely and behind the interface, forwarding to the one singleton.
        // The concrete registration is what lets a replication package resolve the *undecorated*
        // local store when it replaces ISecretStore with a decorator — asking the container for
        // ISecretStore there would resolve the decorator itself and recurse forever.
        services.TryAddSingleton(sp =>
            new SqliteSecretStore(sp.GetRequiredService<SecretsSqliteConnectionFactory>()));

        services.TryAddSingleton<ISecretStore>(sp => sp.GetRequiredService<SqliteSecretStore>());

        services.TryAddSingleton<ISecretReplicator, NoOpSecretReplicator>();

        // ONE shared resolver instance backs both the read seam (ISecretResolver) and the
        // cache-invalidation seam (ISecretCacheInvalidator) so a write-path invalidation hits the
        // exact cache the resolver reads from.
        services.TryAddSingleton<DefaultSecretResolver>(sp =>
        {
            // Resolve the audit writer LAZILY so this picks up the app's AddZbAudit registration
            // regardless of call order; fall back to a no-op writer when the app registers none.
            IAuditWriter audit = sp.GetService<IAuditWriter>() ?? NoOpAuditWriter.Instance;
            ISecretActorAccessor? actorAccessor = sp.GetService<ISecretActorAccessor>();
            TimeProvider? timeProvider = sp.GetService<TimeProvider>();
            TimeSpan ttl = sp.GetRequiredService<IOptions<SecretsOptions>>().Value.ResolveCacheTtl;

            return new DefaultSecretResolver(
                sp.GetRequiredService<ISecretStore>(),
                sp.GetRequiredService<ISecretCipher>(),
                audit,
                ttl,
                actorAccessor,
                timeProvider);
        });

        services.TryAddSingleton<ISecretResolver>(sp => sp.GetRequiredService<DefaultSecretResolver>());
        services.TryAddSingleton<ISecretCacheInvalidator>(sp => sp.GetRequiredService<DefaultSecretResolver>());

        // Migrator: singleton, constructed from the already-registered connection factory. Needed
        // before any store operations so the schema exists. Registered BOTH concretely (for callers
        // that want the SQLite migrator specifically) and behind the ISecretsStoreMigrator seam that
        // the hosted service and CLI consume — a replication package that swaps the store registers
        // its own ISecretsStoreMigrator BEFORE calling AddZbSecrets, and TryAdd leaves it in place.
        services.TryAddSingleton(sp =>
            new SqliteSecretsStoreMigrator(sp.GetRequiredService<SecretsSqliteConnectionFactory>()));

        services.TryAddSingleton<ISecretsStoreMigrator>(sp =>
            sp.GetRequiredService<SqliteSecretsStoreMigrator>());

        // Hosted service that runs migrations on startup when SecretsOptions.RunMigrationsOnStartup.
        services.AddHostedService<SecretsMigrationHostedService>();

        return services;
    }
}
