UserController.java

package com.archiweb.api;

import com.archiweb.dto.ParticipationDTO;
import com.archiweb.dto.RoleDTO;
import com.archiweb.dto.UserProfileDto;
import com.archiweb.dto.UserWithParticipationsDTO;
import com.archiweb.model.Participation;
import com.archiweb.model.Role;
import com.archiweb.model.User;
import com.archiweb.model.Win;
import com.archiweb.repository.RoleRepository;
import com.archiweb.repository.UserRepository;
import com.archiweb.repository.WinRepository;
import com.archiweb.security.JwtService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/users")
@Tag(
        name = "Utilisateurs",
        description = """
        Gestion complète des utilisateurs :
        - Consultation du profil et des rôles
        - Mise à jour complète ou partielle du profil
        - Suppression d’un compte utilisateur
        - Consultation de l’historique des gains
        - Pagination et filtrage des utilisateurs
        """
)
public class UserController {

    private final UserRepository userRepository;
    private final RoleRepository roleRepository;
    private final WinRepository winRepository;
    private final JwtService jwtService;

    public UserController(UserRepository userRepository,
                          RoleRepository roleRepository,
                          WinRepository winRepository,
                          JwtService jwtService) {
        this.userRepository = userRepository;
        this.roleRepository = roleRepository;
        this.winRepository = winRepository;
        this.jwtService = jwtService;
    }

    // ================================================================
    // 1️⃣ Lister les utilisateurs (paginé)
    // ================================================================
    @Operation(
            summary = "Lister les utilisateurs (paginé)",
            description = """
            Retourne la liste paginée des utilisateurs avec leurs rôles et informations de base.
            Les paramètres `page` et `size` permettent de contrôler la pagination.
            """,
            parameters = {
                    @Parameter(name = "page", description = "Numéro de la page (par défaut : 0)"),
                    @Parameter(name = "size", description = "Taille de la page (par défaut : 10)")
            },
            responses = {
                    @ApiResponse(responseCode = "200", description = "Liste paginée des utilisateurs récupérée avec succès",
                            content = @Content(mediaType = "application/json",
                                    schema = @Schema(implementation = Page.class),
                                    examples = @ExampleObject(value = """
                                            {
                                              "content": [
                                                {
                                                  "id": 1,
                                                  "username": "john_doe",
                                                  "email": "john@example.com",
                                                  "role": { "name": "ROLE_USER" }
                                                }
                                              ],
                                              "pageable": { "pageNumber": 0, "pageSize": 10 },
                                              "totalElements": 25
                                            }
                                            """)
                            ))
            }
    )
    @GetMapping
    public ResponseEntity<Page<UserWithParticipationsDTO>> getAllUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size
    ) {
        Pageable pageable = PageRequest.of(page, size);

        // Charger les utilisateurs avec leurs participations (via @EntityGraph)
        Page<User> users = userRepository.findAll(pageable);

        Page<UserWithParticipationsDTO> dtoPage = users.map(user -> {
            UserWithParticipationsDTO dto = new UserWithParticipationsDTO();

            dto.setId(user.getId());
            dto.setUsername(user.getUsername());
            dto.setEmail(user.getEmail());
            dto.setConsentGiven(user.isConsentGiven());

            dto.setFirstName(user.getFirstName());
            dto.setLastName(user.getLastName());
            dto.setPhone(user.getPhone());
            dto.setBirthDate(user.getBirthDate());
            dto.setActive(user.isActive());
            
            // Mapper le rôle en créant un DTO pour éviter les problèmes de proxy Hibernate
            if (user.getRole() != null) {
                // Forcer l'initialisation du rôle en accédant à ses propriétés
                Role role = user.getRole();
                RoleDTO roleDTO = new RoleDTO();
                roleDTO.setId(role.getId());
                roleDTO.setName(role.getName());
                dto.setRole(roleDTO);
            }

            // Mapper les participations
            if (user.getParticipations() != null && !user.getParticipations().isEmpty()) {
                List<ParticipationDTO> participationDTOs = user.getParticipations().stream()
                    .map(participation -> {
                        ParticipationDTO pDto = new ParticipationDTO();
                        pDto.setId(participation.getId());
                        pDto.setCode(participation.getCode() != null ? participation.getCode().getValeur() : null);
                        pDto.setPrizeType(participation.getPrizeName() != null ? participation.getPrizeName() : 
                                         (participation.getCode() != null ? participation.getCode().getPrizeType().name() : null));
                        pDto.setClaimed(participation.isClaimed());
                        return pDto;
                    })
                    .collect(java.util.stream.Collectors.toList());
                dto.setParticipations(participationDTOs);
            } else {
                dto.setParticipations(new java.util.ArrayList<>());
            }

            return dto;
        });

        return ResponseEntity.ok(dtoPage);
    }



    // ================================================================
    // 2️⃣ Supprimer un utilisateur
    // ================================================================
    @Operation(
            summary = "Supprimer un utilisateur",
            description = "Supprime définitivement un utilisateur à partir de son identifiant unique.",
            parameters = @Parameter(name = "id", description = "Identifiant de l'utilisateur à supprimer"),
            responses = {
                    @ApiResponse(responseCode = "200", description = "Utilisateur supprimé avec succès",
                            content = @Content(mediaType = "application/json",
                                    examples = @ExampleObject(value = """
                                            "Utilisateur supprimé avec succès"
                                            """))),
                    @ApiResponse(responseCode = "404", description = "Utilisateur introuvable")
            }
    )
    @DeleteMapping("/{id}")
    public ResponseEntity<?> deleteUser(@PathVariable Long id) {
        if (!userRepository.existsById(id)) {
            return ResponseEntity.notFound().build();
        }
        userRepository.deleteById(id);
        return ResponseEntity.ok("Utilisateur supprimé avec succès");
    }

    // ================================================================
    // 3️⃣ Mettre à jour complètement un utilisateur (PUT)
    // ================================================================
    @Operation(
            summary = "Mettre à jour un utilisateur (PUT)",
            description = """
            Remplace toutes les informations d’un utilisateur existant par celles fournies dans la requête.
            Les champs non fournis seront remplacés par des valeurs nulles.
            """,
            requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
                    description = "Informations de l'utilisateur à mettre à jour",
                    required = true,
                    content = @Content(
                            schema = @Schema(implementation = User.class),
                            examples = @ExampleObject(value = """
                                    {
                                      "username": "john_doe",
                                      "email": "john@example.com",
                                      "role": { "name": "ROLE_ADMIN" }
                                    }
                                    """)
                    )
            ),
            responses = {
                    @ApiResponse(responseCode = "200", description = "Utilisateur mis à jour avec succès"),
                    @ApiResponse(responseCode = "404", description = "Utilisateur introuvable")
            }
    )
    @PutMapping("/{id}")
    public ResponseEntity<?> updateUser(@PathVariable Long id, @RequestBody User updatedUser) {
        return userRepository.findById(id)
                .map(user -> {

                    // Champs basiques
                    user.setUsername(updatedUser.getUsername());
                    user.setEmail(updatedUser.getEmail());
                    user.setFirstName(updatedUser.getFirstName());
                    user.setLastName(updatedUser.getLastName());
                    user.setPhone(updatedUser.getPhone());
                    user.setBirthDate(updatedUser.getBirthDate());

                    // Nouveau champ "active"
                    user.setActive(updatedUser.isActive());

                    // Consentement RGPD
                    user.setConsentGiven(updatedUser.isConsentGiven());

                    // Role
                    if (updatedUser.getRole() != null) {
                        Role role = roleRepository.findByName(updatedUser.getRole().getName())
                                .orElseThrow(() -> new RuntimeException("Rôle non trouvé : " + updatedUser.getRole().getName()));
                        user.setRole(role);
                    }

                    // Audit updateAt sera mis automatiquement via @PreUpdate
                    userRepository.save(user);

                    return ResponseEntity.ok(user);
                })
                .orElse(ResponseEntity.notFound().build());
    }


    // ================================================================
    // 4️⃣ Récupérer le profil de l'utilisateur connecté
    // ================================================================
    @Operation(
            summary = "Obtenir le profil de l'utilisateur connecté",
            description = """
            Récupère les informations de l'utilisateur à partir du token JWT transmis dans l'en-tête `Authorization`.
            """,
            parameters = @Parameter(name = "Authorization", description = "Header contenant le token JWT (ex: Bearer eyJhbGci...)"),
            responses = {
                    @ApiResponse(responseCode = "200", description = "Utilisateur récupéré avec succès",
                            content = @Content(mediaType = "application/json",
                                    schema = @Schema(implementation = User.class),
                                    examples = @ExampleObject(value = """
                                            {
                                              "id": 1,
                                              "email": "john@example.com",
                                              "username": "john_doe",
                                              "role": { "name": "ROLE_USER" }
                                            }
                                            """)
                            )),
                    @ApiResponse(responseCode = "400", description = "Token manquant ou invalide"),
                    @ApiResponse(responseCode = "404", description = "Utilisateur non trouvé")
            }
    )
    @GetMapping("/me")
    public ResponseEntity<?> getCurrentUser(@RequestHeader("Authorization") String authHeader) {
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return ResponseEntity.badRequest().body("Token manquant ou invalide");
        }

        String token = authHeader.substring(7);
        String email = jwtService.extractEmail(token);

        Optional<User> user = userRepository.findByEmail(email);

        if (user.isEmpty()) {
            return ResponseEntity.status(404).body("Utilisateur non trouvé");
        }

        User u = user.get();

        return ResponseEntity.ok(u);

    }


    // ================================================================
    // 5️⃣ Récupérer l'historique des gains d’un utilisateur
    // ================================================================
    @Operation(
            summary = "Obtenir l’historique des gains d’un utilisateur",
            description = """
            Renvoie la liste complète des gains associés à un utilisateur à partir de son identifiant.
            """,
            parameters = @Parameter(name = "id", description = "Identifiant de l'utilisateur"),
            responses = {
                    @ApiResponse(responseCode = "200", description = "Historique récupéré avec succès",
                            content = @Content(mediaType = "application/json",
                                    examples = @ExampleObject(value = """
                                            [
                                              { "id": 1, "prizeType": "INFUSEUR", "date": "2025-01-15" },
                                              { "id": 2, "prizeType": "COFFRET_39", "date": "2025-02-10" }
                                            ]
                                            """)
                            )),
                    @ApiResponse(responseCode = "404", description = "Utilisateur introuvable")
            }
    )
    @GetMapping("/{id}/wins")
    public ResponseEntity<List<Win>> getUserWins(@PathVariable Long id) {
        if (!userRepository.existsById(id)) {
            return ResponseEntity.notFound().build();
        }
        List<Win> wins = winRepository.findByUserId(id);
        return ResponseEntity.ok(wins);
    }

    // ================================================================
    // 6️⃣ Mise à jour partielle (PATCH)
    // ================================================================
    @Operation(
            summary = "Mettre à jour partiellement un utilisateur (PATCH)",
            description = """
            Permet de modifier uniquement certains champs du profil utilisateur sans écraser les autres données.
            """,
            requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
                    description = "Champs à mettre à jour (un ou plusieurs)",
                    required = true,
                    content = @Content(
                            schema = @Schema(implementation = User.class),
                            examples = @ExampleObject(value = """
                                    {
                                      "firstName": "John",
                                      "lastName": "Doe",
                                      "phone": "+33612345678"
                                    }
                                    """)
                    )
            ),
            responses = {
                    @ApiResponse(responseCode = "200", description = "Utilisateur mis à jour avec succès"),
                    @ApiResponse(responseCode = "404", description = "Utilisateur introuvable")
            }
    )
    @PatchMapping("/{id}")
    public ResponseEntity<User> updatePartialUser(@PathVariable Long id, @RequestBody User updatedData) {
        Optional<User> optionalUser = userRepository.findById(id);
        if (optionalUser.isEmpty()) {
            return ResponseEntity.notFound().build();
        }

        User user = optionalUser.get();

        // Champs classiques
        if (updatedData.getUsername() != null)
            user.setUsername(updatedData.getUsername());
        if (updatedData.getEmail() != null)
            user.setEmail(updatedData.getEmail());
        if (updatedData.getFirstName() != null)
            user.setFirstName(updatedData.getFirstName());
        if (updatedData.getLastName() != null)
            user.setLastName(updatedData.getLastName());
        if (updatedData.getPhone() != null)
            user.setPhone(updatedData.getPhone());
        if (updatedData.getBirthDate() != null)
            user.setBirthDate(updatedData.getBirthDate());

        // ✔ Nouveau champ : active
        if (updatedData.isActive() != user.isActive())
            user.setActive(updatedData.isActive());

        // ✔ Nouveau champ : consentGiven
        if (updatedData.isConsentGiven() != user.isConsentGiven())
            user.setConsentGiven(updatedData.isConsentGiven());

        // ✔ Préférences de cookies
        if (updatedData.getCookiePreferences() != null)
            user.setCookiePreferences(updatedData.getCookiePreferences());
        if (updatedData.isCookieConsentGiven() != user.isCookieConsentGiven())
            user.setCookieConsentGiven(updatedData.isCookieConsentGiven());

        // ✔ Rôle si fourni - Vérification améliorée
        if (updatedData.getRole() != null && updatedData.getRole().getName() != null) {
            String roleName = updatedData.getRole().getName();
            
            Optional<Role> roleOptional = roleRepository.findByName(roleName);
            Role role;
            
            if (roleOptional.isPresent()) {
                role = roleOptional.get();
            } else {
                // Si le rôle n'existe pas, le créer
                Role newRole = new Role();
                newRole.setName(roleName);
                newRole.setDescription("Rôle personnalisé : " + roleName);
                newRole.setActive(true);
                role = roleRepository.save(newRole);
            }
            
            // Toujours mettre à jour le rôle (même s'il est identique pour forcer la sauvegarde)
            String currentRoleName = (user.getRole() != null) ? user.getRole().getName() : null;
            if (!roleName.equals(currentRoleName)) {
                user.setRole(role);
            } else {
                // Même si identique, on force la mise à jour pour s'assurer que la relation est bien sauvegardée
                user.setRole(role);
            }
        }

        User savedUser = userRepository.save(user);

        return ResponseEntity.ok(user);
    }

}