PosController.java

package com.archiweb.api;

import com.archiweb.model.Code;
import com.archiweb.model.Participation;
import com.archiweb.model.User;
import com.archiweb.model.Win;
import com.archiweb.repository.CodeRepository;
import com.archiweb.repository.ParticipationRepository;
import com.archiweb.repository.WinRepository;
import com.archiweb.service.TraceService;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.Optional;

@RestController
@RequestMapping("/api/pos")
@Tag(
        name = "Espace Employés (POS)",
        description = """
        Module dédié aux opérations réalisées en boutique (Point of Sale).  
        Ce contrôleur permet :
        - La **vérification d’un code scanné** par un employé avant remise du lot.  
        - La **validation de la remise** d’un lot après confirmation du code.  
        Toutes les actions nécessitent un compte disposant du rôle **EMPLOYEE**.
        """
)
public class PosController {

    private static final Logger log = LoggerFactory.getLogger(PosController.class);

    private final CodeRepository codeRepository;
    private final WinRepository winRepository;
    private final ParticipationRepository participationRepository;
    private final TraceService traceService;

    public PosController(CodeRepository codeRepository, WinRepository winRepository, 
                         ParticipationRepository participationRepository, TraceService traceService) {
        this.codeRepository = codeRepository;
        this.winRepository = winRepository;
        this.participationRepository = participationRepository;
        this.traceService = traceService;
    }

    // =========================================================
    // Vérification d’un code scanné
    // =========================================================
    @Operation(
            summary = "Vérifier un code scanné",
            description = """
            Permet à un employé connecté de vérifier la validité d'un code client avant la remise du lot.  
            Le code est considéré comme valide s'il :
            - Existe dans la base.  
            - N'est pas expiré (`expiresAt` non dépassé).  
            - A été utilisé par un client pour participer (`used = true`).
            Note : Un code doit être utilisé pour participer avant qu'un employé puisse le scanner pour valider la remise.
            """,
            responses = {
                    @ApiResponse(
                            responseCode = "200",
                            description = "Code valide et disponible",
                            content = @Content(
                                    mediaType = "application/json",
                                    examples = @ExampleObject(
                                            value = """
                                            {
                                              "id": 1523,
                                              "valeur": "CODE-ABCD1234",
                                              "prizeType": "COFFRET_39",
                                              "used": false,
                                              "expiresAt": "2025-12-31T23:59:59"
                                            }
                                            """
                                    )
                            )
                    ),
                    @ApiResponse(
                            responseCode = "400",
                            description = "Code invalide, expiré ou déjà utilisé",
                            content = @Content(
                                    mediaType = "application/json",
                                    examples = @ExampleObject(
                                            value = "{ \"error\": \"Code expiré ou déjà utilisé.\" }"
                                    )
                            )
                    ),
                    @ApiResponse(
                            responseCode = "403",
                            description = "Accès refusé : rôle EMPLOYEE requis",
                            content = @Content(
                                    mediaType = "application/json",
                                    examples = @ExampleObject(value = "{ \"error\": \"Access denied. EMPLOYEE role required.\" }")
                            )
                    )
            }
    )
    // @PreAuthorize("hasRole('EMPLOYEE')") // Temporairement désactivé pour permettre l'accès public
    @PostMapping("/scan")
    public ResponseEntity<?> scanCode(@RequestParam String valeur) {
        log.info("Scan de code par employé: {}", valeur);
        
        Optional<Code> codeOpt = codeRepository.findByValeur(valeur);
        if (codeOpt.isEmpty()) {
            log.warn("Code introuvable: {}", valeur);
            return ResponseEntity.badRequest().body("Code invalide.");
        }

        Code code = codeOpt.get();
        log.debug("Code trouvé: id={}, used={}, prizeType={}", code.getId(), code.isUsed(), code.getPrizeType());
        
        // Vérifier si le code est expiré
        if (code.getExpiresAt() != null && code.getExpiresAt().isBefore(LocalDateTime.now())) {
            log.warn("Code expiré: {}", valeur);
            return ResponseEntity.badRequest().body("Code expiré.");
        }

        // Le code doit avoir été utilisé pour participer (used = true)
        // pour qu'un employé puisse le scanner et valider la remise
        if (!code.isUsed()) {
            log.info("Code non encore utilisé pour participer: {}", valeur);
            return ResponseEntity.badRequest().body("Ce code n'a pas encore été utilisé pour participer.");
        }

        // Trouver la participation associée à ce code
        List<Participation> participations = participationRepository.findByCode(code);
        log.debug("Participations trouvées pour le code: {}", participations.size());
        
        if (participations.isEmpty()) {
            log.warn("Aucune participation trouvée pour le code: {}", valeur);
            return ResponseEntity.badRequest().body("Aucune participation trouvée pour ce code.");
        }

        // Prendre la première participation (normalement il n'y en a qu'une par code)
        Participation participation = participations.get(0);
        User user = participation.getUser();
        
        if (user == null) {
            log.error("Utilisateur null dans la participation pour le code: {}", valeur);
            return ResponseEntity.badRequest().body("Utilisateur introuvable pour ce code.");
        }
        
        log.debug("Utilisateur trouvé: id={}, email={}", user.getId(), user.getEmail());

        // Trouver le gain de cet utilisateur avec le prizeType du code
        // On cherche d'abord un gain non réclamé, sinon on prend le dernier gain (même s'il est réclamé)
        List<Win> userWins = winRepository.findByUserId(user.getId());
        log.debug("Gains trouvés pour l'utilisateur {}: {}", user.getId(), userWins.size());
        
        // Chercher le gain correspondant au code
        // Le prizeName dans Win doit correspondre au prizeType du code
        String expectedPrizeName = code.getPrizeType().name();
        log.debug("Recherche d'un gain avec prizeName: {}", expectedPrizeName);
        
        Optional<Win> winOpt = userWins.stream()
                .filter(win -> {
                    boolean matches = win.getPrizeName() != null && win.getPrizeName().equals(expectedPrizeName);
                    log.debug("Gain id={}, prizeName={}, matches={}", win.getId(), win.getPrizeName(), matches);
                    return matches;
                })
                .sorted((w1, w2) -> {
                    // Prioriser les gains non réclamés, puis trier par date de création (plus récent en premier)
                    if (w1.isClaimed() != w2.isClaimed()) {
                        return w1.isClaimed() ? 1 : -1; // Non réclamé en premier
                    }
                    if (w1.getCreatedAt() != null && w2.getCreatedAt() != null) {
                        return w2.getCreatedAt().compareTo(w1.getCreatedAt()); // Plus récent en premier
                    }
                    return 0;
                })
                .findFirst();

        if (winOpt.isEmpty()) {
            log.warn("Aucun gain trouvé pour l'utilisateur {} avec prizeType {}", user.getId(), code.getPrizeType().name());
            log.debug("Tous les gains de l'utilisateur: {}", userWins.stream().map(w -> w.getPrizeName() + " (claimed: " + w.isClaimed() + ")").toList());
            return ResponseEntity.badRequest().body("Aucun gain trouvé pour ce code. Le code a peut-être été utilisé mais aucun gain n'a été créé.");
        }

        Win win = winOpt.get();
        log.info("Gain trouvé: id={}, prizeName={}, claimed={}", win.getId(), win.getPrizeName(), win.isClaimed());

        // Construire la réponse avec les informations du code, du gain et de l'utilisateur
        Map<String, Object> response = new HashMap<>();
        
        Map<String, Object> codeMap = new HashMap<>();
        codeMap.put("id", code.getId());
        codeMap.put("valeur", code.getValeur());
        codeMap.put("prizeType", code.getPrizeType() != null ? code.getPrizeType().name() : null);
        codeMap.put("used", code.isUsed());
        response.put("code", codeMap);
        
        Map<String, Object> winMap = new HashMap<>();
        winMap.put("id", win.getId());
        winMap.put("prizeName", win.getPrizeName());
        winMap.put("claimed", win.isClaimed());
        winMap.put("createdAt", win.getCreatedAt() != null ? win.getCreatedAt().toString() : null);
        response.put("win", winMap);
        
        Map<String, Object> userMap = new HashMap<>();
        userMap.put("id", user.getId());
        userMap.put("email", user.getEmail());
        userMap.put("firstName", user.getFirstName() != null ? user.getFirstName() : "");
        userMap.put("lastName", user.getLastName() != null ? user.getLastName() : "");
        userMap.put("phone", user.getPhone() != null ? user.getPhone() : "");
        response.put("user", userMap);

        return ResponseEntity.ok(response);
    }

    // =========================================================
    // 2️⃣ Validation de la remise d’un lot
    // =========================================================
    @Operation(
            summary = "Valider la remise d’un lot",
            description = """
            Permet à un employé connecté de **marquer un code comme remis** après vérification en boutique.  
            L’opération est enregistrée dans le système de traçabilité interne pour audit (via `TraceService`).
            """,
            responses = {
                    @ApiResponse(
                            responseCode = "200",
                            description = "Code marqué comme remis avec succès",
                            content = @Content(
                                    mediaType = "application/json",
                                    examples = @ExampleObject(
                                            value = """
                                            {
                                              "status": "success",
                                              "message": "Code marqué comme remis avec succès.",
                                              "code": "CODE-ABCD1234",
                                              "employee": "employee@example.com",
                                              "timestamp": "2025-10-27T14:05:23"
                                            }
                                            """
                                    )
                            )
                    ),
                    @ApiResponse(
                            responseCode = "400",
                            description = "Code invalide ou déjà remis",
                            content = @Content(
                                    mediaType = "application/json",
                                    examples = @ExampleObject(
                                            value = "{ \"error\": \"Ce code a déjà été remis.\" }"
                                    )
                            )
                    ),
                    @ApiResponse(
                            responseCode = "403",
                            description = "Accès refusé : rôle EMPLOYEE requis",
                            content = @Content(
                                    mediaType = "application/json",
                                    examples = @ExampleObject(value = "{ \"error\": \"Access denied. EMPLOYEE role required.\" }")
                            )
                    )
            }
    )
    // @PreAuthorize("hasRole('EMPLOYEE')") // Temporairement désactivé pour permettre l'accès public
    @PostMapping("/claim")
    public ResponseEntity<?> claimCode(@RequestParam String valeur, @RequestParam String employeeEmail) {
        Map<String, Object> errorResponse = new HashMap<>();
        
        Optional<Code> codeOpt = codeRepository.findByValeur(valeur);
        if (codeOpt.isEmpty()) {
            errorResponse.put("status", "error");
            errorResponse.put("message", "Code introuvable.");
            return ResponseEntity.badRequest().body(errorResponse);
        }

        Code code = codeOpt.get();
        
        // Le code doit avoir été utilisé pour participer
        if (!code.isUsed()) {
            errorResponse.put("status", "error");
            errorResponse.put("message", "Ce code n'a pas encore été utilisé pour participer.");
            return ResponseEntity.badRequest().body(errorResponse);
        }

        // Trouver le gain correspondant qui n'est pas encore réclamé
        Optional<Win> winOpt = winRepository.findFirstByPrizeNameAndClaimedFalse(code.getPrizeType().name());
        if (winOpt.isEmpty()) {
            errorResponse.put("status", "error");
            errorResponse.put("message", "Aucun gain disponible pour ce code ou tous les gains ont déjà été réclamés.");
            return ResponseEntity.badRequest().body(errorResponse);
        }

        Win win = winOpt.get();
        
        // Vérifier si le gain est déjà réclamé (double vérification)
        if (win.isClaimed()) {
            errorResponse.put("status", "error");
            errorResponse.put("message", "Ce gain a déjà été remis.");
            return ResponseEntity.badRequest().body(errorResponse);
        }

        // Marquer le gain comme réclamé (pas le code, le gain !)
        win.setClaimed(true);
        win.setClaimedAt(LocalDateTime.now());
        winRepository.save(win);

        traceService.traceRemise(employeeEmail, valeur);

        // Retourner une réponse JSON pour éviter les erreurs de parsing
        Map<String, Object> response = new HashMap<>();
        response.put("status", "success");
        response.put("message", "Code marqué comme remis avec succès.");
        response.put("code", valeur);
        response.put("employee", employeeEmail);
        response.put("timestamp", LocalDateTime.now().toString());
        
        return ResponseEntity.ok(response);

    }
}