using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Options;

namespace ChessCubing.Server.Auth;

public sealed class KeycloakAuthService(HttpClient httpClient, IOptions<KeycloakAuthOptions> options)
{
    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
    {
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    };

    private readonly HttpClient _httpClient = httpClient;
    private readonly KeycloakAuthOptions _options = options.Value;

    public async Task<KeycloakUserInfo> LoginAsync(string username, string password, CancellationToken cancellationToken)
    {
        var tokenResponse = await RequestPasswordTokenAsync(username, password, cancellationToken);
        return await GetUserInfoAsync(tokenResponse.AccessToken!, cancellationToken);
    }

    public async Task<KeycloakUserInfo> RegisterAsync(RegisterRequest request, CancellationToken cancellationToken)
    {
        var adminToken = await RequestAdminTokenAsync(cancellationToken);
        var userId = await CreateUserAsync(adminToken, request, cancellationToken);
        await SetPasswordAsync(adminToken, userId, request.Password, cancellationToken);
        await TryAssignPlayerRoleAsync(adminToken, userId, cancellationToken);
        return await LoginAsync(request.Username, request.Password, cancellationToken);
    }

    private async Task<TokenSuccessResponse> RequestPasswordTokenAsync(string username, string password, CancellationToken cancellationToken)
    {
        var formData = new Dictionary<string, string>
        {
            ["client_id"] = _options.ClientId,
            ["grant_type"] = "password",
            ["scope"] = "openid profile email",
            ["username"] = username,
            ["password"] = password,
        };

        using var response = await _httpClient.PostAsync(
            $"{GetRealmBaseUrl()}/protocol/openid-connect/token",
            new FormUrlEncodedContent(formData),
            cancellationToken);

        if (!response.IsSuccessStatusCode)
        {
            var error = await ReadJsonAsync<TokenErrorResponse>(response, cancellationToken);
            var message = error?.ErrorDescription switch
            {
                not null when error.ErrorDescription.Contains("Account is not fully set up", StringComparison.OrdinalIgnoreCase)
                    => "Le compte existe mais n'est pas encore actif dans Keycloak.",
                not null => "Identifiants invalides ou connexion Keycloak indisponible.",
                _ => "Connexion Keycloak impossible.",
            };

            throw new KeycloakAuthException(message, (int)response.StatusCode);
        }

        var token = await ReadJsonAsync<TokenSuccessResponse>(response, cancellationToken);
        if (token?.AccessToken is null)
        {
            throw new KeycloakAuthException("Keycloak n'a pas retourne de jeton utilisable.");
        }

        return token;
    }

    private async Task<string> RequestAdminTokenAsync(CancellationToken cancellationToken)
    {
        var formData = new Dictionary<string, string>
        {
            ["client_id"] = _options.AdminClientId,
            ["grant_type"] = "password",
            ["username"] = _options.AdminUsername,
            ["password"] = _options.AdminPassword,
        };

        using var response = await _httpClient.PostAsync(
            $"{GetBaseUrl()}/realms/{Uri.EscapeDataString(_options.AdminRealm)}/protocol/openid-connect/token",
            new FormUrlEncodedContent(formData),
            cancellationToken);

        if (!response.IsSuccessStatusCode)
        {
            throw new KeycloakAuthException("Impossible d'obtenir un acces admin Keycloak.", (int)response.StatusCode);
        }

        var token = await ReadJsonAsync<TokenSuccessResponse>(response, cancellationToken);
        if (string.IsNullOrWhiteSpace(token?.AccessToken))
        {
            throw new KeycloakAuthException("Keycloak n'a pas retourne de jeton admin.");
        }

        return token.AccessToken;
    }

    private async Task<KeycloakUserInfo> GetUserInfoAsync(string accessToken, CancellationToken cancellationToken)
    {
        using var request = new HttpRequestMessage(HttpMethod.Get, $"{GetRealmBaseUrl()}/protocol/openid-connect/userinfo");
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

        using var response = await _httpClient.SendAsync(request, cancellationToken);
        if (!response.IsSuccessStatusCode)
        {
            throw new KeycloakAuthException("Impossible de recuperer le profil utilisateur Keycloak.", (int)response.StatusCode);
        }

        var userInfo = await ReadJsonAsync<KeycloakUserInfo>(response, cancellationToken);
        if (userInfo is null)
        {
            throw new KeycloakAuthException("Le profil Keycloak est invalide.");
        }

        userInfo.Roles = ExtractRealmRoles(accessToken);
        return userInfo;
    }

    private async Task<string> CreateUserAsync(string adminToken, RegisterRequest request, CancellationToken cancellationToken)
    {
        using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"{GetAdminBaseUrl()}/users")
        {
            Content = JsonContent.Create(new
            {
                username = request.Username.Trim(),
                email = request.Email.Trim(),
                enabled = true,
                emailVerified = false,
                firstName = string.IsNullOrWhiteSpace(request.FirstName) ? null : request.FirstName.Trim(),
                lastName = string.IsNullOrWhiteSpace(request.LastName) ? null : request.LastName.Trim(),
            }, options: JsonOptions)
        };
        httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);

        using var response = await _httpClient.SendAsync(httpRequest, cancellationToken);
        if (response.StatusCode == HttpStatusCode.Conflict)
        {
            throw new KeycloakAuthException("Ce nom d'utilisateur ou cet email existe deja.", StatusCodes.Status409Conflict);
        }

        if (!response.IsSuccessStatusCode)
        {
            throw new KeycloakAuthException("Creation du compte impossible dans Keycloak.", (int)response.StatusCode);
        }

        var userId = response.Headers.Location?.Segments.LastOrDefault();
        if (!string.IsNullOrWhiteSpace(userId))
        {
            return userId.Trim('/');
        }

        var fallbackUserId = await FindUserIdByUsernameAsync(adminToken, request.Username, cancellationToken);
        if (!string.IsNullOrWhiteSpace(fallbackUserId))
        {
            return fallbackUserId;
        }

        throw new KeycloakAuthException("Le compte a ete cree mais l'identifiant Keycloak est introuvable.");
    }

    private async Task SetPasswordAsync(string adminToken, string userId, string password, CancellationToken cancellationToken)
    {
        using var httpRequest = new HttpRequestMessage(HttpMethod.Put, $"{GetAdminBaseUrl()}/users/{Uri.EscapeDataString(userId)}/reset-password")
        {
            Content = JsonContent.Create(new
            {
                type = "password",
                value = password,
                temporary = false,
            })
        };
        httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);

        using var response = await _httpClient.SendAsync(httpRequest, cancellationToken);
        if (!response.IsSuccessStatusCode)
        {
            throw new KeycloakAuthException("Le compte a ete cree mais le mot de passe n'a pas pu etre defini.", (int)response.StatusCode);
        }
    }

    private async Task TryAssignPlayerRoleAsync(string adminToken, string userId, CancellationToken cancellationToken)
    {
        using var roleRequest = new HttpRequestMessage(HttpMethod.Get, $"{GetAdminBaseUrl()}/roles/player");
        roleRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);

        using var roleResponse = await _httpClient.SendAsync(roleRequest, cancellationToken);
        if (!roleResponse.IsSuccessStatusCode)
        {
            return;
        }

        var role = await ReadJsonAsync<RealmRoleRepresentation>(roleResponse, cancellationToken);
        if (role?.Name is null)
        {
            return;
        }

        using var assignRequest = new HttpRequestMessage(HttpMethod.Post, $"{GetAdminBaseUrl()}/users/{Uri.EscapeDataString(userId)}/role-mappings/realm")
        {
            Content = JsonContent.Create(new[]
            {
                new RealmRoleRepresentation
                {
                    Id = role.Id,
                    Name = role.Name,
                    Description = role.Description,
                }
            })
        };
        assignRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);

        using var assignResponse = await _httpClient.SendAsync(assignRequest, cancellationToken);
        _ = assignResponse.IsSuccessStatusCode;
    }

    private async Task<string?> FindUserIdByUsernameAsync(string adminToken, string username, CancellationToken cancellationToken)
    {
        using var httpRequest = new HttpRequestMessage(
            HttpMethod.Get,
            $"{GetAdminBaseUrl()}/users?username={Uri.EscapeDataString(username)}&exact=true");
        httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", adminToken);

        using var response = await _httpClient.SendAsync(httpRequest, cancellationToken);
        if (!response.IsSuccessStatusCode)
        {
            return null;
        }

        var users = await ReadJsonAsync<List<UserRepresentation>>(response, cancellationToken);
        return users?.FirstOrDefault()?.Id;
    }

    private string[] ExtractRealmRoles(string accessToken)
    {
        try
        {
            var tokenParts = accessToken.Split('.');
            if (tokenParts.Length < 2)
            {
                return [];
            }

            var payloadBytes = DecodeBase64Url(tokenParts[1]);
            using var document = JsonDocument.Parse(payloadBytes);
            if (!document.RootElement.TryGetProperty("realm_access", out var realmAccess) ||
                !realmAccess.TryGetProperty("roles", out var rolesElement) ||
                rolesElement.ValueKind != JsonValueKind.Array)
            {
                return [];
            }

            return rolesElement
                .EnumerateArray()
                .Select(role => role.GetString())
                .Where(role => !string.IsNullOrWhiteSpace(role))
                .Cast<string>()
                .ToArray();
        }
        catch
        {
            return [];
        }
    }

    private static byte[] DecodeBase64Url(string input)
    {
        var normalized = input.Replace('-', '+').Replace('_', '/');
        normalized = normalized.PadRight(normalized.Length + (4 - normalized.Length % 4) % 4, '=');
        return Convert.FromBase64String(normalized);
    }

    private string GetBaseUrl()
        => _options.BaseUrl.TrimEnd('/');

    private string GetRealmBaseUrl()
        => $"{GetBaseUrl()}/realms/{Uri.EscapeDataString(_options.Realm)}";

    private string GetAdminBaseUrl()
        => $"{GetBaseUrl()}/admin/realms/{Uri.EscapeDataString(_options.Realm)}";

    private static async Task<T?> ReadJsonAsync<T>(HttpResponseMessage response, CancellationToken cancellationToken)
    {
        var content = await response.Content.ReadAsStringAsync(cancellationToken);
        if (string.IsNullOrWhiteSpace(content))
        {
            return default;
        }

        return JsonSerializer.Deserialize<T>(content, JsonOptions);
    }

    private sealed class TokenSuccessResponse
    {
        [JsonPropertyName("access_token")]
        public string? AccessToken { get; init; }
    }

    private sealed class TokenErrorResponse
    {
        [JsonPropertyName("error")]
        public string? Error { get; init; }

        [JsonPropertyName("error_description")]
        public string? ErrorDescription { get; init; }
    }

    private sealed class UserRepresentation
    {
        [JsonPropertyName("id")]
        public string? Id { get; init; }
    }

    private sealed class RealmRoleRepresentation
    {
        [JsonPropertyName("id")]
        public string? Id { get; init; }

        [JsonPropertyName("name")]
        public string? Name { get; init; }

        [JsonPropertyName("description")]
        public string? Description { get; init; }
    }
}

public sealed class KeycloakAuthException(string message, int statusCode = StatusCodes.Status400BadRequest) : Exception(message)
{
    public int StatusCode { get; } = statusCode;
}
