using System; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using StellaOps.Feedser.Source.Cccs.Configuration; using StellaOps.Feedser.Source.Common.Fetch; namespace StellaOps.Feedser.Source.Cccs.Internal; public sealed class CccsFeedClient { private static readonly string[] AcceptHeaders = { "application/json", "application/vnd.api+json;q=0.9", "text/json;q=0.8", "application/*+json;q=0.7", }; private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web) { PropertyNameCaseInsensitive = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; private readonly SourceFetchService _fetchService; private readonly ILogger _logger; public CccsFeedClient(SourceFetchService fetchService, ILogger logger) { _fetchService = fetchService ?? throw new ArgumentNullException(nameof(fetchService)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } internal async Task FetchAsync(CccsFeedEndpoint endpoint, TimeSpan requestTimeout, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(endpoint); if (endpoint.Uri is null) { throw new InvalidOperationException("Feed endpoint URI must be configured."); } var request = new SourceFetchRequest(CccsOptions.HttpClientName, CccsConnectorPlugin.SourceName, endpoint.Uri) { AcceptHeaders = AcceptHeaders, TimeoutOverride = requestTimeout, Metadata = new Dictionary(StringComparer.Ordinal) { ["cccs.language"] = endpoint.Language, ["cccs.feedUri"] = endpoint.Uri.ToString(), }, }; try { var result = await _fetchService.FetchContentAsync(request, cancellationToken).ConfigureAwait(false); if (!result.IsSuccess || result.Content is null) { _logger.LogWarning("CCCS feed fetch returned no content for {Uri} (status={Status})", endpoint.Uri, result.StatusCode); return CccsFeedResult.Empty; } var feedResponse = Deserialize(result.Content); if (feedResponse is null || feedResponse.Error) { _logger.LogWarning("CCCS feed response flagged an error for {Uri}", endpoint.Uri); return CccsFeedResult.Empty; } var taxonomy = await FetchTaxonomyAsync(endpoint, requestTimeout, cancellationToken).ConfigureAwait(false); var items = (IReadOnlyList)feedResponse.Response ?? Array.Empty(); return new CccsFeedResult(items, taxonomy, result.LastModified); } catch (Exception ex) when (ex is JsonException or InvalidOperationException) { _logger.LogError(ex, "CCCS feed deserialization failed for {Uri}", endpoint.Uri); throw; } catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException) { _logger.LogWarning(ex, "CCCS feed fetch failed for {Uri}", endpoint.Uri); throw; } } private async Task> FetchTaxonomyAsync(CccsFeedEndpoint endpoint, TimeSpan timeout, CancellationToken cancellationToken) { var taxonomyUri = endpoint.BuildTaxonomyUri(); var request = new SourceFetchRequest(CccsOptions.HttpClientName, CccsConnectorPlugin.SourceName, taxonomyUri) { AcceptHeaders = AcceptHeaders, TimeoutOverride = timeout, Metadata = new Dictionary(StringComparer.Ordinal) { ["cccs.language"] = endpoint.Language, ["cccs.taxonomyUri"] = taxonomyUri.ToString(), }, }; try { var result = await _fetchService.FetchContentAsync(request, cancellationToken).ConfigureAwait(false); if (!result.IsSuccess || result.Content is null) { _logger.LogDebug("CCCS taxonomy fetch returned no content for {Uri}", taxonomyUri); return new Dictionary(0); } var taxonomyResponse = Deserialize(result.Content); if (taxonomyResponse is null || taxonomyResponse.Error) { _logger.LogDebug("CCCS taxonomy response indicated error for {Uri}", taxonomyUri); return new Dictionary(0); } var map = new Dictionary(taxonomyResponse.Response.Count); foreach (var item in taxonomyResponse.Response) { if (!string.IsNullOrWhiteSpace(item.Title)) { map[item.Id] = item.Title!; } } return map; } catch (Exception ex) when (ex is JsonException or InvalidOperationException) { _logger.LogWarning(ex, "Failed to deserialize CCCS taxonomy for {Uri}", taxonomyUri); return new Dictionary(0); } catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException) { _logger.LogWarning(ex, "CCCS taxonomy fetch failed for {Uri}", taxonomyUri); return new Dictionary(0); } } private static T? Deserialize(byte[] content) => JsonSerializer.Deserialize(content, SerializerOptions); }