package services

import (
	"context"
	"fmt"
	"github.com/google/uuid"
	"time"

	"veza-backend-api/internal/models"

	"go.uber.org/zap"
	"gorm.io/gorm"
)

// PlaybackAnalyticsService gère les analytics de lecture de tracks
// T0357: Create Playback Analytics Service
// T0381: Create Playback Analytics Performance Optimization
type PlaybackAnalyticsService struct {
	db        *gorm.DB
	logger    *zap.Logger
	cache     *CacheService // Optionnel, pour le cache des agrégations
	cacheTTL  time.Duration // TTL pour le cache des statistiques
	batchSize int           // Taille du batch pour l'enregistrement en lot
}

// NewPlaybackAnalyticsService crée un nouveau service d'analytics de lecture
func NewPlaybackAnalyticsService(db *gorm.DB, logger *zap.Logger) *PlaybackAnalyticsService {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &PlaybackAnalyticsService{
		db:        db,
		logger:    logger,
		cache:     nil,             // Cache optionnel
		cacheTTL:  5 * time.Minute, // TTL par défaut de 5 minutes
		batchSize: 100,             // Taille de batch par défaut
	}
}

// NewPlaybackAnalyticsServiceWithCache crée un nouveau service avec cache
// T0381: Create Playback Analytics Performance Optimization
func NewPlaybackAnalyticsServiceWithCache(db *gorm.DB, cache *CacheService, logger *zap.Logger) *PlaybackAnalyticsService {
	service := NewPlaybackAnalyticsService(db, logger)
	service.cache = cache
	return service
}

// SetBatchSize définit la taille du batch pour l'enregistrement en lot
// T0381: Create Playback Analytics Performance Optimization
func (s *PlaybackAnalyticsService) SetBatchSize(size int) {
	if size > 0 {
		s.batchSize = size
	}
}

// RecordPlayback enregistre un événement d'analytics de lecture
// T0357: Create Playback Analytics Service
func (s *PlaybackAnalyticsService) RecordPlayback(ctx context.Context, analytics *models.PlaybackAnalytics) error {
	// Valider les paramètres
	if analytics.TrackID == uuid.Nil {
		return fmt.Errorf("invalid track ID: 0")
	}
	if analytics.UserID == uuid.Nil {
		return fmt.Errorf("invalid user ID: nil UUID")
	}
	if analytics.PlayTime < 0 {
		return fmt.Errorf("invalid play time: %d", analytics.PlayTime)
	}
	if analytics.PauseCount < 0 {
		return fmt.Errorf("invalid pause count: %d", analytics.PauseCount)
	}
	if analytics.SeekCount < 0 {
		return fmt.Errorf("invalid seek count: %d", analytics.SeekCount)
	}
	if analytics.CompletionRate < 0 || analytics.CompletionRate > 100 {
		return fmt.Errorf("invalid completion rate: %f (must be between 0 and 100)", analytics.CompletionRate)
	}
	if analytics.StartedAt.IsZero() {
		return fmt.Errorf("started_at is required")
	}

	// Vérifier que le track existe
	var track models.Track
	if err := s.db.WithContext(ctx).First(&track, analytics.TrackID).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return fmt.Errorf("track not found: %s", analytics.TrackID)
		}
		return fmt.Errorf("failed to get track: %w", err)
	}

	// Calculer le taux de complétion si non fourni
	if analytics.CompletionRate == 0 && track.Duration > 0 {
		analytics.CompletionRate = s.CalculateCompletionRate(analytics.PlayTime, track.Duration)
	}

	// Enregistrer l'analytics avec retry logic
	// T0385: Create Playback Analytics Error Handling
	maxRetries := 3
	var lastErr error
	for attempt := 0; attempt < maxRetries; attempt++ {
		err := s.db.WithContext(ctx).Create(analytics).Error
		if err == nil {
			// Succès
			if attempt > 0 {
				s.logger.Info("Playback analytics recorded after retry",
					zap.Int("attempt", attempt+1),
					zap.String("track_id", analytics.TrackID.String()),
					zap.String("user_id", analytics.UserID.String()))
			}
			break
		}

		lastErr = err

		// Logger l'erreur
		s.logger.Warn("Failed to record playback analytics, retrying",
			zap.Error(err),
			zap.Int("attempt", attempt+1),
			zap.Int("max_retries", maxRetries),
			zap.String("track_id", analytics.TrackID.String()),
			zap.String("user_id", analytics.UserID.String()))

		// Ne pas retry pour certaines erreurs (contraintes, etc.)
		if attempt < maxRetries-1 {
			// Attendre avant de retry (exponential backoff)
			backoffDuration := time.Duration(attempt+1) * 100 * time.Millisecond
			time.Sleep(backoffDuration)
		}
	}

	if lastErr != nil {
		s.logger.Error("Failed to record playback analytics after all retries",
			zap.Error(lastErr),
			zap.Int("max_retries", maxRetries),
			zap.String("track_id", analytics.TrackID.String()),
			zap.String("user_id", analytics.UserID.String()))
		return fmt.Errorf("failed to record playback analytics after %d retries: %w", maxRetries, lastErr)
	}

	// Invalider le cache si disponible
	if s.cache != nil {
		cacheKey := fmt.Sprintf("playback_stats:track:%s", analytics.TrackID)
		if err := s.cache.Delete(ctx, cacheKey); err != nil {
			s.logger.Warn("Failed to invalidate cache", zap.Error(err), zap.String("track_id", analytics.TrackID.String()))
		}
	}

	s.logger.Info("Playback analytics recorded",
		zap.String("id", analytics.ID.String()),
		zap.String("track_id", analytics.TrackID.String()),
		zap.String("user_id", analytics.UserID.String()),
		zap.Int("play_time", analytics.PlayTime),
		zap.Float64("completion_rate", analytics.CompletionRate))

	return nil
}

// RecordPlaybackBatch enregistre plusieurs analytics en lot pour optimiser les performances
// T0381: Create Playback Analytics Performance Optimization
func (s *PlaybackAnalyticsService) RecordPlaybackBatch(ctx context.Context, analyticsList []*models.PlaybackAnalytics) error {
	if len(analyticsList) == 0 {
		return fmt.Errorf("analytics list cannot be empty")
	}

	// Valider tous les analytics avant l'insertion
	for i, analytics := range analyticsList {
		if analytics.TrackID == uuid.Nil {
			return fmt.Errorf("invalid track ID at index %d: 0", i)
		}
		if analytics.UserID == uuid.Nil {
			return fmt.Errorf("invalid user ID at index %d: nil UUID", i)
		}
		if analytics.PlayTime < 0 {
			return fmt.Errorf("invalid play time at index %d: %d", i, analytics.PlayTime)
		}
		if analytics.StartedAt.IsZero() {
			return fmt.Errorf("started_at is required at index %d", i)
		}
	}

	// Enregistrer par batch pour optimiser les performances
	trackIDs := make(map[uuid.UUID]bool)
	for i := 0; i < len(analyticsList); i += s.batchSize {
		end := i + s.batchSize
		if end > len(analyticsList) {
			end = len(analyticsList)
		}

		batch := analyticsList[i:end]
		if err := s.db.WithContext(ctx).Create(batch).Error; err != nil {
			s.logger.Error("Failed to record playback analytics batch",
				zap.Error(err),
				zap.Int("batch_start", i),
				zap.Int("batch_end", end))
			return fmt.Errorf("failed to record playback analytics batch: %w", err)
		}

		// Collecter les track IDs pour invalider le cache
		for _, analytics := range batch {
			trackIDs[analytics.TrackID] = true
		}
	}

	// Invalider le cache pour tous les tracks affectés
	if s.cache != nil {
		for trackID := range trackIDs {
			cacheKey := fmt.Sprintf("playback_stats:track:%s", trackID)
			if err := s.cache.Delete(ctx, cacheKey); err != nil {
				s.logger.Warn("Failed to invalidate cache", zap.Error(err), zap.String("track_id", trackID.String()))
			}
		}
	}

	s.logger.Info("Playback analytics batch recorded",
		zap.Int("count", len(analyticsList)),
		zap.Int("batches", (len(analyticsList)+s.batchSize-1)/s.batchSize))

	return nil
}

// CalculateCompletionRate calcule le taux de complétion en pourcentage
// playTime: temps de lecture en secondes
// trackDuration: durée totale du track en secondes
// Retourne le taux de complétion (0-100)
func (s *PlaybackAnalyticsService) CalculateCompletionRate(playTime int, trackDuration int) float64 {
	if trackDuration <= 0 {
		return 0.0
	}
	if playTime < 0 {
		return 0.0
	}

	rate := float64(playTime) / float64(trackDuration) * 100.0

	// Limiter à 100%
	if rate > 100.0 {
		rate = 100.0
	}

	return rate
}

// PlaybackStats représente les statistiques agrégées de lecture
type PlaybackStats struct {
	TotalSessions     int64   `json:"total_sessions"`
	TotalPlayTime     int64   `json:"total_play_time"`   // seconds
	AveragePlayTime   float64 `json:"average_play_time"` // seconds
	TotalPauses       int64   `json:"total_pauses"`
	AveragePauses     float64 `json:"average_pauses"`
	TotalSeeks        int64   `json:"total_seeks"`
	AverageSeeks      float64 `json:"average_seeks"`
	AverageCompletion float64 `json:"average_completion"` // percentage
	CompletionRate    float64 `json:"completion_rate"`    // percentage of sessions with >90% completion
}

// GetTrackStats récupère les statistiques agrégées pour un track
// T0381: Optimisé avec cache
func (s *PlaybackAnalyticsService) GetTrackStats(ctx context.Context, trackID uuid.UUID) (*PlaybackStats, error) {
	if trackID == uuid.Nil {
		return nil, fmt.Errorf("invalid track ID: 0")
	}

	// Vérifier le cache si disponible
	if s.cache != nil {
		cacheKey := fmt.Sprintf("playback_stats:track:%s", trackID)
		var cachedStats PlaybackStats
		if err := s.cache.Get(ctx, cacheKey, &cachedStats); err == nil {
			s.logger.Debug("Cache hit for track stats", zap.String("track_id", trackID.String()))
			return &cachedStats, nil
		}
	}

	// Vérifier que le track existe
	var track models.Track
	if err := s.db.WithContext(ctx).First(&track, trackID).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, fmt.Errorf("track not found: %s", trackID)
		}
		return nil, fmt.Errorf("failed to get track: %w", err)
	}

	var stats PlaybackStats

	// Total sessions
	if err := s.db.WithContext(ctx).Model(&models.PlaybackAnalytics{}).
		Where("track_id = ?", trackID).
		Count(&stats.TotalSessions).Error; err != nil {
		return nil, fmt.Errorf("failed to count sessions: %w", err)
	}

	if stats.TotalSessions == 0 {
		return &stats, nil
	}

	// Total play time
	var totalPlayTime int64
	if err := s.db.WithContext(ctx).Model(&models.PlaybackAnalytics{}).
		Where("track_id = ?", trackID).
		Select("COALESCE(SUM(play_time), 0)").
		Scan(&totalPlayTime).Error; err != nil {
		return nil, fmt.Errorf("failed to calculate total play time: %w", err)
	}
	stats.TotalPlayTime = totalPlayTime

	// Average play time
	stats.AveragePlayTime = float64(totalPlayTime) / float64(stats.TotalSessions)

	// Total pauses
	var totalPauses int64
	if err := s.db.WithContext(ctx).Model(&models.PlaybackAnalytics{}).
		Where("track_id = ?", trackID).
		Select("COALESCE(SUM(pause_count), 0)").
		Scan(&totalPauses).Error; err != nil {
		return nil, fmt.Errorf("failed to calculate total pauses: %w", err)
	}
	stats.TotalPauses = totalPauses
	stats.AveragePauses = float64(totalPauses) / float64(stats.TotalSessions)

	// Total seeks
	var totalSeeks int64
	if err := s.db.WithContext(ctx).Model(&models.PlaybackAnalytics{}).
		Where("track_id = ?", trackID).
		Select("COALESCE(SUM(seek_count), 0)").
		Scan(&totalSeeks).Error; err != nil {
		return nil, fmt.Errorf("failed to calculate total seeks: %w", err)
	}
	stats.TotalSeeks = totalSeeks
	stats.AverageSeeks = float64(totalSeeks) / float64(stats.TotalSessions)

	// Average completion rate
	var avgCompletion float64
	if err := s.db.WithContext(ctx).Model(&models.PlaybackAnalytics{}).
		Where("track_id = ?", trackID).
		Select("COALESCE(AVG(completion_rate), 0)").
		Scan(&avgCompletion).Error; err != nil {
		return nil, fmt.Errorf("failed to calculate average completion: %w", err)
	}
	stats.AverageCompletion = avgCompletion

	// Completion rate (sessions with >90% completion)
	var completedSessions int64
	if err := s.db.WithContext(ctx).Model(&models.PlaybackAnalytics{}).
		Where("track_id = ? AND completion_rate >= 90", trackID).
		Count(&completedSessions).Error; err != nil {
		return nil, fmt.Errorf("failed to count completed sessions: %w", err)
	}
	if stats.TotalSessions > 0 {
		stats.CompletionRate = float64(completedSessions) / float64(stats.TotalSessions) * 100.0
	}

	// Mettre en cache si disponible
	if s.cache != nil {
		cacheKey := fmt.Sprintf("playback_stats:track:%s", trackID)
		if err := s.cache.Set(ctx, cacheKey, stats, s.cacheTTL); err != nil {
			s.logger.Warn("Failed to cache track stats", zap.Error(err), zap.String("track_id", trackID.String()))
		}
	}

	return &stats, nil
}

// GetUserStats récupère les statistiques agrégées pour un utilisateur
func (s *PlaybackAnalyticsService) GetUserStats(ctx context.Context, userID uuid.UUID) (*PlaybackStats, error) {
	if userID == uuid.Nil {
		return nil, fmt.Errorf("invalid user ID: nil UUID")
	}

	// Vérifier que l'utilisateur existe
	var user models.User
	if err := s.db.WithContext(ctx).First(&user, userID).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil, fmt.Errorf("user not found: %s", userID)
		}
		return nil, fmt.Errorf("failed to get user: %w", err)
	}

	var stats PlaybackStats

	// Total sessions
	if err := s.db.WithContext(ctx).Model(&models.PlaybackAnalytics{}).
		Where("user_id = ?", userID).
		Count(&stats.TotalSessions).Error; err != nil {
		return nil, fmt.Errorf("failed to count sessions: %w", err)
	}

	if stats.TotalSessions == 0 {
		return &stats, nil
	}

	// Total play time
	var totalPlayTime int64
	if err := s.db.WithContext(ctx).Model(&models.PlaybackAnalytics{}).
		Where("user_id = ?", userID).
		Select("COALESCE(SUM(play_time), 0)").
		Scan(&totalPlayTime).Error; err != nil {
		return nil, fmt.Errorf("failed to calculate total play time: %w", err)
	}
	stats.TotalPlayTime = totalPlayTime
	stats.AveragePlayTime = float64(totalPlayTime) / float64(stats.TotalSessions)

	// Total pauses
	var totalPauses int64
	if err := s.db.WithContext(ctx).Model(&models.PlaybackAnalytics{}).
		Where("user_id = ?", userID).
		Select("COALESCE(SUM(pause_count), 0)").
		Scan(&totalPauses).Error; err != nil {
		return nil, fmt.Errorf("failed to calculate total pauses: %w", err)
	}
	stats.TotalPauses = totalPauses
	stats.AveragePauses = float64(totalPauses) / float64(stats.TotalSessions)

	// Total seeks
	var totalSeeks int64
	if err := s.db.WithContext(ctx).Model(&models.PlaybackAnalytics{}).
		Where("user_id = ?", userID).
		Select("COALESCE(SUM(seek_count), 0)").
		Scan(&totalSeeks).Error; err != nil {
		return nil, fmt.Errorf("failed to calculate total seeks: %w", err)
	}
	stats.TotalSeeks = totalSeeks
	stats.AverageSeeks = float64(totalSeeks) / float64(stats.TotalSessions)

	// Average completion rate
	var avgCompletion float64
	if err := s.db.WithContext(ctx).Model(&models.PlaybackAnalytics{}).
		Where("user_id = ?", userID).
		Select("COALESCE(AVG(completion_rate), 0)").
		Scan(&avgCompletion).Error; err != nil {
		return nil, fmt.Errorf("failed to calculate average completion: %w", err)
	}
	stats.AverageCompletion = avgCompletion

	// Completion rate (sessions with >90% completion)
	var completedSessions int64
	if err := s.db.WithContext(ctx).Model(&models.PlaybackAnalytics{}).
		Where("user_id = ? AND completion_rate >= 90", userID).
		Count(&completedSessions).Error; err != nil {
		return nil, fmt.Errorf("failed to count completed sessions: %w", err)
	}
	if stats.TotalSessions > 0 {
		stats.CompletionRate = float64(completedSessions) / float64(stats.TotalSessions) * 100.0
	}

	return &stats, nil
}

// GetSessionsByDateRange récupère les sessions dans une plage de dates
func (s *PlaybackAnalyticsService) GetSessionsByDateRange(ctx context.Context, trackID uuid.UUID, startDate, endDate time.Time) ([]models.PlaybackAnalytics, error) {
	return s.GetSessionsByDateRangePaginated(ctx, trackID, startDate, endDate, 0, 0)
}

// PaginationParams représente les paramètres de pagination
// T0381: Create Playback Analytics Performance Optimization
type PaginationParams struct {
	Page     int // Numéro de page (commence à 1)
	PageSize int // Taille de la page
}

// PaginatedResult représente un résultat paginé
// T0381: Create Playback Analytics Performance Optimization
type PaginatedResult[T any] struct {
	Data       []T   `json:"data"`
	Total      int64 `json:"total"`
	Page       int   `json:"page"`
	PageSize   int   `json:"page_size"`
	TotalPages int   `json:"total_pages"`
}

// GetSessionsByDateRangePaginated récupère les sessions dans une plage de dates avec pagination
// T0381: Create Playback Analytics Performance Optimization
func (s *PlaybackAnalyticsService) GetSessionsByDateRangePaginated(ctx context.Context, trackID uuid.UUID, startDate, endDate time.Time, page, pageSize int) ([]models.PlaybackAnalytics, error) {
	if trackID == uuid.Nil {
		return nil, fmt.Errorf("invalid track ID: 0")
	}

	query := s.db.WithContext(ctx).Model(&models.PlaybackAnalytics{}).
		Where("track_id = ? AND created_at >= ? AND created_at <= ?", trackID, startDate, endDate).
		Order("created_at DESC")

	// Appliquer la pagination si spécifiée
	if pageSize > 0 {
		offset := (page - 1) * pageSize
		if offset < 0 {
			offset = 0
		}
		query = query.Offset(offset).Limit(pageSize)
	}

	var sessions []models.PlaybackAnalytics
	err := query.Find(&sessions).Error

	if err != nil {
		return nil, fmt.Errorf("failed to get sessions: %w", err)
	}

	return sessions, nil
}

// GetSessionsByDateRangePaginatedResult récupère les sessions avec pagination complète
// T0381: Create Playback Analytics Performance Optimization
func (s *PlaybackAnalyticsService) GetSessionsByDateRangePaginatedResult(ctx context.Context, trackID uuid.UUID, startDate, endDate time.Time, page, pageSize int) (*PaginatedResult[models.PlaybackAnalytics], error) {
	if trackID == uuid.Nil {
		return nil, fmt.Errorf("invalid track ID: 0")
	}

	if page < 1 {
		page = 1
	}
	if pageSize < 1 {
		pageSize = 50 // Taille par défaut
	}
	if pageSize > 1000 {
		pageSize = 1000 // Limite maximale
	}

	// Compter le total
	var total int64
	err := s.db.WithContext(ctx).Model(&models.PlaybackAnalytics{}).
		Where("track_id = ? AND created_at >= ? AND created_at <= ?", trackID, startDate, endDate).
		Count(&total).Error
	if err != nil {
		return nil, fmt.Errorf("failed to count sessions: %w", err)
	}

	// Récupérer les données paginées
	sessions, err := s.GetSessionsByDateRangePaginated(ctx, trackID, startDate, endDate, page, pageSize)
	if err != nil {
		return nil, err
	}

	totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))

	return &PaginatedResult[models.PlaybackAnalytics]{
		Data:       sessions,
		Total:      total,
		Page:       page,
		PageSize:   pageSize,
		TotalPages: totalPages,
	}, nil
}

// TrackCompletion détecte et enregistre la completion d'un track (≥95%)
// T0366: Create Playback Completion Tracking
func (s *PlaybackAnalyticsService) TrackCompletion(ctx context.Context, analytics *models.PlaybackAnalytics, trackDuration int) error {
	if analytics == nil {
		return fmt.Errorf("analytics cannot be nil")
	}

	if analytics.ID == uuid.Nil {
		return fmt.Errorf("analytics must be saved before tracking completion")
	}

	if trackDuration <= 0 {
		return fmt.Errorf("invalid track duration: %d", trackDuration)
	}

	// Calculer le taux de complétion
	completionRate := s.CalculateCompletionRate(analytics.PlayTime, trackDuration)
	analytics.CompletionRate = completionRate

	// Détecter si le track est complété (≥95%)
	if completionRate >= 95.0 {
		// Marquer comme complété en définissant EndedAt
		now := time.Now()
		analytics.EndedAt = &now

		s.logger.Info("Track completion detected",
			zap.String("analytics_id", analytics.ID.String()),
			zap.String("track_id", analytics.TrackID.String()),
			zap.String("user_id", analytics.UserID.String()),
			zap.Float64("completion_rate", completionRate),
			zap.Int("play_time", analytics.PlayTime),
			zap.Int("track_duration", trackDuration))
	}

	// Mettre à jour les analytics dans la base de données
	if err := s.db.WithContext(ctx).Save(analytics).Error; err != nil {
		s.logger.Error("Failed to update analytics completion",
			zap.Error(err),
			zap.String("analytics_id", analytics.ID.String()),
			zap.String("track_id", analytics.TrackID.String()))
		return fmt.Errorf("failed to update analytics completion: %w", err)
	}

	return nil
}

// UpdatePlaybackProgress met à jour le progrès de lecture et détecte la completion
// T0366: Create Playback Completion Tracking
func (s *PlaybackAnalyticsService) UpdatePlaybackProgress(ctx context.Context, analyticsID uuid.UUID, playTime int, trackDuration int) error {
	if analyticsID == uuid.Nil {
		return fmt.Errorf("invalid analytics ID: 0")
	}

	if playTime < 0 {
		return fmt.Errorf("invalid play time: %d", playTime)
	}

	if trackDuration <= 0 {
		return fmt.Errorf("invalid track duration: %d", trackDuration)
	}

	// Récupérer l'analytics existant
	var analytics models.PlaybackAnalytics
	if err := s.db.WithContext(ctx).First(&analytics, analyticsID).Error; err != nil {
		if err == gorm.ErrRecordNotFound {
			return fmt.Errorf("analytics not found: %s", analyticsID)
		}
		return fmt.Errorf("failed to get analytics: %w", err)
	}

	// Mettre à jour le temps de lecture
	analytics.PlayTime = playTime

	// Utiliser TrackCompletion pour calculer et détecter la completion
	return s.TrackCompletion(ctx, &analytics, trackDuration)
}
