package handlers

import (
	"context"
	"fmt"
	"math"
	"net/http"
	"strconv"
	"time"

	"veza-backend-api/internal/dto"
	apperrors "veza-backend-api/internal/errors"
	"veza-backend-api/internal/models"
	"veza-backend-api/internal/services"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"go.uber.org/zap"
)

// PlaybackAnalyticsServiceInterfaceForHandler defines methods needed for playback analytics handler
type PlaybackAnalyticsServiceInterfaceForHandler interface {
	RecordPlayback(ctx context.Context, analytics *models.PlaybackAnalytics) error
	GetTrackStats(ctx context.Context, trackID uuid.UUID) (*services.PlaybackStats, error)
	GetSessionsByDateRange(ctx context.Context, trackID uuid.UUID, startDate, endDate time.Time) ([]models.PlaybackAnalytics, error)
}

// PlaybackAnalyticsRateLimiterInterface defines methods needed for rate limiter
type PlaybackAnalyticsRateLimiterInterface interface {
	CheckRateLimit(ctx context.Context, userID uuid.UUID) (*services.RateLimitResult, error)
	RecordRequest(ctx context.Context, userID uuid.UUID) error
	GetQuotaInfo(ctx context.Context, userID uuid.UUID) (map[string]interface{}, error)
}

// PlaybackHeatmapServiceInterface defines methods needed for heatmap service
type PlaybackHeatmapServiceInterface interface {
	GenerateHeatmap(ctx context.Context, trackID uuid.UUID, segmentSize int) (*services.HeatmapData, error)
}

// PlaybackAnalyticsHandler gère les requêtes pour les analytics de lecture
// T0358: Create Playback Analytics Endpoint
type PlaybackAnalyticsHandler struct {
	analyticsService PlaybackAnalyticsServiceInterfaceForHandler
	heatmapService   PlaybackHeatmapServiceInterface
	rateLimiter      PlaybackAnalyticsRateLimiterInterface // T0389: Create Playback Analytics Rate Limiting
	commonHandler    *CommonHandler
}

// NewPlaybackAnalyticsHandler crée un nouveau handler d'analytics de lecture
func NewPlaybackAnalyticsHandler(analyticsService *services.PlaybackAnalyticsService, logger *zap.Logger) *PlaybackAnalyticsHandler {
	return NewPlaybackAnalyticsHandlerWithInterface(analyticsService, logger)
}

// NewPlaybackAnalyticsHandlerWithInterface crée un nouveau handler avec interfaces (pour tests)
func NewPlaybackAnalyticsHandlerWithInterface(analyticsService PlaybackAnalyticsServiceInterfaceForHandler, logger *zap.Logger) *PlaybackAnalyticsHandler {
	return &PlaybackAnalyticsHandler{
		analyticsService: analyticsService,
		heatmapService:   nil,
		rateLimiter:      nil, // Rate limiter optionnel
		commonHandler:    NewCommonHandler(logger),
	}
}

// NewPlaybackAnalyticsHandlerWithRateLimiter crée un nouveau handler avec rate limiter
// T0389: Create Playback Analytics Rate Limiting
func NewPlaybackAnalyticsHandlerWithRateLimiter(analyticsService *services.PlaybackAnalyticsService, rateLimiter *services.PlaybackAnalyticsRateLimiter, logger *zap.Logger) *PlaybackAnalyticsHandler {
	return &PlaybackAnalyticsHandler{
		analyticsService: analyticsService,
		heatmapService:   nil,
		rateLimiter:      rateLimiter,
		commonHandler:    NewCommonHandler(logger),
	}
}

// NewPlaybackAnalyticsHandlerWithHeatmap crée un nouveau handler avec service heatmap
func NewPlaybackAnalyticsHandlerWithHeatmap(analyticsService *services.PlaybackAnalyticsService, heatmapService *services.PlaybackHeatmapService, logger *zap.Logger) *PlaybackAnalyticsHandler {
	return &PlaybackAnalyticsHandler{
		analyticsService: analyticsService,
		heatmapService:   heatmapService,
		rateLimiter:      nil,
		commonHandler:    NewCommonHandler(logger),
	}
}

// NewPlaybackAnalyticsHandlerFull crée un nouveau handler avec tous les services
// T0389: Create Playback Analytics Rate Limiting
func NewPlaybackAnalyticsHandlerFull(analyticsService *services.PlaybackAnalyticsService, heatmapService *services.PlaybackHeatmapService, rateLimiter *services.PlaybackAnalyticsRateLimiter, logger *zap.Logger) *PlaybackAnalyticsHandler {
	return &PlaybackAnalyticsHandler{
		analyticsService: analyticsService,
		heatmapService:   heatmapService,
		rateLimiter:      rateLimiter,
		commonHandler:    NewCommonHandler(logger),
	}
}

// RecordAnalyticsRequest représente la requête pour enregistrer des analytics de lecture
// T0388: Create Playback Analytics Validation - Amélioré avec validation
// MOD-P1-001: Ajout tags validate pour validation systématique
type RecordAnalyticsRequest struct {
	PlayTime       int        `json:"play_time" binding:"required,min=0" validate:"required,min=0"` // seconds
	PauseCount     int        `json:"pause_count" binding:"min=0" validate:"omitempty,min=0"`       // optional, default 0
	SeekCount      int        `json:"seek_count" binding:"min=0" validate:"omitempty,min=0"`        // optional, default 0
	CompletionRate *float64   `json:"completion_rate,omitempty" validate:"omitempty,gte=0,lte=1"`   // optional, will be calculated if not provided
	StartedAt      time.Time  `json:"started_at" binding:"required" validate:"required"`            // ISO 8601 format
	EndedAt        *time.Time `json:"ended_at,omitempty"`                                           // optional
}

// ValidationResult représente le résultat d'une validation
// T0388: Create Playback Analytics Validation
// GO-013: Utilise dto.ValidationError pour éviter les cycles d'import
type ValidationResult struct {
	Valid     bool
	Errors    []dto.ValidationError
	Sanitized *RecordAnalyticsRequest
}

// RecordAnalytics gère la requête POST /api/v1/tracks/:id/playback/analytics
// Enregistre les analytics de lecture pour un track
// T0358: Create Playback Analytics Endpoint
func (h *PlaybackAnalyticsHandler) RecordAnalytics(c *gin.Context) {
	userID, ok := GetUserIDUUID(c)
	if !ok {
		return // Erreur déjà envoyée par GetUserIDUUID
	}
	if userID == uuid.Nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		RespondWithAppError(c, apperrors.NewUnauthorizedError("unauthorized"))
		return
	}

	// Récupérer l'ID du track depuis les paramètres de l'URL
	trackIDStr := c.Param("id")
	trackID, err := uuid.Parse(trackIDStr)
	if err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "invalid track id"))
		return
	}

	// Valider et parser le body de la requête
	var req RecordAnalyticsRequest
	if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
		RespondWithAppError(c, appErr)
		return
	}

	// T0388: Create Playback Analytics Validation
	// Valider et sanitizer les données
	validationResult := h.validateAndSanitizeAnalyticsRequest(&req, trackID)
	if !validationResult.Valid {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		details := make([]apperrors.ErrorDetail, 0, len(validationResult.Errors))
		for _, ve := range validationResult.Errors {
			details = append(details, apperrors.ErrorDetail{
				Field:   ve.Field,
				Message: ve.Message,
			})
		}
		RespondWithAppError(c, apperrors.NewValidationError("Validation failed", details...))
		return
	}

	// Utiliser les données sanitizées
	req = *validationResult.Sanitized

	// T0389: Create Playback Analytics Rate Limiting
	// Vérifier le rate limiting si activé
	if h.rateLimiter != nil {
		rateLimitResult, err := h.rateLimiter.CheckRateLimit(c.Request.Context(), userID)
		if err != nil {
			// MOD-P2-003: Utiliser AppError au lieu de gin.H
			RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "failed to check rate limit"))
			return
		}

		if !rateLimitResult.Allowed {
			// Ajouter les headers de rate limiting
			c.Header("X-RateLimit-Remaining", "0")
			c.Header("X-RateLimit-Retry-After", strconv.FormatInt(int64(rateLimitResult.RetryAfter.Seconds()), 10))
			c.Header("X-RateLimit-Reason", rateLimitResult.Reason)

			c.JSON(http.StatusTooManyRequests, gin.H{
				"error":       "Rate limit exceeded",
				"reason":      rateLimitResult.Reason,
				"retry_after": int(rateLimitResult.RetryAfter.Seconds()),
				"quota_used":  rateLimitResult.QuotaUsed,
				"quota_limit": rateLimitResult.QuotaLimit,
			})
			return
		}

		// Ajouter les headers de rate limiting
		c.Header("X-RateLimit-Remaining", strconv.Itoa(rateLimitResult.Remaining))
	}

	// Créer le modèle PlaybackAnalytics
	analytics := &models.PlaybackAnalytics{
		TrackID:    trackID,
		UserID:     userID,
		PlayTime:   req.PlayTime,
		PauseCount: req.PauseCount,
		SeekCount:  req.SeekCount,
		StartedAt:  req.StartedAt,
		EndedAt:    req.EndedAt,
	}

	// Définir le completion_rate si fourni
	if req.CompletionRate != nil {
		analytics.CompletionRate = *req.CompletionRate
	}

	// Enregistrer les analytics via le service
	err = h.analyticsService.RecordPlayback(c.Request.Context(), analytics)
	if err != nil {
		// Gérer les erreurs spécifiques
		if err.Error() == "invalid track ID: 0" ||
			err.Error() == "invalid user ID: 0" ||
			err.Error()[:14] == "invalid play time" ||
			err.Error()[:14] == "invalid pause" ||
			err.Error()[:14] == "invalid seek" ||
			err.Error()[:14] == "invalid completion" ||
			err.Error() == "started_at is required" {
			// MOD-P2-003: Utiliser AppError au lieu de gin.H
			RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, err.Error()))
			return
		}
		if err.Error()[:13] == "track not found" {
			// MOD-P2-003: Utiliser AppError au lieu de gin.H
			RespondWithAppError(c, apperrors.NewNotFoundError("track"))
			return
		}
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, err.Error()))
		return
	}

	// T0389: Create Playback Analytics Rate Limiting
	// Enregistrer la requête dans le rate limiter si activé
	if h.rateLimiter != nil {
		if err := h.rateLimiter.RecordRequest(c.Request.Context(), userID); err != nil {
			// Logger l'erreur mais ne pas échouer la requête
			// Le rate limiting est une fonctionnalité de protection, pas critique
		}
	}

	// Retourner le succès
	RespondSuccess(c, http.StatusOK, gin.H{
		"status": "recorded",
		"id":     analytics.ID,
	})
}

// GetQuotaInfo gère la requête GET /api/v1/playback/analytics/quota
// Retourne les informations de quota pour l'utilisateur actuel
// T0389: Create Playback Analytics Rate Limiting
func (h *PlaybackAnalyticsHandler) GetQuotaInfo(c *gin.Context) {
	// Récupérer l'ID de l'utilisateur depuis le contexte
	userID, ok := GetUserIDUUID(c)
	if !ok {
		return // Erreur déjà envoyée par GetUserIDUUID
	}
	if userID == uuid.Nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		RespondWithAppError(c, apperrors.NewUnauthorizedError("unauthorized"))
		return
	}

	if h.rateLimiter == nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H (503 -> ErrCodeInternal avec message approprié)
		RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "rate limiting not enabled"))
		return
	}

	quotaInfo, err := h.rateLimiter.GetQuotaInfo(c.Request.Context(), userID)
	if err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "failed to get quota info"))
		return
	}

	RespondSuccess(c, http.StatusOK, gin.H{
		"quota": quotaInfo,
	})
}

// DashboardData représente les données du dashboard d'analytics
// T0363: Create Playback Analytics Dashboard Endpoint
type DashboardData struct {
	Stats      *services.PlaybackStats `json:"stats"`
	Trends     *TrendsData             `json:"trends"`
	TimeSeries []TimeSeriesPoint       `json:"time_series"`
}

// TrendsData représente les tendances d'analytics
type TrendsData struct {
	PlayTimeTrend       float64 `json:"play_time_trend"`       // % de changement sur 7 jours
	CompletionTrend     float64 `json:"completion_trend"`      // % de changement sur 7 jours
	SessionsTrend       float64 `json:"sessions_trend"`        // % de changement sur 7 jours
	AveragePlayTime     float64 `json:"average_play_time"`     // Moyenne sur 7 jours
	AverageCompletion   float64 `json:"average_completion"`    // Moyenne sur 7 jours
	TotalSessions7Days  int64   `json:"total_sessions_7days"`  // Total sur 7 jours
	TotalSessions30Days int64   `json:"total_sessions_30days"` // Total sur 30 jours
}

// TimeSeriesPoint représente un point dans une série temporelle
type TimeSeriesPoint struct {
	Date              string  `json:"date"` // Format: YYYY-MM-DD
	Sessions          int64   `json:"sessions"`
	TotalPlayTime     int64   `json:"total_play_time"`    // seconds
	AveragePlayTime   float64 `json:"average_play_time"`  // seconds
	AverageCompletion float64 `json:"average_completion"` // percentage
}

// GetDashboard gère la requête GET /api/v1/tracks/:id/playback/dashboard
// Retourne les statistiques agrégées, graphiques et tendances pour un track
// T0363: Create Playback Analytics Dashboard Endpoint
func (h *PlaybackAnalyticsHandler) GetDashboard(c *gin.Context) {
	// Récupérer l'ID du track depuis les paramètres de l'URL
	trackIDStr := c.Param("id")
	trackID, err := uuid.Parse(trackIDStr)
	if err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "invalid track id"))
		return
	}

	if trackID == uuid.Nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "invalid track id"))
		return
	}

	// Récupérer les statistiques globales
	stats, err := h.analyticsService.GetTrackStats(c.Request.Context(), trackID)
	if err != nil {
		errMsg := err.Error()
		if len(errMsg) >= 13 && errMsg[:13] == "track not found" {
			// MOD-P2-003: Utiliser AppError au lieu de gin.H
			RespondWithAppError(c, apperrors.NewNotFoundError("track"))
			return
		}
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, errMsg))
		return
	}

	// Calculer les tendances (comparaison 7 jours vs 14-7 jours)
	trends, err := h.calculateTrends(c.Request.Context(), trackID)
	if err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "failed to calculate trends: "+err.Error()))
		return
	}

	// Calculer les séries temporelles (30 derniers jours)
	timeSeries, err := h.calculateTimeSeries(c.Request.Context(), trackID, 30)
	if err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "failed to calculate time series: "+err.Error()))
		return
	}

	// Construire la réponse
	dashboard := DashboardData{
		Stats:      stats,
		Trends:     trends,
		TimeSeries: timeSeries,
	}

	RespondSuccess(c, http.StatusOK, gin.H{
		"dashboard": dashboard,
	})
}

// calculateTrends calcule les tendances d'analytics
func (h *PlaybackAnalyticsHandler) calculateTrends(ctx context.Context, trackID uuid.UUID) (*TrendsData, error) {
	now := time.Now()
	sevenDaysAgo := now.AddDate(0, 0, -7)
	fourteenDaysAgo := now.AddDate(0, 0, -14)
	thirtyDaysAgo := now.AddDate(0, 0, -30)

	// Statistiques sur les 7 derniers jours
	stats7Days, err := h.getStatsForDateRange(ctx, trackID, sevenDaysAgo, now)
	if err != nil {
		return nil, err
	}

	// Statistiques sur les 7 jours précédents (14-7 jours)
	statsPrev7Days, err := h.getStatsForDateRange(ctx, trackID, fourteenDaysAgo, sevenDaysAgo)
	if err != nil {
		return nil, err
	}

	// Statistiques sur les 30 derniers jours
	stats30Days, err := h.getStatsForDateRange(ctx, trackID, thirtyDaysAgo, now)
	if err != nil {
		return nil, err
	}

	trends := &TrendsData{
		TotalSessions7Days:  stats7Days.TotalSessions,
		TotalSessions30Days: stats30Days.TotalSessions,
		AveragePlayTime:     stats7Days.AveragePlayTime,
		AverageCompletion:   stats7Days.AverageCompletion,
	}

	// Calculer les tendances en pourcentage
	if statsPrev7Days.TotalSessions > 0 {
		// Tendance des sessions
		trends.SessionsTrend = float64(stats7Days.TotalSessions-statsPrev7Days.TotalSessions) / float64(statsPrev7Days.TotalSessions) * 100.0
	} else if stats7Days.TotalSessions > 0 {
		trends.SessionsTrend = 100.0 // Nouvelle donnée
	}

	if statsPrev7Days.AveragePlayTime > 0 {
		// Tendance du temps de lecture
		trends.PlayTimeTrend = (stats7Days.AveragePlayTime - statsPrev7Days.AveragePlayTime) / statsPrev7Days.AveragePlayTime * 100.0
	} else if stats7Days.AveragePlayTime > 0 {
		trends.PlayTimeTrend = 100.0 // Nouvelle donnée
	}

	if statsPrev7Days.AverageCompletion > 0 {
		// Tendance du taux de complétion
		trends.CompletionTrend = (stats7Days.AverageCompletion - statsPrev7Days.AverageCompletion) / statsPrev7Days.AverageCompletion * 100.0
	} else if stats7Days.AverageCompletion > 0 {
		trends.CompletionTrend = 100.0 // Nouvelle donnée
	}

	return trends, nil
}

// getStatsForDateRange récupère les statistiques pour une plage de dates
func (h *PlaybackAnalyticsHandler) getStatsForDateRange(ctx context.Context, trackID uuid.UUID, startDate, endDate time.Time) (*services.PlaybackStats, error) {
	sessions, err := h.analyticsService.GetSessionsByDateRange(ctx, trackID, startDate, endDate)
	if err != nil {
		return nil, err
	}

	if len(sessions) == 0 {
		return &services.PlaybackStats{}, nil
	}

	var totalPlayTime int64
	var totalPauses int64
	var totalSeeks int64
	var totalCompletion float64

	for _, session := range sessions {
		totalPlayTime += int64(session.PlayTime)
		totalPauses += int64(session.PauseCount)
		totalSeeks += int64(session.SeekCount)
		totalCompletion += session.CompletionRate
	}

	totalSessions := int64(len(sessions))
	avgPlayTime := float64(totalPlayTime) / float64(totalSessions)
	avgPauses := float64(totalPauses) / float64(totalSessions)
	avgSeeks := float64(totalSeeks) / float64(totalSessions)
	avgCompletion := totalCompletion / float64(totalSessions)

	// Compter les sessions complétées (>90%)
	var completedSessions int64
	for _, session := range sessions {
		if session.CompletionRate >= 90 {
			completedSessions++
		}
	}
	completionRate := float64(completedSessions) / float64(totalSessions) * 100.0

	return &services.PlaybackStats{
		TotalSessions:     totalSessions,
		TotalPlayTime:     totalPlayTime,
		AveragePlayTime:   avgPlayTime,
		TotalPauses:       totalPauses,
		AveragePauses:     avgPauses,
		TotalSeeks:        totalSeeks,
		AverageSeeks:      avgSeeks,
		AverageCompletion: avgCompletion,
		CompletionRate:    completionRate,
	}, nil
}

// calculateTimeSeries calcule les séries temporelles pour les N derniers jours
func (h *PlaybackAnalyticsHandler) calculateTimeSeries(ctx context.Context, trackID uuid.UUID, days int) ([]TimeSeriesPoint, error) {
	now := time.Now()
	startDate := now.AddDate(0, 0, -days)

	// Récupérer toutes les sessions dans la plage
	sessions, err := h.analyticsService.GetSessionsByDateRange(ctx, trackID, startDate, now)
	if err != nil {
		return nil, err
	}

	// Grouper par jour
	dailyStats := make(map[string]*dailyStat)
	for _, session := range sessions {
		dateKey := session.CreatedAt.Format("2006-01-02")
		if dailyStats[dateKey] == nil {
			dailyStats[dateKey] = &dailyStat{}
		}
		stat := dailyStats[dateKey]
		stat.sessions++
		stat.totalPlayTime += int64(session.PlayTime)
		stat.totalCompletion += session.CompletionRate
	}

	// Créer les points de série temporelle pour tous les jours
	var timeSeries []TimeSeriesPoint
	for i := days - 1; i >= 0; i-- {
		date := now.AddDate(0, 0, -i)
		dateKey := date.Format("2006-01-02")

		stat := dailyStats[dateKey]
		if stat == nil {
			stat = &dailyStat{}
		}

		var avgPlayTime float64
		var avgCompletion float64
		if stat.sessions > 0 {
			avgPlayTime = float64(stat.totalPlayTime) / float64(stat.sessions)
			avgCompletion = stat.totalCompletion / float64(stat.sessions)
		}

		timeSeries = append(timeSeries, TimeSeriesPoint{
			Date:              dateKey,
			Sessions:          stat.sessions,
			TotalPlayTime:     stat.totalPlayTime,
			AveragePlayTime:   avgPlayTime,
			AverageCompletion: avgCompletion,
		})
	}

	return timeSeries, nil
}

// dailyStat représente les statistiques d'un jour
type dailyStat struct {
	sessions        int64
	totalPlayTime   int64
	totalCompletion float64
}

// SummaryData représente le résumé des analytics de lecture
// T0370: Create Playback Analytics Summary Endpoint
type SummaryData struct {
	TotalPlays      int64   `json:"total_plays"`       // Nombre total de lectures
	CompletionRate  float64 `json:"completion_rate"`   // Taux de complétion moyen (%)
	AveragePlayTime float64 `json:"average_play_time"` // Temps de lecture moyen (secondes)
}

// GetSummary gère la requête GET /api/v1/tracks/:id/playback/summary
// Retourne un résumé des analytics de lecture pour un track
// T0370: Create Playback Analytics Summary Endpoint
func (h *PlaybackAnalyticsHandler) GetSummary(c *gin.Context) {
	// Récupérer l'ID du track depuis les paramètres de l'URL
	trackIDStr := c.Param("id")
	trackID, err := uuid.Parse(trackIDStr)
	if err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "invalid track id"))
		return
	}

	if trackID == uuid.Nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "invalid track id"))
		return
	}

	// Récupérer les statistiques via le service
	stats, err := h.analyticsService.GetTrackStats(c.Request.Context(), trackID)
	if err != nil {
		errMsg := err.Error()
		if len(errMsg) >= 13 && errMsg[:13] == "track not found" {
			// MOD-P2-003: Utiliser AppError au lieu de gin.H
			RespondWithAppError(c, apperrors.NewNotFoundError("track"))
			return
		}
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, errMsg))
		return
	}

	// Construire le résumé
	summary := SummaryData{
		TotalPlays:      stats.TotalSessions,
		CompletionRate:  stats.CompletionRate,
		AveragePlayTime: stats.AveragePlayTime,
	}

	RespondSuccess(c, http.StatusOK, gin.H{
		"summary": summary,
	})
}

// GetHeatmap gère la requête GET /api/v1/tracks/:id/playback/heatmap
// Retourne les données de heatmap pour un track
// T0376: Create Playback Analytics Heatmap Generation
func (h *PlaybackAnalyticsHandler) GetHeatmap(c *gin.Context) {
	if h.heatmapService == nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H (503 -> ErrCodeInternal avec message approprié)
		RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "heatmap service not available"))
		return
	}

	// Récupérer l'ID du track depuis les paramètres de l'URL
	trackIDStr := c.Param("id")
	trackID, err := uuid.Parse(trackIDStr)
	if err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "invalid track id"))
		return
	}

	if trackID == uuid.Nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "invalid track id"))
		return
	}

	// Récupérer la taille de segment depuis les query params (optionnel, défaut: 5)
	segmentSize := 5
	if segmentSizeStr := c.Query("segment_size"); segmentSizeStr != "" {
		if parsed, err := strconv.Atoi(segmentSizeStr); err == nil && parsed > 0 {
			segmentSize = parsed
		}
	}

	// Générer la heatmap via le service
	heatmap, err := h.heatmapService.GenerateHeatmap(c.Request.Context(), trackID, segmentSize)
	if err != nil {
		errMsg := err.Error()
		if len(errMsg) >= 13 && errMsg[:13] == "track not found" {
			// MOD-P2-003: Utiliser AppError au lieu de gin.H
			RespondWithAppError(c, apperrors.NewNotFoundError("track"))
			return
		}
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, errMsg))
		return
	}

	RespondSuccess(c, http.StatusOK, gin.H{
		"heatmap": heatmap,
	})
}

// validateAndSanitizeAnalyticsRequest valide et sanitize une requête d'analytics
// T0388: Create Playback Analytics Validation
func (h *PlaybackAnalyticsHandler) validateAndSanitizeAnalyticsRequest(req *RecordAnalyticsRequest, _ uuid.UUID) ValidationResult {
	result := ValidationResult{
		Valid:     true,
		Errors:    make([]dto.ValidationError, 0),
		Sanitized: &RecordAnalyticsRequest{},
	}

	// Copier les données pour la sanitization
	sanitized := *req

	// 1. Validation du schéma - PlayTime
	if req.PlayTime < 0 {
		result.Valid = false
		result.Errors = append(result.Errors, dto.ValidationError{
			Field:   "play_time",
			Message: "play_time must be greater than or equal to 0",
			Value:   fmt.Sprintf("%d", req.PlayTime),
		})
	} else {
		// Limiter play_time à une valeur raisonnable (max 24 heures = 86400 secondes)
		if req.PlayTime > 86400 {
			result.Valid = false
			result.Errors = append(result.Errors, dto.ValidationError{
				Field:   "play_time",
				Message: "play_time cannot exceed 86400 seconds (24 hours)",
				Value:   fmt.Sprintf("%d", req.PlayTime),
			})
		}
		sanitized.PlayTime = req.PlayTime
	}

	// 2. Validation du schéma - PauseCount
	if req.PauseCount < 0 {
		result.Valid = false
		result.Errors = append(result.Errors, dto.ValidationError{
			Field:   "pause_count",
			Message: "pause_count must be greater than or equal to 0",
			Value:   fmt.Sprintf("%d", req.PauseCount),
		})
	} else {
		// Limiter pause_count à une valeur raisonnable (max 1000)
		if req.PauseCount > 1000 {
			sanitized.PauseCount = 1000
		} else {
			sanitized.PauseCount = req.PauseCount
		}
	}

	// 3. Validation du schéma - SeekCount
	if req.SeekCount < 0 {
		result.Valid = false
		result.Errors = append(result.Errors, dto.ValidationError{
			Field:   "seek_count",
			Message: "seek_count must be greater than or equal to 0",
			Value:   fmt.Sprintf("%d", req.SeekCount),
		})
	} else {
		// Limiter seek_count à une valeur raisonnable (max 1000)
		if req.SeekCount > 1000 {
			sanitized.SeekCount = 1000
		} else {
			sanitized.SeekCount = req.SeekCount
		}
	}

	// 4. Validation du schéma - CompletionRate
	if req.CompletionRate != nil {
		rate := *req.CompletionRate
		if math.IsNaN(rate) || math.IsInf(rate, 0) {
			result.Valid = false
			result.Errors = append(result.Errors, dto.ValidationError{
				Field:   "completion_rate",
				Message: "completion_rate must be a valid number",
				Value:   fmt.Sprintf("%f", rate),
			})
		} else if rate < 0 || rate > 100 {
			result.Valid = false
			result.Errors = append(result.Errors, dto.ValidationError{
				Field:   "completion_rate",
				Message: "completion_rate must be between 0 and 100",
				Value:   fmt.Sprintf("%f", rate),
			})
		} else {
			// Arrondir à 2 décimales
			roundedRate := math.Round(rate*100) / 100
			sanitized.CompletionRate = &roundedRate
		}
	}

	// 5. Validation du schéma - StartedAt
	if req.StartedAt.IsZero() {
		result.Valid = false
		result.Errors = append(result.Errors, dto.ValidationError{
			Field:   "started_at",
			Message: "started_at is required",
		})
	} else {
		now := time.Now()
		// Vérifier que started_at n'est pas dans le futur (avec une marge de 1 minute pour les décalages d'horloge)
		if req.StartedAt.After(now.Add(1 * time.Minute)) {
			result.Valid = false
			result.Errors = append(result.Errors, dto.ValidationError{
				Field:   "started_at",
				Message: "started_at cannot be in the future",
				Value:   req.StartedAt.Format(time.RFC3339),
			})
		} else {
			// Vérifier que started_at n'est pas trop ancien (max 30 jours)
			thirtyDaysAgo := now.AddDate(0, 0, -30)
			if req.StartedAt.Before(thirtyDaysAgo) {
				result.Valid = false
				result.Errors = append(result.Errors, dto.ValidationError{
					Field:   "started_at",
					Message: "started_at cannot be older than 30 days",
					Value:   req.StartedAt.Format(time.RFC3339),
				})
			} else {
				sanitized.StartedAt = req.StartedAt
			}
		}
	}

	// 6. Validation du schéma - EndedAt
	if req.EndedAt != nil {
		endedAt := *req.EndedAt
		if endedAt.IsZero() {
			// Si ended_at est fourni mais est zero, le traiter comme nil
			sanitized.EndedAt = nil
		} else {
			// Vérifier que ended_at n'est pas dans le futur
			now := time.Now()
			if endedAt.After(now.Add(1 * time.Minute)) {
				result.Valid = false
				result.Errors = append(result.Errors, dto.ValidationError{
					Field:   "ended_at",
					Message: "ended_at cannot be in the future",
					Value:   endedAt.Format(time.RFC3339),
				})
			} else {
				sanitized.EndedAt = &endedAt
			}
		}
	}

	// 7. Vérification de cohérence - EndedAt doit être après StartedAt
	if !req.StartedAt.IsZero() && req.EndedAt != nil && !req.EndedAt.IsZero() {
		if req.EndedAt.Before(req.StartedAt) {
			result.Valid = false
			result.Errors = append(result.Errors, dto.ValidationError{
				Field:   "ended_at",
				Message: "ended_at must be after started_at",
				Value:   req.EndedAt.Format(time.RFC3339),
			})
		}
	}

	// 8. Vérification de cohérence - PlayTime doit être cohérent avec les dates
	if !req.StartedAt.IsZero() && req.EndedAt != nil && !req.EndedAt.IsZero() {
		duration := req.EndedAt.Sub(req.StartedAt).Seconds()
		// Le play_time ne devrait pas être significativement supérieur à la durée entre started_at et ended_at
		// (avec une marge de 10% pour les pauses)
		maxExpectedPlayTime := duration * 1.1
		if float64(req.PlayTime) > maxExpectedPlayTime && maxExpectedPlayTime > 0 {
			result.Valid = false
			result.Errors = append(result.Errors, dto.ValidationError{
				Field:   "play_time",
				Message: fmt.Sprintf("play_time (%.0f seconds) is inconsistent with session duration (%.0f seconds)", float64(req.PlayTime), duration),
				Value:   fmt.Sprintf("%d", req.PlayTime),
			})
		}
	}

	// 9. Vérification de cohérence - CompletionRate doit être cohérent avec PlayTime si fourni
	// Cette vérification nécessite la durée du track, donc elle sera faite après la récupération du track
	// Pour l'instant, on valide juste que le completion_rate est dans une plage raisonnable

	// 10. Vérification de cohérence - PauseCount et SeekCount doivent être raisonnables par rapport à PlayTime
	if req.PlayTime > 0 {
		// Si play_time est très court (< 10 secondes), pause_count et seek_count devraient être faibles
		if req.PlayTime < 10 {
			if req.PauseCount > 5 {
				result.Valid = false
				result.Errors = append(result.Errors, dto.ValidationError{
					Field:   "pause_count",
					Message: "pause_count is too high for such a short play_time",
					Value:   fmt.Sprintf("%d", req.PauseCount),
				})
			}
			if req.SeekCount > 10 {
				result.Valid = false
				result.Errors = append(result.Errors, dto.ValidationError{
					Field:   "seek_count",
					Message: "seek_count is too high for such a short play_time",
					Value:   fmt.Sprintf("%d", req.SeekCount),
				})
			}
		}
	}

	result.Sanitized = &sanitized
	return result
}
