using System.Net;
using System.Text.Json.Nodes;
using Csob.Client;
using Csob.Configuration;
using Csob.Credentials;
using Csob.Models;

namespace Csob.Services;

/// <summary>
/// OAuth2 Authorization Code helper for the ČSOB PSD2 flow. Builds the PSU authorization URL and
/// exchanges/refreshes tokens against the ČSOB token endpoint. The token endpoint is behind mutual
/// TLS, so the client certificate header is required for the token/refresh calls. Client id/secret
/// are taken from per-request headers (this service is multi-tenant and stores no app credentials).
/// </summary>
public sealed class OAuthService
{
    private readonly RequestCredentialsProvider _credentials;
    private readonly CsobHttpClientProvider _httpClientProvider;
    private readonly CsobSettings _settings;

    public OAuthService(
        RequestCredentialsProvider credentials,
        CsobHttpClientProvider httpClientProvider,
        CsobSettings settings)
    {
        _credentials = credentials;
        _httpClientProvider = httpClientProvider;
        _settings = settings;
    }

    /// <summary>Builds the authorization URL to which the PSU must be redirected.</summary>
    public AuthorizationUrlResponse BuildAuthorizationUrl(string redirectUri, string? scope, string? state)
    {
        var clientId = RequireHeader(CredentialConstants.ClientIdHeader);

        var query = new Dictionary<string, string?>
        {
            ["response_type"] = "code",
            ["client_id"] = clientId,
            ["redirect_uri"] = redirectUri,
            ["scope"] = scope,
            ["state"] = state,
        };

        var url = _settings.OAuthAuthorizeUrl + "?" + string.Join("&", query
            .Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value))
            .Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value!)}"));

        return new AuthorizationUrlResponse { AuthorizationUrl = url, State = state };
    }

    public Task<JsonNode?> ExchangeCodeAsync(TokenExchangeRequest request, CancellationToken ct)
        => PostTokenAsync(new Dictionary<string, string>
        {
            ["grant_type"] = "authorization_code",
            ["code"] = request.Code,
            ["redirect_uri"] = request.RedirectUri,
        }, ct);

    public Task<JsonNode?> RefreshAsync(TokenRefreshRequest request, CancellationToken ct)
        => PostTokenAsync(new Dictionary<string, string>
        {
            ["grant_type"] = "refresh_token",
            ["refresh_token"] = request.RefreshToken,
        }, ct);

    private async Task<JsonNode?> PostTokenAsync(Dictionary<string, string> form, CancellationToken ct)
    {
        var clientId = RequireHeader(CredentialConstants.ClientIdHeader);
        var clientSecret = RequireHeader(CredentialConstants.ClientSecretHeader);

        var certificate = _credentials.TryBuildCertificate()
            ?? throw new MissingCredentialsException(new[] { CredentialConstants.CertificateHeader });

        form["client_id"] = clientId;
        form["client_secret"] = clientSecret;

        var http = _httpClientProvider.GetClient(certificate);

        using var content = new FormUrlEncodedContent(form);
        using var response = await http.PostAsync(_settings.OAuthTokenUrl, content, ct);
        var payload = await response.Content.ReadAsStringAsync(ct);

        if (!response.IsSuccessStatusCode)
        {
            throw new CsobApiException(response.StatusCode, ParseOAuthError(payload), payload);
        }

        return string.IsNullOrWhiteSpace(payload) ? null : JsonNode.Parse(payload);
    }

    private string RequireHeader(string name)
        => _credentials.Header(name) ?? throw new MissingCredentialsException(new[] { name });

    private static IReadOnlyList<string> ParseOAuthError(string payload)
    {
        if (string.IsNullOrWhiteSpace(payload))
        {
            return Array.Empty<string>();
        }

        try
        {
            // OAuth2 errors use { "error": "...", "error_description": "..." }.
            var error = JsonNode.Parse(payload)?["error"]?.GetValue<string>();
            return string.IsNullOrWhiteSpace(error) ? Array.Empty<string>() : new[] { error! };
        }
        catch (System.Text.Json.JsonException)
        {
            return Array.Empty<string>();
        }
    }
}
