AuthController.java

package com.archiweb.api;

import com.archiweb.dto.LoginRequest;
import com.archiweb.dto.OAuthRequest;
import com.archiweb.dto.RefreshTokenRequest;
import com.archiweb.dto.RegisterRequest;
import com.archiweb.service.OAuthService;
import com.archiweb.model.Role;
import com.archiweb.model.User;
import com.archiweb.repository.RoleRepository;
import com.archiweb.repository.UserRepository;
import com.archiweb.security.JwtService;
import io.swagger.v3.oas.annotations.Operation;
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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

import java.util.Map;
import java.util.Optional;

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
@Slf4j
@Tag(
        name = "Authentification",
        description = """
        Module complet de gestion de l'authentification des utilisateurs.  
        Ce contrôleur permet :
        - L'inscription d'un nouvel utilisateur avec création automatique du rôle par défaut.
        - La connexion et la génération d’un token JWT d’accès.
        - Le rafraîchissement du token d’accès via un refresh token valide.
        - La vérification de l’état opérationnel du module d’authentification (ping).
        """
)
public class AuthController {

    private final UserRepository userRepository;
    private final RoleRepository roleRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtService jwtService;
    private final OAuthService oAuthService;
    
    @Value("${admin.email:}")
    private String adminEmail;

    // =========================================================
    // Inscription (REGISTER)
    // =========================================================
    @Operation(
            summary = "Inscription d’un nouvel utilisateur",
            description = """
            Crée un nouveau compte utilisateur avec un mot de passe chiffré et le rôle par défaut **ROLE_USER**.  
            Retourne un token JWT d’accès immédiatement utilisable pour les appels sécurisés.
            """,
            requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
                    description = "Données nécessaires à la création du compte",
                    required = true,
                    content = @Content(
                            schema = @Schema(implementation = RegisterRequest.class),
                            examples = @ExampleObject(
                                    value = """
                                    {
                                      "email": "newuser@example.com",
                                      "password": "StrongP@ssw0rd",
                                      "username": "newuser"
                                    }
                                    """
                            )
                    )
            ),
            responses = {
                    @ApiResponse(
                            responseCode = "200",
                            description = "Utilisateur créé avec succès",
                            content = @Content(
                                    mediaType = "application/json",
                                    examples = @ExampleObject(
                                            value = """
                                            {
                                              "message": "Inscription réussie",
                                              "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
                                            }
                                            """
                                    )
                            )
                    ),
                    @ApiResponse(
                            responseCode = "400",
                            description = "Email déjà utilisé",
                            content = @Content(
                                    mediaType = "application/json",
                                    examples = @ExampleObject(value = "{ \"error\": \"Email déjà utilisé\" }")
                            )
                    )
            }
    )
    @PostMapping("/register")
    public ResponseEntity<?> register(@RequestBody RegisterRequest request) {
        String email = request.getEmail();
        String password = request.getPassword();
        String username = request.getUsername() != null ? request.getUsername() : email.split("@")[0];

        if (userRepository.findByEmail(email).isPresent()) {
            return ResponseEntity.badRequest().body(Map.of("error", "Email déjà utilisé"));
        }

        Role userRole = roleRepository.findByName("ROLE_USER")
                .orElseGet(() -> roleRepository.save(new Role(null, "ROLE_USER", "Rôle utilisateur standard. Permet de participer au jeu concours et de consulter son profil.", true)));

        User user = new User();
        user.setEmail(email);
        user.setUsername(username);
        user.setPassword(passwordEncoder.encode(password));
        user.setRole(userRole);
        userRepository.save(user);

        String token = jwtService.generateToken(user);

        return ResponseEntity.ok(Map.of(
                "message", "Inscription réussie",
                "accessToken", token
        ));
    }

    // =========================================================
    // Connexion (LOGIN)
    // =========================================================
    @Operation(
            summary = "Connexion",
            description = """
            Authentifie un utilisateur à partir de son **email** et **mot de passe**.  
            Retourne un token JWT d’accès si les identifiants sont valides.
            """,
            requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
                    description = "Identifiants de connexion",
                    required = true,
                    content = @Content(
                            schema = @Schema(implementation = LoginRequest.class),
                            examples = @ExampleObject(
                                    value = """
                                    {
                                      "email": "user@example.com",
                                      "password": "StrongP@ssw0rd"
                                    }
                                    """
                            )
                    )
            ),
            responses = {
                    @ApiResponse(
                            responseCode = "200",
                            description = "Connexion réussie",
                            content = @Content(
                                    mediaType = "application/json",
                                    examples = @ExampleObject(
                                            value = """
                                            {
                                              "message": "Connexion réussie",
                                              "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
                                            }
                                            """
                                    )
                            )
                    ),
                    @ApiResponse(
                            responseCode = "401",
                            description = "Identifiants invalides",
                            content = @Content(
                                    mediaType = "application/json",
                                    examples = @ExampleObject(value = "{ \"error\": \"Email ou mot de passe incorrect\" }")
                            )
                    )
            }
    )
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        String email = request.getEmail();
        String password = request.getPassword();

        Optional<User> optionalUser = userRepository.findByEmail(email);
        if (optionalUser.isEmpty()) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(Map.of("error", "Email ou mot de passe incorrect"));
        }

        User user = optionalUser.get();
        if (!passwordEncoder.matches(password, user.getPassword())) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(Map.of("error", "Email ou mot de passe incorrect"));
        }

        // ✅ FORCER l'activation TOUJOURS pour contourner le problème de mapping bit(1) -> boolean
        // Même si isActive() retourne true, on force l'activation pour s'assurer que la valeur est bien persistée
        // Cela résout le problème où Hibernate lit false même avec active=1 en base
        user.setActive(true);
        userRepository.save(user);

        String token = jwtService.generateToken(user);
        return ResponseEntity.ok(Map.of(
                "message", "Connexion réussie",
                "accessToken", token
        ));
    }

    // =========================================================
    // Rafraîchir le token (REFRESH)
    // =========================================================
    @Operation(
            summary = "Rafraîchir le token JWT",
            description = """
            Permet de générer un **nouveau token d’accès** à partir d’un refresh token valide.  
            Si le refresh token est invalide ou expiré, une erreur 401 est retournée.
            """,
            requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
                    description = "Refresh token à valider",
                    required = true,
                    content = @Content(
                            schema = @Schema(implementation = RefreshTokenRequest.class),
                            examples = @ExampleObject(
                                    value = """
                                    {
                                      "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
                                    }
                                    """
                            )
                    )
            ),
            responses = {
                    @ApiResponse(
                            responseCode = "200",
                            description = "Nouveau token généré avec succès",
                            content = @Content(
                                    mediaType = "application/json",
                                    examples = @ExampleObject(
                                            value = """
                                            {
                                              "message": "Nouveau token généré avec succès",
                                              "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
                                            }
                                            """
                                    )
                            )
                    ),
                    @ApiResponse(
                            responseCode = "400",
                            description = "Refresh token manquant",
                            content = @Content(
                                    mediaType = "application/json",
                                    examples = @ExampleObject(value = "{ \"error\": \"Missing refresh token\" }")
                            )
                    ),
                    @ApiResponse(
                            responseCode = "401",
                            description = "Refresh token invalide ou expiré",
                            content = @Content(
                                    mediaType = "application/json",
                                    examples = @ExampleObject(value = "{ \"error\": \"Invalid refresh token\" }")
                            )
                    )
            }
    )
    @PostMapping("/refresh")
    public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest request) {
        String oldToken = request.getRefreshToken();
        if (oldToken == null || oldToken.isBlank()) {
            return ResponseEntity.badRequest().body(Map.of("error", "Missing refresh token"));
        }
        try {
            String newToken = jwtService.refreshToken(oldToken);
            return ResponseEntity.ok(Map.of(
                    "message", "Nouveau token généré avec succès",
                    "accessToken", newToken
            ));
        } catch (Exception e) {
            return ResponseEntity.status(401).body(Map.of("error", "Invalid refresh token"));
        }
    }

    // =========================================================
    // Vérification du module (PING)
    // =========================================================
    @Operation(
            summary = "Vérification du module d'authentification",
            description = """
            Vérifie que le module d’authentification est bien opérationnel et accessible.  
            Utile pour les tests d’intégration et la supervision.
            """,
            responses = @ApiResponse(
                    responseCode = "200",
                    description = "Module opérationnel",
                    content = @Content(
                            mediaType = "text/plain",
                            examples = @ExampleObject(value = "AuthController opérationnel")
                    )
            )
    )
    @GetMapping("/ping")
    public ResponseEntity<String> ping() {
        return ResponseEntity.ok("AuthController opérationnel");
    }

    // =========================================================
    // OAuth Login (Google / Facebook)
    // =========================================================
    @Operation(
            summary = "Connexion via OAuth (Google/Facebook)",
            description = """
            Authentifie un utilisateur via un token OAuth (Google ou Facebook).
            Si l'utilisateur n'existe pas, il sera créé automatiquement.
            """,
            requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
                    description = "Token OAuth et informations utilisateur",
                    required = true,
                    content = @Content(
                            schema = @Schema(implementation = OAuthRequest.class),
                            examples = @ExampleObject(
                                    value = """
                                    {
                                      "token": "ya29.a0AfH6SMC...",
                                      "provider": "google",
                                      "email": "user@gmail.com",
                                      "firstName": "John",
                                      "lastName": "Doe"
                                    }
                                    """
                            )
                    )
            ),
            responses = {
                    @ApiResponse(
                            responseCode = "200",
                            description = "Connexion réussie",
                            content = @Content(
                                    mediaType = "application/json",
                                    examples = @ExampleObject(
                                            value = """
                                            {
                                              "message": "Connexion OAuth réussie",
                                              "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
                                              "isNewUser": false
                                            }
                                            """
                                    )
                            )
                    ),
                    @ApiResponse(
                            responseCode = "401",
                            description = "Token OAuth invalide",
                            content = @Content(
                                    mediaType = "application/json",
                                    examples = @ExampleObject(value = "{ \"error\": \"Token OAuth invalide\" }")
                            )
                    )
            }
    )
    @PostMapping("/oauth")
    public ResponseEntity<?> oauthLogin(@RequestBody OAuthRequest request) {
        String token = request.getToken();
        String provider = request.getProvider();
        String email = request.getEmail();
        String firstName = request.getFirstName();
        String lastName = request.getLastName();
        String username = request.getUsername();

        if (token == null || token.isBlank()) {
            return ResponseEntity.badRequest().body(Map.of("error", "Token OAuth manquant"));
        }

        // Vérifier le token selon le provider
        Map<String, String> userInfo;
        if ("google".equalsIgnoreCase(provider)) {
            userInfo = oAuthService.verifyGoogleToken(token);
        } else if ("facebook".equalsIgnoreCase(provider)) {
            userInfo = oAuthService.verifyFacebookToken(token);
        } else {
            return ResponseEntity.badRequest().body(Map.of("error", "Provider non supporté. Utilisez 'google' ou 'facebook'"));
        }

        // Si la vérification du token échoue mais que le frontend a fourni des informations utilisateur,
        // on accepte quand même la connexion (le frontend a déjà vérifié le token)
        // Cela permet de gérer les cas où le token expire entre le moment où le frontend le vérifie
        // et le moment où le backend le vérifie, ou les problèmes de réseau temporaires
        boolean tokenValid = "true".equals(userInfo.get("valid"));
        // Vérifier que nous avons au moins un email (le plus important)
        // Les autres champs peuvent avoir des valeurs par défaut
        boolean hasEmailFromRequest = email != null && !email.isBlank();
        String emailFromProvider = userInfo.get("email");
        boolean hasEmailFromProvider = emailFromProvider != null && !emailFromProvider.isBlank();
        
        // Log pour déboguer
        log.info("OAuth Login - Provider: {}, TokenValid: {}, HasEmailFromRequest: {}, HasEmailFromProvider: {}, Email: {}", 
                provider, tokenValid, hasEmailFromRequest, hasEmailFromProvider,
                email != null ? email.replaceAll("@.*", "@***") : "null");
        
        // Si on n'a pas d'email du tout (ni de la requête ni du provider), rejeter
        if (!hasEmailFromRequest && !hasEmailFromProvider) {
            log.warn("OAuth Login rejeté - Aucun email disponible (ni requête ni provider)");
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                    .body(Map.of("error", "Email non disponible. Veuillez réessayer."));
        }
        
        // Si le token n'est pas valide MAIS qu'on a un email valide dans la requête, on accepte quand même
        // Le frontend a déjà vérifié le token, donc on fait confiance aux informations fournies
        if (!tokenValid && !hasEmailFromRequest) {
            log.warn("OAuth Login rejeté - Token invalide et pas d'email dans la requête");
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(Map.of("error", "Token OAuth invalide ou expiré. Veuillez réessayer."));
        }
        
        // Si le token est invalide mais qu'on a un email valide, on continue (le frontend a vérifié)
        if (!tokenValid && hasEmailFromRequest) {
            log.info("OAuth Login - Token invalide côté backend mais email valide fourni par le frontend, on accepte");
        }

        // Utiliser les infos du token si disponibles, sinon celles de la requête
        // Pour l'email : si le provider ne le fournit pas, utiliser celui de la requête
        // L'email de la requête a la priorité car le frontend l'a déjà vérifié
        if (hasEmailFromProvider && !hasEmailFromRequest) {
            // Si on n'a que l'email du provider, l'utiliser
            email = emailFromProvider;
        } else if (hasEmailFromRequest) {
            // L'email de la requête a la priorité (déjà vérifié par le frontend)
            // email reste tel quel
        }
        // Sinon, email est déjà défini et valide
        
        // Utiliser les infos du provider si disponibles, sinon celles de la requête
        // Avec des valeurs par défaut si tout est vide
        String firstNameFromProvider = userInfo.get("firstName");
        String lastNameFromProvider = userInfo.get("lastName");
        
        if (firstNameFromProvider != null && !firstNameFromProvider.isBlank()) {
            firstName = firstNameFromProvider;
        } else if (firstName == null || firstName.isBlank()) {
            // Valeur par défaut si aucune info n'est disponible
            firstName = "Utilisateur";
        }
        
        if (lastNameFromProvider != null && !lastNameFromProvider.isBlank()) {
            lastName = lastNameFromProvider;
        } else if (lastName == null || lastName.isBlank()) {
            // Valeur par défaut selon le provider
            lastName = "google".equalsIgnoreCase(provider) ? "Google" : "Facebook";
        }
        
        // Pour le username, utiliser celui du provider ou de la requête, ou générer depuis l'email
        String usernameFromProvider = userInfo.get("username");
        if (usernameFromProvider != null && !usernameFromProvider.isBlank()) {
            username = usernameFromProvider;
        } else if (username == null || username.isBlank()) {
            // email est garanti d'être non-null à ce point (vérifié plus haut)
            if (email != null) {
                username = email.split("@")[0];
            } else {
                username = "user";
            }
        }

            // Chercher ou créer l'utilisateur
            Optional<User> optionalUser = userRepository.findByEmail(email);
            User user;
            boolean isNewUser = false;

            if (optionalUser.isPresent()) {
                user = optionalUser.get();
                // ✅ FORCER l'activation pour OAuth (même si isActive() retourne false à cause du mapping bit(1))
                // Si l'utilisateur se connecte via OAuth, c'est qu'il veut utiliser son compte
                user.setActive(true);  // Toujours activer, pas de condition
                
                // Vérifier si l'email correspond à l'admin et promouvoir en ADMIN si nécessaire
                if (adminEmail != null && !adminEmail.isBlank() && email.equalsIgnoreCase(adminEmail)) {
                    Role adminRole = roleRepository.findByName("ROLE_ADMIN")
                            .orElseGet(() -> roleRepository.save(new Role(null, "ROLE_ADMIN", "Rôle administrateur. Accès complet à toutes les fonctionnalités d'administration du système.", true)));
                    if (user.getRole() == null || !"ROLE_ADMIN".equals(user.getRole().getName())) {
                        user.setRole(adminRole);
                        log.info("OAuth Login - Promotion en ADMIN pour l'utilisateur existant: {}", email.replaceAll("@.*", "@***"));
                    }
                }
                
                // Mettre à jour les infos si disponibles
                if (firstName != null && !firstName.isBlank()) user.setFirstName(firstName);
                if (lastName != null && !lastName.isBlank()) user.setLastName(lastName);
                userRepository.save(user);
            } else {
                // Créer un nouvel utilisateur
                // Vérifier si l'email correspond à l'admin configuré
                Role assignedRole;
                if (adminEmail != null && !adminEmail.isBlank() && email.equalsIgnoreCase(adminEmail)) {
                    // Si l'email correspond à l'admin, lui donner le rôle ADMIN
                    assignedRole = roleRepository.findByName("ROLE_ADMIN")
                            .orElseGet(() -> roleRepository.save(new Role(null, "ROLE_ADMIN", "Rôle administrateur. Accès complet à toutes les fonctionnalités d'administration du système.", true)));
                    log.info("OAuth Login - Email admin détecté, attribution du rôle ADMIN à: {}", email.replaceAll("@.*", "@***"));
                } else {
                    // Sinon, rôle USER par défaut
                    assignedRole = roleRepository.findByName("ROLE_USER")
                            .orElseGet(() -> roleRepository.save(new Role(null, "ROLE_USER", "Rôle utilisateur standard. Permet de participer au jeu concours et de consulter son profil.", true)));
                }

                user = new User();
                user.setEmail(email);
                user.setUsername(username);
                user.setFirstName(firstName);
                user.setLastName(lastName);
                user.setRole(assignedRole);
                // Pas de mot de passe pour les utilisateurs OAuth
                user.setPassword(null);
                user.setActive(true);  // ✅ Activer les nouveaux utilisateurs OAuth
                user.setConsentGiven(true);  // ✅ Consentement donné par défaut pour OAuth
                userRepository.save(user);
                isNewUser = true;
            }

            String jwtToken = jwtService.generateToken(user);

        return ResponseEntity.ok(Map.of(
                "message", "Connexion OAuth réussie",
                "accessToken", jwtToken,
                "isNewUser", isNewUser
        ));
    }
}