using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using TrajetKmTracker.Api.Models.DTOs;
using TrajetKmTracker.Api.Services.Exceptions;
namespace TrajetKmTracker.Api.Services;
///
/// Client typé de l'API BAN. Appelle GET /search/?q={query}&limit=5,
/// parse la réponse GeoJSON (FeatureCollection) et mappe les features en
/// . Lève
/// en cas d'échec ou de timeout.
///
public class BANClient : IBANClient
{
private const int MaxResultats = 5;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
private readonly HttpClient _httpClient;
public BANClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task> RechercherAsync(
string query,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(query))
{
return Array.Empty();
}
var requestUri = $"/search/?q={Uri.EscapeDataString(query)}&limit={MaxResultats}";
BANFeatureCollection? collection;
try
{
using var response = await _httpClient.GetAsync(requestUri, cancellationToken);
response.EnsureSuccessStatusCode();
collection = await response.Content.ReadFromJsonAsync(
JsonOptions, cancellationToken);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
// Le timeout configuré sur le HttpClient déclenche une annulation
// non demandée par l'appelant.
throw new BANIndisponibleException(
"L'appel à l'API BAN a dépassé le délai imparti (timeout).");
}
catch (HttpRequestException ex)
{
throw new BANIndisponibleException(
"L'API BAN est indisponible ou a renvoyé une réponse en erreur.", ex);
}
catch (JsonException ex)
{
throw new BANIndisponibleException(
"La réponse de l'API BAN n'a pas pu être interprétée.", ex);
}
if (collection?.Features is null || collection.Features.Count == 0)
{
return Array.Empty();
}
var resultats = new List(collection.Features.Count);
foreach (var feature in collection.Features)
{
var coordinates = feature.Geometry?.Coordinates;
if (coordinates is null || coordinates.Count < 2)
{
continue;
}
// GeoJSON : coordinates = [longitude, latitude].
var longitude = coordinates[0];
var latitude = coordinates[1];
resultats.Add(new BANAdresseDto(
Label: feature.Properties?.Label ?? string.Empty,
Latitude: latitude,
Longitude: longitude,
Postcode: feature.Properties?.Postcode ?? string.Empty,
City: feature.Properties?.City ?? string.Empty));
}
return resultats;
}
// --- Modèles internes de désérialisation de la réponse GeoJSON BAN ---
private sealed class BANFeatureCollection
{
[JsonPropertyName("features")]
public List? Features { get; set; }
}
private sealed class BANFeature
{
[JsonPropertyName("properties")]
public BANProperties? Properties { get; set; }
[JsonPropertyName("geometry")]
public BANGeometry? Geometry { get; set; }
}
private sealed class BANProperties
{
[JsonPropertyName("label")]
public string? Label { get; set; }
[JsonPropertyName("postcode")]
public string? Postcode { get; set; }
[JsonPropertyName("city")]
public string? City { get; set; }
}
private sealed class BANGeometry
{
// [longitude, latitude]
[JsonPropertyName("coordinates")]
public List? Coordinates { get; set; }
}
}