using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using EntKube.Web.Client.Pages;
using EntKube.Web.Components;
using EntKube.Web.Components.Account;
using EntKube.Web.Data;
using EntKube.Web.Services;

namespace EntKube.Web;

public class Program
{
    public static void Main(string[] args)
    {
        WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

        // Add services to the container.
        builder.Services.AddRazorComponents()
            .AddInteractiveServerComponents()
            .AddInteractiveWebAssemblyComponents()
            .AddAuthenticationStateSerialization();

        builder.Services.AddCascadingAuthenticationState();
        builder.Services.AddScoped<IdentityRedirectManager>();
        builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();

        builder.Services.AddAuthentication(options =>
            {
                options.DefaultScheme = IdentityConstants.ApplicationScheme;
                options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
            })
            .AddIdentityCookies();

        // The database provider is selected based on the "DatabaseProvider" config value.
        // Supported values: "Sqlite" (default for local dev), "Postgres", "SqlServer".
        // Each provider uses its own connection string and DbContext subclass so that
        // migrations are generated and applied independently per provider.

        string databaseProvider = builder.Configuration.GetValue<string>("DatabaseProvider") ?? "Sqlite";
        string connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
            ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");

        // Register both AddDbContext (for Identity/migrations — needs derived type mapping)
        // and a DbContext factory for Blazor Server concurrency safety.
        // A single scoped DbContext is NOT safe in Blazor Server because multiple
        // async operations (different components) can run concurrently on the same circuit.

        switch (databaseProvider)
        {
            case "Postgres":
                builder.Services.AddDbContext<ApplicationDbContext, PostgresApplicationDbContext>(options =>
                    options.UseNpgsql(connectionString));
                builder.Services.AddSingleton<IDbContextFactory<ApplicationDbContext>>(
                    _ => new DelegatingDbContextFactory(() =>
                    {
                        DbContextOptionsBuilder<PostgresApplicationDbContext> opts = new();
                        opts.UseNpgsql(connectionString);
                        return new PostgresApplicationDbContext(opts.Options);
                    }));
                break;

            case "SqlServer":
                builder.Services.AddDbContext<ApplicationDbContext, SqlServerApplicationDbContext>(options =>
                    options.UseSqlServer(connectionString));
                builder.Services.AddSingleton<IDbContextFactory<ApplicationDbContext>>(
                    _ => new DelegatingDbContextFactory(() =>
                    {
                        DbContextOptionsBuilder<SqlServerApplicationDbContext> opts = new();
                        opts.UseSqlServer(connectionString);
                        return new SqlServerApplicationDbContext(opts.Options);
                    }));
                break;

            default: // "Sqlite"
                builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
                    options.UseSqlite(connectionString));
                break;
        }

        builder.Services.AddDatabaseDeveloperPageExceptionFilter();

        builder.Services.AddIdentityCore<ApplicationUser>(options =>
            {
                options.SignIn.RequireConfirmedAccount = true;
                options.Stores.SchemaVersion = IdentitySchemaVersions.Version3;
            })
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddSignInManager()
            .AddDefaultTokenProviders();

        builder.Services.AddSingleton<IEmailSender<ApplicationUser>, IdentityNoOpEmailSender>();
        builder.Services.AddHttpClient();
        builder.Services.AddScoped<TenantService>();

        // Vault encryption: the root key is loaded from configuration.
        // In production this should come from a secure source (env var, key vault, etc.)
        string rootKeyBase64 = builder.Configuration.GetValue<string>("Vault:RootKey")
            ?? throw new InvalidOperationException("Vault:RootKey must be configured (32-byte base64-encoded key).");
        byte[] rootKey = Convert.FromBase64String(rootKeyBase64);
        builder.Services.AddSingleton(new VaultEncryptionService(rootKey));
        builder.Services.AddScoped<VaultService>();
        builder.Services.AddScoped<DeploymentService>();
        builder.Services.AddScoped<CustomerAccessService>();
        builder.Services.AddScoped<KubernetesOperationsService>();
        builder.Services.AddScoped<PrometheusService>();
        builder.Services.AddScoped<ComponentLifecycleService>();
        builder.Services.AddScoped<ExternalRouteService>();
        builder.Services.AddScoped<DatabaseService>();
        builder.Services.AddScoped<CnpgService>();
        builder.Services.AddScoped<MongoService>();
        builder.Services.AddScoped<IKubernetesClientFactory, KubernetesClientFactory>();
        builder.Services.AddScoped<OpenStackS3Service>();
        builder.Services.AddScoped<StorageService>();
        builder.Services.AddScoped<ComponentScanService>();
        builder.Services.AddScoped<KeycloakService>();

        WebApplication app = builder.Build();

        // Apply pending migrations automatically on startup.
        // In a containerized or deployed environment the database may not be
        // immediately reachable, so we retry with exponential backoff.

        using (IServiceScope scope = app.Services.CreateScope())
        {
            ApplicationDbContext db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
            MigrateWithRetry(db, app.Logger);
        }

        // Configure the HTTP request pipeline.
        if (app.Environment.IsDevelopment())
        {
            app.UseWebAssemblyDebugging();
            app.UseMigrationsEndPoint();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }

        app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
        app.UseHttpsRedirection();

        app.UseAntiforgery();

        app.MapStaticAssets();
        app.MapRazorComponents<EntKube.Web.Components.App>()
            .AddInteractiveServerRenderMode()
            .AddInteractiveWebAssemblyRenderMode()
            .AddAdditionalAssemblies(typeof(Client._Imports).Assembly);

        // Add additional endpoints required by the Identity /Account Razor components.
        app.MapAdditionalIdentityEndpoints();

        app.Run();
    }

    /// <summary>
    /// Attempts to apply pending EF Core migrations, retrying up to 5 times with
    /// exponential backoff. This handles the common scenario in containerized
    /// deployments where the database container isn't ready when the app starts.
    /// </summary>
    private static void MigrateWithRetry(DbContext db, ILogger logger)
    {
        int maxRetries = 5;
        int delayMs = 1000;

        for (int attempt = 1; attempt <= maxRetries; attempt++)
        {
            try
            {
                db.Database.Migrate();
                logger.LogInformation("Database migrations applied successfully.");
                return;
            }
            catch (Exception ex) when (attempt < maxRetries)
            {
                logger.LogWarning(ex, "Migration attempt {Attempt} failed. Retrying in {Delay}ms...", attempt, delayMs);
                Thread.Sleep(delayMs);
                delayMs *= 2;
            }
        }
    }
}

/// <summary>
/// A simple IDbContextFactory adapter that uses a delegate to create DbContext instances.
/// Needed because AddDbContextFactory doesn't support derived DbContext types the way
/// AddDbContext does with its TContext/TImplementation overload.
/// </summary>
file sealed class DelegatingDbContextFactory(Func<ApplicationDbContext> factory) : IDbContextFactory<ApplicationDbContext>
{
    public ApplicationDbContext CreateDbContext() => factory();
}
