using MemberCenter.Application.Abstractions;
using MemberCenter.Domain.Entities;
using MemberCenter.Infrastructure.Configuration;
using MemberCenter.Infrastructure.Identity;
using MemberCenter.Infrastructure.Persistence;
using MemberCenter.Infrastructure.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Npgsql;
using OpenIddict.Abstractions;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.CommandLine;

EnvLoader.LoadDotEnvIfDevelopment();

var root = new RootCommand("Member Center installer");

var connectionStringOption = new Option<string>(
    name: "--connection-string",
    description: "Database connection string");
connectionStringOption.AddAlias("-c");

var appsettingsOption = new Option<string?>(
    name: "--appsettings",
    description: "Path to appsettings.json");

var noPromptOption = new Option<bool>(
    name: "--no-prompt",
    description: "Disable interactive prompts");

var verboseOption = new Option<bool>(
    name: "--verbose",
    description: "Enable verbose output");

var forceOption = new Option<bool>(
    name: "--force",
    description: "Force execution even if already installed");

var adminEmailOption = new Option<string?>(
    name: "--admin-email",
    description: "Admin email");

var adminPasswordOption = new Option<string?>(
    name: "--admin-password",
    description: "Admin password");

var adminDisplayNameOption = new Option<string?>(
    name: "--admin-display-name",
    description: "Admin display name (optional)");

var targetMigrationOption = new Option<string?>(
    name: "--target",
    description: "Target migration");

var initCommand = new Command("init", "Initialize database (migrate + seed + superuser)");
initCommand.AddOption(connectionStringOption);
initCommand.AddOption(appsettingsOption);
initCommand.AddOption(noPromptOption);
initCommand.AddOption(verboseOption);
initCommand.AddOption(forceOption);
initCommand.AddOption(adminEmailOption);
initCommand.AddOption(adminPasswordOption);
initCommand.AddOption(adminDisplayNameOption);

initCommand.SetHandler(async (string? connectionString, string? appsettings, bool noPrompt, bool verbose, bool force, string? adminEmail, string? adminPassword) =>
{
    var resolvedConnection = ResolveConnectionString(connectionString, appsettings, noPrompt);
    if (string.IsNullOrWhiteSpace(resolvedConnection))
    {
        Console.Error.WriteLine("Connection string is required.");
        return;
    }

    var services = BuildServices(resolvedConnection);
    await using var scope = services.CreateAsyncScope();
    var db = scope.ServiceProvider.GetRequiredService<MemberCenterDbContext>();
    var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
    var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<ApplicationRole>>();

    if (!force && await IsInstalledAsync(db))
    {
        Console.Error.WriteLine("System already installed. Use --force to override.");
        return;
    }

    if (!noPrompt)
    {
        adminEmail ??= Prompt("Superuser email", "admin@example.com");
        adminPassword ??= PromptSecret("Superuser password");
    }

    adminEmail ??= "admin@example.com";
    if (string.IsNullOrWhiteSpace(adminPassword))
    {
        Console.Error.WriteLine("Superuser password is required.");
        return;
    }

    await db.Database.MigrateAsync();
    var registry = scope.ServiceProvider.GetRequiredService<IAuthResourceRegistryService>();
    await registry.EnsureDefaultsAsync();
    var adminPermissionSeeder = scope.ServiceProvider.GetRequiredService<IAdminPermissionSeeder>();
    await adminPermissionSeeder.EnsureDefaultsAsync();

    await EnsureRoleAsync(roleManager, "superuser");
    await EnsureRoleAsync(roleManager, "admin");
    await EnsureRoleAsync(roleManager, "support");

    var admin = await userManager.FindByEmailAsync(adminEmail);
    if (admin is null)
    {
        admin = new ApplicationUser
        {
            Id = Guid.NewGuid(),
            UserName = adminEmail,
            Email = adminEmail,
            EmailConfirmed = true
        };
        var createResult = await userManager.CreateAsync(admin, adminPassword);
        if (!createResult.Succeeded)
        {
            Console.Error.WriteLine(string.Join(Environment.NewLine, createResult.Errors.Select(e => e.Description)));
            return;
        }
    }

    if (!await userManager.IsInRoleAsync(admin, "superuser"))
    {
        await userManager.AddToRoleAsync(admin, "superuser");
    }

    await SetInstalledFlagAsync(db);

    if (verbose)
    {
        Console.WriteLine("Init completed.");
    }
}, connectionStringOption, appsettingsOption, noPromptOption, verboseOption, forceOption, adminEmailOption, adminPasswordOption);

var addAdminCommand = new Command("add-superuser", "Add or elevate superuser");
addAdminCommand.AddAlias("add-admin");
addAdminCommand.AddOption(connectionStringOption);
addAdminCommand.AddOption(appsettingsOption);
addAdminCommand.AddOption(noPromptOption);
addAdminCommand.AddOption(adminEmailOption);
addAdminCommand.AddOption(adminPasswordOption);
addAdminCommand.SetHandler(async (string? connectionString, string? appsettings, bool noPrompt, string? adminEmail, string? adminPassword) =>
{
    var resolvedConnection = ResolveConnectionString(connectionString, appsettings, noPrompt);
    if (string.IsNullOrWhiteSpace(resolvedConnection))
    {
        Console.Error.WriteLine("Connection string is required.");
        return;
    }

    if (!noPrompt)
    {
        adminEmail ??= Prompt("Superuser email", "admin@example.com");
        adminPassword ??= PromptSecret("Superuser password");
    }

    adminEmail ??= "admin@example.com";
    if (string.IsNullOrWhiteSpace(adminPassword))
    {
        Console.Error.WriteLine("Superuser password is required.");
        return;
    }

    var services = BuildServices(resolvedConnection);
    await using var scope = services.CreateAsyncScope();
    var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
    var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<ApplicationRole>>();

    await EnsureRoleAsync(roleManager, "superuser");

    var admin = await userManager.FindByEmailAsync(adminEmail);
    if (admin is null)
    {
        admin = new ApplicationUser
        {
            Id = Guid.NewGuid(),
            UserName = adminEmail,
            Email = adminEmail,
            EmailConfirmed = true
        };
        var createResult = await userManager.CreateAsync(admin, adminPassword);
        if (!createResult.Succeeded)
        {
            Console.Error.WriteLine(string.Join(Environment.NewLine, createResult.Errors.Select(e => e.Description)));
            return;
        }
    }

    if (!await userManager.IsInRoleAsync(admin, "superuser"))
    {
        await userManager.AddToRoleAsync(admin, "superuser");
    }

    Console.WriteLine("Superuser ready.");
}, connectionStringOption, appsettingsOption, noPromptOption, adminEmailOption, adminPasswordOption);

var resetCommand = new Command("reset-superuser-password", "Reset superuser password");
resetCommand.AddAlias("reset-admin-password");
resetCommand.AddOption(connectionStringOption);
resetCommand.AddOption(appsettingsOption);
resetCommand.AddOption(noPromptOption);
resetCommand.AddOption(adminEmailOption);
resetCommand.AddOption(adminPasswordOption);
resetCommand.SetHandler(async (string? connectionString, string? appsettings, bool noPrompt, string? adminEmail, string? adminPassword) =>
{
    var resolvedConnection = ResolveConnectionString(connectionString, appsettings, noPrompt);
    if (string.IsNullOrWhiteSpace(resolvedConnection))
    {
        Console.Error.WriteLine("Connection string is required.");
        return;
    }

    if (!noPrompt)
    {
        adminEmail ??= Prompt("Superuser email", "admin@example.com");
        adminPassword ??= PromptSecret("New password");
    }

    adminEmail ??= "admin@example.com";
    if (string.IsNullOrWhiteSpace(adminPassword))
    {
        Console.Error.WriteLine("Superuser password is required.");
        return;
    }

    var services = BuildServices(resolvedConnection);
    await using var scope = services.CreateAsyncScope();
    var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();

    var admin = await userManager.FindByEmailAsync(adminEmail);
    if (admin is null)
    {
        Console.Error.WriteLine("Superuser not found.");
        return;
    }

    var resetToken = await userManager.GeneratePasswordResetTokenAsync(admin);
    var result = await userManager.ResetPasswordAsync(admin, resetToken, adminPassword);
    if (!result.Succeeded)
    {
        Console.Error.WriteLine(string.Join(Environment.NewLine, result.Errors.Select(e => e.Description)));
        return;
    }

    Console.WriteLine("Password reset completed.");
}, connectionStringOption, appsettingsOption, noPromptOption, adminEmailOption, adminPasswordOption);

var migrateCommand = new Command("migrate", "Run migrations only");
migrateCommand.AddOption(connectionStringOption);
migrateCommand.AddOption(appsettingsOption);
migrateCommand.AddOption(targetMigrationOption);
migrateCommand.SetHandler(async (string? connectionString, string? appsettings, string? target) =>
{
    var resolvedConnection = ResolveConnectionString(connectionString, appsettings, noPrompt: true);
    if (string.IsNullOrWhiteSpace(resolvedConnection))
    {
        Console.Error.WriteLine("Connection string is required.");
        return;
    }

    var services = BuildServices(resolvedConnection);
    await using var scope = services.CreateAsyncScope();
    var db = scope.ServiceProvider.GetRequiredService<MemberCenterDbContext>();
    if (string.IsNullOrWhiteSpace(target))
    {
        await db.Database.MigrateAsync();
        var registry = scope.ServiceProvider.GetRequiredService<IAuthResourceRegistryService>();
        await registry.EnsureDefaultsAsync();
        var adminPermissionSeeder = scope.ServiceProvider.GetRequiredService<IAdminPermissionSeeder>();
        await adminPermissionSeeder.EnsureDefaultsAsync();
    }
    else
    {
        var migrator = scope.ServiceProvider.GetRequiredService<IMigrator>();
        await migrator.MigrateAsync(target);
    }

    Console.WriteLine("Migrations completed.");
}, connectionStringOption, appsettingsOption, targetMigrationOption);

var syncOAuthClientsCommand = new Command("sync-oauth-clients", "Refresh OAuth client permissions from usage defaults");
syncOAuthClientsCommand.AddOption(connectionStringOption);
syncOAuthClientsCommand.AddOption(appsettingsOption);
syncOAuthClientsCommand.SetHandler(async (string? connectionString, string? appsettings) =>
{
    var resolvedConnection = ResolveConnectionString(connectionString, appsettings, noPrompt: true);
    if (string.IsNullOrWhiteSpace(resolvedConnection))
    {
        Console.Error.WriteLine("Connection string is required.");
        return;
    }

    var services = BuildServices(resolvedConnection);
    await using var scope = services.CreateAsyncScope();
    var registry = scope.ServiceProvider.GetRequiredService<IAuthResourceRegistryService>();
    await registry.EnsureDefaultsAsync();
    var adminPermissionSeeder = scope.ServiceProvider.GetRequiredService<IAdminPermissionSeeder>();
    await adminPermissionSeeder.EnsureDefaultsAsync();

    var applicationManager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
    var applications = new List<object>();
    await foreach (var application in applicationManager.ListAsync())
    {
        applications.Add(application);
    }

    var updated = 0;
    foreach (var application in applications)
    {
        var properties = await applicationManager.GetPropertiesAsync(application);
        if (!properties.TryGetValue("usage", out var usageElement))
        {
            continue;
        }

        var usage = usageElement.GetString();
        if (string.IsNullOrWhiteSpace(usage))
        {
            continue;
        }

        var descriptor = new OpenIddictApplicationDescriptor();
        await applicationManager.PopulateAsync(descriptor, application);
        await ApplyOAuthClientPermissionsAsync(descriptor, registry, usage);
        await applicationManager.UpdateAsync(application, descriptor);
        updated++;
    }

    Console.WriteLine($"OAuth clients synchronized: {updated}.");
}, connectionStringOption, appsettingsOption);

root.AddCommand(initCommand);
root.AddCommand(addAdminCommand);
root.AddCommand(resetCommand);
root.AddCommand(migrateCommand);
root.AddCommand(syncOAuthClientsCommand);

return await root.InvokeAsync(args);

static IServiceProvider BuildServices(string connectionString)
{
    var services = new ServiceCollection();
    var configuration = new ConfigurationBuilder()
        .AddEnvironmentVariables()
        .Build();
    var identitySecurity = configuration.GetSection("IdentitySecurity").Get<IdentitySecurityOptions>() ?? new();
    SecurityRuntimeOptionsValidator.Validate(identitySecurity);
    services.AddLogging(builder => builder.AddConsole());
    services.AddSingleton<IConfiguration>(configuration);
    services.AddDbContext<MemberCenterDbContext>(options =>
    {
        options.UseNpgsql(connectionString);
        options.UseOpenIddict();
    });

    services.AddOpenIddict()
        .AddCore(options =>
        {
            options.UseEntityFrameworkCore()
                .UseDbContext<MemberCenterDbContext>();
        });

    services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
    {
        options.User.RequireUniqueEmail = true;
        options.Password.RequireDigit = identitySecurity.Password.RequireDigit;
        options.Password.RequireLowercase = identitySecurity.Password.RequireLowercase;
        options.Password.RequireUppercase = identitySecurity.Password.RequireUppercase;
        options.Password.RequireNonAlphanumeric = identitySecurity.Password.RequireNonAlphanumeric;
        options.Password.RequiredLength = identitySecurity.Password.RequiredLength;
        options.Lockout.AllowedForNewUsers = identitySecurity.Lockout.AllowedForNewUsers;
        options.Lockout.MaxFailedAccessAttempts = identitySecurity.Lockout.MaxFailedAccessAttempts;
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(identitySecurity.Lockout.DefaultLockoutMinutes);
    })
    .AddEntityFrameworkStores<MemberCenterDbContext>()
    .AddDefaultTokenProviders();

    services.AddScoped<IAuthResourceRegistryService, AuthResourceRegistryService>();
    services.AddScoped<AdminPermissionService>();
    services.AddScoped<IAdminPermissionChecker>(provider => provider.GetRequiredService<AdminPermissionService>());
    services.AddScoped<IAdminPermissionSeeder>(provider => provider.GetRequiredService<AdminPermissionService>());

    return services.BuildServiceProvider();
}

static async Task ApplyOAuthClientPermissionsAsync(
    OpenIddictApplicationDescriptor descriptor,
    IAuthResourceRegistryService registry,
    string usage)
{
    descriptor.Permissions.Clear();
    descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Token);

    if (UsesAuthorizationCodeFlow(usage))
    {
        descriptor.Permissions.Add(OpenIddictConstants.Permissions.Endpoints.Authorization);
        descriptor.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode);
        descriptor.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.RefreshToken);
        descriptor.Permissions.Add(OpenIddictConstants.Permissions.ResponseTypes.Code);
    }
    else
    {
        descriptor.Permissions.Add(OpenIddictConstants.Permissions.GrantTypes.ClientCredentials);
    }

    var scopes = await registry.GetAllowedScopesForUsageAsync(usage);
    foreach (var scope in scopes)
    {
        descriptor.Permissions.Add(OpenIddictConstants.Permissions.Prefixes.Scope + scope);
    }
}

static bool UsesAuthorizationCodeFlow(string usage) =>
    string.Equals(usage, "web_login", StringComparison.OrdinalIgnoreCase)
    || string.Equals(usage, "webhook_outbound", StringComparison.OrdinalIgnoreCase);

static string? ResolveConnectionString(string? connectionString, string? appsettingsPath, bool noPrompt)
{
    var targetPath = ResolveAppsettingsPath(appsettingsPath);

    var envConnection = Environment.GetEnvironmentVariable("ConnectionStrings__Default")
        ?? Environment.GetEnvironmentVariable("MEMBERCENTER_CONNECTION");
    if (!string.IsNullOrWhiteSpace(envConnection))
    {
        return envConnection;
    }

    if (!string.IsNullOrWhiteSpace(connectionString))
    {
        WriteConnectionString(targetPath, connectionString);
        return connectionString;
    }

    if (File.Exists(targetPath))
    {
        var config = new ConfigurationBuilder()
            .AddJsonFile(targetPath, optional: true)
            .Build();
        var existing = config.GetConnectionString("Default");
        if (!string.IsNullOrWhiteSpace(existing))
        {
            return existing;
        }
    }

    if (noPrompt)
    {
        return null;
    }

    var host = Prompt("DB host", "localhost");
    var database = Prompt("DB name", "member_center");
    var username = Prompt("DB user", "postgres");
    var password = PromptSecret("DB password");
    var generated = $"Host={host};Database={database};Username={username};Password={password}";
    WriteConnectionString(targetPath, generated);
    return generated;
}

static string ResolveAppsettingsPath(string? appsettingsPath)
{
    if (!string.IsNullOrWhiteSpace(appsettingsPath))
    {
        return appsettingsPath;
    }

    var apiDefault = Path.Combine("src", "MemberCenter.Api", "appsettings.json");
    if (File.Exists(apiDefault))
    {
        return apiDefault;
    }

    return "appsettings.json";
}

static void WriteConnectionString(string path, string connectionString)
{
    JsonNode root;
    if (File.Exists(path))
    {
        var json = File.ReadAllText(path);
        root = JsonNode.Parse(json) ?? new JsonObject();
    }
    else
    {
        root = new JsonObject();
    }

    var obj = root as JsonObject ?? new JsonObject();
    var connectionStrings = obj["ConnectionStrings"] as JsonObject ?? new JsonObject();
    connectionStrings["Default"] = connectionString;
    obj["ConnectionStrings"] = connectionStrings;

    var options = new JsonSerializerOptions { WriteIndented = true };
    File.WriteAllText(path, obj.ToJsonString(options));
}

static async Task<bool> IsInstalledAsync(MemberCenterDbContext db)
{
    try
    {
        var flag = await db.SystemFlags.FirstOrDefaultAsync(f => f.Key == "installed");
        return flag is not null && string.Equals(flag.Value, "true", StringComparison.OrdinalIgnoreCase);
    }
    catch (PostgresException ex) when (ex.SqlState == "42P01")
    {
        return false;
    }
}

static async Task SetInstalledFlagAsync(MemberCenterDbContext db)
{
    var flag = await db.SystemFlags.FirstOrDefaultAsync(f => f.Key == "installed");
    if (flag is null)
    {
        db.SystemFlags.Add(new SystemFlag
        {
            Id = Guid.NewGuid(),
            Key = "installed",
            Value = "true",
            UpdatedAt = DateTimeOffset.UtcNow
        });
    }
    else
    {
        flag.Value = "true";
        flag.UpdatedAt = DateTimeOffset.UtcNow;
    }

    await db.SaveChangesAsync();
}

static async Task EnsureRoleAsync(RoleManager<ApplicationRole> roleManager, string roleName)
{
    if (!await roleManager.RoleExistsAsync(roleName))
    {
        var result = await roleManager.CreateAsync(new ApplicationRole
        {
            Id = Guid.NewGuid(),
            Name = roleName,
            NormalizedName = roleName.ToUpperInvariant()
        });

        if (!result.Succeeded)
        {
            throw new InvalidOperationException(string.Join(Environment.NewLine, result.Errors.Select(e => e.Description)));
        }
    }
}

static string Prompt(string label, string defaultValue)
{
    Console.Write($"{label} [{defaultValue}]: ");
    var input = Console.ReadLine();
    return string.IsNullOrWhiteSpace(input) ? defaultValue : input.Trim();
}

static string PromptSecret(string label)
{
    Console.Write($"{label}: ");
    var password = string.Empty;
    ConsoleKeyInfo key;
    while ((key = Console.ReadKey(true)).Key != ConsoleKey.Enter)
    {
        if (key.Key == ConsoleKey.Backspace && password.Length > 0)
        {
            password = password[..^1];
            Console.Write("\b \b");
            continue;
        }

        if (!char.IsControl(key.KeyChar))
        {
            password += key.KeyChar;
            Console.Write("*");
        }
    }
    Console.WriteLine();
    return password;
}
