package handlers import ( "net/http" "strconv" "time" apperrors "veza-backend-api/internal/errors" "veza-backend-api/internal/services" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" ) // AnalyticsHandler gère les opérations d'analytics de lecture de tracks type AnalyticsHandler struct { analyticsService *services.AnalyticsService commonHandler *CommonHandler } // NewAnalyticsHandler crée un nouveau handler d'analytics func NewAnalyticsHandler(analyticsService *services.AnalyticsService, logger *zap.Logger) *AnalyticsHandler { return &AnalyticsHandler{ analyticsService: analyticsService, commonHandler: NewCommonHandler(logger), } } // RecordPlayRequest représente la requête pour enregistrer une lecture // MOD-P1-001: Ajout tags validate pour validation systématique type RecordPlayRequest struct { Duration int `json:"duration" binding:"required,min=1" validate:"required,min=1"` Device string `json:"device,omitempty" validate:"omitempty,max=100"` } // RecordPlay gère l'enregistrement d'une lecture de track func (h *AnalyticsHandler) RecordPlay(c *gin.Context) { trackIDStr := c.Param("id") if trackIDStr == "" { RespondWithAppError(c, apperrors.NewValidationError("track id is required")) return } trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid track id")) return } var req RecordPlayRequest if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil { RespondWithAppError(c, appErr) return } // Récupérer user_id si authentifié (optionnel pour analytics anonymes) var userID *uuid.UUID if uid, ok := c.Get("user_id"); ok { if uidUUID, ok := uid.(uuid.UUID); ok { userID = &uidUUID } } // Récupérer IP address et device ipAddress := c.ClientIP() device := req.Device if device == "" { device = c.GetHeader("User-Agent") } err = h.analyticsService.RecordPlay(c.Request.Context(), trackID, userID, req.Duration, device, ipAddress) if err != nil { if err.Error() == "track not found" { RespondWithAppError(c, apperrors.NewNotFoundError("track")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to record play", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"message": "play recorded"}) } // GetTrackStats gère la récupération des statistiques d'un track func (h *AnalyticsHandler) GetTrackStats(c *gin.Context) { trackIDStr := c.Param("id") if trackIDStr == "" { RespondWithAppError(c, apperrors.NewValidationError("track id is required")) return } trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid track id")) return } stats, err := h.analyticsService.GetTrackStats(c.Request.Context(), trackID) if err != nil { if err.Error() == "track not found" { RespondWithAppError(c, apperrors.NewNotFoundError("track")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get track stats", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"stats": stats}) } // GetTopTracks gère la récupération des tracks les plus écoutés func (h *AnalyticsHandler) GetTopTracks(c *gin.Context) { // Parse limit limit := 10 if limitStr := c.Query("limit"); limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 { limit = l } else { RespondWithAppError(c, apperrors.NewValidationError("invalid limit (must be between 1 and 100)")) return } } // Parse start_date (optionnel) var startDate *time.Time if startDateStr := c.Query("start_date"); startDateStr != "" { parsed, err := time.Parse(time.RFC3339, startDateStr) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid start_date format (use RFC3339)")) return } startDate = &parsed } // Parse end_date (optionnel) var endDate *time.Time if endDateStr := c.Query("end_date"); endDateStr != "" { parsed, err := time.Parse(time.RFC3339, endDateStr) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid end_date format (use RFC3339)")) return } endDate = &parsed } topTracks, err := h.analyticsService.GetTopTracks(c.Request.Context(), limit, startDate, endDate) if err != nil { RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get top tracks", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"tracks": topTracks}) } // GetPlaysOverTime gère la récupération des lectures sur une période func (h *AnalyticsHandler) GetPlaysOverTime(c *gin.Context) { trackIDStr := c.Param("id") if trackIDStr == "" { RespondWithAppError(c, apperrors.NewValidationError("track id is required")) return } trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid track id")) return } // Parse start_date (optionnel, défaut: 30 jours) startDate := time.Now().AddDate(0, 0, -30) if startDateStr := c.Query("start_date"); startDateStr != "" { parsed, err := time.Parse(time.RFC3339, startDateStr) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid start_date format (use RFC3339)")) return } startDate = parsed } // Parse end_date (optionnel, défaut: maintenant) endDate := time.Now() if endDateStr := c.Query("end_date"); endDateStr != "" { parsed, err := time.Parse(time.RFC3339, endDateStr) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid end_date format (use RFC3339)")) return } endDate = parsed } // Parse interval (optionnel, défaut: day) interval := c.DefaultQuery("interval", "day") validIntervals := map[string]bool{"hour": true, "day": true, "week": true, "month": true} if !validIntervals[interval] { RespondWithAppError(c, apperrors.NewValidationError("invalid interval (must be: hour, day, week, month)")) return } points, err := h.analyticsService.GetPlaysOverTime(c.Request.Context(), trackID, startDate, endDate, interval) if err != nil { if err.Error() == "track not found" { RespondWithAppError(c, apperrors.NewNotFoundError("track")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get plays over time", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"points": points}) } // GetUserStats gère la récupération des statistiques d'un utilisateur func (h *AnalyticsHandler) GetUserStats(c *gin.Context) { userIDStr := c.Param("id") if userIDStr == "" { RespondWithAppError(c, apperrors.NewValidationError("user id is required")) return } userID, err := uuid.Parse(userIDStr) if err != nil { RespondWithAppError(c, apperrors.NewValidationError("invalid user id")) return } // Vérifier que l'utilisateur peut accéder à ses propres stats var authenticatedUserID *uuid.UUID if uid, ok := c.Get("user_id"); ok { if uidUUID, ok := uid.(uuid.UUID); ok { authenticatedUserID = &uidUUID } } if authenticatedUserID != nil && *authenticatedUserID != userID { RespondWithAppError(c, apperrors.NewForbiddenError("cannot access other user's stats")) return } stats, err := h.analyticsService.GetUserStats(c.Request.Context(), userID) if err != nil { if err.Error() == "user not found" { RespondWithAppError(c, apperrors.NewNotFoundError("user")) return } RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get user stats", err)) return } RespondSuccess(c, http.StatusOK, gin.H{"stats": stats}) }