using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Templates;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Maintenance;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Services;

namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;

public static class ServiceCollectionExtensions
{
    /// <summary>
    /// Registers the ScadaBridgeDbContext with the provided SQL Server connection string.
    /// </summary>
    /// <param name="services">The service collection to register into.</param>
    /// <param name="connectionString">SQL Server connection string for the central configuration database.</param>
    /// <returns>The same <paramref name="services"/> collection, for chaining.</returns>
    public static IServiceCollection AddConfigurationDatabase(this IServiceCollection services, string connectionString)
    {
        // The DbContext is constructed via the (options, IDataProtectionProvider) overload so
        // secret-bearing configuration columns are encrypted at rest. AddDataProtection below
        // registers IDataProtectionProvider as a singleton; resolving it here does not recurse
        // because key-ring loading is lazy (first Protect/Unprotect), not triggered by
        // CreateProtector during model building.
        //
        // POOLING IS DELIBERATELY NOT USED (WP2.2 — verified, not overlooked).
        // AddDbContextPool requires a context with a SINGLE public constructor taking
        // only DbContextOptions<TContext>; EF Core constructs pooled instances through
        // its own activator and cannot supply anything else. ScadaBridgeDbContext has
        // two public constructors and the runtime one takes IDataProtectionProvider,
        // because the encrypting value converter for secret-bearing columns is built
        // during OnModelCreating from that provider. Worse, the model itself DIFFERS
        // between the two constructors (no provider ⇒ no encrypting converter), so a
        // pooled activator would silently produce a context that reads secret columns
        // as ciphertext. Making this poolable means moving the protector out of the
        // constructor and into a DbContextOptions extension — a change to the
        // secrets-at-rest path, which is not a performance refactor. The registration
        // below (a scoped factory overriding AddDbContext's activator) is what makes
        // the provider reach the context at all, and it also bypasses pooling by
        // construction. Revisit only alongside a deliberate secrets-plumbing change.
        services.AddDbContext<ScadaBridgeDbContext>((serviceProvider, options) =>
        {
            options.UseSqlServer(
                    connectionString,
                    sql => sql.EnableRetryOnFailure(
                        maxRetryCount: 5,
                        maxRetryDelay: TimeSpan.FromSeconds(30),
                        errorNumbersToAdd: null))
                .ConfigureWarnings(w => w.Ignore(
                    Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning));
        });

        // AddDbContext registers ScadaBridgeDbContext via EF's activator, which only injects
        // DbContextOptions. Override that registration (last registration wins for resolution)
        // with a factory that also supplies the IDataProtectionProvider, so the encrypting
        // value converter for secret columns is always wired up at runtime.
        services.AddScoped(serviceProvider =>
        {
            var options = serviceProvider.GetRequiredService<DbContextOptions<ScadaBridgeDbContext>>();
            var protectionProvider = serviceProvider.GetRequiredService<IDataProtectionProvider>();
            return new ScadaBridgeDbContext(options, protectionProvider);
        });

        // Process-wide template/instance version watermark. Singleton because the
        // writer (TemplateEngineRepository.SaveChangesAsync, scoped) and the readers
        // (the flatten-session cache and the staleness fast path, also scoped) must
        // observe the same counters across DI scopes. TryAdd so the DeploymentManager
        // registration of the same interface is idempotent.
        services.TryAddSingleton<ITemplateGraphWatermark, TemplateGraphWatermark>();

        services.AddScoped<ISecurityRepository, SecurityRepository>();
        services.AddScoped<ICentralUiRepository, CentralUiRepository>();
        services.AddScoped<ITemplateEngineRepository, TemplateEngineRepository>();
        services.AddScoped<IDeploymentManagerRepository, DeploymentManagerRepository>();
        services.AddScoped<ISiteRepository, SiteRepository>();
        services.AddScoped<IExternalSystemRepository, ExternalSystemRepository>();
        services.AddScoped<INotificationRepository, NotificationRepository>();
        services.AddScoped<INotificationOutboxRepository, NotificationOutboxRepository>();
        services.AddScoped<IAuditLogRepository, AuditLogRepository>();
        services.AddScoped<ISiteCallAuditRepository, SiteCallAuditRepository>();
        services.AddScoped<ISecuredWriteRepository, SecuredWriteRepository>();
        services.AddScoped<ISharedSchemaRepository, SharedSchemaRepository>();
        services.AddScoped<IKpiHistoryRepository, KpiHistoryRepository>();
        // Auth re-arch: inbound API keys are no longer persisted in SQL Server —
        // the repository now exposes only API-method access, so a plain scoped
        // registration suffices (no peppered-hasher accessor to wire).
        services.AddScoped<IInboundApiRepository, InboundApiRepository>();
        services.AddScoped<IAuditCorrelationContext, AuditCorrelationContext>();
        services.AddScoped<IAuditService, AuditService>();
        services.AddScoped<IInstanceLocator, InstanceLocator>();

        // IPartitionMaintenance drives the daily roll-forward
        // of pf_AuditLog_Month from the central AuditLogPartitionMaintenanceService
        // hosted service. Scoped because the implementation reuses the per-scope
        // ScadaBridgeDbContext for raw-SQL execution; the hosted service opens a
        // fresh scope on each tick (mirrors AuditLogPurgeActor / AuditLogIngestActor).
        services.AddScoped<IPartitionMaintenance, AuditLogPartitionMaintenance>();

        services.AddDataProtection()
            .PersistKeysToDbContext<ScadaBridgeDbContext>();

        return services;
    }

    /// <summary>
    /// Obsolete parameterless overload. This previously registered nothing, which meant a
    /// central node wired up with it failed late and opaquely — the first repository
    /// resolution threw a DI exception far from the actual misconfiguration. Use
    /// <see cref="AddConfigurationDatabase(IServiceCollection, string)"/> and pass the
    /// configured connection string.
    /// </summary>
    /// <param name="services">The service collection (unused; this overload always throws).</param>
    /// <exception cref="InvalidOperationException">
    /// Always thrown. The connection string is required; there is no valid no-op registration.
    /// </exception>
    /// <returns>Never returns; always throws <see cref="InvalidOperationException"/>.</returns>
    [Obsolete(
        "AddConfigurationDatabase() with no connection string registers nothing and is not a " +
        "valid configuration. Call AddConfigurationDatabase(connectionString) instead.",
        error: true)]
    public static IServiceCollection AddConfigurationDatabase(this IServiceCollection services)
    {
        // Defence-in-depth: even if a caller suppresses the compile-time obsolete error,
        // fail fast at wire-up time rather than silently registering nothing and surfacing
        // an opaque DI resolution failure much later.
        throw new InvalidOperationException(
            "AddConfigurationDatabase() requires a connection string. Call " +
            "AddConfigurationDatabase(connectionString) with the configured " +
            "'ScadaBridge:Database:ConfigurationDb' value.");
    }
}
