using Femto.Common;
using Femto.Common.Infrastructure;
using Femto.Common.Infrastructure.DbConnection;
using Femto.Common.Infrastructure.Outbox;
using Femto.Common.Integration;
using Femto.Modules.Auth.Data;
using Femto.Modules.Auth.Infrastructure;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Quartz;

namespace Femto.Modules.Auth.Application;

public static class AuthStartup
{
    public static void InitializeAuthenticationModule(
        this IServiceCollection rootContainer,
        string connectionString,
        IEventBus eventBus,
        ILoggerFactory loggerFactory,
        TimeProvider timeProvider
    )
    {
        var hostBuilder = Host.CreateDefaultBuilder();

        hostBuilder.ConfigureServices(services =>
            ConfigureServices(services, connectionString, eventBus, loggerFactory, timeProvider)
        );

        var host = hostBuilder.Build();

        rootContainer.AddKeyedScoped<ScopeBinding>(
            "AuthServiceScope",
            (s, o) =>
            {
                var scope = host.Services.CreateScope();
                return new ScopeBinding(scope);
            }
        );

        rootContainer.ExposeScopedService<IAuthService>();

        rootContainer.AddHostedService(services => new AuthApplication(host));
        eventBus.Subscribe(
            (evt, cancellationToken) => EventSubscriber(evt, host.Services, cancellationToken)
        );
    }

    private static void ConfigureServices(
        IServiceCollection services,
        string connectionString,
        IEventPublisher publisher,
        ILoggerFactory loggerFactory,
        TimeProvider timeProvider
    )
    {
        services.AddSingleton(timeProvider);

        services.AddTransient<IDbConnectionFactory>(_ => new DbConnectionFactory(connectionString));

        services.AddDbContext<AuthContext>(builder =>
        {
            builder.UseNpgsql(connectionString);
            builder.UseSnakeCaseNamingConvention();
            builder.UseLoggerFactory(loggerFactory);
            // #if DEBUG
            // builder.EnableSensitiveDataLogging();
            // #endif
        });

        services.AddSingleton(loggerFactory);
        services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));

        services.AddQuartzHostedService(options =>
        {
            options.WaitForJobsToComplete = true;
        });
        // #endif
        services.AddOutbox<AuthContext, OutboxMessageHandler>();

        services.AddMediatR(c => c.RegisterServicesFromAssembly(typeof(AuthStartup).Assembly));

        services.ConfigureDomainServices<AuthContext>();

        services.AddSingleton(publisher);
        services.AddSingleton<SessionStorage>();

        services.AddScoped(typeof(IPipelineBehavior<,>), typeof(SaveChangesPipelineBehaviour<,>));

        services.AddScoped<IAuthService, AuthService>();
    }

    private static async Task EventSubscriber(
        IEvent evt,
        IServiceProvider provider,
        CancellationToken cancellationToken
    )
    {
        using var scope = provider.CreateScope();

        var context = scope.ServiceProvider.GetRequiredService<AuthContext>();
        var logger = scope.ServiceProvider.GetRequiredService<ILogger<AuthApplication>>();
        var publisher = scope.ServiceProvider.GetRequiredService<IPublisher>();

        IEventHandler? handler = evt switch
        {
            _ => null,
        };

        if (handler is null)
            return;

        await handler.Handle(evt, cancellationToken);

        if (context.ChangeTracker.HasChanges())
        {
            await context.EmitDomainEvents(logger, publisher, cancellationToken);
            await context.SaveChangesAsync(cancellationToken);
        }
    }
}

internal static class AuthServiceCollectionExtensions
{
    public static void ExposeScopedService<T>(this IServiceCollection container)
        where T : class
    {
        container.AddScoped<T>(services =>
            services.GetRequiredKeyedService<ScopeBinding>("AuthServiceScope").GetService<T>()
        );
    }
}
