package track

import (
	"context"
	"errors"
	"fmt"
	"io"
	"mime/multipart"
	"os"
	"path/filepath"
	"strings" // Removed strconv
	"time"    // MOD-P2-008: Ajouté pour timeout asynchrone

	"veza-backend-api/internal/models"
	"veza-backend-api/internal/monitoring"
	"veza-backend-api/internal/services"
	"veza-backend-api/internal/types"

	"github.com/google/uuid"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

// Constantes pour les quotas utilisateur
const (
	MaxTracksPerUser  = 1000                     // Nombre maximum de tracks par utilisateur
	MaxStoragePerUser = 100 * 1024 * 1024 * 1024 // 100GB par utilisateur
)

// Types d'erreurs spécifiques pour les tracks
var (
	// ErrInvalidTrackFormat est retourné quand le format du fichier est invalide
	ErrInvalidTrackFormat = errors.New("invalid track format")
	// ErrTrackTooLarge est retourné quand le fichier dépasse la taille maximale
	ErrTrackTooLarge = errors.New("track file too large")
	// ErrTrackQuotaExceeded est retourné quand l'utilisateur a atteint son quota de tracks
	ErrTrackQuotaExceeded = errors.New("track quota exceeded")
	// ErrStorageQuotaExceeded est retourné quand l'utilisateur a atteint son quota de stockage
	ErrStorageQuotaExceeded = errors.New("storage quota exceeded")
	// ErrTrackNotFound est retourné quand un track n'est pas trouvé
	ErrTrackNotFound = errors.New("track not found")
	// ErrNetworkError est retourné en cas d'erreur réseau (timeout, connexion)
	ErrNetworkError = errors.New("network error")
	// ErrStorageError est retourné en cas d'erreur de stockage
	ErrStorageError = errors.New("storage error")
	// ErrForbidden est retourné quand l'utilisateur n'a pas la permission d'effectuer l'action
	ErrForbidden = errors.New("forbidden")
)

// TrackService gère les opérations sur les tracks
// BE-SVC-001: Add cache service for track metadata
type TrackService struct {
	db           *gorm.DB
	logger       *zap.Logger
	uploadDir    string
	maxFileSize  int64
	cacheService *services.CacheService
}

// NewTrackService crée un nouveau service de tracks
func NewTrackService(db *gorm.DB, logger *zap.Logger, uploadDir string) *TrackService {
	if uploadDir == "" {
		uploadDir = "uploads/tracks"
	}
	return &TrackService{
		db:          db,
		logger:      logger,
		uploadDir:   uploadDir,
		maxFileSize: 100 * 1024 * 1024, // 100MB
	}
}

// SetCacheService définit le service de cache pour TrackService
// BE-SVC-001: Implement caching layer for frequently accessed data
func (s *TrackService) SetCacheService(cacheService *services.CacheService) {
	s.cacheService = cacheService
}

// ValidateTrackFile valide le format et la taille d'un fichier audio
func (s *TrackService) ValidateTrackFile(fileHeader *multipart.FileHeader) error {
	// Valider la taille
	if fileHeader.Size > s.maxFileSize {
		return fmt.Errorf("%w: file size exceeds maximum allowed size of 100MB", ErrTrackTooLarge)
	}

	if fileHeader.Size == 0 {
		return fmt.Errorf("%w: file is empty", ErrInvalidTrackFormat)
	}

	// Valider l'extension
	ext := strings.ToLower(filepath.Ext(fileHeader.Filename))
	allowedExtensions := []string{".mp3", ".flac", ".wav", ".ogg", ".m4a", ".aac"}
	isValidExt := false
	for _, allowedExt := range allowedExtensions {
		if ext == allowedExt {
			isValidExt = true
			break
		}
	}

	if !isValidExt {
		return fmt.Errorf("%w: invalid file format. Allowed formats: MP3, FLAC, WAV, OGG", ErrInvalidTrackFormat)
	}

	// Valider le type MIME en ouvrant le fichier
	file, err := fileHeader.Open()
	if err != nil {
		// FIX #10: Logger l'erreur avec contexte
		s.logger.Error("Failed to open file for validation",
			zap.String("filename", fileHeader.Filename),
			zap.Int64("size", fileHeader.Size),
			zap.Error(err),
		)
		return fmt.Errorf("failed to open file: %w", err)
	}
	defer file.Close()

	// Lire les premiers bytes pour vérifier le magic number
	header := make([]byte, 12)
	n, err := file.Read(header)
	if err != nil && err != io.EOF {
		// FIX #10: Logger l'erreur avec contexte
		s.logger.Error("Failed to read file header for validation",
			zap.String("filename", fileHeader.Filename),
			zap.Error(err),
		)
		return fmt.Errorf("failed to read file header: %w", err)
	}

	if n < 4 {
		return fmt.Errorf("file too small to validate")
	}

	// Vérifier les magic numbers pour les formats audio
	isValidFormat := false
	headerStr := string(header[:n])

	// MP3: ID3v2 (starts with "ID3") or MPEG frame sync (0xFF 0xFB/E/F)
	if strings.HasPrefix(headerStr, "ID3") || (header[0] == 0xFF && (header[1]&0xE0) == 0xE0) {
		isValidFormat = true
	}
	// FLAC: "fLaC"
	if strings.HasPrefix(headerStr, "fLaC") {
		isValidFormat = true
	}
	// WAV: "RIFF" followed by "WAVE"
	if strings.HasPrefix(headerStr, "RIFF") && len(headerStr) >= 12 && string(header[8:12]) == "WAVE" {
		isValidFormat = true
	}
	// OGG: "OggS"
	if strings.HasPrefix(headerStr, "OggS") {
		isValidFormat = true
	}
	// M4A/AAC: "ftyp" avec "M4A" ou "mp4"
	if strings.Contains(headerStr, "ftyp") && (strings.Contains(headerStr, "M4A") || strings.Contains(headerStr, "mp4")) {
		isValidFormat = true
	}

	if !isValidFormat {
		return fmt.Errorf("%w: invalid audio file format", ErrInvalidTrackFormat)
	}

	return nil
}

// TrackMetadata contient les métadonnées optionnelles pour un upload
type TrackMetadata struct {
	Title    string
	Artist   string
	Album    string
	Genre    string
	Year     int
	IsPublic bool
}

// UploadTrack upload un fichier audio et crée un enregistrement Track en base
// MOD-P2-008: Implémentation asynchrone - crée le Track immédiatement et lance la copie en goroutine
// Retourne le Track avec Status=Uploading, la copie se fait en arrière-plan
func (s *TrackService) UploadTrack(ctx context.Context, userID uuid.UUID, fileHeader *multipart.FileHeader, metadata TrackMetadata) (*models.Track, error) {
	// Vérifier le quota utilisateur
	if err := s.CheckUserQuota(ctx, userID, fileHeader.Size); err != nil {
		// FIX #10: Logger l'erreur avec contexte
		s.logger.Warn("User quota check failed",
			zap.String("user_id", userID.String()),
			zap.Int64("file_size", fileHeader.Size),
			zap.String("filename", fileHeader.Filename),
			zap.Error(err),
		)
		return nil, err
	}

	// Valider le fichier
	if err := s.ValidateTrackFile(fileHeader); err != nil {
		// FIX #10: Logger l'erreur avec contexte
		s.logger.Warn("Track file validation failed",
			zap.String("user_id", userID.String()),
			zap.String("filename", fileHeader.Filename),
			zap.Int64("file_size", fileHeader.Size),
			zap.Error(err),
		)
		return nil, err
	}

	// Créer le répertoire d'upload s'il n'existe pas
	fmt.Printf("📁 [UPLOAD] Vérification dossier upload: %s\n", s.uploadDir)
	if err := os.MkdirAll(s.uploadDir, 0755); err != nil {
		fmt.Printf("❌ [UPLOAD] Erreur création dossier: %v\n", err)
		return nil, fmt.Errorf("%w: failed to create upload directory: %w", ErrStorageError, err)
	}
	fmt.Printf("✅ [UPLOAD] Dossier upload créé/vérifié: %s\n", s.uploadDir)

	// Générer un nom de fichier unique
	timestamp := uuid.New()
	ext := filepath.Ext(fileHeader.Filename)
	filename := fmt.Sprintf("%s_%s%s", userID.String(), timestamp.String(), ext) // Fixed format to use strings for UUID
	filePath := filepath.Join(s.uploadDir, filename)
	fmt.Printf("💾 [UPLOAD] Chemin fichier de destination: %s\n", filePath)

	// Déterminer le format depuis l'extension
	format := strings.TrimPrefix(strings.ToUpper(ext), ".")
	if format == "M4A" {
		format = "AAC"
	}

	// Déterminer le titre (métadonnée ou nom de fichier)
	title := metadata.Title
	if title == "" {
		title = strings.TrimSuffix(fileHeader.Filename, ext)
	}

	// MOD-P2-008: Créer l'enregistrement Track en base AVANT la copie (sémantique asynchrone)
	// Le fichier n'existe pas encore, mais on crée l'enregistrement pour traçabilité
	// FileID est NULL temporairement (sera mis à jour après création du fichier)
	track := &models.Track{
		UserID:        userID,
		FileID:        nil, // NULL temporairement - sera mis à jour après création fichier
		Title:         title,
		Artist:        metadata.Artist,
		Album:         metadata.Album,
		Genre:         metadata.Genre,
		Year:          metadata.Year,
		FilePath:      filePath,
		FileSize:      fileHeader.Size,
		Format:        format,
		Duration:      0, // Sera mis à jour lors du traitement asynchrone
		IsPublic:      metadata.IsPublic,
		Status:        models.TrackStatusUploading,
		StatusMessage: "Upload started",
	}

	if err := s.db.WithContext(ctx).Create(track).Error; err != nil {
		fmt.Printf("❌ [UPLOAD] Erreur création enregistrement track: %v\n", err)
		return nil, fmt.Errorf("failed to create track record: %w", err)
	}
	fmt.Printf("✅ [UPLOAD] Enregistrement track créé en DB (ID: %s)\n", track.ID.String())

	// MOD-P2-008: Lancer la copie fichier en goroutine avec suivi (context + cancellation)
	// La goroutine mettra à jour le Status quand terminé
	fmt.Printf("🚀 [UPLOAD] Lancement copie fichier en asynchrone...\n")
	go s.copyFileAsync(ctx, track.ID, fileHeader, filePath, userID)

	// MOD-P2-003: Enregistrer la métrique business
	monitoring.RecordTrackUploaded()

	s.logger.Info("Track upload initiated (async)",
		zap.String("track_id", track.ID.String()),
		zap.String("user_id", userID.String()),
		zap.String("filename", filename),
		zap.Int64("file_size", fileHeader.Size),
	)

	return track, nil
}

// copyFileAsync copie le fichier de manière asynchrone et met à jour le Status du Track
// MOD-P2-008: Goroutine suivie avec context + cancellation + nettoyage en cas d'erreur
func (s *TrackService) copyFileAsync(ctx context.Context, trackID uuid.UUID, fileHeader *multipart.FileHeader, filePath string, userID uuid.UUID) {
	// Créer un contexte avec timeout pour la copie (5 minutes max)
	copyCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
	defer cancel()

	// Ouvrir le fichier source
	fmt.Printf("📂 [UPLOAD ASYNC] Ouverture fichier source...\n")
	src, err := fileHeader.Open()
	if err != nil {
		fmt.Printf("❌ [UPLOAD ASYNC] Erreur ouverture fichier source: %v\n", err)
		s.updateTrackStatus(copyCtx, trackID, models.TrackStatusFailed, fmt.Sprintf("Failed to open uploaded file: %v", err))
		s.cleanupFailedUpload(filePath, trackID, "failed to open source file")
		return
	}
	defer src.Close()
	fmt.Printf("✅ [UPLOAD ASYNC] Fichier source ouvert\n")

	// Créer le fichier de destination
	fmt.Printf("💾 [UPLOAD ASYNC] Création fichier destination: %s\n", filePath)
	dst, err := os.Create(filePath)
	if err != nil {
		fmt.Printf("❌ [UPLOAD ASYNC] Erreur création fichier destination: %v\n", err)
		s.updateTrackStatus(copyCtx, trackID, models.TrackStatusFailed, fmt.Sprintf("Failed to create destination file: %v", err))
		s.cleanupFailedUpload(filePath, trackID, "failed to create destination file")
		return
	}
	defer dst.Close()
	fmt.Printf("✅ [UPLOAD ASYNC] Fichier destination créé\n")

	// Copier le fichier avec gestion d'erreurs
	fmt.Printf("📋 [UPLOAD ASYNC] Début copie fichier...\n")
	bytesWritten, err := io.Copy(dst, src)
	if err != nil {
		fmt.Printf("❌ [UPLOAD ASYNC] Erreur copie fichier: %v\n", err)
		s.updateTrackStatus(copyCtx, trackID, models.TrackStatusFailed, fmt.Sprintf("Failed to save file: %v", err))
		s.cleanupFailedUpload(filePath, trackID, fmt.Sprintf("copy failed: %v", err))
		return
	}
	fmt.Printf("✅ [UPLOAD ASYNC] Fichier copié avec succès (%d bytes écrits)\n", bytesWritten)

	// Vérifier si le contexte a été annulé
	select {
	case <-copyCtx.Done():
		s.updateTrackStatus(copyCtx, trackID, models.TrackStatusFailed, fmt.Sprintf("Upload cancelled: %v", copyCtx.Err()))
		s.cleanupFailedUpload(filePath, trackID, "upload cancelled")
		return
	default:
		// Continuer
	}

	// Vérifier que tous les bytes ont été copiés
	if bytesWritten != fileHeader.Size {
		s.updateTrackStatus(copyCtx, trackID, models.TrackStatusFailed, fmt.Sprintf("Incomplete copy: %d/%d bytes", bytesWritten, fileHeader.Size))
		s.cleanupFailedUpload(filePath, trackID, fmt.Sprintf("incomplete copy: %d/%d bytes", bytesWritten, fileHeader.Size))
		return
	}

	// Copie réussie - mettre à jour le Status
	s.updateTrackStatus(copyCtx, trackID, models.TrackStatusProcessing, "File uploaded, processing...")

	s.logger.Info("Track file copied successfully (async)",
		zap.String("track_id", trackID.String()),
		zap.String("user_id", userID.String()),
		zap.Int64("bytes_written", bytesWritten),
		zap.String("file_path", filePath),
	)
}

// updateTrackStatus met à jour le Status et StatusMessage d'un Track
// MOD-P2-008: Helper pour mettre à jour le Status de manière thread-safe
func (s *TrackService) updateTrackStatus(ctx context.Context, trackID uuid.UUID, status models.TrackStatus, message string) {
	if err := s.db.WithContext(ctx).Model(&models.Track{}).
		Where("id = ?", trackID).
		Updates(map[string]interface{}{
			"status":         status,
			"status_message": message,
		}).Error; err != nil {
		s.logger.Error("Failed to update track status",
			zap.String("track_id", trackID.String()),
			zap.String("status", string(status)),
			zap.String("message", message),
			zap.Error(err),
		)
	} else {
		s.logger.Info("Track status updated",
			zap.String("track_id", trackID.String()),
			zap.String("status", string(status)),
			zap.String("message", message),
		)
	}
}

// cleanupFailedUpload nettoie le fichier et le Track en cas d'échec
// MOD-P2-008: Nettoyage automatique en cas d'erreur
func (s *TrackService) cleanupFailedUpload(filePath string, trackID uuid.UUID, reason string) {
	// Supprimer le fichier s'il existe
	if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
		s.logger.Warn("Failed to cleanup file after upload failure",
			zap.String("file_path", filePath),
			zap.String("track_id", trackID.String()),
			zap.String("reason", reason),
			zap.Error(err),
		)
	}

	s.logger.Info("Cleaned up failed upload",
		zap.String("track_id", trackID.String()),
		zap.String("file_path", filePath),
		zap.String("reason", reason),
	)
}

// CreateTrackFromPath crée un track à partir d'un fichier déjà sauvegardé
func (s *TrackService) CreateTrackFromPath(ctx context.Context, userID uuid.UUID, filePath, filename string, fileSize int64, format string) (*models.Track, error) {
	ext := filepath.Ext(filename)
	title := strings.TrimSuffix(filename, ext)

	track := &models.Track{
		UserID:        userID,
		Title:         title,
		FilePath:      filePath,
		FileSize:      fileSize,
		Format:        format,
		Duration:      0, // Sera mis à jour lors du traitement asynchrone
		IsPublic:      true,
		Status:        models.TrackStatusUploading,
		StatusMessage: "Upload completed",
	}

	if err := s.db.WithContext(ctx).Create(track).Error; err != nil {
		return nil, fmt.Errorf("failed to create track record: %w", err)
	}

	s.logger.Info("Track created from path",
		zap.String("track_id", track.ID.String()),
		zap.String("user_id", userID.String()),
		zap.String("file_path", filePath),
		zap.Int64("file_size", fileSize),
	)

	return track, nil
}

// UserQuota représente les informations de quota d'un utilisateur
type UserQuota struct {
	TracksCount  int64 `json:"tracks_count"`
	TracksLimit  int64 `json:"tracks_limit"`
	StorageUsed  int64 `json:"storage_used"`  // bytes
	StorageLimit int64 `json:"storage_limit"` // bytes
}

// CheckUserQuota vérifie si l'utilisateur peut uploader un fichier selon son quota
func (s *TrackService) CheckUserQuota(ctx context.Context, userID uuid.UUID, fileSize int64) error {
	var trackCount int64
	// MOD-P2-008: Utiliser creator_id (nom de colonne réel) au lieu de user_id
	if err := s.db.WithContext(ctx).Model(&models.Track{}).Where("creator_id = ?", userID).Count(&trackCount).Error; err != nil {
		// FIX #10: Logger l'erreur avec contexte
		s.logger.Error("Failed to check track count for quota",
			zap.String("user_id", userID.String()),
			zap.Error(err),
		)
		return fmt.Errorf("failed to check track count: %w", err)
	}

	if trackCount >= MaxTracksPerUser {
		s.logger.Warn("Track quota exceeded",
			zap.String("user_id", userID.String()),
			zap.Int64("track_count", trackCount),
			zap.Int64("max_tracks", MaxTracksPerUser),
		)
		return ErrTrackQuotaExceeded
	}

	var totalSize int64
	if err := s.db.WithContext(ctx).Model(&models.Track{}).
		Where("creator_id = ?", userID).
		Select("COALESCE(SUM(file_size), 0)").
		Scan(&totalSize).Error; err != nil {
		// FIX #10: Logger l'erreur avec contexte
		s.logger.Error("Failed to check storage usage for quota",
			zap.String("user_id", userID.String()),
			zap.Error(err),
		)
		return fmt.Errorf("failed to check storage usage: %w", err)
	}

	if totalSize+fileSize > MaxStoragePerUser {
		s.logger.Warn("Storage quota exceeded",
			zap.String("user_id", userID.String()),
			zap.Int64("total_size", totalSize),
			zap.Int64("file_size", fileSize),
			zap.Int64("max_storage", MaxStoragePerUser),
		)
		return ErrStorageQuotaExceeded
	}

	return nil
}

// GetUserQuota récupère les informations de quota d'un utilisateur
func (s *TrackService) GetUserQuota(ctx context.Context, userID uuid.UUID) (*UserQuota, error) {
	var trackCount int64
	if err := s.db.WithContext(ctx).Model(&models.Track{}).Where("creator_id = ?", userID).Count(&trackCount).Error; err != nil {
		return nil, fmt.Errorf("failed to get track count: %w", err)
	}

	var totalSize int64
	if err := s.db.WithContext(ctx).Model(&models.Track{}).
		Where("creator_id = ?", userID).
		Select("COALESCE(SUM(file_size), 0)").
		Scan(&totalSize).Error; err != nil {
		return nil, fmt.Errorf("failed to get storage usage: %w", err)
	}

	return &UserQuota{
		TracksCount:  trackCount,
		TracksLimit:  MaxTracksPerUser,
		StorageUsed:  totalSize,
		StorageLimit: MaxStoragePerUser,
	}, nil
}

// TrackListParams représente les paramètres de filtrage et pagination pour la liste des tracks
type TrackListParams struct {
	Page      int
	Limit     int
	UserID    *uuid.UUID
	Genre     *string
	Format    *string
	SortBy    string // "created_at", "title", "popularity"
	SortOrder string // "asc", "desc"
}

// ListTracks récupère une liste de tracks avec pagination, filtres et tri
func (s *TrackService) ListTracks(ctx context.Context, params TrackListParams) ([]*models.Track, int64, error) {
	// Créer la requête de base avec filtre sur le statut
	query := s.db.WithContext(ctx).Model(&models.Track{}).Where("status = ?", models.TrackStatusCompleted)

	// Appliquer les filtres
	if params.UserID != nil {
		query = query.Where("creator_id = ?", *params.UserID)
	}
	if params.Genre != nil && *params.Genre != "" {
		query = query.Where("genre = ?", *params.Genre)
	}
	if params.Format != nil && *params.Format != "" {
		query = query.Where("format = ?", *params.Format)
	}

	// Compter le total avant pagination
	var total int64
	if err := query.Count(&total).Error; err != nil {
		return nil, 0, fmt.Errorf("failed to count tracks: %w", err)
	}

	// Appliquer le tri
	sortOrder := "DESC"
	if params.SortOrder == "asc" {
		sortOrder = "ASC"
	}

	// Valider et appliquer SortBy
	sortBy := params.SortBy
	if sortBy == "" {
		sortBy = "created_at"
	}
	// Sécurité: valider que sortBy est un champ valide
	validSortFields := map[string]bool{
		"created_at": true,
		"title":      true,
		"popularity": true,
	}
	if !validSortFields[sortBy] {
		sortBy = "created_at"
	}

	// Pour "popularity", on utilise play_count + like_count
	if sortBy == "popularity" {
		query = query.Order(fmt.Sprintf("(play_count + like_count) %s", sortOrder))
	} else {
		query = query.Order(fmt.Sprintf("%s %s", sortBy, sortOrder))
	}

	// Appliquer la pagination
	if params.Limit <= 0 {
		params.Limit = 20 // Par défaut
	}
	if params.Limit > 100 {
		params.Limit = 100 // Maximum
	}
	if params.Page <= 0 {
		params.Page = 1
	}
	offset := (params.Page - 1) * params.Limit
	query = query.Offset(offset).Limit(params.Limit)

	// Exécuter la requête
	var tracks []*models.Track
	if err := query.Preload("User").Find(&tracks).Error; err != nil {
		return nil, 0, fmt.Errorf("failed to list tracks: %w", err)
	}

	return tracks, total, nil
}

// GetTrackByID récupère un track par son ID
// MOD-P1-003: Preload User pour éviter N+1 queries si User est accédé plus tard
// BE-SVC-001: Add caching for track metadata
func (s *TrackService) GetTrackByID(ctx context.Context, trackID uuid.UUID) (*models.Track, error) { // Changed trackID to uuid.UUID
	cacheConfig := services.DefaultCacheConfig()

	// Try to get from cache first
	if s.cacheService != nil {
		var cachedTrack models.Track
		if err := s.cacheService.GetTrack(ctx, trackID, &cachedTrack); err == nil {
			// Cache hit
			return &cachedTrack, nil
		}
	}

	// Cache miss - fetch from database
	var track models.Track
	if err := s.db.WithContext(ctx).
		Preload("User").
		First(&track, "id = ?", trackID).Error; err != nil { // Updated query
		if err == gorm.ErrRecordNotFound {
			return nil, ErrTrackNotFound
		}
		return nil, fmt.Errorf("failed to get track: %w", err)
	}

	// Cache the track
	if s.cacheService != nil {
		if err := s.cacheService.SetTrack(ctx, trackID, track, cacheConfig); err != nil {
			s.logger.Warn("Failed to cache track", zap.Error(err), zap.String("track_id", trackID.String()))
		}
	}

	return &track, nil
}

// UpdateTrackParams représente les paramètres de mise à jour d'un track
type UpdateTrackParams struct {
	Title    *string `json:"title"`
	Artist   *string `json:"artist"`
	Album    *string `json:"album"`
	Genre    *string `json:"genre"`
	Year     *int    `json:"year"`
	IsPublic *bool   `json:"is_public"`
}

// UpdateTrack met à jour les métadonnées d'un track
// BE-SVC-001: Invalidate cache on track update
func (s *TrackService) UpdateTrack(ctx context.Context, trackID uuid.UUID, userID uuid.UUID, params UpdateTrackParams) (*models.Track, error) { // Changed trackID to uuid.UUID
	// Récupérer le track existant
	track, err := s.GetTrackByID(ctx, trackID)
	if err != nil {
		return nil, err
	}

	// MOD-P1-003: Vérifier que l'utilisateur est propriétaire du track ou admin
	// Check if user is admin (passed via context value)
	isAdmin := false
	if adminVal := ctx.Value("is_admin"); adminVal != nil {
		if admin, ok := adminVal.(bool); ok {
			isAdmin = admin
		}
	}

	if track.UserID != userID && !isAdmin {
		return nil, ErrForbidden
	}

	// Construire les mises à jour
	updates := make(map[string]interface{})
	if params.Title != nil {
		if *params.Title == "" {
			return nil, fmt.Errorf("title cannot be empty")
		}
		updates["title"] = *params.Title
	}
	if params.Artist != nil {
		updates["artist"] = *params.Artist
	}
	if params.Album != nil {
		updates["album"] = *params.Album
	}
	if params.Genre != nil {
		updates["genre"] = *params.Genre
	}
	if params.Year != nil {
		if *params.Year < 0 {
			return nil, fmt.Errorf("year cannot be negative")
		}
		updates["year"] = *params.Year
	}
	if params.IsPublic != nil {
		updates["is_public"] = *params.IsPublic
	}

	// Invalidate cache before update
	if s.cacheService != nil {
		if err := s.cacheService.InvalidateTrackCache(ctx, trackID); err != nil {
			s.logger.Warn("Failed to invalidate track cache", zap.Error(err), zap.String("track_id", trackID.String()))
		}
	}

	// Si aucune mise à jour n'est demandée
	if len(updates) == 0 {
		return track, nil
	}

	// Appliquer les mises à jour
	if err := s.db.WithContext(ctx).Model(track).Updates(updates).Error; err != nil {
		return nil, fmt.Errorf("failed to update track: %w", err)
	}

	// Recharger le track pour obtenir les valeurs mises à jour
	updatedTrack, err := s.GetTrackByID(ctx, trackID)
	if err != nil {
		return nil, err
	}

	s.logger.Info("Track updated",
		zap.Any("track_id", trackID), // Changed to zap.Any for uuid.UUID
		zap.String("user_id", userID.String()),
		zap.Any("updates", updates),
	)

	return updatedTrack, nil
}

// DeleteTrack supprime un track et son fichier physique
func (s *TrackService) DeleteTrack(ctx context.Context, trackID uuid.UUID, userID uuid.UUID) error { // Changed trackID to uuid.UUID
	// Récupérer le track existant
	track, err := s.GetTrackByID(ctx, trackID)
	if err != nil {
		return err
	}

	// MOD-P1-003: Vérifier que l'utilisateur est propriétaire du track ou admin
	// Check if user is admin (passed via context value)
	isAdmin := false
	if adminVal := ctx.Value("is_admin"); adminVal != nil {
		if admin, ok := adminVal.(bool); ok {
			isAdmin = admin
		}
	}

	if track.UserID != userID && !isAdmin {
		return ErrForbidden
	}

	// Supprimer le fichier physique
	if track.FilePath != "" {
		if err := os.Remove(track.FilePath); err != nil && !os.IsNotExist(err) {
			s.logger.Warn("Failed to delete track file",
				zap.Any("track_id", trackID), // Changed to zap.Any for uuid.UUID
				zap.String("file_path", track.FilePath),
				zap.Error(err),
			)
			// On continue même si la suppression du fichier échoue
		}
	}

	// Supprimer les fichiers associés (waveform, cover art)
	if track.WaveformPath != "" {
		if err := os.Remove(track.WaveformPath); err != nil && !os.IsNotExist(err) {
			s.logger.Warn("Failed to delete waveform file",
				zap.Any("track_id", trackID), // Changed to zap.Any for uuid.UUID
				zap.String("waveform_path", track.WaveformPath),
				zap.Error(err),
			)
		}
	}

	if track.CoverArtPath != "" {
		if err := os.Remove(track.CoverArtPath); err != nil && !os.IsNotExist(err) {
			s.logger.Warn("Failed to delete cover art file",
				zap.Any("track_id", trackID), // Changed to zap.Any for uuid.UUID
				zap.String("cover_art_path", track.CoverArtPath),
				zap.Error(err),
			)
		}
	}

	// Supprimer de la base de données
	// GORM gérera automatiquement les relations en cascade grâce aux contraintes OnDelete:CASCADE
	if err := s.db.WithContext(ctx).Delete(track).Error; err != nil {
		return fmt.Errorf("failed to delete track: %w", err)
	}

	s.logger.Info("Track deleted",
		zap.Any("track_id", trackID), // Changed to zap.Any for uuid.UUID
		zap.String("user_id", userID.String()),
		zap.String("file_path", track.FilePath),
	)

	return nil
}

// UpdateStreamStatus updates the stream status and manifest URL of a track
func (s *TrackService) UpdateStreamStatus(ctx context.Context, trackID uuid.UUID, status string, manifestURL string) error { // Changed trackID to uuid.UUID
	updates := map[string]interface{}{
		"stream_status": status,
	}
	if manifestURL != "" {
		updates["stream_manifest_url"] = manifestURL
	}

	switch status {
	case "ready":
		updates["status"] = models.TrackStatusCompleted
		updates["status_message"] = "Ready for streaming"
	case "error":
		updates["status"] = models.TrackStatusFailed
		updates["status_message"] = "Transcoding failed"
	}

	if err := s.db.WithContext(ctx).Model(&models.Track{}).Where("id = ?", trackID).Updates(updates).Error; err != nil {
		return fmt.Errorf("failed to update stream status: %w", err)
	}

	s.logger.Info("Track stream status updated",
		zap.Any("track_id", trackID), // Changed to zap.Any for uuid.UUID
		zap.String("status", status),
		zap.String("manifest_url", manifestURL),
	)

	return nil
}

// TrackStats représente les statistiques d'un track
type TrackStats struct {
	Views         int64 `json:"views"`
	Likes         int64 `json:"likes"`
	Comments      int64 `json:"comments"`
	TotalPlayTime int64 `json:"total_play_time"` // seconds
	Downloads     int64 `json:"downloads"`
}

// GetTrackStats récupère les statistiques d'un track
func (s *TrackService) GetTrackStats(ctx context.Context, trackID uuid.UUID) (*types.TrackStats, error) { // Changed trackID to uuid.UUID
	// Vérifier que le track existe
	var track models.Track
	if err := s.db.WithContext(ctx).First(&track, "id = ?", trackID).Error; err != nil { // Updated query
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, ErrTrackNotFound
		}
		return nil, fmt.Errorf("failed to get track: %w", err)
	}

	var stats types.TrackStats

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

	// Count comments (excluding soft-deleted)
	if err := s.db.WithContext(ctx).Model(&models.TrackComment{}).
		Where("track_id = ?", trackID).
		Count(&stats.Comments).Error; err != nil {
		return nil, fmt.Errorf("failed to count comments: %w", err)
	}

	// Count views (total plays) and sum total play time
	type PlayStats struct {
		Views         int64
		TotalPlayTime int64
	}
	var playStats PlayStats
	if err := s.db.WithContext(ctx).Model(&models.TrackPlay{}).
		Where("track_id = ?", trackID).
		Select("COUNT(*) as views, COALESCE(SUM(duration), 0) as total_play_time").
		Scan(&playStats).Error; err != nil {
		return nil, fmt.Errorf("failed to get play statistics: %w", err)
	}
	stats.Views = playStats.Views
	stats.TotalPlayTime = playStats.TotalPlayTime

	// Count downloads (sum of access_count from track_shares where permissions include 'download')
	// Note: access_count is incremented when a share link with download permission is accessed
	if err := s.db.WithContext(ctx).Model(&models.TrackShare{}).
		Where("track_id = ? AND permissions LIKE ?", trackID, "%download%").
		Select("COALESCE(SUM(access_count), 0)").
		Scan(&stats.Downloads).Error; err != nil {
		return nil, fmt.Errorf("failed to count downloads: %w", err)
	}

	s.logger.Info("Track stats retrieved",
		zap.Any("track_id", trackID), // Changed to zap.Any for uuid.UUID
		zap.Int64("views", stats.Views),
		zap.Int64("likes", stats.Likes),
		zap.Int64("comments", stats.Comments),
		zap.Int64("total_play_time", stats.TotalPlayTime),
		zap.Int64("downloads", stats.Downloads),
	)

	return &stats, nil
}

// BatchDeleteResult représente le résultat d'une suppression en lot
type BatchDeleteResult struct {
	Deleted []uuid.UUID        `json:"deleted"` // Changed to uuid.UUID
	Failed  []BatchDeleteError `json:"failed"`
}

// BatchDeleteError représente une erreur lors de la suppression d'un track
type BatchDeleteError struct {
	TrackID uuid.UUID `json:"track_id"` // Changed to uuid.UUID
	Error   string    `json:"error"`
}

// BatchDeleteTracks supprime plusieurs tracks en une seule requête
func (s *TrackService) BatchDeleteTracks(ctx context.Context, trackIDs []uuid.UUID, userID uuid.UUID) (*BatchDeleteResult, error) { // Changed trackIDs to []uuid.UUID
	if len(trackIDs) == 0 {
		return &BatchDeleteResult{
			Deleted: []uuid.UUID{},
			Failed:  []BatchDeleteError{},
		}, nil
	}

	// Limiter le nombre de tracks à supprimer en une seule fois pour éviter les surcharges
	const maxBatchSize = 100
	if len(trackIDs) > maxBatchSize {
		return nil, fmt.Errorf("batch size exceeds maximum of %d tracks", maxBatchSize)
	}

	result := &BatchDeleteResult{
		Deleted: []uuid.UUID{},
		Failed:  []BatchDeleteError{},
	}

	// Récupérer tous les tracks en une seule requête
	var tracks []models.Track
	if err := s.db.WithContext(ctx).Where("id IN ?", trackIDs).Find(&tracks).Error; err != nil {
		return nil, fmt.Errorf("failed to fetch tracks: %w", err)
	}

	// Créer un map pour un accès rapide
	trackMap := make(map[uuid.UUID]*models.Track) // Changed to uuid.UUID
	for i := range tracks {
		trackMap[tracks[i].ID] = &tracks[i]
	}

	// MOD-P1-003: Check if user is admin (passed via context value)
	isAdmin := false
	if adminVal := ctx.Value("is_admin"); adminVal != nil {
		if admin, ok := adminVal.(bool); ok {
			isAdmin = admin
		}
	}

	// Traiter chaque track
	for _, trackID := range trackIDs {
		track, exists := trackMap[trackID]
		if !exists {
			result.Failed = append(result.Failed, BatchDeleteError{
				TrackID: trackID,
				Error:   "track not found",
			})
			continue
		}

		// MOD-P1-003: Vérifier l'ownership (admin peut bypass)
		if track.UserID != userID && !isAdmin {
			result.Failed = append(result.Failed, BatchDeleteError{
				TrackID: trackID,
				Error:   "forbidden: track does not belong to user",
			})
			continue
		}

		// Supprimer le track (réutiliser la logique de DeleteTrack)
		if err := s.deleteTrackFiles(ctx, track); err != nil {
			s.logger.Warn("Failed to delete track files",
				zap.Any("track_id", trackID), // Changed to zap.Any for uuid.UUID
				zap.Error(err),
			)
			// On continue même si la suppression des fichiers échoue
		}

		// Supprimer de la base de données
		if err := s.db.WithContext(ctx).Delete(track).Error; err != nil {
			result.Failed = append(result.Failed, BatchDeleteError{
				TrackID: trackID,
				Error:   fmt.Sprintf("failed to delete from database: %v", err),
			})
			continue
		}

		result.Deleted = append(result.Deleted, trackID)

		s.logger.Info("Track deleted in batch",
			zap.Any("track_id", trackID), // Changed to zap.Any for uuid.UUID
			zap.String("user_id", userID.String()),
		)
	}

	return result, nil
}

// deleteTrackFiles supprime les fichiers physiques d'un track (logique extraite de DeleteTrack)
func (s *TrackService) deleteTrackFiles(ctx context.Context, track *models.Track) error {
	var errors []error

	// Supprimer le fichier principal
	if track.FilePath != "" {
		if err := os.Remove(track.FilePath); err != nil && !os.IsNotExist(err) {
			errors = append(errors, fmt.Errorf("failed to delete track file %s: %w", track.FilePath, err))
		}
	}

	// Supprimer le fichier waveform
	if track.WaveformPath != "" {
		if err := os.Remove(track.WaveformPath); err != nil && !os.IsNotExist(err) {
			errors = append(errors, fmt.Errorf("failed to delete waveform file %s: %w", track.WaveformPath, err))
		}
	}

	// Supprimer le fichier cover art
	if track.CoverArtPath != "" {
		if err := os.Remove(track.CoverArtPath); err != nil && !os.IsNotExist(err) {
			errors = append(errors, fmt.Errorf("failed to delete cover art file %s: %w", track.CoverArtPath, err))
		}
	}

	// Retourner la première erreur si il y en a, sinon nil
	if len(errors) > 0 {
		return errors[0]
	}

	return nil
}

// BatchUpdateResult représente le résultat d'une mise à jour en lot
type BatchUpdateResult struct {
	Updated []uuid.UUID        `json:"updated"` // Changed to uuid.UUID
	Failed  []BatchUpdateError `json:"failed"`
}

// BatchUpdateError représente une erreur lors de la mise à jour d'un track
type BatchUpdateError struct {
	TrackID uuid.UUID `json:"track_id"` // Changed to uuid.UUID
	Error   string    `json:"error"`
}

// BatchUpdateTracks met à jour plusieurs tracks en une seule requête
func (s *TrackService) BatchUpdateTracks(ctx context.Context, trackIDs []uuid.UUID, userID uuid.UUID, updates map[string]interface{}) (*BatchUpdateResult, error) { // Changed trackIDs to []uuid.UUID
	if len(trackIDs) == 0 {
		return &BatchUpdateResult{
			Updated: []uuid.UUID{},
			Failed:  []BatchUpdateError{},
		}, nil
	}

	// Limiter le nombre de tracks à mettre à jour en une seule fois
	const maxBatchSize = 100
	if len(trackIDs) > maxBatchSize {
		return nil, fmt.Errorf("batch size exceeds maximum of %d tracks", maxBatchSize)
	}

	// Valider que les updates ne sont pas vides
	if len(updates) == 0 {
		return nil, fmt.Errorf("no valid fields to update")
	}

	// Liste des champs autorisés pour la mise à jour en lot
	allowedFields := map[string]bool{
		"is_public": true,
		"title":     true,
		"artist":    true,
		"album":     true,
		"genre":     true,
		"year":      true,
	}

	// Filtrer les champs autorisés et valider les valeurs
	filteredUpdates := make(map[string]interface{})
	for key, value := range updates {
		if !allowedFields[key] {
			continue // Ignorer les champs non autorisés
		}

		// Validation spécifique selon le champ
		switch key {
		case "is_public":
			if _, ok := value.(bool); !ok {
				return nil, fmt.Errorf("invalid value for is_public: must be boolean")
			}
		case "title":
			if str, ok := value.(string); ok {
				if len(str) == 0 {
					return nil, fmt.Errorf("title cannot be empty")
				}
				if len(str) > 255 {
					return nil, fmt.Errorf("title exceeds maximum length of 255 characters")
				}
			} else {
				return nil, fmt.Errorf("invalid value for title: must be string")
			}
		case "artist", "album", "genre":
			if str, ok := value.(string); ok {
				if key == "genre" && len(str) > 100 {
					return nil, fmt.Errorf("genre exceeds maximum length of 100 characters")
				}
			} else {
				return nil, fmt.Errorf("invalid value for %s: must be string", key)
			}
		case "year":
			if num, ok := value.(float64); ok {
				year := int(num)
				if year < 1900 || year > 2100 {
					return nil, fmt.Errorf("year must be between 1900 and 2100")
				}
				filteredUpdates[key] = year
				continue
			} else if num, ok := value.(int); ok {
				if num < 1900 || num > 2100 {
					return nil, fmt.Errorf("year must be between 1900 and 2100")
				}
			} else {
				return nil, fmt.Errorf("invalid value for year: must be integer")
			}
		}

		filteredUpdates[key] = value
	}

	if len(filteredUpdates) == 0 {
		return nil, fmt.Errorf("no valid fields to update")
	}

	result := &BatchUpdateResult{
		Updated: []uuid.UUID{},
		Failed:  []BatchUpdateError{},
	}

	// Récupérer tous les tracks en une seule requête
	var tracks []models.Track
	if err := s.db.WithContext(ctx).Where("id IN ?", trackIDs).Find(&tracks).Error; err != nil {
		return nil, fmt.Errorf("failed to fetch tracks: %w", err)
	}

	// Créer un map pour un accès rapide
	trackMap := make(map[uuid.UUID]*models.Track) // Changed to uuid.UUID
	for i := range tracks {
		trackMap[tracks[i].ID] = &tracks[i]
	}

	// MOD-P1-003: Check if user is admin (passed via context value)
	isAdmin := false
	if adminVal := ctx.Value("is_admin"); adminVal != nil {
		if admin, ok := adminVal.(bool); ok {
			isAdmin = admin
		}
	}

	// Traiter chaque track
	for _, trackID := range trackIDs {
		track, exists := trackMap[trackID]
		if !exists {
			result.Failed = append(result.Failed, BatchUpdateError{
				TrackID: trackID,
				Error:   "track not found",
			})
			continue
		}

		// MOD-P1-003: Vérifier l'ownership (admin peut bypass)
		if track.UserID != userID && !isAdmin {
			result.Failed = append(result.Failed, BatchUpdateError{
				TrackID: trackID,
				Error:   "forbidden: track does not belong to user",
			})
			continue
		}

		// Appliquer les mises à jour
		if err := s.db.WithContext(ctx).Model(track).Updates(filteredUpdates).Error; err != nil {
			result.Failed = append(result.Failed, BatchUpdateError{
				TrackID: trackID,
				Error:   fmt.Sprintf("failed to update: %v", err),
			})
			continue
		}

		result.Updated = append(result.Updated, trackID)

		s.logger.Info("Track updated in batch",
			zap.Any("track_id", trackID), // Changed to zap.Any for uuid.UUID
			zap.String("user_id", userID.String()),
			zap.Any("updates", filteredUpdates),
		)
	}

	return result, nil
}

// UpdateStreamStatus updates the stream status and manifest URL of a track
