using Predictalytics.Application.Interfaces; using Predictalytics.Application.Services; using Predictalytics.Infrastructure.Services; using Predictalytics.Domain.Interfaces; using Predictalytics.Infrastructure.Data; using Predictalytics.Infrastructure.Data.Repositories; using Predictalytics.Infrastructure.Providers.Azuro; using Predictalytics.Infrastructure.Providers.Limitless; using Predictalytics.Infrastructure.Providers.Polymarket; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Predictalytics.Infrastructure; public static class DependencyInjection { public static IServiceCollection AddPredictalytics(this IServiceCollection services, IConfiguration configuration, string? connectionStringOverride = null, bool dbDebug = false) { if (dbDebug) Serilog.Log.Warning(">>> INFRASTRUCTURE: AddPredictalytics STARTING"); // MySQL / EF Core var connectionString = connectionStringOverride; if (string.IsNullOrWhiteSpace(connectionString)) { connectionString = configuration.GetConnectionString("DefaultConnection") ?? "Server=localhost;Database=Predictalytics_dev;User=root;Password=;"; } // Use MySqlConnectionStringBuilder to ensure valid format and parse components var csBuilder = new MySqlConnector.MySqlConnectionStringBuilder(connectionString); // Final safety check if (string.IsNullOrWhiteSpace(csBuilder.Database)) { Serilog.Log.Error("❌ INVALID CONNECTION STRING: Database name is empty! (Input length: {Length})", connectionString.Length); throw new InvalidOperationException("The connection string is missing a valid 'Database' parameter."); } var maskedCs = csBuilder.ConnectionString.Replace(csBuilder.Password, "****"); if (dbDebug) { Serilog.Log.Warning("🗄️ Initializing database connection: {ConnectionString}", maskedCs); Serilog.Log.Warning("🗄️ Target Server: {Server}, Database: {Database}", csBuilder.Server, csBuilder.Database); } // Explicitly register the connection string so we can use it elsewhere if needed services.AddSingleton(csBuilder.ConnectionString); services.AddDbContext(options => { // Log exactly what is being used at the moment of configuration if (dbDebug) Serilog.Log.Warning("🛠️ EF: Configuring AppDbContext. Target DB: '{Database}'", csBuilder.Database); options.UseMySql(csBuilder.ConnectionString, new MySqlServerVersion(new Version(8, 0, 31)), mysql => { mysql.EnableRetryOnFailure(3, TimeSpan.FromSeconds(10), null); mysql.CommandTimeout(600); // 10 minutes timeout for large migrations (e.g. ALTER TABLE Trades) }); }); // Repositories services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); // Application Services services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddSingleton(); services.AddSingleton(); // Platform Providers services.AddHttpClient(); services.AddHttpClient("LimitlessApi", c => { c.BaseAddress = new Uri("https://api.limitless.exchange/"); c.DefaultRequestHeaders.Add("Accept", "application/json"); c.Timeout = TimeSpan.FromSeconds(60); }); services.AddSingleton(); services.AddSingleton(); services.AddHttpClient(); services.AddScoped(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); return services; } /// /// Applies pending EF Core migrations and seeds default platform rows. /// Requires the target database to already be "stamped" with the InitialBaseline /// migration in __EFMigrationsHistory (see UMSETZUNGSPLAN.md, section B1) if it was /// previously created via the old EnsureCreated + manual ALTER approach. /// public static async Task EnsureDatabaseAsync(IServiceProvider services, bool dbDebug = false) { using var scope = services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); try { if (dbDebug) { var maskedConnStr = System.Text.RegularExpressions.Regex.Replace( db.Database.GetDbConnection().ConnectionString ?? "NULL", "Password=[^;]+", "Password=****"); Serilog.Log.Warning("🔍 DEBUG: Applying EF Core migrations. ConnectionString: {CS}", maskedConnStr); } await db.Database.MigrateAsync(); if (dbDebug) Serilog.Log.Warning("✅ DEBUG: Migrations applied successfully."); // Seed default platform rows (idempotent) var conn = db.Database.GetDbConnection(); if (conn.State != System.Data.ConnectionState.Open) await conn.OpenAsync(); using var seedPlatform = conn.CreateCommand(); seedPlatform.CommandText = @" INSERT IGNORE INTO `PlatformConfigs` (`Id`, `Name`, `DisplayName`, `IsActive`, `CreatedAt`, `UpdatedAt`) VALUES (0, 'Unknown', 'Unknown Platform', 1, UTC_TIMESTAMP(), UTC_TIMESTAMP()), (1, 'Polymarket', 'Polymarket', 1, UTC_TIMESTAMP(), UTC_TIMESTAMP()), (2, 'Azuro', 'Azuro', 1, UTC_TIMESTAMP(), UTC_TIMESTAMP()), (3, 'Limitless', 'Limitless', 1, UTC_TIMESTAMP(), UTC_TIMESTAMP());"; await seedPlatform.ExecuteNonQueryAsync(); await conn.CloseAsync(); } catch (Exception ex) { Serilog.Log.Warning("⚠️ Could not connect to database or apply migrations: {Message}. Background workers will retry connection automatically.", ex.Message); } } }