using Microsoft.EntityFrameworkCore;
using SchotzIadKmTracker.Api.Data;
using SchotzIadKmTracker.Api.Models.DTOs;
using SchotzIadKmTracker.Api.Models.Entities;
using SchotzIadKmTracker.Api.Services.Exceptions;
namespace SchotzIadKmTracker.Api.Services;
///
public class ProfilService : IProfilService
{
// Le profil est un singleton : une seule ligne dont l'identifiant est toujours 1.
private const int ProfilId = 1;
private readonly AppDbContext _context;
private readonly IBANClient _banClient;
private readonly IAdresseCacheService _adresseCacheService;
private readonly ILogger _logger;
public ProfilService(
AppDbContext context,
IBANClient banClient,
IAdresseCacheService adresseCacheService,
ILogger logger)
{
_context = context;
_banClient = banClient;
_adresseCacheService = adresseCacheService;
_logger = logger;
}
///
public async Task GetProfilAsync(CancellationToken cancellationToken = default)
{
var profil = await GetOrCreateProfilAsync(cancellationToken);
return ToDto(profil);
}
///
public async Task SaveProfilAsync(SaveProfilDto dto, CancellationToken cancellationToken = default)
{
var profil = await GetOrCreateProfilAsync(cancellationToken);
profil.Nom = dto.Nom;
profil.Prenom = dto.Prenom;
profil.Matricule = dto.Matricule;
profil.AdresseDomicile = dto.AdresseDomicile;
profil.UpdatedAt = DateTime.UtcNow;
// Géocodage BAN de l'Adresse_Domicile. En cas de succès on stocke les coordonnées et
// DomicileGeocode = true ; en cas d'échec on persiste l'adresse textuelle sans coordonnées
// et DomicileGeocode = false (aucune exception propagée) — Requirements 10.7, 10.8.
await GeocoderDomicileAsync(profil, dto.AdresseDomicile, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return ToDto(profil);
}
///
public async Task GetEtatAsync(CancellationToken cancellationToken = default)
{
// On ne crée pas le profil ici : l'état doit refléter la base telle quelle (Property 31).
var profil = await _context.Profil
.AsNoTracking()
.FirstOrDefaultAsync(p => p.Id == ProfilId, cancellationToken);
var nomPrenomRenseignes = profil is not null
&& !string.IsNullOrWhiteSpace(profil.Nom)
&& !string.IsNullOrWhiteSpace(profil.Prenom);
var auMoinsUnVehicule = await _context.Vehicules.AnyAsync(cancellationToken);
return new EtatProfilDto(nomPrenomRenseignes && auMoinsUnVehicule);
}
///
/// Tente de résoudre les coordonnées de l'Adresse_Domicile via la BAN. Met à jour le profil
/// (coordonnées + DomicileGeocode) et alimente le Cache_Adresses en cas de succès. Ne lève jamais :
/// tout échec BAN est traité comme un géocodage non résolu.
///
private async Task GeocoderDomicileAsync(Profil profil, string? adresseDomicile, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(adresseDomicile))
{
// Pas d'adresse à géocoder : on repart d'un état non géocodé.
profil.DomicileLatitude = null;
profil.DomicileLongitude = null;
profil.DomicileGeocode = false;
return;
}
try
{
var resultats = await _banClient.RechercherAsync(adresseDomicile, cancellationToken);
var meilleur = resultats.Count > 0 ? resultats[0] : null;
if (meilleur is null)
{
// BAN a répondu mais aucune adresse ne correspond : adresse conservée sans coordonnées.
profil.DomicileLatitude = null;
profil.DomicileLongitude = null;
profil.DomicileGeocode = false;
return;
}
profil.DomicileLatitude = meilleur.Latitude;
profil.DomicileLongitude = meilleur.Longitude;
profil.DomicileGeocode = true;
// Mise en cache de l'adresse géocodée pour réutilisation ultérieure (Requirement 9.1).
await _adresseCacheService.UpsertAdresseAsync(
adresseDomicile,
meilleur.Latitude,
meilleur.Longitude,
geometrieJson: null,
cancellationToken);
}
catch (BANIndisponibleException ex)
{
// BAN indisponible : l'adresse est conservée sans coordonnées, sans échec de la sauvegarde.
_logger.LogWarning(ex,
"Géocodage BAN de l'adresse de domicile indisponible ; l'adresse est conservée sans coordonnées.");
profil.DomicileLatitude = null;
profil.DomicileLongitude = null;
profil.DomicileGeocode = false;
}
}
///
/// Récupère le profil singleton (Id = 1) ou le crée à la volée s'il est absent.
///
private async Task GetOrCreateProfilAsync(CancellationToken cancellationToken)
{
var profil = await _context.Profil.FirstOrDefaultAsync(p => p.Id == ProfilId, cancellationToken);
if (profil is not null)
{
return profil;
}
profil = new Profil { Id = ProfilId };
await _context.Profil.AddAsync(profil, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return profil;
}
private static ProfilDto ToDto(Profil p) =>
new(p.Nom, p.Prenom, p.Matricule, p.AdresseDomicile, p.DomicileGeocode);
}