package com.lions.dev.service; import com.lions.dev.dto.request.friends.FriendshipCreateOneRequestDTO; import com.lions.dev.dto.request.friends.FriendshipReadFriendDetailsRequestDTO; import com.lions.dev.dto.request.friends.FriendshipReadStatusRequestDTO; import com.lions.dev.dto.response.friends.FriendshipCreateOneResponseDTO; import com.lions.dev.dto.response.friends.FriendshipReadFriendDetailsResponseDTO; import com.lions.dev.dto.response.friends.FriendshipReadStatusResponseDTO; import com.lions.dev.entity.friends.Friendship; import com.lions.dev.entity.friends.FriendshipStatus; import com.lions.dev.entity.users.Users; import com.lions.dev.exception.FriendshipNotFoundException; import com.lions.dev.exception.UserNotFoundException; import com.lions.dev.repository.FriendshipRepository; import com.lions.dev.repository.UsersRepository; import com.lions.dev.dto.events.NotificationEvent; import org.eclipse.microprofile.reactive.messaging.Channel; import org.eclipse.microprofile.reactive.messaging.Emitter; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import org.jboss.logging.Logger; /** * Service pour gérer les relations d'amitié. * Contient la logique métier pour envoyer, accepter, rejeter, et supprimer des relations d'amitié. */ @ApplicationScoped public class FriendshipService { @Inject FriendshipRepository friendshipRepository; // Injecte le repository des amitiés @Inject UsersRepository usersRepository; // Injecte le repository des utilisateurs @Inject NotificationService notificationService; // Injecte le service de notifications @Inject @Channel("notifications") Emitter notificationEmitter; // v2.0 - Publie dans Kafka private static final Logger logger = Logger.getLogger(FriendshipService.class); /** * Envoie une demande d'amitié entre deux utilisateurs. * * @param request DTO contenant les informations sur l'utilisateur et l'ami. * @return Le DTO de la relation d'amitié créée. */ @Transactional public FriendshipCreateOneResponseDTO sendFriendRequest(FriendshipCreateOneRequestDTO request) { logger.info("[LOG] Envoi d'une demande d'amitié de l'utilisateur " + request.getUserId() + " à l'utilisateur " + request.getFriendId()); // Récupérer les utilisateurs concernés Users user = usersRepository.findById(request.getUserId()); Users friend = usersRepository.findById(request.getFriendId()); if (user == null || friend == null) { String notFoundId = user == null ? request.getUserId().toString() : request.getFriendId().toString(); logger.error("[ERROR] Utilisateur non trouvé pour l'ID : " + notFoundId); throw new UserNotFoundException("Utilisateur avec l'ID " + notFoundId + " introuvable."); } // VALIDATION: Empêcher l'utilisateur de s'ajouter lui-même comme ami if (user.getId().equals(friend.getId())) { logger.error("[ERROR] Tentative d'ajout de soi-même comme ami bloquée pour l'utilisateur : " + user.getId()); throw new IllegalArgumentException("Vous ne pouvez pas vous ajouter vous-même comme ami."); } // Vérifier s'il existe déjà une relation d'amitié Friendship existingFriendship = friendshipRepository.findByUsers(user, friend).orElse(null); if (existingFriendship != null) { logger.error("[ERROR] Relation d'amitié déjà existante entre les utilisateurs."); throw new IllegalArgumentException("Relation d'amitié déjà existante."); } // Créer et persister une nouvelle relation d'amitié Friendship friendship = new Friendship(user, friend, FriendshipStatus.PENDING); friendshipRepository.persist(friendship); // TEMPS RÉEL: Publier dans Kafka (v2.0) try { Map notificationData = new HashMap<>(); notificationData.put("requestId", friendship.getId().toString()); notificationData.put("senderId", user.getId().toString()); // v2.0 - Utiliser les nouveaux noms de champs notificationData.put("senderName", user.getFirstName() + " " + user.getLastName()); notificationData.put("senderProfileImage", user.getProfileImageUrl() != null ? user.getProfileImageUrl() : ""); NotificationEvent event = new NotificationEvent( friend.getId().toString(), // userId destinataire (clé Kafka) "friend_request_received", notificationData ); notificationEmitter.send(event); logger.info("[LOG] Événement friend_request_received publié dans Kafka pour : " + friend.getId()); } catch (Exception e) { logger.error("[ERROR] Erreur lors de la publication dans Kafka : " + e.getMessage(), e); // Ne pas bloquer la demande d'amitié si Kafka échoue } logger.info("[LOG] Demande d'amitié envoyée avec succès."); return new FriendshipCreateOneResponseDTO(friendship); } /** * Accepter une demande d'amitié. * * @param friendshipId ID de la demande à accepter. * @return Le DTO de la relation d'amitié acceptée. */ @Transactional public FriendshipCreateOneResponseDTO acceptFriendRequest(UUID friendshipId) { // Vérification de l'ID de la demande d'amitié if (friendshipId == null) { logger.error(String.format("[ERROR] L'ID de la demande d'amitié est nul.")); throw new IllegalArgumentException("L'ID de la demande d'amitié est nul."); } // Rechercher l'amitié dans la base de données Friendship friendship = friendshipRepository.findById(friendshipId); // Si l'amitié n'est pas trouvée, lever une exception if (friendship == null) { logger.error(String.format("[ERROR] Demande d'amitié introuvable pour l'ID: %s", friendshipId)); // Correctement formaté throw new FriendshipNotFoundException("Demande d'amitié introuvable."); } // Vérifier que la demande n'est pas déjà acceptée if (friendship.getStatus() == FriendshipStatus.ACCEPTED) { logger.error(String.format("[ERROR] Demande d'amitié déjà acceptée pour l'ID: %s", friendshipId)); // Correctement formaté throw new IllegalArgumentException("Demande d'amitié déjà acceptée."); } // Accepter la demande friendship.setStatus(FriendshipStatus.ACCEPTED); friendshipRepository.persist(friendship); // Log de succès logger.info(String.format("[LOG] Demande d'amitié acceptée avec succès pour l'ID: %s", friendshipId)); // Correctement formaté // TEMPS RÉEL: Publier dans Kafka (v2.0) try { Users user = friendship.getUser(); Users friend = friendship.getFriend(); // v2.0 - Utiliser les nouveaux noms de champs String friendName = friend.getFirstName() + " " + friend.getLastName(); Map notificationData = new HashMap<>(); notificationData.put("acceptedBy", friendName); notificationData.put("friendshipId", friendshipId.toString()); notificationData.put("accepterId", friend.getId().toString()); notificationData.put("accepterProfileImage", friend.getProfileImageUrl() != null ? friend.getProfileImageUrl() : ""); NotificationEvent event = new NotificationEvent( user.getId().toString(), // userId émetteur (destinataire de la notification) "friend_request_accepted", notificationData ); notificationEmitter.send(event); logger.info("[LOG] Événement friend_request_accepted publié dans Kafka pour : " + user.getId()); } catch (Exception e) { logger.error("[ERROR] Erreur lors de la publication dans Kafka : " + e.getMessage(), e); // Ne pas bloquer l'acceptation si Kafka échoue } // Créer des notifications pour les deux utilisateurs try { Users user = friendship.getUser(); Users friend = friendship.getFriend(); // v2.0 - Utiliser les nouveaux noms de champs String userName = user.getFirstName() + " " + user.getLastName(); String friendName = friend.getFirstName() + " " + friend.getLastName(); // Notification pour l'utilisateur qui a envoyé la demande notificationService.createNotification( "Demande d'amitié acceptée", friendName + " a accepté votre demande d'amitié", "friend", user.getId(), null ); // Notification pour l'utilisateur qui a accepté la demande notificationService.createNotification( "Nouveaux amis", "Vous êtes maintenant ami(e) avec " + userName, "friend", friend.getId(), null ); logger.info("[LOG] Notifications d'amitié créées pour les deux utilisateurs"); } catch (Exception e) { logger.error("[ERROR] Erreur lors de la création des notifications d'amitié : " + e.getMessage()); } // Retourner la réponse avec les informations de la relation d'amitié return new FriendshipCreateOneResponseDTO(friendship); } /** * Rejeter une demande d'amitié. * * @param friendshipId ID de la demande à rejeter. */ @Transactional public void rejectFriendRequest(UUID friendshipId) { Friendship friendship = friendshipRepository.findById(friendshipId); if (friendship == null) { throw new FriendshipNotFoundException("Demande d'amitié introuvable."); } friendship.setStatus(FriendshipStatus.REJECTED); friendshipRepository.persist(friendship); // TEMPS RÉEL: Publier dans Kafka (v2.0) try { Users user = friendship.getUser(); Map notificationData = new HashMap<>(); notificationData.put("friendshipId", friendshipId.toString()); notificationData.put("rejectedAt", System.currentTimeMillis()); NotificationEvent event = new NotificationEvent( user.getId().toString(), // userId émetteur (destinataire de la notification) "friend_request_rejected", notificationData ); notificationEmitter.send(event); logger.info("[LOG] Événement friend_request_rejected publié dans Kafka pour : " + user.getId()); } catch (Exception e) { logger.error("[ERROR] Erreur lors de la publication dans Kafka : " + e.getMessage(), e); // Ne pas bloquer le rejet si Kafka échoue } logger.info("[LOG] Demande d'amitié rejetée."); } /** * Supprimer une relation d'amitié. * * @param friendshipId ID de la relation à supprimer. */ @Transactional public void removeFriend(UUID friendshipId) { Friendship friendship = friendshipRepository.findById(friendshipId); if (friendship == null) { throw new FriendshipNotFoundException("Relation d'amitié introuvable."); } friendshipRepository.delete(friendship); logger.info("[LOG] Relation d'amitié supprimée."); } /** * Récupère les détails d'un ami spécifique pour un utilisateur donné. * * @param request DTO contenant l'ID de l'utilisateur et de l'ami. * @return Le DTO des détails de l'ami. */ @Transactional public FriendshipReadFriendDetailsResponseDTO getFriendDetails( FriendshipReadFriendDetailsRequestDTO request) { logger.info("[LOG] Tentative de récupération des détails de l'ami avec l'ID : " + request.getFriendId() + " pour l'utilisateur : " + request.getUserId()); // Récupération de l'utilisateur et de l'ami Users user = usersRepository.findById(request.getUserId()); Users friend = usersRepository.findById(request.getFriendId()); if (user == null) { logger.error("[ERROR] Utilisateur introuvable avec l'ID : " + request.getUserId()); throw new UserNotFoundException("Utilisateur introuvable avec l'ID " + request.getUserId()); } if (friend == null) { logger.error("[ERROR] Ami introuvable avec l'ID : " + request.getFriendId()); throw new UserNotFoundException("Ami introuvable avec l'ID " + request.getFriendId()); } // Récupérer la relation d'amitié entre les deux utilisateurs Friendship friendship = friendshipRepository.findByUsers(user, friend).orElse(null); if (friendship == null) { logger.error("[ERROR] Aucune relation d'amitié trouvée entre l'utilisateur et l'ami."); throw new FriendshipNotFoundException("Relation d'amitié introuvable."); } logger.info("[LOG] Détails de l'ami récupérés avec succès pour l'utilisateur : " + user.getId() + ", ami ID : " + friend.getId()); // Création du DTO de réponse à partir des informations de l'ami et de la relation // v2.0 - Utiliser les nouveaux noms de champs return new FriendshipReadFriendDetailsResponseDTO( user.getId(), // ID de l'utilisateur friend.getId(), // ID de l'ami friend.getLastName(), // Nom de famille de l'ami (v2.0) friend.getFirstName(), // Prénom de l'ami (v2.0) friend.getEmail(), // Email de l'ami friend.getProfileImageUrl(), // URL de l'image de profil de l'ami friendship.getStatus(), // Statut de la relation friendship.getCreatedAt(), // Date de création de la relation friendship.getUpdatedAt() // Date de mise à jour de la relation ); } /** * Récupérer la liste des amis d'un utilisateur. * * @param userId ID de l'utilisateur. * @param page Numéro de la page. * @param size Taille de la page. * @return Liste paginée des relations d'amitié. */ public List listFriends(UUID userId, int page, int size) { Users user = usersRepository.findById(userId); if (user == null) { logger.error("[ERROR] Utilisateur non trouvé."); throw new UserNotFoundException("Utilisateur introuvable."); } List friendships = friendshipRepository.findFriendsByUser(user, page, size); logger.info("[LOG] " + friendships.size() + " amis récupérés (avant filtrage des doublons)."); // Utilisation d'un ensemble pour stocker des clés uniques basées sur les IDs des amis Set uniqueFriendKeys = new HashSet<>(); return friendships.stream() .map(friendship -> { Users friend = friendship.getUser().equals(user) ? friendship.getFriend() : friendship.getUser(); // v2.0 - Utiliser les nouveaux noms de champs return new FriendshipReadFriendDetailsResponseDTO( user.getId(), friend.getId(), friend.getLastName(), // v2.0 friend.getFirstName(), // v2.0 friend.getEmail(), friend.getProfileImageUrl(), friendship.getStatus(), friendship.getCreatedAt(), friendship.getUpdatedAt() ); }) .filter(dto -> uniqueFriendKeys.add(dto.getUserId().toString() + "-" + dto.getFriendId().toString())) .limit(size) // Limite la taille au paramètre 'size' requis .toList(); } /** * Récupérer les demandes d'amitié avec un statut spécifique. * * @param request DTO contenant les informations de filtrage (statut). * @return Liste des demandes d'amitié avec le statut spécifié. */ public List listFriendRequestsByStatus(FriendshipReadStatusRequestDTO request) { Users user = usersRepository.findById(request.getUserId()); if (user == null) { logger.error("[ERROR] Utilisateur non trouvé."); throw new UserNotFoundException("Utilisateur introuvable."); } // Récupérer les demandes d'amitié selon le statut List friendships = friendshipRepository.findByUserAndStatus(user, request.getStatus(), request.getPage() - 1, request.getSize()); logger.info("[LOG] " + friendships.size() + " demandes d'amitié récupérées."); return friendships.stream().map(FriendshipReadStatusResponseDTO::new).toList(); } /** * Récupérer les demandes d'amitié envoyées par un utilisateur. * * @param userId ID de l'utilisateur. * @param page Numéro de la page. * @param size Taille de la page. * @return Liste des demandes d'amitié envoyées. */ public List listSentFriendRequests(UUID userId, int page, int size) { Users user = usersRepository.findById(userId); if (user == null) { logger.error("[ERROR] Utilisateur non trouvé."); throw new UserNotFoundException("Utilisateur introuvable."); } List friendships = friendshipRepository.findSentRequestsByUser(user, FriendshipStatus.PENDING, page - 1, size); logger.info("[LOG] " + friendships.size() + " demandes d'amitié envoyées récupérées."); return friendships.stream().map(FriendshipReadStatusResponseDTO::new).toList(); } /** * Récupérer les demandes d'amitié reçues par un utilisateur. * * @param userId ID de l'utilisateur. * @param page Numéro de la page. * @param size Taille de la page. * @return Liste des demandes d'amitié reçues. */ public List listReceivedFriendRequests(UUID userId, int page, int size) { Users user = usersRepository.findById(userId); if (user == null) { logger.error("[ERROR] Utilisateur non trouvé."); throw new UserNotFoundException("Utilisateur introuvable."); } List friendships = friendshipRepository.findReceivedRequestsByUser(user, FriendshipStatus.PENDING, page - 1, size); logger.info("[LOG] " + friendships.size() + " demandes d'amitié reçues récupérées."); return friendships.stream().map(FriendshipReadStatusResponseDTO::new).toList(); } /** * Récupérer les suggestions d'amis pour un utilisateur. * * L'algorithme suggère des utilisateurs basés sur : * 1. Amis d'amis (utilisateurs qui ont des amis en commun) * 2. Utilisateurs récents (qui ne sont ni amis ni ont de demandes en attente) * * @param userId ID de l'utilisateur. * @param limit Nombre maximum de suggestions à retourner. * @return Liste des suggestions d'amis. */ public List getFriendSuggestions(UUID userId, int limit) { logger.info("[LOG] Récupération des suggestions d'amis pour l'utilisateur : " + userId); Users user = usersRepository.findById(userId); if (user == null) { logger.error("[ERROR] Utilisateur non trouvé."); throw new UserNotFoundException("Utilisateur introuvable."); } // Récupérer tous les amis actuels de l'utilisateur (ACCEPTED) // Utiliser une taille de page élevée pour récupérer tous les résultats List currentFriendships = friendshipRepository.findByUserAndStatus(user, FriendshipStatus.ACCEPTED, 0, 10000); Set currentFriendIds = new HashSet<>(); for (Friendship friendship : currentFriendships) { // Ajouter les IDs des amis (que l'utilisateur ait envoyé ou reçu la demande) if (friendship.getUser().getId().equals(userId)) { currentFriendIds.add(friendship.getFriend().getId()); } else { currentFriendIds.add(friendship.getUser().getId()); } } // Récupérer toutes les demandes en attente pour exclure ces utilisateurs List pendingFriendships = friendshipRepository.findByUserAndStatus(user, FriendshipStatus.PENDING, 0, 10000); Set pendingUserIds = new HashSet<>(); for (Friendship friendship : pendingFriendships) { if (friendship.getUser().getId().equals(userId)) { pendingUserIds.add(friendship.getFriend().getId()); } else { pendingUserIds.add(friendship.getUser().getId()); } } // Map pour compter les amis en commun Map mutualFriendsCount = new HashMap<>(); // Pour chaque ami, trouver ses amis (amis d'amis) for (UUID friendId : currentFriendIds) { Users friend = usersRepository.findById(friendId); if (friend == null) continue; List friendsOfFriend = friendshipRepository.findByUserAndStatus(friend, FriendshipStatus.ACCEPTED, 0, 10000); for (Friendship fof : friendsOfFriend) { UUID potentialFriendId; if (fof.getUser().getId().equals(friendId)) { potentialFriendId = fof.getFriend().getId(); } else { potentialFriendId = fof.getUser().getId(); } // Exclure l'utilisateur lui-même, ses amis actuels et les demandes en attente if (!potentialFriendId.equals(userId) && !currentFriendIds.contains(potentialFriendId) && !pendingUserIds.contains(potentialFriendId)) { mutualFriendsCount.put(potentialFriendId, mutualFriendsCount.getOrDefault(potentialFriendId, 0) + 1); } } } // Trier par nombre d'amis en commun (décroissant) List> sortedSuggestions = mutualFriendsCount.entrySet() .stream() .sorted(Map.Entry.comparingByValue().reversed()) .limit(limit) .toList(); // Créer les DTOs List suggestions = new ArrayList<>(); for (Map.Entry entry : sortedSuggestions) { Users suggestedUser = usersRepository.findById(entry.getKey()); if (suggestedUser != null) { String reason = entry.getValue() > 1 ? entry.getValue() + " amis en commun" : "1 ami en commun"; suggestions.add(new com.lions.dev.dto.response.users.FriendSuggestionResponseDTO( suggestedUser, entry.getValue(), reason )); } } // Si pas assez de suggestions, ajouter des utilisateurs récents if (suggestions.size() < limit) { int remaining = limit - suggestions.size(); Set excludedIds = new HashSet<>(currentFriendIds); excludedIds.addAll(pendingUserIds); excludedIds.add(userId); excludedIds.addAll(mutualFriendsCount.keySet()); // Récupérer des utilisateurs récents qui ne sont pas dans les exclusions List recentUsers = usersRepository.findAll() .stream() .filter(u -> !excludedIds.contains(u.getId())) .limit(remaining) .toList(); for (Users recentUser : recentUsers) { suggestions.add(new com.lions.dev.dto.response.users.FriendSuggestionResponseDTO( recentUser, 0, "Nouvel utilisateur" )); } } logger.info("[LOG] " + suggestions.size() + " suggestions d'amis générées."); return suggestions; } }