using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using TrueCV.Application.DTOs; using TrueCV.Infrastructure.Configuration; namespace TrueCV.Infrastructure.ExternalApis; public sealed class CompaniesHouseClient { private readonly HttpClient _httpClient; private readonly ILogger _logger; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNameCaseInsensitive = true }; public CompaniesHouseClient( HttpClient httpClient, IOptions settings, ILogger logger) { _httpClient = httpClient; _logger = logger; var apiKey = settings.Value.ApiKey; var authValue = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{apiKey}:")); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authValue); _httpClient.BaseAddress = new Uri(settings.Value.BaseUrl); } public async Task SearchCompaniesAsync( string query, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(query); var encodedQuery = Uri.EscapeDataString(query); var requestUrl = $"/search/companies?q={encodedQuery}"; _logger.LogDebug("Searching Companies House for: {Query}", query); try { var response = await _httpClient.GetAsync(requestUrl, cancellationToken); if (response.StatusCode == HttpStatusCode.TooManyRequests) { _logger.LogWarning("Rate limit exceeded for Companies House API"); throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later."); } response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync( JsonOptions, cancellationToken); _logger.LogDebug("Found {Count} companies matching query: {Query}", result?.Items?.Count ?? 0, query); return result; } catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests) { _logger.LogWarning("Rate limit exceeded for Companies House API"); throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later.", ex); } } public async Task GetCompanyAsync( string companyNumber, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrWhiteSpace(companyNumber); var requestUrl = $"/company/{companyNumber}"; _logger.LogDebug("Fetching company details for: {CompanyNumber}", companyNumber); try { var response = await _httpClient.GetAsync(requestUrl, cancellationToken); if (response.StatusCode == HttpStatusCode.NotFound) { _logger.LogDebug("Company not found: {CompanyNumber}", companyNumber); return null; } if (response.StatusCode == HttpStatusCode.TooManyRequests) { _logger.LogWarning("Rate limit exceeded for Companies House API"); throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later."); } response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync( JsonOptions, cancellationToken); _logger.LogDebug("Retrieved company: {CompanyName} ({CompanyNumber})", result?.CompanyName, companyNumber); return result; } catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests) { _logger.LogWarning("Rate limit exceeded for Companies House API"); throw new CompaniesHouseRateLimitException("Rate limit exceeded. Please try again later.", ex); } } } // DTOs for Companies House API responses public sealed record CompaniesHouseSearchResponse { public int TotalResults { get; init; } public int ItemsPerPage { get; init; } public int StartIndex { get; init; } public List Items { get; init; } = []; } public sealed record CompaniesHouseSearchItem { public required string CompanyNumber { get; init; } public required string Title { get; init; } public string? CompanyStatus { get; init; } public string? CompanyType { get; init; } public string? DateOfCreation { get; init; } public string? DateOfCessation { get; init; } public CompaniesHouseAddress? Address { get; init; } public string? AddressSnippet { get; init; } } public sealed record CompaniesHouseCompany { public required string CompanyNumber { get; init; } public required string CompanyName { get; init; } public string? CompanyStatus { get; init; } public string? Type { get; init; } public string? DateOfCreation { get; init; } public string? DateOfCessation { get; init; } public CompaniesHouseAddress? RegisteredOfficeAddress { get; init; } } public sealed record CompaniesHouseAddress { public string? Premises { get; init; } public string? AddressLine1 { get; init; } public string? AddressLine2 { get; init; } public string? Locality { get; init; } public string? Region { get; init; } public string? PostalCode { get; init; } public string? Country { get; init; } } public class CompaniesHouseRateLimitException : Exception { public CompaniesHouseRateLimitException(string message) : base(message) { } public CompaniesHouseRateLimitException(string message, Exception innerException) : base(message, innerException) { } }