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 ResumeService : IResumeService
{
private const int MoisMin = 1;
private const int MoisMax = 12;
private const int AnneeMin = 2000;
private const int AnneeMax = 2100;
private readonly AppDbContext _context;
private readonly IPrecisionDistanceService _precisionDistanceService;
private readonly IAdresseCacheService _adresseCacheService;
public ResumeService(
AppDbContext context,
IPrecisionDistanceService precisionDistanceService,
IAdresseCacheService adresseCacheService)
{
_context = context;
_precisionDistanceService = precisionDistanceService;
_adresseCacheService = adresseCacheService;
}
///
public async Task GetResumeMensuelAsync(
int annee, int mois, int? vehiculeId = null, CancellationToken cancellationToken = default)
{
ValiderAnnee(annee);
ValiderMois(mois);
var precision = await _precisionDistanceService.ObtenirAsync(cancellationToken);
var trajets = await ChargerTrajetsDuMoisAsync(annee, mois, vehiculeId, cancellationToken);
// Coordonnées incluses : ce résumé alimente l'écran de consultation/saisie des Trajets,
// dont l'insertion (« Insérer avant/après ») réutilise les coordonnées déjà géocodées
// pour éviter un re-géocodage BAN.
return await ConstruireResumeMensuelAvecCoordonneesAsync(annee, mois, trajets, precision, cancellationToken);
}
///
public async Task GetResumeAnnuelAsync(
int annee, int? vehiculeId = null, CancellationToken cancellationToken = default)
{
ValiderAnnee(annee);
var precision = await _precisionDistanceService.ObtenirAsync(cancellationToken);
// Charge une seule fois tous les trajets de l'année, puis répartit par mois.
var query = _context.Trajets
.AsNoTracking()
.Where(t => t.Date.Year == annee);
if (vehiculeId.HasValue)
{
query = query.Where(t => t.VehiculeId == vehiculeId.Value);
}
var trajetsAnnee = await query.ToListAsync(cancellationToken);
var resumesMensuels = new List(MoisMax);
for (var mois = MoisMin; mois <= MoisMax; mois++)
{
var trajetsDuMois = trajetsAnnee
.Where(t => t.Date.Month == mois)
.ToList();
resumesMensuels.Add(ConstruireResumeMensuel(annee, mois, trajetsDuMois, precision));
}
// Total annuel = somme des 12 totaux mensuels, déjà arrondis (Exigences 4.3, 4.7, 6.6, 5.2, 5.3).
var totalAnnuel = resumesMensuels.Sum(r => r.TotalKm);
return new ResumeAnnuelDto(annee, resumesMensuels, totalAnnuel);
}
///
/// Charge les Trajets d'un mois donné, filtrés éventuellement par véhicule, triés par
/// date croissante puis par Position croissante (Exigence 3.2).
///
private async Task> ChargerTrajetsDuMoisAsync(
int annee, int mois, int? vehiculeId, CancellationToken cancellationToken)
{
var query = _context.Trajets
.AsNoTracking()
.Where(t => t.Date.Year == annee && t.Date.Month == mois);
if (vehiculeId.HasValue)
{
query = query.Where(t => t.VehiculeId == vehiculeId.Value);
}
// Tri par Position (decimal) non traduisible par SQLite : ordonnancement côté client.
var trajets = await query.ToListAsync(cancellationToken);
return trajets
.OrderBy(t => t.Date)
.ThenBy(t => t.Position)
.ToList();
}
///
/// Construit un à partir d'une liste de Trajets déjà triée.
/// Chaque distance est arrondie (Precision_Distance) AVANT doublement aller/retour ; le total
/// est la somme de ces valeurs déjà arrondies, sans second arrondi (Property 1, Property 2, Property 3).
///
private ResumeMensuelDto ConstruireResumeMensuel(
int annee, int mois, IReadOnlyList trajets, int precision)
{
// Calculée une seule fois par trajet, puis réutilisée pour le DTO et la somme du total
// afin de garantir la même valeur (Distance_Effective_Arrondie) aux deux endroits.
var distancesArrondies = trajets.Select(t => (t, distance: DistanceEffectiveArrondie(t, precision))).ToList();
var trajetsDto = distancesArrondies.Select(x => ToDto(x.t, x.distance)).ToList();
// Total = somme des Distance_Effective_Arrondie déjà calculées par trajet, null → 0 (Property 1, Property 3).
var totalKm = distancesArrondies.Sum(x => x.distance ?? 0m);
return new ResumeMensuelDto(annee, mois, trajetsDto, totalKm);
}
///
/// Variante de enrichissant chaque
/// des coordonnées de ses adresses, résolues via le Cache_Adresses (même mécanisme que
/// TrajetsController.GetJournee). Réservée au résumé MENSUEL (écran de consultation/
/// saisie des Trajets, dont l'insertion réutilise ces coordonnées) : le résumé ANNUEL continue
/// d'utiliser sans lookup, les totaux annuels n'ayant pas
/// besoin du détail des coordonnées de chaque trajet (évite 2×N lookups cache inutiles).
///
private async Task ConstruireResumeMensuelAvecCoordonneesAsync(
int annee, int mois, IReadOnlyList trajets, int precision, CancellationToken cancellationToken)
{
var distancesArrondies = trajets.Select(t => (t, distance: DistanceEffectiveArrondie(t, precision))).ToList();
var trajetsDto = new List(distancesArrondies.Count);
foreach (var (t, distance) in distancesArrondies)
{
var depart = await _adresseCacheService.ObtenirParAdresseAsync(t.AdresseDepart, cancellationToken);
var arrivee = await _adresseCacheService.ObtenirParAdresseAsync(t.AdresseArrivee, cancellationToken);
trajetsDto.Add(ToDto(t, distance, depart?.Latitude, depart?.Longitude, arrivee?.Latitude, arrivee?.Longitude));
}
var totalKm = distancesArrondies.Sum(x => x.distance ?? 0m);
return new ResumeMensuelDto(annee, mois, trajetsDto, totalKm);
}
///
/// Calcule la Distance_Effective_Arrondie d'un trajet : arrondi (Precision_Distance) appliqué
/// à la distance SIMPLE brute AVANT doublement aller/retour (Exigence 4.1, 4.2, 4.3). null si la
/// distance brute n'a pas pu être calculée (Exigence 2.4 du résumé existant).
///
private decimal? DistanceEffectiveArrondie(Trajet t, int precision)
{
if (!t.Distance.HasValue)
{
return null;
}
var distanceArrondie = _precisionDistanceService.ArrondirDistance(t.Distance.Value, precision);
return t.AllerRetour ? distanceArrondie * 2m : distanceArrondie;
}
private void ValiderMois(int mois)
{
if (mois < MoisMin || mois > MoisMax)
{
throw new ResumeParametreInvalideException(
$"Le mois doit être compris entre {MoisMin} et {MoisMax} inclus ; valeur reçue : {mois}.");
}
}
private void ValiderAnnee(int annee)
{
if (annee < AnneeMin || annee > AnneeMax)
{
throw new ResumeParametreInvalideException(
$"L'année doit être comprise entre {AnneeMin} et {AnneeMax} inclus ; valeur reçue : {annee}.");
}
}
private static TrajetDto ToDto(
Trajet t,
decimal? distanceEffectiveArrondie,
double? departLatitude = null,
double? departLongitude = null,
double? arriveeLatitude = null,
double? arriveeLongitude = null) =>
new(
t.Id,
t.Date,
t.AdresseDepart,
t.AdresseArrivee,
// Distance_Effective_Arrondie (arrondi Precision_Distance puis doublement si aller/retour),
// alignée sur le TrajetDto commun (au lieu de DistanceEffective brut).
distanceEffectiveArrondie,
t.Commentaire,
t.Position,
t.VehiculeId,
t.PuissanceFiscale,
t.AllerRetour,
departLatitude,
departLongitude,
arriveeLatitude,
arriveeLongitude);
}