using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using EntKube.Web.Authorization;
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 async Task 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;
            })
            .AddRoles<IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddSignInManager()
            .AddDefaultTokenProviders();

        builder.Services.AddAuthorization(options =>
        {
            options.AddPolicy("HasTenantAccess", policy =>
                policy.Requirements.Add(new HasTenantAccessRequirement()));
        });
        builder.Services.AddScoped<IAuthorizationHandler, HasTenantAccessHandler>();

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

        // 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<DockerRegistryService>();
        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<RegisteredPostgresService>();
        builder.Services.AddScoped<IKubernetesClientFactory, KubernetesClientFactory>();
        builder.Services.AddScoped<OpenStackS3Service>();
        builder.Services.AddScoped<StorageService>();
        builder.Services.AddScoped<StorageBrowserService>();
        builder.Services.AddScoped<ComponentScanService>();
        builder.Services.AddScoped<KeycloakService>();
        builder.Services.AddScoped<RabbitMQService>();
        builder.Services.AddScoped<RedisService>();
        builder.Services.AddScoped<HarborService>();
        builder.Services.AddScoped<AuditService>();
        builder.Services.AddScoped<IncidentService>();
        builder.Services.AddScoped<NotificationService>();
        builder.Services.AddScoped<LokiService>();
        builder.Services.AddScoped<BackupService>();
        builder.Services.AddScoped<VpnService>();
        builder.Services.AddScoped<AppGovernanceService>();
        builder.Services.AddScoped<GitOperationsService>();
        builder.Services.AddScoped<GitRepositoryService>();
        builder.Services.AddScoped<AppOfAppsService>();
        builder.Services.AddSingleton<GitSyncService>();
        builder.Services.AddScoped<GitWebhookService>();
        builder.Services.AddHostedService<DeploymentSyncService>();
        builder.Services.AddHostedService<ExternalRouteHealthService>();
        builder.Services.AddHostedService<AlertSyncService>();
        builder.Services.AddHostedService<UptimeTrackingService>();
        builder.Services.AddHostedService(sp => sp.GetRequiredService<GitSyncService>());

        builder.Services.AddHttpClient("Notifications", client =>
        {
            client.Timeout = TimeSpan.FromSeconds(10);
        });

        // Dedicated client for external route health probes — short timeout,
        // follows redirects, ignores SSL cert errors for internal/self-signed endpoints.
        builder.Services.AddHttpClient("RouteHealth", client =>
        {
            client.Timeout = TimeSpan.FromSeconds(15);
        }).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
        {
            AllowAutoRedirect = true,
            MaxAutomaticRedirections = 3,
            ServerCertificateCustomValidationCallback =
                HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
        });

        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);

            RoleManager<IdentityRole> roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
            if (!await roleManager.RoleExistsAsync("Admin"))
                await roleManager.CreateAsync(new IdentityRole("Admin"));

            // If Seed:AdminEmail is set in config, ensure that user gets the Admin role.
            // Useful for bootstrapping a fresh environment or recovering access.
            string? seedAdminEmail = builder.Configuration.GetValue<string>("Seed:AdminEmail");
            if (!string.IsNullOrWhiteSpace(seedAdminEmail))
            {
                UserManager<ApplicationUser> userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
                ApplicationUser? adminUser = await userManager.FindByEmailAsync(seedAdminEmail);
                if (adminUser is not null && !await userManager.IsInRoleAsync(adminUser, "Admin"))
                {
                    await userManager.AddToRoleAsync(adminUser, "Admin");
                    app.Logger.LogInformation("Granted Admin role to seeded user {Email}.", seedAdminEmail);
                }
            }
        }

        // 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();

        // Backup download — streams a gzip-compressed JSON bundle of the full app state.
        // Requires the Admin role; the bundle contains plaintext secrets so access is restricted.
        app.MapGet("/api/admin/backup", async (BackupService backupService, HttpContext httpContext) =>
        {
            System.Security.Claims.ClaimsPrincipal user = httpContext.User;
            if (user.Identity?.IsAuthenticated != true || !user.IsInRole("Admin"))
                return Results.Forbid();

            string performedBy = user.Identity?.Name ?? user.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value ?? "unknown";
            byte[] data = await backupService.ExportAsync(performedBy);
            string filename = $"entkube-backup-{DateTime.UtcNow:yyyy-MM-dd-HHmmss}.json.gz";
            return Results.File(data, "application/gzip", filename);
        }).RequireAuthorization();

        // Git webhook endpoint — receives push events from GitHub, Azure DevOps, etc.
        // Route: POST /api/git/webhook/{tenantSlug}
        // No authentication required (public endpoint); signature verification is
        // optional and done per-repo via the vault-stored WEBHOOK_SECRET.
        app.MapPost("/api/git/webhook/{tenantSlug}", async (
            string tenantSlug,
            HttpContext httpContext,
            GitWebhookService webhookService,
            CancellationToken ct) =>
        {
            using StreamReader reader = new(httpContext.Request.Body);
            string body = await reader.ReadToEndAsync(ct);
            string? signature = httpContext.Request.Headers["X-Hub-Signature-256"].FirstOrDefault();

            string result = await webhookService.HandleAsync(tenantSlug, body, signature, ct);
            return Results.Ok(result);
        });

        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();
}
