using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Configuration; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.AI; using NexusReader.Application.Common; using GeminiDotnet; using GeminiDotnet.Extensions.AI; using NexusReader.Data.Persistence; using NexusReader.Application.Abstractions.Messaging; using NexusReader.Application.Abstractions.Persistence; using NexusReader.Application.Abstractions.Services; using NexusReader.Infrastructure.Persistence; using NexusReader.Infrastructure.RealTime; using NexusReader.Infrastructure.Services; using NexusReader.Infrastructure.Configuration; using Polly; using Polly.Retry; using NexusReader.Domain.Entities; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Authorization; using NexusReader.Application.Security.Authorization; using Qdrant.Client; using Neo4j.Driver; using Hangfire; using Hangfire.PostgreSql; namespace NexusReader.Infrastructure; public static class DependencyInjection { public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) { var pgConnectionString = configuration.GetConnectionString("PostgresConnection"); if (!string.IsNullOrEmpty(pgConnectionString)) { services.AddDbContextFactory(options => options.UseNpgsql(pgConnectionString), ServiceLifetime.Scoped); // Also register a scoped DbContext for repositories that need it services.AddDbContext(options => options.UseNpgsql(pgConnectionString)); } else { var sqliteConnectionString = configuration.GetConnectionString("SqliteConnection") ?? "Data Source=nexus.db"; services.AddDbContextFactory(options => options.UseSqlite(sqliteConnectionString), ServiceLifetime.Scoped); services.AddDbContext(options => options.UseSqlite(sqliteConnectionString)); } // Qdrant Client registration var qdrantUrl = configuration.GetConnectionString("QdrantConnection") ?? "http://localhost:6334"; var qdrantApiKey = configuration["Qdrant:ApiKey"]; services.AddSingleton(sp => { if (!string.IsNullOrEmpty(qdrantApiKey)) { return new QdrantClient(new Uri(qdrantUrl), apiKey: qdrantApiKey); } return new QdrantClient(new Uri(qdrantUrl)); }); // Neo4j Driver registration (supports optional authentication) var neo4jUrl = configuration.GetConnectionString("Neo4jConnection") ?? "bolt://localhost:7687"; var neo4jUser = configuration["Neo4j:Username"]; var neo4jPass = configuration["Neo4j:Password"]; var neo4jAuth = !string.IsNullOrEmpty(neo4jUser) ? AuthTokens.Basic(neo4jUser, neo4jPass ?? string.Empty) : AuthTokens.None; services.AddSingleton(sp => GraphDatabase.Driver(neo4jUrl, neo4jAuth)); // Hangfire registration if (!string.IsNullOrEmpty(pgConnectionString)) { services.AddHangfire(config => config .UseRecommendedSerializerSettings() .UsePostgreSqlStorage(options => options.UseNpgsqlConnection(pgConnectionString))); services.AddHangfireServer(); } services.Configure(configuration.GetSection(AiSettings.SectionName)); services.Configure(configuration.GetSection(StripeSettings.SectionName)); services.Configure(configuration.GetSection(RagMonetizationOptions.SectionName)); services.Configure(configuration.GetSection(HtmlSanitizerSettings.SectionName)); var aiSettings = configuration.GetSection(AiSettings.SectionName).Get() ?? new AiSettings(); if (string.IsNullOrWhiteSpace(aiSettings.ApiKey) || aiSettings.ApiKey == "PLACEHOLDER") { Console.WriteLine("[Infrastructure] WARNING: AI API Key is missing or placeholder!"); } services.AddResiliencePipeline("ai-retry", builder => { builder.AddRetry(new RetryStrategyOptions { ShouldHandle = new PredicateBuilder().Handle(ex => ex.Message.Contains("429") || ex.Message.Contains("Too Many Requests") || ex.Message.Contains("quota") || ex.Message.Contains("503") || ex.Message.Contains("ServiceUnavailable") || ex.Message.Contains("demand")), BackoffType = DelayBackoffType.Exponential, UseJitter = true, MaxRetryAttempts = aiSettings.RetryAttempts, Delay = TimeSpan.FromSeconds(2) }); }); services.AddChatClient(new GeminiChatClient(new GeminiClientOptions { ApiKey = aiSettings.ApiKey, ModelId = aiSettings.Model })); services.AddEmbeddingGenerator(new GeminiEmbeddingGenerator(new GeminiClientOptions { ApiKey = aiSettings.ApiKey, ModelId = aiSettings.EmbeddingModel ?? "gemini-embedding-001" })); // Application-layer service implementations services.AddScoped(); services.AddTransient(); services.AddTransient(); services.AddTransient(); // Fix #4: Scoped instead of Singleton — BookStorageService uses path resolution // that is environment-specific and incompatible with Singleton lifetime in MAUI. services.AddScoped(); services.AddScoped(); services.AddSingleton(); // Fix #1: Ebook repository (scoped, matches AppDbContext lifetime) services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); // Fix #2: SignalR broadcaster (scoped, wraps IHubContext which is itself a singleton wrapper) services.AddScoped(); services.AddAuthorizationCore(options => { options.AddPolicy("ProUser", policy => policy.Requirements.Add(new ProUserRequirement())); }); services.AddScoped(); services.AddScoped(); return services; } public static System.Reflection.Assembly Assembly => typeof(DependencyInjection).Assembly; } public interface IInfrastructureMarker { } internal class InfrastructureMarker : IInfrastructureMarker { }