using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.LocalDb.Internal;

namespace ZB.MOM.WW.LocalDb;

/// <summary>DI registration for the embedded SQLite local database.</summary>
public static class LocalDbServiceCollectionExtensions
{
    /// <summary>
    /// Binds and validates <see cref="LocalDbOptions"/> from the <c>"LocalDb"</c> configuration section and
    /// registers <see cref="ILocalDb"/> as a singleton.
    /// </summary>
    /// <param name="onReady">
    /// Invoked once, on the ILocalDb, immediately after it is constructed and before it is returned to the first
    /// caller. This is where consumers create their tables (via the passed <see cref="ILocalDb"/>) and call
    /// <see cref="ILocalDb.RegisterReplicated"/> to opt them into replication.
    /// </param>
    /// <remarks>Subsequent calls are no-ops if an <see cref="ILocalDb"/> registration already exists (first registration wins).</remarks>
    public static IServiceCollection AddZbLocalDb(
        this IServiceCollection services,
        IConfiguration configuration,
        Action<ILocalDb>? onReady = null)
    {
        // Idempotent: a second call must not re-bind options or capture a second onReady closure.
        if (services.Any(d => d.ServiceType == typeof(ILocalDb)))
            return services;

        services.AddOptions<LocalDbOptions>()
            .Bind(configuration.GetSection("LocalDb"))
            .ValidateOnStart();
        services.TryAddEnumerable(
            ServiceDescriptor.Singleton<IValidateOptions<LocalDbOptions>, LocalDbOptionsValidator>());

        services.AddSingleton<ILocalDb>(sp =>
        {
            var options = sp.GetRequiredService<IOptions<LocalDbOptions>>().Value;
            var db = new SqliteLocalDb(options);
            try
            {
                onReady?.Invoke(db);
            }
            catch
            {
                // A throwing callback must not leak the open master connection: MS.DI does not
                // dispose instances a failed factory never returned. Guard the cleanup so a Dispose
                // failure can never replace the consumer's original onReady exception.
                try
                {
                    db.Dispose();
                }
                catch
                {
                    // Swallow: the onReady exception below is the one the caller must see.
                }
                throw;
            }
            return db;
        });

        return services;
    }
}
