using System.Net;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using PowderCoating.Application.Constants;
using PowderCoating.Application.DTOs.Columbia;
using PowderCoating.Application.Interfaces;

namespace PowderCoating.Infrastructure.Services;

/// <summary>
/// HTTP client for the Columbia Coatings product catalog API. Reads the API key and base URL from
/// configuration (<c>Columbia:ApiKey</c> / <c>Columbia:BaseUrl</c>), sends the <c>X-API-Key</c>
/// header, and pages the catalog via <c>GET /products</c>. Honors the documented rate limit
/// (120 requests / 60s) by retrying on HTTP 429 after the <c>Retry-After</c> interval.
/// </summary>
public class ColumbiaCoatingsApiClient : IColumbiaCoatingsApiClient
{
    private const int MaxRetriesPer429 = 5;
    private const int DefaultRetryAfterSeconds = 5;
    private const int MaxPagesSafetyCap = 1000; // guards against a server that never reports last page

    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IConfiguration _config;
    private readonly ILogger<ColumbiaCoatingsApiClient> _logger;

    /// <summary>
    /// Columbia returns snake_case JSON; the snake-case naming policy binds it to our PascalCase DTOs
    /// without per-property attributes. Case-insensitive as a belt-and-braces fallback.
    /// </summary>
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
        PropertyNameCaseInsensitive = true,
        Converters = { new ColumbiaImageJsonConverter() },
    };

    public ColumbiaCoatingsApiClient(
        IHttpClientFactory httpClientFactory,
        IConfiguration config,
        ILogger<ColumbiaCoatingsApiClient> logger)
    {
        _httpClientFactory = httpClientFactory;
        _config = config;
        _logger = logger;
    }

    private string? ApiKey => _config[ColumbiaIntegrationConstants.ConfigApiKey];

    private string BaseUrl =>
        (_config[ColumbiaIntegrationConstants.ConfigBaseUrl] ?? ColumbiaIntegrationConstants.DefaultBaseUrl)
            .TrimEnd('/');

    private string ApiBasePath =>
        (_config[ColumbiaIntegrationConstants.ConfigApiBasePath] ?? ColumbiaIntegrationConstants.DefaultApiBasePath)
            .Trim('/');

    /// <summary>Fully-qualified products endpoint: host + configurable API base path + /products.</summary>
    private string ProductsUrl => $"{BaseUrl}/{ApiBasePath}{ColumbiaIntegrationConstants.ProductsResource}";

    public bool IsConfigured => !string.IsNullOrWhiteSpace(ApiKey);

    /// <inheritdoc />
    public async Task<ColumbiaProductsResponse> GetProductsPageAsync(
        int page, int perPage, CancellationToken cancellationToken = default)
    {
        EnsureConfigured();
        perPage = Math.Clamp(perPage, 1, ColumbiaIntegrationConstants.MaxPerPage);
        var url = $"{ProductsUrl}?page={page}&per_page={perPage}";

        var json = await SendWithRetryAsync(url, $"page {page}", cancellationToken);
        if (json == null)
            return new ColumbiaProductsResponse();

        return JsonSerializer.Deserialize<ColumbiaProductsResponse>(json, JsonOptions) ?? new ColumbiaProductsResponse();
    }

    /// <inheritdoc />
    public async Task<ColumbiaProduct?> GetProductBySkuAsync(string sku, CancellationToken cancellationToken = default)
    {
        EnsureConfigured();
        if (string.IsNullOrWhiteSpace(sku))
            return null;

        var url = $"{ProductsUrl}?sku={Uri.EscapeDataString(sku)}&per_page=1";
        var json = await SendWithRetryAsync(url, $"sku {sku}", cancellationToken);
        if (json == null)
            return null;

        var response = JsonSerializer.Deserialize<ColumbiaProductsResponse>(json, JsonOptions);
        return response?.Items.FirstOrDefault();
    }

    /// <inheritdoc />
    public async Task<ColumbiaProduct?> GetProductByIdAsync(int id, CancellationToken cancellationToken = default)
    {
        EnsureConfigured();
        // The by-id endpoint returns a bare product object (not the {items,pagination} envelope).
        var url = $"{ProductsUrl}/{id}";
        var json = await SendWithRetryAsync(url, $"id {id}", cancellationToken);
        if (json == null)
            return null;

        return JsonSerializer.Deserialize<ColumbiaProduct>(json, JsonOptions);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<ColumbiaProduct>> GetAllProductsAsync(
        CancellationToken cancellationToken = default)
    {
        EnsureConfigured();

        var all = new List<ColumbiaProduct>();

        for (var page = 1; page <= MaxPagesSafetyCap; page++)
        {
            var response = await GetProductsPageAsync(page, ColumbiaIntegrationConstants.MaxPerPage, cancellationToken);

            if (response.Items.Count == 0)
                break;

            all.AddRange(response.Items);

            // Stop when the pagination block says we've reached the last page.
            if (response.Pagination is { TotalPages: > 0 } p && page >= p.TotalPages)
                break;
        }

        _logger.LogInformation("Columbia API: retrieved {Count} products across paged requests.", all.Count);
        return all;
    }

    /// <summary>Throws when no API key is configured so callers fail fast rather than 401.</summary>
    private void EnsureConfigured()
    {
        if (!IsConfigured)
            throw new InvalidOperationException("Columbia Coatings API key is not configured (Columbia:ApiKey).");
    }

    /// <summary>
    /// Issues a GET with the API key header and returns the response body. Retries on HTTP 429
    /// (honoring Retry-After) up to <see cref="MaxRetriesPer429"/>. Returns null on 404 so
    /// single-product lookups surface "not found" without throwing; throws on any other non-success.
    /// <paramref name="describe"/> is a short label (e.g. "page 3", "sku ABC") for log/error context.
    /// </summary>
    private async Task<string?> SendWithRetryAsync(string url, string describe, CancellationToken cancellationToken)
    {
        for (var attempt = 1; ; attempt++)
        {
            using var request = new HttpRequestMessage(HttpMethod.Get, url);
            request.Headers.Add("X-API-Key", ApiKey);

            var client = _httpClientFactory.CreateClient();
            using var response = await client.SendAsync(request, cancellationToken);

            if (response.StatusCode == HttpStatusCode.TooManyRequests)
            {
                if (attempt > MaxRetriesPer429)
                    throw new HttpRequestException(
                        $"Columbia API still rate-limiting after {MaxRetriesPer429} retries ({describe}).");

                var delaySeconds = GetRetryAfterSeconds(response) ?? DefaultRetryAfterSeconds;
                _logger.LogWarning(
                    "Columbia API returned 429 ({Describe}, attempt {Attempt}); waiting {Delay}s before retry.",
                    describe, attempt, delaySeconds);
                await Task.Delay(TimeSpan.FromSeconds(delaySeconds), cancellationToken);
                continue;
            }

            if (response.StatusCode == HttpStatusCode.NotFound)
                return null;

            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync(cancellationToken);
        }
    }

    /// <summary>
    /// Parses the <c>Retry-After</c> header (delta-seconds or HTTP-date form) into whole seconds,
    /// or null when absent/unparseable so the caller can fall back to a default.
    /// </summary>
    private static int? GetRetryAfterSeconds(HttpResponseMessage response)
    {
        var retryAfter = response.Headers.RetryAfter;
        if (retryAfter == null)
            return null;

        if (retryAfter.Delta is { } delta)
            return Math.Max(1, (int)Math.Ceiling(delta.TotalSeconds));

        if (retryAfter.Date is { } date)
        {
            var seconds = (int)Math.Ceiling((date - DateTimeOffset.UtcNow).TotalSeconds);
            return seconds > 0 ? seconds : 1;
        }

        return null;
    }
}
