StatsService.java

package com.archiweb.service;

import com.archiweb.dto.StatsResponse;
import com.archiweb.model.Code;
import com.archiweb.model.Participation;
import com.archiweb.model.PrizeType;
import com.archiweb.model.User;
import com.archiweb.model.Win;
import com.archiweb.repository.CodeRepository;
import com.archiweb.repository.ParticipationRepository;
import com.archiweb.repository.UserRepository;
import com.archiweb.repository.WinRepository;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.Period;
import java.util.*;
import java.util.stream.Collectors;
import java.util.List;

@Service
public class StatsService {

    private final UserRepository userRepository;
    private final WinRepository winRepository;
    private final CodeRepository codeRepository;
    private final ParticipationRepository participationRepository;

    public StatsService(UserRepository userRepository,
                        WinRepository winRepository,
                        CodeRepository codeRepository,
                        ParticipationRepository participationRepository) {

        this.userRepository = userRepository;
        this.winRepository = winRepository;
        this.codeRepository = codeRepository;
        this.participationRepository = participationRepository;
    }

    // =============================
    // 1️⃣ - Statistiques globales sans filtres
    // =============================
    @Cacheable(value = "globalStats", unless = "#result == null")
    public StatsResponse getStats() {
        try {
            // Optimisation : utiliser des requêtes SQL natives pour réduire le nombre d'appels
            // Une seule requête pour les stats des codes
            Object[] codeStats = codeRepository.getCodeStatsNative();
            if (codeStats == null || codeStats.length < 4) {
                // Fallback si la requête native échoue
                return getStatsFallback();
            }
            long totalCodes = convertToLong(codeStats[0]);
            long usedCodes = convertToLong(codeStats[1]);
            long unusedCodes = convertToLong(codeStats[2]);
            long expiredCodes = convertToLong(codeStats[3]);

            // Une seule requête pour les stats des participations
            Object[] participationStats = participationRepository.getParticipationStatsNative();
            if (participationStats == null || participationStats.length < 3) {
                // Fallback si la requête native échoue
                return getStatsFallback();
            }
            long totalParticipations = convertToLong(participationStats[0]);
            long claimed = convertToLong(participationStats[1]);
            long unclaimed = convertToLong(participationStats[2]);

            // Optimisation : utiliser une requête SQL GROUP BY au lieu de charger tous les codes
            Map<PrizeType, Long> repartition = new HashMap<>();
            List<Object[]> results = codeRepository.countByPrizeTypeGrouped();
            for (Object[] result : results) {
                PrizeType prizeType = (PrizeType) result[0];
                Long count = convertToLong(result[1]);
                repartition.put(prizeType, count);
            }

            return StatsResponse.builder()
                    .totalCodes(totalCodes)
                    .codesUtilises(usedCodes)
                    .codesNonUtilises(unusedCodes)
                    .codesExpires(expiredCodes)
                    .totalParticipations(totalParticipations)
                    .participationsReclamees(claimed)
                    .participationsNonReclamees(unclaimed)
                    .repartitionLots(repartition)
                    .build();
        } catch (Exception e) {
            // En cas d'erreur avec les requêtes natives, utiliser le fallback
            System.err.println("Erreur lors de la récupération des stats natives: " + e.getMessage());
            e.printStackTrace();
            return getStatsFallback();
        }
    }

    // =============================
    // 2️⃣ - Statistiques globales filtrées
    // =============================
    public StatsResponse getStats(String lot, String boutique, String periode) {

        StatsResponse base = getStats();
        base.setLotFiltre(lot);
        base.setBoutiqueFiltre(boutique);
        base.setPeriodeFiltre(periode);

        return base;
    }

    // =============================
    // 3️⃣ - Statistiques démographiques
    // =============================
    @Cacheable(value = "demographicsStats", unless = "#result == null")
    public Map<String, Object> getDemographicStats() {

        Map<String, Object> stats = new HashMap<>();

        // Optimisation : utiliser count() au lieu de findAll().size()
        long totalUsers = userRepository.count();
        
        LocalDate today = LocalDate.now();
        LocalDate minValidDate = today.minusYears(120); // Maximum 120 ans
        LocalDate maxValidDate = today.minusYears(5); // Minimum 5 ans (pour éviter les dates de test)

        // Optimisation : calculer l'âge moyen directement en SQL
        Double avgAgeResult = userRepository.calculateAverageAge(minValidDate, maxValidDate);
        double avgAge = avgAgeResult != null ? avgAgeResult : 0.0;

        // Optimisation : utiliser countByConsentGiven au lieu de stream().filter().count()
        long consentGiven = userRepository.countByConsentGiven(true);

        stats.put("totalUsers", totalUsers);
        stats.put("averageAge", Math.round(avgAge * 10.0) / 10.0); // Arrondir à 1 décimale
        stats.put("consentGiven", consentGiven);

        return stats;
    }

    // =============================
    // 4️⃣ - Statistiques des gains
    // =============================
    @Cacheable(value = "winsStats", unless = "#result == null")
    public Map<String, Long> getWinsStats() {
        // Optimisation : utiliser une requête SQL GROUP BY au lieu de charger tous les wins
        Map<String, Long> stats = new HashMap<>();
        List<Object[]> results = winRepository.countByPrizeNameGrouped();
        for (Object[] result : results) {
            String prizeName = (String) result[0];
            Long count = (Long) result[1];
            stats.put(prizeName, count);
        }
        return stats;
    }

    // =============================
    // 5️⃣ - Statistiques des codes
    // =============================
    public Map<String, Long> getCodesStats() {

        Map<String, Long> stats = new HashMap<>();

        stats.put("totalCodes", codeRepository.count());
        stats.put("usedCodes", codeRepository.countByUsed(true));
        stats.put("unusedCodes", codeRepository.countByUsed(false));
        stats.put("expiredCodes", codeRepository.countExpiredCodes());

        return stats;
    }

    /**
     * Convertit un objet en long, en gérant tous les types numériques possibles
     * (BigInteger, Long, Integer, BigDecimal, etc.)
     * Gère aussi le cas où la valeur est un tableau d'objets (problème avec certaines requêtes SQL natives)
     */
    private long convertToLong(Object value) {
        if (value == null) {
            return 0L;
        }
        
        // Si c'est un tableau, prendre le premier élément et le convertir récursivement
        if (value.getClass().isArray()) {
            Object[] array = (Object[]) value;
            if (array.length > 0) {
                return convertToLong(array[0]);
            }
            return 0L;
        }
        
        // Si c'est un Number, convertir directement
        if (value instanceof Number) {
            return ((Number) value).longValue();
        }
        
        // Si c'est une String, essayer de la parser
        if (value instanceof String) {
            try {
                return Long.parseLong((String) value);
            } catch (NumberFormatException e) {
                return 0L;
            }
        }
        
        // Par défaut, retourner 0
        return 0L;
    }

    /**
     * Méthode de fallback si les requêtes SQL natives échouent
     * Utilise les méthodes de repository standard
     */
    private StatsResponse getStatsFallback() {
        long totalCodes = codeRepository.count();
        long usedCodes = codeRepository.countByUsed(true);
        long unusedCodes = codeRepository.countByUsed(false);
        long expiredCodes = codeRepository.countExpiredCodes();
        
        long totalParticipations = participationRepository.count();
        long claimed = participationRepository.countByClaimed(true);
        long unclaimed = participationRepository.countByClaimed(false);
        
        Map<PrizeType, Long> repartition = new HashMap<>();
        List<Object[]> results = codeRepository.countByPrizeTypeGrouped();
        for (Object[] result : results) {
            PrizeType prizeType = (PrizeType) result[0];
            Long count = convertToLong(result[1]);
            repartition.put(prizeType, count);
        }
        
        return StatsResponse.builder()
                .totalCodes(totalCodes)
                .codesUtilises(usedCodes)
                .codesNonUtilises(unusedCodes)
                .codesExpires(expiredCodes)
                .totalParticipations(totalParticipations)
                .participationsReclamees(claimed)
                .participationsNonReclamees(unclaimed)
                .repartitionLots(repartition)
                .build();
    }
}