using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Csob.Configuration;
using Csob.Credentials;

namespace Csob.Client;

/// <summary>
/// Thin HTTP wrapper around the ČSOB PSD2 resource API for a single request. It attaches the
/// mandatory COBS headers (<c>Authorization</c>, <c>APIKEY</c>, <c>TPP-Name</c>, <c>X-Request-ID</c>,
/// <c>Date</c>, <c>User-Involved</c>), sends the call over the per-request mutual-TLS client and
/// returns the response JSON verbatim (<see cref="JsonNode"/>) so no field is lost in translation.
/// Non-success responses become a <see cref="CsobApiException"/>.
/// </summary>
public sealed class CsobApiClient
{
    /// <summary>Web defaults (camelCase) match the COBS JSON contract; null properties are omitted on write.</summary>
    public static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
    {
        DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
    };

    private readonly HttpClient _http;
    private readonly CsobCredentials _credentials;
    private readonly string _baseUrl;

    public CsobApiClient(HttpClient http, CsobCredentials credentials, CsobSettings settings)
    {
        _http = http;
        _credentials = credentials;
        _baseUrl = settings.ApiBaseUrl.TrimEnd('/');
    }

    public Task<JsonNode?> GetAsync(string path, IReadOnlyDictionary<string, string?>? query, CancellationToken ct)
        => SendAsync(HttpMethod.Get, path, query, body: null, ct);

    public Task<JsonNode?> PostAsync(string path, object? body, CancellationToken ct)
        => SendAsync(HttpMethod.Post, path, query: null, body, ct);

    public Task<JsonNode?> PutAsync(string path, object? body, CancellationToken ct)
        => SendAsync(HttpMethod.Put, path, query: null, body, ct);

    public Task<JsonNode?> DeleteAsync(string path, CancellationToken ct)
        => SendAsync(HttpMethod.Delete, path, query: null, body: null, ct);

    private async Task<JsonNode?> SendAsync(
        HttpMethod method, string path, IReadOnlyDictionary<string, string?>? query, object? body, CancellationToken ct)
    {
        using var request = new HttpRequestMessage(method, _baseUrl + path + BuildQuery(query));
        ApplyHeaders(request);

        if (body is not null)
        {
            var json = body is JsonNode node ? node.ToJsonString(JsonOptions) : JsonSerializer.Serialize(body, JsonOptions);
            request.Content = new StringContent(json, Encoding.UTF8, "application/json");
        }

        using var response = await _http.SendAsync(request, ct);
        var payload = await response.Content.ReadAsStringAsync(ct);

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

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

    private void ApplyHeaders(HttpRequestMessage request)
    {
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _credentials.AccessToken);
        request.Headers.Date = DateTimeOffset.UtcNow;
        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        request.Headers.TryAddWithoutValidation("APIKEY", _credentials.ApiKey);
        request.Headers.TryAddWithoutValidation("TPP-Name", _credentials.TppName);
        request.Headers.TryAddWithoutValidation("X-Request-ID", Guid.NewGuid().ToString());
        request.Headers.TryAddWithoutValidation("User-Involved", _credentials.UserInvolved ? "true" : "false");

        if (!string.IsNullOrWhiteSpace(_credentials.UserIpAddress))
        {
            request.Headers.TryAddWithoutValidation("User-IP-Address", _credentials.UserIpAddress);
        }
    }

    private static string BuildQuery(IReadOnlyDictionary<string, string?>? query)
    {
        if (query is null)
        {
            return string.Empty;
        }

        var parts = query
            .Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value))
            .Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value!)}")
            .ToArray();

        return parts.Length == 0 ? string.Empty : "?" + string.Join("&", parts);
    }

    /// <summary>Best-effort parse of the COBS error shape <c>{ "errors": [ { "error": "CODE" } ] }</c>.</summary>
    private static IReadOnlyList<string> ParseErrorCodes(string payload)
    {
        if (string.IsNullOrWhiteSpace(payload))
        {
            return Array.Empty<string>();
        }

        try
        {
            var node = JsonNode.Parse(payload);
            if (node?["errors"] is JsonArray errors)
            {
                return errors
                    .Select(e => e?["error"]?.GetValue<string>())
                    .Where(code => !string.IsNullOrWhiteSpace(code))
                    .Select(code => code!)
                    .ToArray();
            }
        }
        catch (JsonException)
        {
            // Upstream returned a non-JSON error body; the raw payload is still carried on the exception.
        }

        return Array.Empty<string>();
    }
}
