using System.Security.Claims;
using System.Net.Mail;
using ChessCubing.Server.Admin;
using ChessCubing.Server.Auth;
using ChessCubing.Server.Data;
using ChessCubing.Server.Users;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOptions<KeycloakAuthOptions>()
    .Configure<IConfiguration>((options, configuration) =>
    {
        options.BaseUrl = configuration["KEYCLOAK_BASE_URL"] ?? options.BaseUrl;
        options.Realm = configuration["KEYCLOAK_REALM"] ?? options.Realm;
        options.ClientId = configuration["KEYCLOAK_CLIENT_ID"] ?? options.ClientId;
        options.AdminRealm = configuration["KEYCLOAK_ADMIN_REALM"] ?? options.AdminRealm;
        options.AdminClientId = configuration["KEYCLOAK_ADMIN_CLIENT_ID"] ?? options.AdminClientId;
        options.AdminUsername = configuration["KEYCLOAK_ADMIN_USERNAME"] ?? options.AdminUsername;
        options.AdminPassword = configuration["KEYCLOAK_ADMIN_PASSWORD"] ?? options.AdminPassword;
    });

builder.Services.AddOptions<SiteDataOptions>()
    .Configure<IConfiguration>((options, configuration) =>
    {
        options.Host = configuration["SITE_DB_HOST"] ?? options.Host;
        options.Database = configuration["SITE_DB_NAME"] ?? options.Database;
        options.Username = configuration["SITE_DB_USER"] ?? options.Username;
        options.Password = configuration["SITE_DB_PASSWORD"] ?? options.Password;

        if (int.TryParse(configuration["SITE_DB_PORT"], out var port) && port > 0)
        {
            options.Port = port;
        }
    });

builder.Services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.Name = "chesscubing.auth";
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.Lax;
        options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
        options.SlidingExpiration = true;
        options.ExpireTimeSpan = TimeSpan.FromDays(7);
        options.Events = new CookieAuthenticationEvents
        {
            OnRedirectToLogin = context =>
            {
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                return Task.CompletedTask;
            },
            OnRedirectToAccessDenied = context =>
            {
                context.Response.StatusCode = StatusCodes.Status403Forbidden;
                return Task.CompletedTask;
            },
        };
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy => policy.RequireRole("admin"));
});
builder.Services.AddHttpClient<KeycloakAuthService>();
builder.Services.AddSingleton<MySqlUserProfileStore>();

var app = builder.Build();

await using (var scope = app.Services.CreateAsyncScope())
{
    var profileStore = scope.ServiceProvider.GetRequiredService<MySqlUserProfileStore>();
    await profileStore.InitializeAsync(CancellationToken.None);
}

app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/api/health", () => TypedResults.Ok(new { status = "ok" }));

app.MapGet("/api/auth/session", (ClaimsPrincipal user) =>
    TypedResults.Ok(AuthSessionResponse.FromUser(user)));

app.MapGet("/api/users/me", async Task<IResult> (
    ClaimsPrincipal user,
    MySqlUserProfileStore profileStore,
    CancellationToken cancellationToken) =>
{
    var siteUser = AuthenticatedSiteUserFactory.FromClaimsPrincipal(user);
    if (siteUser is null)
    {
        return TypedResults.BadRequest(new ApiErrorResponse("La session utilisateur est incomplete."));
    }

    var profile = await profileStore.GetOrCreateAsync(siteUser, cancellationToken);
    return TypedResults.Ok(profile);
}).RequireAuthorization();

app.MapPut("/api/users/me", async Task<IResult> (
    UpdateUserProfileRequest request,
    ClaimsPrincipal user,
    MySqlUserProfileStore profileStore,
    CancellationToken cancellationToken) =>
{
    var siteUser = AuthenticatedSiteUserFactory.FromClaimsPrincipal(user);
    if (siteUser is null)
    {
        return TypedResults.BadRequest(new ApiErrorResponse("La session utilisateur est incomplete."));
    }

    try
    {
        var profile = await profileStore.UpdateAsync(siteUser, request, cancellationToken);
        return TypedResults.Ok(profile);
    }
    catch (UserProfileValidationException exception)
    {
        return TypedResults.BadRequest(new ApiErrorResponse(exception.Message));
    }
}).RequireAuthorization();

var adminGroup = app.MapGroup("/api/admin")
    .RequireAuthorization("AdminOnly");

adminGroup.MapGet("/users", async Task<IResult> (
    KeycloakAuthService keycloak,
    MySqlUserProfileStore profileStore,
    CancellationToken cancellationToken) =>
{
    var identityUsersTask = keycloak.GetAdminUsersAsync(cancellationToken);
    var siteProfilesTask = profileStore.ListAsync(cancellationToken);

    await Task.WhenAll(identityUsersTask, siteProfilesTask);

    var siteProfilesBySubject = (await siteProfilesTask)
        .ToDictionary(profile => profile.Subject, StringComparer.Ordinal);

    var users = (await identityUsersTask)
        .Select(identity => MapAdminSummary(identity, siteProfilesBySubject.GetValueOrDefault(identity.Subject)))
        .OrderByDescending(user => user.SiteProfileUpdatedUtc ?? user.AccountCreatedUtc ?? DateTime.MinValue)
        .ThenBy(user => user.Username, StringComparer.OrdinalIgnoreCase)
        .ToArray();

    return TypedResults.Ok(users);
});

adminGroup.MapPost("/users", async Task<IResult> (
    AdminCreateUserRequest request,
    KeycloakAuthService keycloak,
    MySqlUserProfileStore profileStore,
    CancellationToken cancellationToken) =>
{
    try
    {
        var normalized = NormalizeAdminCreate(request);
        var fallbackDisplayName = BuildIdentityDisplayNameFromParts(normalized.FirstName, normalized.LastName, normalized.Username);
        var siteProfileRequest = new UpdateUserProfileRequest
        {
            DisplayName = request.DisplayName,
            Club = request.Club,
            City = request.City,
            PreferredFormat = request.PreferredFormat,
            FavoriteCube = request.FavoriteCube,
            Bio = request.Bio,
        };

        profileStore.ValidateAdminUpdate(fallbackDisplayName, siteProfileRequest);

        var createdIdentity = await keycloak.CreateAdminUserAsync(
            new AdminIdentityUserCreateRequest(
                normalized.Username,
                normalized.Email,
                normalized.Password,
                normalized.FirstName,
                normalized.LastName,
                normalized.IsEnabled,
                normalized.IsEmailVerified),
            cancellationToken);

        var createdProfile = await profileStore.AdminUpsertAsync(
            createdIdentity.Subject,
            createdIdentity.Username,
            createdIdentity.Email,
            BuildIdentityDisplayName(createdIdentity),
            siteProfileRequest,
            cancellationToken);

        return TypedResults.Created($"/api/admin/users/{Uri.EscapeDataString(createdIdentity.Subject)}", MapAdminDetail(createdIdentity, createdProfile));
    }
    catch (AdminUserValidationException exception)
    {
        return TypedResults.BadRequest(new ApiErrorResponse(exception.Message));
    }
    catch (UserProfileValidationException exception)
    {
        return TypedResults.BadRequest(new ApiErrorResponse(exception.Message));
    }
    catch (KeycloakAuthException exception)
    {
        return TypedResults.Json(new ApiErrorResponse(exception.Message), statusCode: exception.StatusCode);
    }
});

adminGroup.MapGet("/users/{subject}", async Task<IResult> (
    string subject,
    KeycloakAuthService keycloak,
    MySqlUserProfileStore profileStore,
    CancellationToken cancellationToken) =>
{
    try
    {
        var identityTask = keycloak.GetAdminUserAsync(subject, cancellationToken);
        var profileTask = profileStore.FindBySubjectAsync(subject, cancellationToken);

        await Task.WhenAll(identityTask, profileTask);
        return TypedResults.Ok(MapAdminDetail(await identityTask, await profileTask));
    }
    catch (KeycloakAuthException exception)
    {
        return TypedResults.Json(new ApiErrorResponse(exception.Message), statusCode: exception.StatusCode);
    }
});

adminGroup.MapDelete("/users/{subject}", async Task<IResult> (
    string subject,
    KeycloakAuthService keycloak,
    MySqlUserProfileStore profileStore,
    CancellationToken cancellationToken) =>
{
    try
    {
        await keycloak.DeleteAdminUserAsync(subject, cancellationToken);
        await profileStore.DeleteAsync(subject, cancellationToken);
        return TypedResults.NoContent();
    }
    catch (KeycloakAuthException exception)
    {
        return TypedResults.Json(new ApiErrorResponse(exception.Message), statusCode: exception.StatusCode);
    }
});

adminGroup.MapPut("/users/{subject}", async Task<IResult> (
    string subject,
    AdminUpdateUserRequest request,
    KeycloakAuthService keycloak,
    MySqlUserProfileStore profileStore,
    CancellationToken cancellationToken) =>
{
    try
    {
        var normalized = NormalizeAdminUpdate(request);
        var fallbackDisplayName = BuildIdentityDisplayNameFromParts(normalized.FirstName, normalized.LastName, normalized.Username);
        var siteProfileRequest = new UpdateUserProfileRequest
        {
            DisplayName = request.DisplayName,
            Club = request.Club,
            City = request.City,
            PreferredFormat = request.PreferredFormat,
            FavoriteCube = request.FavoriteCube,
            Bio = request.Bio,
        };

        profileStore.ValidateAdminUpdate(fallbackDisplayName, siteProfileRequest);

        var updatedIdentity = await keycloak.UpdateAdminUserAsync(
            subject,
            new AdminIdentityUserUpdateRequest(
                normalized.Username,
                normalized.Email,
                normalized.FirstName,
                normalized.LastName,
                normalized.IsEnabled,
                normalized.IsEmailVerified),
            cancellationToken);

        var updatedProfile = await profileStore.AdminUpsertAsync(
            updatedIdentity.Subject,
            updatedIdentity.Username,
            updatedIdentity.Email,
            BuildIdentityDisplayName(updatedIdentity),
            siteProfileRequest,
            cancellationToken);

        return TypedResults.Ok(MapAdminDetail(updatedIdentity, updatedProfile));
    }
    catch (AdminUserValidationException exception)
    {
        return TypedResults.BadRequest(new ApiErrorResponse(exception.Message));
    }
    catch (UserProfileValidationException exception)
    {
        return TypedResults.BadRequest(new ApiErrorResponse(exception.Message));
    }
    catch (KeycloakAuthException exception)
    {
        return TypedResults.Json(new ApiErrorResponse(exception.Message), statusCode: exception.StatusCode);
    }
});

app.MapPost("/api/auth/login", async Task<IResult> (
    LoginRequest request,
    HttpContext httpContext,
    KeycloakAuthService keycloak,
    CancellationToken cancellationToken) =>
{
    if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password))
    {
        return TypedResults.BadRequest(new ApiErrorResponse("Nom d'utilisateur et mot de passe obligatoires."));
    }

    try
    {
        var userInfo = await keycloak.LoginAsync(request.Username.Trim(), request.Password, cancellationToken);
        await SignInAsync(httpContext, userInfo);
        return TypedResults.Ok(AuthSessionResponse.FromUser(httpContext.User));
    }
    catch (KeycloakAuthException exception)
    {
        return TypedResults.Json(new ApiErrorResponse(exception.Message), statusCode: exception.StatusCode);
    }
});

app.MapPost("/api/auth/register", async Task<IResult> (
    RegisterRequest request,
    HttpContext httpContext,
    KeycloakAuthService keycloak,
    CancellationToken cancellationToken) =>
{
    if (string.IsNullOrWhiteSpace(request.Username) ||
        string.IsNullOrWhiteSpace(request.Email) ||
        string.IsNullOrWhiteSpace(request.Password))
    {
        return TypedResults.BadRequest(new ApiErrorResponse("Nom d'utilisateur, email et mot de passe obligatoires."));
    }

    if (!string.Equals(request.Password, request.ConfirmPassword, StringComparison.Ordinal))
    {
        return TypedResults.BadRequest(new ApiErrorResponse("Les mots de passe ne correspondent pas."));
    }

    try
    {
        var userInfo = await keycloak.RegisterAsync(request with
        {
            Username = request.Username.Trim(),
            Email = request.Email.Trim(),
            FirstName = request.FirstName?.Trim(),
            LastName = request.LastName?.Trim(),
        }, cancellationToken);

        await SignInAsync(httpContext, userInfo);
        return TypedResults.Ok(AuthSessionResponse.FromUser(httpContext.User));
    }
    catch (KeycloakAuthException exception)
    {
        return TypedResults.Json(new ApiErrorResponse(exception.Message), statusCode: exception.StatusCode);
    }
});

app.MapPost("/api/auth/logout", async Task<IResult> (HttpContext httpContext) =>
{
    await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    return TypedResults.Ok(AuthSessionResponse.FromUser(new ClaimsPrincipal(new ClaimsIdentity())));
});

app.MapGet("/api/auth/logout/browser", async Task<IResult> (HttpContext httpContext) =>
{
    await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    return TypedResults.Redirect("/index.html");
});

app.Run();

static AdminUserSummaryResponse MapAdminSummary(AdminIdentityUser identity, UserProfileResponse? profile)
    => new()
    {
        Subject = identity.Subject,
        Username = identity.Username,
        Email = identity.Email,
        IdentityDisplayName = BuildIdentityDisplayName(identity),
        SiteDisplayName = profile?.DisplayName,
        IsEnabled = identity.IsEnabled,
        IsEmailVerified = identity.IsEmailVerified,
        HasSiteProfile = profile is not null,
        Club = profile?.Club,
        City = profile?.City,
        PreferredFormat = profile?.PreferredFormat,
        AccountCreatedUtc = identity.CreatedUtc,
        SiteProfileUpdatedUtc = profile?.UpdatedUtc,
    };

static AdminUserDetailResponse MapAdminDetail(AdminIdentityUser identity, UserProfileResponse? profile)
{
    var identityDisplayName = BuildIdentityDisplayName(identity);

    return new AdminUserDetailResponse
    {
        Subject = identity.Subject,
        Username = identity.Username,
        Email = identity.Email,
        FirstName = identity.FirstName,
        LastName = identity.LastName,
        IdentityDisplayName = identityDisplayName,
        IsEnabled = identity.IsEnabled,
        IsEmailVerified = identity.IsEmailVerified,
        AccountCreatedUtc = identity.CreatedUtc,
        HasSiteProfile = profile is not null,
        DisplayName = profile?.DisplayName ?? identityDisplayName,
        Club = profile?.Club,
        City = profile?.City,
        PreferredFormat = profile?.PreferredFormat,
        FavoriteCube = profile?.FavoriteCube,
        Bio = profile?.Bio,
        SiteProfileCreatedUtc = profile?.CreatedUtc,
        SiteProfileUpdatedUtc = profile?.UpdatedUtc,
    };
}

static NormalizedAdminUserUpdate NormalizeAdminUpdate(AdminUpdateUserRequest request)
{
    var username = NormalizeRequiredValue(request.Username, "nom d'utilisateur", 120);
    var email = NormalizeEmail(request.Email);
    var firstName = NormalizeOptionalValue(request.FirstName, "prenom", 120);
    var lastName = NormalizeOptionalValue(request.LastName, "nom", 120);

    return new NormalizedAdminUserUpdate(
        username,
        email,
        firstName,
        lastName,
        request.IsEnabled,
        request.IsEmailVerified);
}

static NormalizedAdminCreateUser NormalizeAdminCreate(AdminCreateUserRequest request)
{
    var username = NormalizeRequiredValue(request.Username, "nom d'utilisateur", 120);
    var email = NormalizeEmail(request.Email);
    var password = NormalizePassword(request.Password, request.ConfirmPassword);
    var firstName = NormalizeOptionalValue(request.FirstName, "prenom", 120);
    var lastName = NormalizeOptionalValue(request.LastName, "nom", 120);

    return new NormalizedAdminCreateUser(
        username,
        email,
        password,
        firstName,
        lastName,
        request.IsEnabled,
        request.IsEmailVerified);
}

static string BuildIdentityDisplayName(AdminIdentityUser identity)
    => BuildIdentityDisplayNameFromParts(identity.FirstName, identity.LastName, identity.Username);

static string BuildIdentityDisplayNameFromParts(string? firstName, string? lastName, string username)
{
    var fullName = string.Join(' ', new[] { firstName, lastName }.Where(value => !string.IsNullOrWhiteSpace(value)));
    return string.IsNullOrWhiteSpace(fullName)
        ? username
        : fullName;
}

static string NormalizeRequiredValue(string? value, string fieldName, int maxLength)
{
    var normalized = NormalizeOptionalValue(value, fieldName, maxLength);
    return normalized ?? throw new AdminUserValidationException($"Le champ {fieldName} est obligatoire.");
}

static string? NormalizeEmail(string? value)
{
    var normalized = NormalizeOptionalValue(value, "email", 255);
    if (normalized is null)
    {
        return null;
    }

    try
    {
        _ = new MailAddress(normalized);
        return normalized;
    }
    catch (FormatException)
    {
        throw new AdminUserValidationException("L'email n'est pas valide.");
    }
}

static string NormalizePassword(string? password, string? confirmPassword)
{
    if (string.IsNullOrWhiteSpace(password))
    {
        throw new AdminUserValidationException("Le mot de passe est obligatoire.");
    }

    if (!string.Equals(password, confirmPassword, StringComparison.Ordinal))
    {
        throw new AdminUserValidationException("Les mots de passe ne correspondent pas.");
    }

    if (password.Length < 8)
    {
        throw new AdminUserValidationException("Le mot de passe doit contenir au moins 8 caracteres.");
    }

    return password;
}

static string? NormalizeOptionalValue(string? value, string fieldName, int maxLength)
{
    var trimmed = value?.Trim();
    if (string.IsNullOrWhiteSpace(trimmed))
    {
        return null;
    }

    if (trimmed.Length > maxLength)
    {
        throw new AdminUserValidationException($"Le champ {fieldName} depasse {maxLength} caracteres.");
    }

    return trimmed;
}

static async Task SignInAsync(HttpContext httpContext, KeycloakUserInfo userInfo)
{
    var claims = new List<Claim>();

    if (!string.IsNullOrWhiteSpace(userInfo.Subject))
    {
        claims.Add(new Claim("sub", userInfo.Subject));
        claims.Add(new Claim(ClaimTypes.NameIdentifier, userInfo.Subject));
    }

    if (!string.IsNullOrWhiteSpace(userInfo.PreferredUsername))
    {
        claims.Add(new Claim("preferred_username", userInfo.PreferredUsername));
        claims.Add(new Claim(ClaimTypes.Name, userInfo.PreferredUsername));
    }

    if (!string.IsNullOrWhiteSpace(userInfo.Name))
    {
        claims.Add(new Claim("name", userInfo.Name));
    }

    if (!string.IsNullOrWhiteSpace(userInfo.Email))
    {
        claims.Add(new Claim("email", userInfo.Email));
        claims.Add(new Claim(ClaimTypes.Email, userInfo.Email));
    }

    foreach (var role in userInfo.Roles.Where(role => !string.IsNullOrWhiteSpace(role)))
    {
        claims.Add(new Claim("role", role));
        claims.Add(new Claim(ClaimTypes.Role, role));
    }

    var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
    var principal = new ClaimsPrincipal(identity);

    await httpContext.SignInAsync(
        CookieAuthenticationDefaults.AuthenticationScheme,
        principal,
        new AuthenticationProperties
        {
            IsPersistent = true,
            AllowRefresh = true,
            ExpiresUtc = DateTimeOffset.UtcNow.AddDays(7),
        });

    httpContext.User = principal;
}

sealed record NormalizedAdminUserUpdate(
    string Username,
    string? Email,
    string? FirstName,
    string? LastName,
    bool IsEnabled,
    bool IsEmailVerified);

sealed record NormalizedAdminCreateUser(
    string Username,
    string? Email,
    string Password,
    string? FirstName,
    string? LastName,
    bool IsEnabled,
    bool IsEmailVerified);
