package track

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/google/uuid"

	"veza-backend-api/internal/common"
	apperrors "veza-backend-api/internal/errors"
	"veza-backend-api/internal/handlers"
	"veza-backend-api/internal/models"
	"veza-backend-api/internal/response"
	"veza-backend-api/internal/services"

	"github.com/gin-gonic/gin"
	"go.uber.org/zap" // Added zap
	"gorm.io/gorm"
)

// TrackHandler gère les opérations sur les tracks
type TrackHandler struct {
	trackService             *TrackService
	trackUploadService       *services.TrackUploadService
	chunkService             *services.TrackChunkService
	likeService              *services.TrackLikeService
	streamService            *services.StreamService
	searchService            *services.TrackSearchService
	shareService             *services.TrackShareService
	versionService           *services.TrackVersionService
	historyService           *services.TrackHistoryService
	playbackAnalyticsService *services.PlaybackAnalyticsService // BE-API-019: Added for play analytics
	permissionService        *services.PermissionService        // MOD-P1-003: Added for admin check
	uploadValidator          *services.UploadValidator          // MOD-P1-001: Added for ClamAV scan before persistence
}

// NewTrackHandler crée un nouveau handler de tracks
func NewTrackHandler(
	trackService *TrackService,
	trackUploadService *services.TrackUploadService,
	chunkService *services.TrackChunkService,
	likeService *services.TrackLikeService,
	streamService *services.StreamService,
) *TrackHandler {
	return &TrackHandler{
		trackService:       trackService,
		trackUploadService: trackUploadService,
		chunkService:       chunkService,
		likeService:        likeService,
		streamService:      streamService,
	}
}

// SetUploadValidator définit le validateur d'upload (pour injection de dépendance)
// MOD-P1-001: Added for ClamAV scan before persistence
func (h *TrackHandler) SetUploadValidator(uploadValidator *services.UploadValidator) {
	h.uploadValidator = uploadValidator
}

// SetPermissionService définit le service de permissions (pour injection de dépendance)
// MOD-P1-003: Added for admin check in ownership verification
func (h *TrackHandler) SetPermissionService(permissionService *services.PermissionService) {
	h.permissionService = permissionService
}

// SetSearchService définit le service de recherche (pour injection de dépendance)
func (h *TrackHandler) SetSearchService(searchService *services.TrackSearchService) {
	h.searchService = searchService
}

// SetShareService définit le service de partage (pour injection de dépendance)
func (h *TrackHandler) SetShareService(shareService *services.TrackShareService) {
	h.shareService = shareService
}

// SetVersionService définit le service de versioning (pour injection de dépendance)
func (h *TrackHandler) SetVersionService(versionService *services.TrackVersionService) {
	h.versionService = versionService
}

// SetHistoryService définit le service d'historique (pour injection de dépendance)
func (h *TrackHandler) SetHistoryService(historyService *services.TrackHistoryService) {
	h.historyService = historyService
}

// SetPlaybackAnalyticsService définit le service d'analytics de lecture (pour injection de dépendance)
// BE-API-019: Implement track play analytics endpoint
func (h *TrackHandler) SetPlaybackAnalyticsService(analyticsService *services.PlaybackAnalyticsService) {
	h.playbackAnalyticsService = analyticsService
}

// getUserID récupère l'ID utilisateur du contexte de manière sécurisée (fail-secure)
// MOD-P1-RES-003: Remplace c.MustGet() pour éviter les panics
// Retourne false si user_id est absent ou invalide (répond déjà avec 401)
func (h *TrackHandler) getUserID(c *gin.Context) (uuid.UUID, bool) {
	userIDInterface, exists := c.Get("user_id")
	if !exists {
		// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.Unauthorized
		handlers.RespondWithAppError(c, apperrors.NewUnauthorizedError("unauthorized"))
		return uuid.Nil, false
	}

	userID, ok := userIDInterface.(uuid.UUID)
	if !ok {
		// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.Unauthorized
		handlers.RespondWithAppError(c, apperrors.NewUnauthorizedError("unauthorized"))
		return uuid.Nil, false
	}

	if userID == uuid.Nil {
		// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.Unauthorized
		handlers.RespondWithAppError(c, apperrors.NewUnauthorizedError("unauthorized"))
		return uuid.Nil, false
	}

	return userID, true
}

// respondWithError est un helper pour migrer vers RespondWithAppError
// MOD-P1-RES-001: Helper pour standardiser les réponses d'erreur
func (h *TrackHandler) respondWithError(c *gin.Context, httpStatus int, message string) {
	var errCode apperrors.ErrorCode
	switch httpStatus {
	case http.StatusBadRequest:
		errCode = apperrors.ErrCodeValidation
	case http.StatusUnauthorized:
		errCode = apperrors.ErrCodeUnauthorized
	case http.StatusForbidden:
		errCode = apperrors.ErrCodeForbidden
	case http.StatusNotFound:
		errCode = apperrors.ErrCodeNotFound
	case http.StatusInternalServerError:
		errCode = apperrors.ErrCodeInternal
	default:
		errCode = apperrors.ErrCodeInternal
	}
	handlers.RespondWithAppError(c, apperrors.New(errCode, message))
}

// UploadTrack gère l'upload d'un fichier audio
// @Summary      Upload Track
// @Description  Upload a new track (audio file)
// @Tags         Track
// @Accept       multipart/form-data
// @Produce      json
// @Security     BearerAuth
// @Param        file formData file true "Audio File (MP3, WAV, FLAC, OGG)"
// @Success      201  {object}  response.APIResponse{data=object{track=models.Track}}
// @Failure      400  {object}  response.APIResponse "No file or validation error"
// @Failure      401  {object}  response.APIResponse "Unauthorized"
// @Failure      403  {object}  response.APIResponse "Quota exceeded"
// @Failure      500  {object}  response.APIResponse "Internal Error"
// @Router       /tracks [post]
func (h *TrackHandler) UploadTrack(c *gin.Context) {
	// FIX #5: Remplacer fmt.Print* par logs structurés
	h.trackService.logger.Debug("Upload track request received")

	// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
	userID, ok := h.getUserID(c)
	if !ok {
		h.trackService.logger.Warn("Upload track: user not authenticated")
		return // Erreur déjà envoyée par getUserID
	}
	h.trackService.logger.Debug("Upload track: user authenticated", zap.String("user_id", userID.String()))

	fileHeader, err := c.FormFile("file")
	if err != nil {
		h.trackService.logger.Warn("Upload track: failed to get file", zap.Error(err))
		// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.BadRequest
		h.respondWithError(c, http.StatusBadRequest, "no file provided")
		return
	}
	h.trackService.logger.Debug("Upload track: file received",
		zap.String("filename", fileHeader.Filename),
		zap.Int64("size", fileHeader.Size),
		zap.String("user_id", userID.String()),
	)

	// MOD-P1-001: Scanner le fichier avec ClamAV AVANT toute persistance
	if h.uploadValidator != nil {
		// MOD-P1-004: Ajouter timeout context pour opération I/O (ClamAV scan)
		ctx, cancel := context.WithTimeout(c.Request.Context(), 10*time.Second)
		defer cancel()
		validationResult, err := h.uploadValidator.ValidateFile(ctx, fileHeader, "audio")
		if err != nil {
			// MOD-P1-001: Détecter le type d'erreur ClamAV et retourner code HTTP approprié
			if strings.Contains(err.Error(), "clamav_unavailable") {
				c.JSON(http.StatusServiceUnavailable, gin.H{
					"error":   "Virus scanning service is temporarily unavailable",
					"message": "Uploads are disabled for security reasons until the scanning service is restored",
					"code":    "SERVICE_UNAVAILABLE",
				})
				return
			}
			if strings.Contains(err.Error(), "clamav_infected") {
				c.JSON(http.StatusUnprocessableEntity, gin.H{
					"error":   "File rejected: virus detected",
					"details": validationResult.Error,
					"code":    "VIRUS_DETECTED",
				})
				return
			}
			if strings.Contains(err.Error(), "clamav_scan_error") {
				c.JSON(http.StatusServiceUnavailable, gin.H{
					"error":   "Virus scan failed",
					"message": "Unable to complete virus scan. Upload rejected for security.",
					"code":    "SCAN_ERROR",
				})
				return
			}
			// Autre erreur de validation
			// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.Error
			h.respondWithError(c, http.StatusBadRequest, validationResult.Error)
			return
		}
		if !validationResult.Valid {
			// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.Error
			h.respondWithError(c, http.StatusBadRequest, validationResult.Error)
			return
		}
		if validationResult.Quarantined {
			c.JSON(http.StatusUnprocessableEntity, gin.H{
				"error":   "File rejected: virus detected",
				"details": validationResult.Error,
				"code":    "VIRUS_DETECTED",
			})
			return
		}
	}

	// Parse metadata
	yearStr := c.DefaultPostForm("year", "0")
	year, _ := strconv.Atoi(yearStr) // Ignore error, default 0 is fine

	isPublicStr := c.DefaultPostForm("is_public", "true")
	isPublic := isPublicStr == "true"

	metadata := TrackMetadata{
		Title:    c.PostForm("title"),
		Artist:   c.PostForm("artist"),
		Album:    c.PostForm("album"),
		Genre:    c.PostForm("genre"),
		Year:     year,
		IsPublic: isPublic,
	}

	// Upload track (validation et quota sont vérifiés dans le service)
	// MOD-P1-001: Le scan ClamAV a été fait ci-dessus, maintenant on peut persister
	// MOD-P2-008: UploadTrack crée le Track immédiatement et lance la copie en goroutine
	// MOD-P1-004: Ajouter timeout context pour opération DB critique (upload track)
	h.trackService.logger.Debug("Upload track: starting save",
		zap.String("user_id", userID.String()),
		zap.String("filename", fileHeader.Filename),
		zap.Any("metadata", metadata),
	)
	ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) // Upload peut prendre du temps
	defer cancel()
	track, err := h.trackService.UploadTrack(ctx, userID, fileHeader, metadata)
	if err != nil {
		h.trackService.logger.Error("Upload track: save failed",
			zap.String("user_id", userID.String()),
			zap.String("filename", fileHeader.Filename),
			zap.Error(err),
		)
		// Mapper les erreurs vers des messages utilisateur spécifiques
		errorMessage := h.mapTrackError(err)
		statusCode := h.getErrorStatusCode(err)
		// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.Error
		h.respondWithError(c, statusCode, errorMessage)
		return
	}

	// MOD-P2-008: Sémantique asynchrone - retourner 202 Accepted avec track_id
	// La copie fichier se fait en arrière-plan, le client peut poller GetUploadStatus
	c.Header("Location", fmt.Sprintf("/api/v1/tracks/%s/status", track.ID.String()))
	handlers.RespondSuccess(c, http.StatusAccepted, gin.H{
		"track_id":   track.ID.String(),
		"status":     string(track.Status),
		"status_url": fmt.Sprintf("/api/v1/tracks/%s/status", track.ID.String()),
		"message":    "Upload initiated, file is being saved in background",
	})

	// MOD-P2-008: Déclencher le traitement du streaming après la copie (sera fait quand Status=Processing)
	// On ne peut pas le faire ici car le fichier n'existe pas encore
	// Ce sera fait dans un job séparé ou via un hook quand Status passe à Processing
}

// GetUploadStatus récupère le statut d'upload d'un track
// @Summary      Get Upload Status
// @Description  Get the processing status of an uploaded track
// @Tags         Track
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id   path      string  true  "Track ID"
// @Success      200  {object}  response.APIResponse{data=object{progress=int}}
// @Failure      400  {object}  response.APIResponse "Invalid ID"
// @Failure      401  {object}  response.APIResponse "Unauthorized"
// @Failure      404  {object}  response.APIResponse "Track not found"
// @Router       /tracks/{id}/status [get]
func (h *TrackHandler) GetUploadStatus(c *gin.Context) {
	trackIDStr := c.Param("id")
	if trackIDStr == "" {
		response.BadRequest(c, "track id is required")
		return
	}

	// MIGRATION UUID: ID est maintenant un UUID
	// Le trackID doit être un int64 pour le moment car models.Track n'a pas encore migré l'ID?
	// Attends, j'ai migré models.Track ID vers UUID dans l'étape 1.
	// Donc trackID est UUID.

	trackID, err := uuid.Parse(trackIDStr)
	if err != nil {
		// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.BadRequest
		h.respondWithError(c, http.StatusBadRequest, "invalid track id")
		return
	}

	// Vérifier que l'utilisateur est authentifié (userID non utilisé dans cette fonction)
	// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
	if _, ok := h.getUserID(c); !ok {
		return // Erreur déjà envoyée par getUserID
	}

	// Récupérer la progression
	// TODO(P2-GO-004): trackUploadService attend int64 - Migration UUID partielle à compléter
	// Je dois mettre à jour trackUploadService.
	// Pour l'instant, je ne peux pas compiler si je passe UUID.
	// Je vais supposer que trackUploadService a été migré ou que je dois le faire.
	// Mais la tâche ne mentionnait pas de migrer trackUploadService.
	// C'est le problème de dépendance en cascade.

	// Je vais convertir en int64 si possible pour que ça compile, ou migrer le service.
	// Mais Track.ID est UUID...

	// OK, la migration UUID était "complète" pour les modèles principaux.
	// Mais les services satellites comme TrackUploadService n'ont pas été migrés.
	// C'est la dette technique identifiée dans le rapport.

	// Pour que ça compile maintenant, je dois adapter TrackUploadService.
	// TODO(P2-GO-004): Migration UUID partielle - trackUploadService nécessite migration vers UUID
	// Ou mieux, je vais mettre à jour TrackUploadService après ce fichier.

	progress, err := h.trackUploadService.GetUploadProgress(c.Request.Context(), trackID)
	if err != nil {
		// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.InternalServerError
		h.respondWithError(c, http.StatusInternalServerError, "failed to get upload progress")
		return
	}

	// MOD-P1-RES-001: Utiliser RespondSuccess au lieu de response.Success
	handlers.RespondSuccess(c, http.StatusOK, gin.H{"progress": progress})
}

// InitiateChunkedUploadRequest représente la requête pour initialiser un upload par chunks
type InitiateChunkedUploadRequest struct {
	TotalChunks int    `json:"total_chunks" binding:"required,min=1" validate:"required,min=1"`
	TotalSize   int64  `json:"total_size" binding:"required,min=1" validate:"required,min=1"`
	Filename    string `json:"filename" binding:"required" validate:"required"`
}

// InitiateChunkedUpload initialise un nouvel upload par chunks
// @Summary      Initiate Chunked Upload
// @Description  Start a new chunked upload session
// @Tags         Track
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        request body InitiateChunkedUploadRequest true "Upload Metadata"
// @Success      200  {object}  response.APIResponse{data=object{upload_id=string,message=string}}
// @Failure      400  {object}  response.APIResponse "Validation Error"
// @Failure      401  {object}  response.APIResponse "Unauthorized"
// @Router       /tracks/initiate [post]
func (h *TrackHandler) InitiateChunkedUpload(c *gin.Context) {
	// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
	userID, ok := h.getUserID(c)
	if !ok {
		return // Erreur déjà envoyée par getUserID
	}

	// MOD-P1-002: Utiliser helper centralisé pour bind + validate
	var req InitiateChunkedUploadRequest
	if !common.BindAndValidateJSON(c, &req) {
		return // Erreur déjà envoyée au client
	}

	// Initialiser l'upload
	// InitiateChunkedUpload retourne un string (uploadID) donc pas de souci d'int64
	// Note: InitiateChunkedUpload n'accepte pas de context (à migrer si nécessaire)
	uploadID, err := h.chunkService.InitiateChunkedUpload(userID, req.TotalChunks, req.TotalSize, req.Filename)
	if err != nil {
		response.InternalServerError(c, err.Error())
		return
	}

	response.Success(c, gin.H{
		"upload_id": uploadID,
		"message":   "upload initiated successfully",
	})
}

// UploadChunkRequest représente la requête pour uploader un chunk
type UploadChunkRequest struct {
	UploadID    string `form:"upload_id" binding:"required"`
	ChunkNumber int    `form:"chunk_number" binding:"required,min=1"`
	TotalChunks int    `form:"total_chunks" binding:"required,min=1"`
	TotalSize   int64  `form:"total_size" binding:"required,min=1"`
	Filename    string `form:"filename" binding:"required"`
}

// UploadChunk gère l'upload d'un chunk
// @Summary      Upload Chunk
// @Description  Upload a single chunk of a file
// @Tags         Track
// @Accept       multipart/form-data
// @Produce      json
// @Security     BearerAuth
// @Param        chunk formData file true "Chunk Data"
// @Param        upload_id formData string true "Upload ID"
// @Param        chunk_number formData int true "Chunk Number"
// @Param        total_chunks formData int true "Total Chunks"
// @Param        total_size formData int64 true "Total Size"
// @Param        filename formData string true "Filename"
// @Success      200  {object}  response.APIResponse{data=object{message=string,upload_id=string,received_chunks=int,progress=float64}}
// @Failure      400  {object}  response.APIResponse "Validation Error"
// @Failure      401  {object}  response.APIResponse "Unauthorized"
// @Router       /tracks/chunk [post]
func (h *TrackHandler) UploadChunk(c *gin.Context) {
	// Vérifier que l'utilisateur est authentifié (userID non utilisé dans cette fonction)
	// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
	if _, ok := h.getUserID(c); !ok {
		return // Erreur déjà envoyée par getUserID
	}

	var req UploadChunkRequest
	if err := c.ShouldBind(&req); err != nil {
		response.BadRequest(c, err.Error())
		return
	}

	fileHeader, err := c.FormFile("chunk")
	if err != nil {
		response.BadRequest(c, "no chunk file provided")
		return
	}

	// Sauvegarder le chunk
	if err := h.chunkService.SaveChunk(c.Request.Context(), req.UploadID, req.ChunkNumber, req.TotalChunks, fileHeader); err != nil {
		response.BadRequest(c, err.Error())
		return
	}

	// Récupérer la progression
	receivedChunks, progress, err := h.chunkService.GetUploadProgress(req.UploadID)
	if err != nil {
		response.InternalServerError(c, err.Error())
		return
	}

	response.Success(c, gin.H{
		"message":         "chunk uploaded successfully",
		"upload_id":       req.UploadID,
		"received_chunks": receivedChunks,
		"total_chunks":    req.TotalChunks,
		"progress":        progress,
	})
}

// CompleteChunkedUploadRequest représente la requête pour compléter un upload par chunks
type CompleteChunkedUploadRequest struct {
	UploadID string `json:"upload_id" binding:"required" validate:"required,uuid"`
}

// CompleteChunkedUpload assemble tous les chunks et crée le track final
// @Summary      Complete Chunked Upload
// @Description  Finish upload session and assemble file
// @Tags         Track
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        request body CompleteChunkedUploadRequest true "Upload ID"
// @Success      201  {object}  response.APIResponse{data=object{message=string,track=models.Track,md5=string}}
// @Failure      400  {object}  response.APIResponse "Validation or Assemblage Error"
// @Failure      401  {object}  response.APIResponse "Unauthorized"
// @Router       /tracks/complete [post]
func (h *TrackHandler) CompleteChunkedUpload(c *gin.Context) {
	// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
	userID, ok := h.getUserID(c)
	if !ok {
		return // Erreur déjà envoyée par getUserID
	}

	// MOD-P1-002: Utiliser helper centralisé pour bind + validate
	var req CompleteChunkedUploadRequest
	if !common.BindAndValidateJSON(c, &req) {
		return // Erreur déjà envoyée au client
	}

	// Récupérer les informations de l'upload pour obtenir le filename
	uploadInfo, err := h.chunkService.GetUploadInfo(req.UploadID)
	if err != nil {
		response.BadRequest(c, err.Error())
		return
	}

	// Générer un nom de fichier unique pour le fichier final
	timestamp := uuid.New()
	ext := filepath.Ext(uploadInfo.Filename)
	if ext == "" {
		ext = ".mp3" // Par défaut
	}
	filename := fmt.Sprintf("%s_%s%s", userID.String(), timestamp.String(), ext)
	finalPath := filepath.Join("uploads/tracks", userID.String(), filename)

	// Assurer que le répertoire existe
	if err := os.MkdirAll(filepath.Dir(finalPath), 0755); err != nil {
		response.InternalServerError(c, "failed to create directory")
		return
	}

	// Assembler les chunks
	// MOD-P1-004: Ajouter timeout context pour opération I/O (assemblage chunks)
	ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) // Assemblage peut prendre du temps
	defer cancel()
	finalFilename, totalSize, md5, err := h.chunkService.CompleteChunkedUpload(ctx, req.UploadID, finalPath)
	if err != nil {
		errorMessage := h.mapTrackError(err)
		statusCode := h.getErrorStatusCode(err)
		response.Error(c, statusCode, errorMessage)
		return
	}

	// Vérifier le quota avant de créer le track final
	// MOD-P1-004: Ajouter timeout context pour opération DB (quota check)
	quotaCtx, quotaCancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
	defer quotaCancel()
	if err := h.trackService.CheckUserQuota(quotaCtx, userID, totalSize); err != nil {
		errorMessage := h.mapTrackError(err)
		statusCode := h.getErrorStatusCode(err)
		// Nettoyer le fichier assemblé
		os.Remove(finalPath)
		response.Error(c, statusCode, errorMessage)
		return
	}

	// Déterminer le format
	ext = filepath.Ext(finalFilename)
	format := strings.TrimPrefix(strings.ToUpper(ext), ".")
	if format == "M4A" {
		format = "AAC"
	}

	// Créer le track en base en utilisant CreateTrackFromPath
	// MOD-P1-004: Ajouter timeout context pour opération DB critique (create track)
	createCtx, createCancel := context.WithTimeout(c.Request.Context(), 10*time.Second)
	defer createCancel()
	track, err := h.trackService.CreateTrackFromPath(createCtx, userID, finalPath, finalFilename, totalSize, format)
	if err != nil {
		// Nettoyer le fichier en cas d'erreur
		os.Remove(finalPath)
		errorMessage := h.mapTrackError(err)
		statusCode := h.getErrorStatusCode(err)
		response.Error(c, statusCode, errorMessage)
		return
	}

	// Mettre à jour le message de statut avec le MD5
	if err := h.trackUploadService.UpdateUploadStatus(c.Request.Context(), track.ID, models.TrackStatusUploading, fmt.Sprintf("Upload completed, MD5: %s", md5)); err != nil {
		// Log l'erreur mais ne pas faire échouer la requête
		h.trackService.logger.Error("Failed to update track upload status after completion", zap.Error(err), zap.Any("track_id", track.ID))
	}

	// Déclencher le traitement du streaming
	if h.streamService != nil {
		// FIX #23: Enrichir le contexte avec le request_id pour propagation
		ctx := c.Request.Context()
		if requestID := c.GetString("request_id"); requestID != "" {
			ctx = context.WithValue(ctx, "request_id", requestID)
		}

		if err := h.streamService.StartProcessing(ctx, track.ID, track.FilePath); err != nil {
			// FIX #10: Logger l'erreur avec contexte
			h.trackService.logger.Error("Failed to start stream processing",
				zap.String("track_id", track.ID.String()),
				zap.String("file_path", track.FilePath),
				zap.Error(err),
			)
		} else {
			// h.trackUploadService.UpdateUploadStatus(c.Request.Context(), track.ID, models.TrackStatusProcessing, "Processing audio...")
		}
	}

	response.Created(c, gin.H{
		"message": "upload completed successfully",
		"track":   track,
		"md5":     md5,
	})
}

// mapTrackError mappe les erreurs techniques vers des messages utilisateur
func (h *TrackHandler) mapTrackError(err error) string {
	if err == nil {
		return "unknown error"
	}

	errStr := err.Error()

	// Erreurs de validation
	if strings.Contains(errStr, "invalid track format") || strings.Contains(errStr, "invalid file format") {
		return "Invalid file format. Allowed formats: MP3, FLAC, WAV, OGG"
	}
	if strings.Contains(errStr, "file size exceeds") || strings.Contains(errStr, "too large") {
		return "File size exceeds maximum allowed size of 100MB"
	}
	if strings.Contains(errStr, "file is empty") {
		return "The uploaded file is empty"
	}

	// Erreurs de quota
	if strings.Contains(errStr, "track quota exceeded") {
		return "You have reached the maximum number of tracks allowed"
	}
	if strings.Contains(errStr, "storage quota exceeded") {
		return "You have reached your storage quota. Please delete some tracks to free up space"
	}

	// Erreurs réseau
	if strings.Contains(errStr, "network error") || strings.Contains(errStr, "timeout") || strings.Contains(errStr, "connection") {
		return "Network error occurred. Please try again"
	}

	// Erreurs de stockage
	if strings.Contains(errStr, "storage error") || strings.Contains(errStr, "failed to save file") {
		return "Failed to save file. Please try again"
	}
	if strings.Contains(errStr, "failed to create upload directory") {
		return "Failed to prepare storage. Please try again later"
	}

	// Erreur par défaut
	return "An error occurred during upload. Please try again"
}

// getErrorStatusCode retourne le code de statut HTTP approprié pour une erreur
func (h *TrackHandler) getErrorStatusCode(err error) int {
	if err == nil {
		return http.StatusInternalServerError
	}

	errStr := err.Error()

	// Erreurs de validation -> 400
	if strings.Contains(errStr, "invalid") || strings.Contains(errStr, "too large") || strings.Contains(errStr, "empty") {
		return http.StatusBadRequest
	}

	// Erreurs de quota -> 403
	if strings.Contains(errStr, "quota exceeded") {
		return http.StatusForbidden
	}

	// Erreurs réseau -> 503 (Service Unavailable)
	if strings.Contains(errStr, "network error") || strings.Contains(errStr, "timeout") || strings.Contains(errStr, "connection") {
		return http.StatusServiceUnavailable
	}

	// Erreurs de stockage -> 500
	if strings.Contains(errStr, "storage error") || strings.Contains(errStr, "failed to save") {
		return http.StatusInternalServerError
	}

	// Par défaut
	return http.StatusInternalServerError
}

// GetUploadQuota récupère les informations de quota d'upload pour un utilisateur
// @Summary      Get Upload Quota
// @Description  Get remaining upload quota for the user
// @Tags         Track
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id   path      string  false "User ID (optional, defaults to current user)"
// @Success      200  {object}  response.APIResponse{data=object{quota=object}}
// @Failure      401  {object}  response.APIResponse "Unauthorized"
// @Failure      403  {object}  response.APIResponse "Forbidden"
// @Router       /tracks/quota/{id} [get]
func (h *TrackHandler) GetUploadQuota(c *gin.Context) {
	// Récupérer l'ID utilisateur depuis l'URL ou depuis le contexte d'authentification
	userIDParam := c.Param("id")
	var userID uuid.UUID
	var err error

	if userIDParam == "" || userIDParam == "me" {
		// Si "me" ou vide, utiliser l'utilisateur authentifié
		// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
		var ok bool
		userID, ok = h.getUserID(c)
		if !ok {
			return // Erreur déjà envoyée par getUserID
		}
	} else {
		// Parse UUID
		userID, err = uuid.Parse(userIDParam)
		if err != nil {
			response.BadRequest(c, "invalid user id")
			return
		}
	}

	// Vérifier que l'utilisateur peut accéder à ces informations (soit lui-même, soit admin)
	// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
	authenticatedUserID, ok := h.getUserID(c)
	if !ok {
		return // Erreur déjà envoyée par getUserID
	}

	// Un utilisateur ne peut voir que son propre quota (sauf admin, mais on simplifie pour l'instant)
	if authenticatedUserID != userID {
		response.Forbidden(c, "forbidden: you can only view your own quota")
		return
	}

	// Récupérer le quota
	quota, err := h.trackService.GetUserQuota(c.Request.Context(), userID)
	if err != nil {
		response.InternalServerError(c, "failed to get quota")
		return
	}

	response.Success(c, gin.H{
		"quota": quota,
	})
}

// ResumeUpload récupère l'état d'un upload pour permettre la reprise
// @Summary      Resume Upload
// @Description  Get state of an interrupted upload
// @Tags         Track
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        uploadId path string true "Upload ID"
// @Success      200  {object}  response.APIResponse{data=object{upload_id=string,chunks_received=int}}
// @Failure      404  {object}  response.APIResponse "Upload session not found"
// @Router       /tracks/resume/{uploadId} [get]
func (h *TrackHandler) ResumeUpload(c *gin.Context) {
	// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
	userID, ok := h.getUserID(c)
	if !ok {
		return // Erreur déjà envoyée par getUserID
	}

	uploadID := c.Param("uploadId")
	if uploadID == "" {
		response.BadRequest(c, "upload_id is required")
		return
	}

	// Récupérer l'état de l'upload
	state, err := h.chunkService.GetUploadState(uploadID)
	if err != nil {
		response.NotFound(c, "upload not found")
		return
	}

	// Vérifier que l'upload appartient à l'utilisateur authentifié
	if state.UserID != userID {
		response.Forbidden(c, "forbidden: you can only resume your own uploads")
		return
	}

	response.Success(c, gin.H{
		"upload_id":       state.UploadID,
		"user_id":         state.UserID,
		"total_chunks":    state.TotalChunks,
		"total_size":      state.TotalSize,
		"filename":        state.Filename,
		"chunks_received": state.ChunksReceived,
		"received_count":  state.ReceivedCount,
		"last_chunk":      state.LastChunk,
		"progress":        state.Progress,
		"created_at":      state.CreatedAt,
		"updated_at":      state.UpdatedAt,
	})
}

// ListTracks gère la liste des tracks avec pagination, filtres et tri
// @Summary      List Tracks
// @Description  Get a paginated list of tracks with filters
// @Tags         Track
// @Accept       json
// @Produce      json
// @Param        page       query     int     false  "Page number" default(1)
// @Param        limit      query     int     false  "Items per page" default(20)
// @Param        user_id    query     string  false  "Filter by User ID"
// @Param        genre      query     string  false  "Filter by Genre"
// @Param        format     query     string  false  "Filter by Format"
// @Param        sort_by    query     string  false  "Sort field" default(created_at)
// @Param        sort_order query     string  false  "Sort order (asc/desc)" default(desc)
// @Success      200  {object}  response.APIResponse{data=object{tracks=[]models.Track,pagination=object}}
// @Failure      500  {object}  response.APIResponse "Internal Error"
// @Router       /tracks [get]
func (h *TrackHandler) ListTracks(c *gin.Context) {
	// Récupérer les paramètres de query
	page := c.DefaultQuery("page", "1")
	limit := c.DefaultQuery("limit", "20")
	userIDStr := c.Query("user_id")
	genre := c.Query("genre")
	format := c.Query("format")
	sortBy := c.DefaultQuery("sort_by", "created_at")
	sortOrder := c.DefaultQuery("sort_order", "desc")

	// Parser les paramètres
	var pageInt, limitInt int
	if _, err := fmt.Sscanf(page, "%d", &pageInt); err != nil || pageInt < 1 {
		pageInt = 1
	}
	if _, err := fmt.Sscanf(limit, "%d", &limitInt); err != nil || limitInt < 1 {
		limitInt = 20
	}

	// Construire les paramètres
	params := TrackListParams{
		Page:      pageInt,
		Limit:     limitInt,
		SortBy:    sortBy,
		SortOrder: sortOrder,
	}

	// Parser user_id si fourni
	if userIDStr != "" {
		if uid, err := uuid.Parse(userIDStr); err == nil {
			params.UserID = &uid
		}
	}

	// Parser genre si fourni
	if genre != "" {
		params.Genre = &genre
	}

	// Parser format si fourni
	if format != "" {
		params.Format = &format
	}

	// Appeler le service
	tracks, total, err := h.trackService.ListTracks(c.Request.Context(), params)
	if err != nil {
		response.InternalServerError(c, "failed to list tracks")
		return
	}

	// INT-007: Standardize pagination format
	pagination := handlers.BuildPaginationData(pageInt, limitInt, total)

	// Masquer l'URL de stream pour les utilisateurs non authentifiés
	_, exists := c.Get("user_id")
	if !exists {
		for _, t := range tracks {
			t.StreamManifestURL = ""
		}
	}

	response.Success(c, gin.H{
		"tracks":     tracks,
		"pagination": pagination,
	})
}

// GetTrack gère la récupération d'un track par son ID
// @Summary      Get Track by ID
// @Description  Get detailed information about a track
// @Tags         Track
// @Accept       json
// @Produce      json
// @Param        id   path      string  true  "Track ID"
// @Success      200  {object}  response.APIResponse{data=object{track=models.Track}}
// @Failure      400  {object}  response.APIResponse "Invalid ID"
// @Failure      404  {object}  response.APIResponse "Track not found"
// @Router       /tracks/{id} [get]
func (h *TrackHandler) GetTrack(c *gin.Context) {
	trackIDStr := c.Param("id")
	if trackIDStr == "" {
		response.BadRequest(c, "track id is required")
		return
	}

	// MIGRATION UUID: TrackID is UUID
	trackID, err := uuid.Parse(trackIDStr)
	if err != nil {
		response.BadRequest(c, "invalid track id")
		return
	}

	track, err := h.trackService.GetTrackByID(c.Request.Context(), trackID)
	if err != nil {
		if errors.Is(err, ErrTrackNotFound) || errors.Is(err, gorm.ErrRecordNotFound) {
			response.NotFound(c, "track not found")
			return
		}
		response.InternalServerError(c, "failed to get track")
		return
	}

	// Masquer l'URL de stream pour les utilisateurs non authentifiés
	_, exists := c.Get("user_id")
	if !exists {
		track.StreamManifestURL = ""
	}

	response.Success(c, gin.H{"track": track})
}

// UpdateTrackRequest représente la requête de mise à jour d'un track
// MOD-P1-002: Added validation tags for systematic input validation
type UpdateTrackRequest struct {
	Title    *string `json:"title" binding:"omitempty,min=1,max=255" validate:"omitempty,min=1,max=255"`
	Artist   *string `json:"artist" binding:"omitempty,max=255" validate:"omitempty,max=255"`
	Album    *string `json:"album" binding:"omitempty,max=255" validate:"omitempty,max=255"`
	Genre    *string `json:"genre" binding:"omitempty,max=100" validate:"omitempty,max=100"`
	Year     *int    `json:"year" binding:"omitempty,min=1900,max=2100" validate:"omitempty,min=1900,max=2100"`
	IsPublic *bool   `json:"is_public"`
}

// UpdateTrack gère la mise à jour d'un track
// @Summary      Update Track
// @Description  Update track metadata
// @Tags         Track
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id      path      string              true  "Track ID"
// @Param        track   body      UpdateTrackRequest  true  "Track Metadata"
// @Success      200     {object}  response.APIResponse{data=object{track=models.Track}}
// @Failure      400     {object}  response.APIResponse "Validation Error"
// @Failure      401     {object}  response.APIResponse "Unauthorized"
// @Failure      403     {object}  response.APIResponse "Forbidden"
// @Failure      404     {object}  response.APIResponse "Track not found"
// @Router       /tracks/{id} [put]
func (h *TrackHandler) UpdateTrack(c *gin.Context) {
	// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
	userID, ok := h.getUserID(c)
	if !ok {
		return // Erreur déjà envoyée par getUserID
	}

	trackIDStr := c.Param("id")
	if trackIDStr == "" {
		response.BadRequest(c, "track id is required")
		return
	}

	// MIGRATION UUID: TrackID is UUID
	trackID, err := uuid.Parse(trackIDStr)
	if err != nil {
		response.BadRequest(c, "invalid track id")
		return
	}

	// MOD-P1-002: Utiliser helper centralisé pour bind + validate
	var req UpdateTrackRequest
	if !common.BindAndValidateJSON(c, &req) {
		return // Erreur déjà envoyée au client
	}

	// Convertir la requête en paramètres de service
	params := UpdateTrackParams{
		Title:    req.Title,
		Artist:   req.Artist,
		Album:    req.Album,
		Genre:    req.Genre,
		Year:     req.Year,
		IsPublic: req.IsPublic,
	}

	// MOD-P1-003: Check if user is admin for ownership bypass
	isAdmin := false
	if h.permissionService != nil {
		hasRole, err := h.permissionService.HasRole(c.Request.Context(), userID, "admin")
		if err == nil && hasRole {
			isAdmin = true
		}
	}

	// Pass isAdmin via context
	ctx := context.WithValue(c.Request.Context(), "is_admin", isAdmin)
	track, err := h.trackService.UpdateTrack(ctx, trackID, userID, params)
	if err != nil {
		if errors.Is(err, ErrTrackNotFound) || errors.Is(err, gorm.ErrRecordNotFound) {
			response.NotFound(c, "track not found")
			return
		}
		if errors.Is(err, ErrForbidden) {
			response.Forbidden(c, "forbidden")
			return
		}
		// Erreur de validation (title empty, year negative, etc.)
		if strings.Contains(err.Error(), "cannot be") {
			// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.BadRequest
			h.respondWithError(c, http.StatusBadRequest, err.Error())
			return
		}
		// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.InternalServerError
		h.respondWithError(c, http.StatusInternalServerError, "failed to update track")
		return
	}

	// MOD-P1-RES-001: Utiliser RespondSuccess au lieu de response.Success
	handlers.RespondSuccess(c, http.StatusOK, gin.H{"track": track})
}

// DeleteTrack gère la suppression d'un track
// @Summary      Delete Track
// @Description  Permanently delete a track
// @Tags         Track
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        id   path      string  true  "Track ID"
// @Success      200  {object}  response.APIResponse{data=object{message=string}}
// @Failure      401  {object}  response.APIResponse "Unauthorized"
// @Failure      403  {object}  response.APIResponse "Forbidden"
// @Failure      404  {object}  response.APIResponse "Track not found"
// @Router       /tracks/{id} [delete]
func (h *TrackHandler) DeleteTrack(c *gin.Context) {
	// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
	userID, ok := h.getUserID(c)
	if !ok {
		return // Erreur déjà envoyée par getUserID
	}

	trackIDStr := c.Param("id")
	if trackIDStr == "" {
		// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de c.JSON
		h.respondWithError(c, http.StatusBadRequest, "track id is required")
		return
	}

	// MIGRATION UUID: TrackID is UUID
	trackID, err := uuid.Parse(trackIDStr)
	if err != nil {
		// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.BadRequest
		h.respondWithError(c, http.StatusBadRequest, "invalid track id")
		return
	}

	// MOD-P1-003: Check if user is admin for ownership bypass
	isAdmin := false
	if h.permissionService != nil {
		hasRole, err := h.permissionService.HasRole(c.Request.Context(), userID, "admin")
		if err == nil && hasRole {
			isAdmin = true
		}
	}

	// Pass isAdmin via context
	ctx := context.WithValue(c.Request.Context(), "is_admin", isAdmin)
	err = h.trackService.DeleteTrack(ctx, trackID, userID)
	if err != nil {
		if errors.Is(err, ErrTrackNotFound) || errors.Is(err, gorm.ErrRecordNotFound) {
			// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.NotFound
			h.respondWithError(c, http.StatusNotFound, "track not found")
			return
		}
		if errors.Is(err, ErrForbidden) {
			// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.Forbidden
			h.respondWithError(c, http.StatusForbidden, "forbidden")
			return
		}
		// MOD-P1-RES-001: Utiliser RespondWithAppError au lieu de response.InternalServerError
		h.respondWithError(c, http.StatusInternalServerError, "failed to delete track")
		return
	}

	// MOD-P1-RES-001: Utiliser RespondSuccess au lieu de response.Success
	handlers.RespondSuccess(c, http.StatusOK, gin.H{"message": "track deleted successfully"})
}

// BatchDeleteRequest représente la requête pour supprimer plusieurs tracks
type BatchDeleteRequest struct {
	TrackIDs []string `json:"track_ids" binding:"required" validate:"required,min=1,dive,uuid"`
}

// BatchDeleteTracks gère la suppression en lot de plusieurs tracks
// POST /api/v1/tracks/batch/delete
// BE-API-024: Implement track batch operations validation
// @Summary      Batch Delete Tracks
// @Description  Delete multiple tracks at once
// @Tags         Track
// @Accept       json
// @Produce      json
// @Security     BearerAuth
// @Param        request body BatchDeleteRequest true "List of Track IDs"
// @Success      200  {object}  response.APIResponse{data=object{deleted=[]string,failed=object}}
// @Failure      400  {object}  response.APIResponse "Validation Error"
// @Failure      500  {object}  response.APIResponse "Internal Error"
// @Router       /tracks/batch/delete [post]
func (h *TrackHandler) BatchDeleteTracks(c *gin.Context) {
	// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
	userID, ok := h.getUserID(c)
	if !ok {
		return // Erreur déjà envoyée par getUserID
	}

	// MOD-P1-002: Utiliser helper centralisé pour bind + validate
	var req BatchDeleteRequest
	if !common.BindAndValidateJSON(c, &req) {
		return // Erreur déjà envoyée au client
	}

	// Convertir les IDs en UUIDs
	var trackUUIDs []uuid.UUID
	for _, idStr := range req.TrackIDs {
		if uid, err := uuid.Parse(idStr); err == nil {
			trackUUIDs = append(trackUUIDs, uid)
		}
	}

	// MOD-P1-003: Check if user is admin for ownership bypass
	isAdmin := false
	if h.permissionService != nil {
		hasRole, err := h.permissionService.HasRole(c.Request.Context(), userID, "admin")
		if err == nil && hasRole {
			isAdmin = true
		}
	}

	// Pass isAdmin via context
	ctx := context.WithValue(c.Request.Context(), "is_admin", isAdmin)
	result, err := h.trackService.BatchDeleteTracks(ctx, trackUUIDs, userID)
	if err != nil {
		// Vérifier si c'est une erreur de taille de batch
		if strings.Contains(err.Error(), "batch size exceeds maximum") {
			handlers.RespondWithAppError(c, apperrors.NewValidationError(err.Error()))
			return
		}
		handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to delete tracks", err))
		return
	}

	// BE-API-024: Standardize batch delete response format
	handlers.RespondSuccess(c, http.StatusOK, gin.H{
		"deleted": result.Deleted,
		"failed":  result.Failed,
	})
}

// BatchUpdateRequest représente la requête pour mettre à jour plusieurs tracks
type BatchUpdateRequest struct {
	TrackIDs []string               `json:"track_ids" binding:"required" validate:"required,min=1,dive,uuid"`
	Updates  map[string]interface{} `json:"updates" binding:"required" validate:"required,min=1"`
}

// BatchUpdateTracks gère la mise à jour en lot de plusieurs tracks
// POST /api/v1/tracks/batch/update
// BE-API-024: Implement track batch operations validation
func (h *TrackHandler) BatchUpdateTracks(c *gin.Context) {
	// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
	userID, ok := h.getUserID(c)
	if !ok {
		return // Erreur déjà envoyée par getUserID
	}

	// MOD-P1-002: Utiliser helper centralisé pour bind + validate
	var req BatchUpdateRequest
	if !common.BindAndValidateJSON(c, &req) {
		return // Erreur déjà envoyée au client
	}

	// Convertir les IDs en UUIDs
	var trackUUIDs []uuid.UUID
	for _, idStr := range req.TrackIDs {
		if uid, err := uuid.Parse(idStr); err == nil {
			trackUUIDs = append(trackUUIDs, uid)
		}
	}

	// MOD-P1-003: Check if user is admin for ownership bypass
	isAdmin := false
	if h.permissionService != nil {
		hasRole, err := h.permissionService.HasRole(c.Request.Context(), userID, "admin")
		if err == nil && hasRole {
			isAdmin = true
		}
	}

	// Pass isAdmin via context
	ctx := context.WithValue(c.Request.Context(), "is_admin", isAdmin)
	result, err := h.trackService.BatchUpdateTracks(ctx, trackUUIDs, userID, req.Updates)
	if err != nil {
		// Vérifier si c'est une erreur de validation
		if strings.Contains(err.Error(), "batch size exceeds maximum") ||
			strings.Contains(err.Error(), "cannot be empty") ||
			strings.Contains(err.Error(), "invalid value") ||
			strings.Contains(err.Error(), "exceeds maximum length") ||
			strings.Contains(err.Error(), "must be between") {
			// BE-API-024: Standardize batch update error response format
			handlers.RespondWithAppError(c, apperrors.NewValidationError(err.Error()))
			return
		}
		// BE-API-024: Standardize batch update error response format
		handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to update tracks", err))
		return
	}

	// BE-API-024: Standardize batch update response format
	handlers.RespondSuccess(c, http.StatusOK, gin.H{
		"updated": result.Updated,
		"failed":  result.Failed,
	})
}

// LikeTrack gère l'ajout d'un like sur un track
func (h *TrackHandler) LikeTrack(c *gin.Context) {
	// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
	userID, ok := h.getUserID(c)
	if !ok {
		return // Erreur déjà envoyée par getUserID
	}

	trackIDStr := c.Param("id")
	if trackIDStr == "" {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusBadRequest, "track id is required")
		return
	}

	// MIGRATION UUID: TrackID is UUID
	trackID, err := uuid.Parse(trackIDStr)
	if err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusBadRequest, "invalid track id")
		return
	}

	if err := h.likeService.LikeTrack(c.Request.Context(), userID, trackID); err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		if err.Error() == "track not found" {
			h.respondWithError(c, http.StatusNotFound, "track not found")
			return
		}
		h.respondWithError(c, http.StatusInternalServerError, err.Error())
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "track liked"})
}

// UnlikeTrack gère la suppression d'un like sur un track
func (h *TrackHandler) UnlikeTrack(c *gin.Context) {
	// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
	userID, ok := h.getUserID(c)
	if !ok {
		return // Erreur déjà envoyée par getUserID
	}

	trackIDStr := c.Param("id")
	if trackIDStr == "" {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusBadRequest, "track id is required")
		return
	}

	// MIGRATION UUID: TrackID is UUID
	trackID, err := uuid.Parse(trackIDStr)
	if err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusBadRequest, "invalid track id")
		return
	}

	if err := h.likeService.UnlikeTrack(c.Request.Context(), userID, trackID); err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusInternalServerError, err.Error())
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "track unliked"})
}

// GetTrackLikes gère la récupération du nombre de likes d'un track
func (h *TrackHandler) GetTrackLikes(c *gin.Context) {
	trackIDStr := c.Param("id")
	if trackIDStr == "" {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusBadRequest, "track id is required")
		return
	}

	// MIGRATION UUID: TrackID is UUID
	trackID, err := uuid.Parse(trackIDStr)
	if err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusBadRequest, "invalid track id")
		return
	}

	count, err := h.likeService.GetTrackLikesCount(c.Request.Context(), trackID)
	if err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusInternalServerError, err.Error())
		return
	}

	// Vérifier si l'utilisateur a liké ce track (optionnel)
	var isLiked bool
	if userIDInterface, exists := c.Get("user_id"); exists {
		userID, ok := userIDInterface.(uuid.UUID)
		if ok && userID != uuid.Nil {
			isLiked, _ = h.likeService.IsLiked(c.Request.Context(), userID, trackID)
		}
	}

	c.JSON(http.StatusOK, gin.H{
		"count":    count,
		"is_liked": isLiked,
	})
}

// GetUserLikedTracks gère la récupération des tracks likés par un utilisateur
// GET /api/v1/users/:id/likes
// BE-API-027: Implement user liked tracks endpoint
func (h *TrackHandler) GetUserLikedTracks(c *gin.Context) {
	userIDStr := c.Param("id")
	if userIDStr == "" {
		handlers.RespondWithAppError(c, apperrors.NewValidationError("user id is required"))
		return
	}

	userID, err := uuid.Parse(userIDStr)
	if err != nil {
		handlers.RespondWithAppError(c, apperrors.NewValidationError("invalid user id"))
		return
	}

	// Parse pagination parameters
	limit := 20 // default
	if limitStr := c.Query("limit"); limitStr != "" {
		if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
			// Limiter à un maximum raisonnable
			if parsedLimit > 100 {
				parsedLimit = 100
			}
			limit = parsedLimit
		}
	}

	offset := 0 // default
	if offsetStr := c.Query("offset"); offsetStr != "" {
		if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
			offset = parsedOffset
		}
	}

	tracks, err := h.likeService.GetUserLikedTracks(c.Request.Context(), userID, limit, offset)
	if err != nil {
		handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to get user liked tracks", err))
		return
	}

	total, err := h.likeService.GetUserLikedTracksCount(c.Request.Context(), userID)
	if err != nil {
		handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to get user liked tracks count", err))
		return
	}

	// BE-API-027: Standardize response format
	handlers.RespondSuccess(c, http.StatusOK, gin.H{
		"tracks": tracks,
		"total":  total,
		"limit":  limit,
		"offset": offset,
	})
}

// SearchTracks gère la recherche avancée de tracks
func (h *TrackHandler) SearchTracks(c *gin.Context) {
	if h.searchService == nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusInternalServerError, "search service not available")
		return
	}

	// Récupérer les paramètres de query
	params := services.TrackSearchParams{
		Query:     c.Query("q"),
		TagMode:   c.DefaultQuery("tag_mode", "OR"),
		Page:      1,
		Limit:     20,
		SortBy:    c.DefaultQuery("sort_by", "created_at"),
		SortOrder: c.DefaultQuery("sort_order", "desc"),
	}

	// Parser page
	if pageStr := c.Query("page"); pageStr != "" {
		if page, err := strconv.Atoi(pageStr); err == nil && page > 0 {
			params.Page = page
		}
	}

	// Parser limit
	if limitStr := c.Query("limit"); limitStr != "" {
		if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 {
			params.Limit = limit
		}
	}

	// Parser tags
	if tagsStr := c.Query("tags"); tagsStr != "" {
		params.Tags = strings.Split(tagsStr, ",")
		for i := range params.Tags {
			params.Tags[i] = strings.TrimSpace(params.Tags[i])
		}
	}

	// Parser min_duration
	if minDurationStr := c.Query("min_duration"); minDurationStr != "" {
		if minDuration, err := strconv.Atoi(minDurationStr); err == nil && minDuration >= 0 {
			params.MinDuration = &minDuration
		}
	}

	// Parser max_duration
	if maxDurationStr := c.Query("max_duration"); maxDurationStr != "" {
		if maxDuration, err := strconv.Atoi(maxDurationStr); err == nil && maxDuration >= 0 {
			params.MaxDuration = &maxDuration
		}
	}

	// Parser min_bpm
	if minBPMStr := c.Query("min_bpm"); minBPMStr != "" {
		if minBPM, err := strconv.Atoi(minBPMStr); err == nil && minBPM >= 0 {
			params.MinBPM = &minBPM
		}
	}

	// Parser max_bpm
	if maxBPMStr := c.Query("max_bpm"); maxBPMStr != "" {
		if maxBPM, err := strconv.Atoi(maxBPMStr); err == nil && maxBPM >= 0 {
			params.MaxBPM = &maxBPM
		}
	}

	// Parser genre
	if genre := c.Query("genre"); genre != "" {
		params.Genre = &genre
	}

	// Parser format
	if format := c.Query("format"); format != "" {
		params.Format = &format
	}

	// Parser min_date
	if minDate := c.Query("min_date"); minDate != "" {
		params.MinDate = &minDate
	}

	// Parser max_date
	if maxDate := c.Query("max_date"); maxDate != "" {
		params.MaxDate = &maxDate
	}

	// Effectuer la recherche avec filtres combinés
	tracks, total, err := h.searchService.SearchTracks(c.Request.Context(), params)
	if err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusInternalServerError, "failed to search tracks")
		return
	}

	// Calculer les métadonnées de pagination
	totalPages := (int(total) + params.Limit - 1) / params.Limit
	if totalPages == 0 {
		totalPages = 1
	}

	c.JSON(http.StatusOK, gin.H{
		"tracks": tracks,
		"pagination": gin.H{
			"page":        params.Page,
			"limit":       params.Limit,
			"total":       total,
			"total_pages": totalPages,
		},
	})
}

// DownloadTrack gère le téléchargement d'un track
func (h *TrackHandler) DownloadTrack(c *gin.Context) {
	// Récupérer l'utilisateur s'il est authentifié
	var userID uuid.UUID
	if userIDInterface, exists := c.Get("user_id"); exists {
		if uid, ok := userIDInterface.(uuid.UUID); ok {
			userID = uid
		}
	}

	trackIDStr := c.Param("id")
	if trackIDStr == "" {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusBadRequest, "track id is required")
		return
	}

	// MIGRATION UUID: TrackID is UUID
	trackID, err := uuid.Parse(trackIDStr)
	if err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusBadRequest, "invalid track id")
		return
	}

	// Récupérer le track
	track, err := h.trackService.GetTrackByID(c.Request.Context(), trackID)
	if err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		if errors.Is(err, ErrTrackNotFound) || errors.Is(err, gorm.ErrRecordNotFound) {
			h.respondWithError(c, http.StatusNotFound, "track not found")
			return
		}
		h.respondWithError(c, http.StatusInternalServerError, "failed to get track")
		return
	}

	// Vérifier les permissions via share token si présent
	if shareToken := c.Query("share_token"); shareToken != "" {
		if h.shareService == nil {
			// MOD-P2-003: Utiliser AppError au lieu de gin.H
			h.respondWithError(c, http.StatusInternalServerError, "share service not available")
			return
		}

		share, err := h.shareService.ValidateShareToken(c.Request.Context(), shareToken)
		if err != nil {
			if errors.Is(err, services.ErrShareNotFound) {
				// MOD-P2-003: Utiliser AppError au lieu de gin.H
				h.respondWithError(c, http.StatusForbidden, "invalid share token")
				return
			}
			if errors.Is(err, services.ErrShareExpired) {
				// MOD-P2-003: Utiliser AppError au lieu de gin.H
				h.respondWithError(c, http.StatusForbidden, "share link expired")
				return
			}
			// MOD-P2-003: Utiliser AppError au lieu de gin.H
			h.respondWithError(c, http.StatusInternalServerError, "failed to validate share token")
			return
		}

		// Vérifier que le share correspond au track
		if share.TrackID != trackID {
			// MOD-P2-003: Utiliser AppError au lieu de gin.H
			h.respondWithError(c, http.StatusForbidden, "invalid share token")
			return
		}

		// Vérifier la permission download
		if !h.shareService.CheckPermission(share, "download") {
			// MOD-P2-003: Utiliser AppError au lieu de gin.H
			h.respondWithError(c, http.StatusForbidden, "download not allowed")
			return
		}
	} else {
		// Vérifier les permissions normales (public ou owner)
		if !track.IsPublic && track.UserID != userID {
			// MOD-P2-003: Utiliser AppError au lieu de gin.H
			h.respondWithError(c, http.StatusForbidden, "forbidden")
			return
		}
	}

	// Vérifier que le fichier existe
	if _, err := os.Stat(track.FilePath); os.IsNotExist(err) {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusNotFound, "track file not found")
		return
	}

	// Servir le fichier avec les headers appropriés
	c.Header("Content-Type", getContentType(track.Format))
	c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", track.Title))
	c.File(track.FilePath)
}

// CreateShareRequest représente la requête pour créer un lien de partage
type CreateShareRequest struct {
	Permissions string     `json:"permissions" binding:"required" validate:"required,oneof=read write admin"`
	ExpiresAt   *time.Time `json:"expires_at,omitempty"`
}

// CreateShare crée un nouveau lien de partage pour un track
func (h *TrackHandler) CreateShare(c *gin.Context) {
	// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
	userID, ok := h.getUserID(c)
	if !ok {
		return // Erreur déjà envoyée par getUserID
	}

	trackIDStr := c.Param("id")
	if trackIDStr == "" {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusBadRequest, "track id is required")
		return
	}

	// MIGRATION UUID: TrackID is UUID
	trackID, err := uuid.Parse(trackIDStr)
	if err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusBadRequest, "invalid track id")
		return
	}

	if h.shareService == nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusInternalServerError, "share service not available")
		return
	}

	// MOD-P1-002: Utiliser helper centralisé pour bind + validate
	var req CreateShareRequest
	if !common.BindAndValidateJSON(c, &req) {
		return // Erreur déjà envoyée au client
	}

	share, err := h.shareService.CreateShare(c.Request.Context(), trackID, userID, req.Permissions, req.ExpiresAt)
	if err != nil {
		if errors.Is(err, ErrForbidden) {
			// MOD-P2-003: Utiliser AppError au lieu de gin.H
			h.respondWithError(c, http.StatusForbidden, "forbidden")
			return
		}
		if errors.Is(err, ErrTrackNotFound) {
			// MOD-P2-003: Utiliser AppError au lieu de gin.H
			h.respondWithError(c, http.StatusNotFound, "track not found")
			return
		}
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusInternalServerError, "failed to create share")
		return
	}

	c.JSON(http.StatusOK, gin.H{"share": share})
}

// GetSharedTrack récupère un track via son token de partage
// GET /api/v1/tracks/shared/:token
// BE-API-029: Implement shared track access endpoint validation
func (h *TrackHandler) GetSharedTrack(c *gin.Context) {
	token := c.Param("token")
	if token == "" {
		handlers.RespondWithAppError(c, apperrors.NewValidationError("share token is required"))
		return
	}

	if h.shareService == nil {
		handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "share service not available", nil))
		return
	}

	share, err := h.shareService.ValidateShareToken(c.Request.Context(), token)
	if err != nil {
		if errors.Is(err, services.ErrShareNotFound) {
			handlers.RespondWithAppError(c, apperrors.NewNotFoundError("share"))
			return
		}
		if errors.Is(err, services.ErrShareExpired) {
			handlers.RespondWithAppError(c, apperrors.NewForbiddenError("share link expired"))
			return
		}
		handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to validate share token", err))
		return
	}

	// Récupérer le track
	track, err := h.trackService.GetTrackByID(c.Request.Context(), share.TrackID)
	if err != nil {
		if errors.Is(err, ErrTrackNotFound) || errors.Is(err, gorm.ErrRecordNotFound) {
			handlers.RespondWithAppError(c, apperrors.NewNotFoundError("track"))
			return
		}
		handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to get track", err))
		return
	}

	// BE-API-029: Standardize response format
	handlers.RespondSuccess(c, http.StatusOK, gin.H{
		"track": track,
		"share": share,
	})
}

// RevokeShare révoque un lien de partage
// DELETE /api/v1/tracks/share/:id
// BE-API-028: Implement track share revoke endpoint validation
func (h *TrackHandler) RevokeShare(c *gin.Context) {
	// MOD-P1-RES-003: Utiliser helper fail-secure au lieu de c.MustGet()
	userID, ok := h.getUserID(c)
	if !ok {
		return // Erreur déjà envoyée par getUserID
	}

	shareIDStr := c.Param("id")
	if shareIDStr == "" {
		handlers.RespondWithAppError(c, apperrors.NewValidationError("share id is required"))
		return
	}

	// MIGRATION UUID: ShareID is UUID
	shareID, err := uuid.Parse(shareIDStr)
	if err != nil {
		handlers.RespondWithAppError(c, apperrors.NewValidationError("invalid share id"))
		return
	}

	if h.shareService == nil {
		handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "share service not available", nil))
		return
	}

	err = h.shareService.RevokeShare(c.Request.Context(), shareID, userID)
	if err != nil {
		if errors.Is(err, services.ErrShareNotFound) {
			handlers.RespondWithAppError(c, apperrors.NewNotFoundError("share"))
			return
		}
		if errors.Is(err, services.ErrForbidden) {
			handlers.RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
			return
		}
		handlers.RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "failed to revoke share", err))
		return
	}

	// BE-API-028: Standardize response format
	handlers.RespondSuccess(c, http.StatusOK, gin.H{"message": "share revoked"})
}

// StreamCallbackRequest represents the request for stream status callback
type StreamCallbackRequest struct {
	Status      string `json:"status" binding:"required" validate:"required,oneof=completed failed processing"`
	ManifestURL string `json:"manifest_url" validate:"omitempty,url"`
	Error       string `json:"error"`
}

// HandleStreamCallback handles the callback from stream server
func (h *TrackHandler) HandleStreamCallback(c *gin.Context) {
	trackIDStr := c.Param("id")
	// MIGRATION UUID: TrackID is UUID
	trackID, err := uuid.Parse(trackIDStr)
	if err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusBadRequest, "invalid track id")
		return
	}

	// MOD-P1-002: Utiliser helper centralisé pour bind + validate
	var req StreamCallbackRequest
	if !common.BindAndValidateJSON(c, &req) {
		return // Erreur déjà envoyée au client
	}

	if err := h.trackService.UpdateStreamStatus(c.Request.Context(), trackID, req.Status, req.ManifestURL); err != nil {
		// MOD-P2-003: Utiliser AppError au lieu de gin.H
		h.respondWithError(c, http.StatusInternalServerError, "failed to update stream status")
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "status updated"})
}

// GetTrackStats stub
func (h *TrackHandler) GetTrackStats(c *gin.Context) {
	// MOD-P2-003: Utiliser AppError au lieu de gin.H
	h.respondWithError(c, http.StatusNotImplemented, "Not implemented")
}

// GetTrackHistory stub
func (h *TrackHandler) GetTrackHistory(c *gin.Context) {
	// MOD-P2-003: Utiliser AppError au lieu de gin.H
	h.respondWithError(c, http.StatusNotImplemented, "Not implemented")
}

// getContentType retourne le Content-Type approprié pour un format audio
func getContentType(format string) string {
	switch strings.ToUpper(format) {
	case "MP3":
		return "audio/mpeg"
	case "FLAC":
		return "audio/flac"
	case "WAV":
		return "audio/wav"
	case "OGG":
		return "audio/ogg"
	case "AAC", "M4A":
		return "audio/aac"
	default:
		return "application/octet-stream"
	}
}

// RecordPlayRequest représente la requête pour enregistrer un événement de lecture
// BE-API-019: Implement track play analytics endpoint
type RecordPlayRequest struct {
	PlayTime int `json:"play_time,omitempty" binding:"omitempty,min=0"` // seconds, optional, defaults to 0
}

// RecordPlay enregistre un événement de lecture pour un track
// POST /api/v1/tracks/:id/play
// BE-API-019: Implement track play analytics endpoint
func (h *TrackHandler) RecordPlay(c *gin.Context) {
	if h.playbackAnalyticsService == nil {
		h.respondWithError(c, http.StatusInternalServerError, "playback analytics service not available")
		return
	}

	// Récupérer l'ID du track depuis l'URL
	trackIDStr := c.Param("id")
	trackID, err := uuid.Parse(trackIDStr)
	if err != nil {
		h.respondWithError(c, http.StatusBadRequest, "invalid track id")
		return
	}

	// Récupérer l'ID utilisateur du contexte
	userID, ok := h.getUserID(c)
	if !ok {
		return // Erreur déjà envoyée par getUserID
	}

	// Parser la requête (optionnelle, peut être vide)
	var req RecordPlayRequest
	if c.Request.ContentLength > 0 {
		if err := c.ShouldBindJSON(&req); err != nil {
			// Si le body est présent mais invalide, retourner une erreur
			h.respondWithError(c, http.StatusBadRequest, "invalid request body")
			return
		}
	}

	// Créer un événement d'analytics basique
	// Note: Le service RecordPlayback vérifie que le track existe
	playTime := req.PlayTime
	if playTime < 0 {
		playTime = 0
	}

	analytics := &models.PlaybackAnalytics{
		TrackID:        trackID,
		UserID:         userID,
		PlayTime:       playTime,
		PauseCount:     0,
		SeekCount:      0,
		CompletionRate: 0, // Sera calculé par le service si track.Duration > 0
		StartedAt:      time.Now(),
		EndedAt:        nil,
	}

	// Enregistrer l'événement via le service
	err = h.playbackAnalyticsService.RecordPlayback(c.Request.Context(), analytics)
	if err != nil {
		h.respondWithError(c, http.StatusInternalServerError, "failed to record play event")
		return
	}

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

// RestoreVersion restaure une version spécifique d'un track
// POST /api/v1/tracks/:id/versions/:versionId/restore
// BE-API-014: Implement track versions restore endpoint
func (h *TrackHandler) RestoreVersion(c *gin.Context) {
	if h.versionService == nil {
		h.respondWithError(c, http.StatusInternalServerError, "version service not available")
		return
	}

	// Récupérer l'ID du track depuis l'URL
	trackIDStr := c.Param("id")
	trackID, err := uuid.Parse(trackIDStr)
	if err != nil {
		h.respondWithError(c, http.StatusBadRequest, "invalid track id")
		return
	}

	// Récupérer l'ID de la version depuis l'URL
	versionIDStr := c.Param("versionId")
	versionID, err := uuid.Parse(versionIDStr)
	if err != nil {
		h.respondWithError(c, http.StatusBadRequest, "invalid version id")
		return
	}

	// Récupérer l'ID utilisateur du contexte
	userID, ok := h.getUserID(c)
	if !ok {
		return // Erreur déjà envoyée par getUserID
	}

	// Restaurer la version
	err = h.versionService.RestoreVersion(c.Request.Context(), trackID, versionID, userID)
	if err != nil {
		if errors.Is(err, services.ErrTrackNotFound) {
			h.respondWithError(c, http.StatusNotFound, "track not found")
			return
		}
		if errors.Is(err, services.ErrVersionNotFound) {
			h.respondWithError(c, http.StatusNotFound, "version not found")
			return
		}
		if errors.Is(err, services.ErrForbidden) {
			h.respondWithError(c, http.StatusForbidden, "forbidden: only track owner can restore versions")
			return
		}
		h.respondWithError(c, http.StatusInternalServerError, "failed to restore version")
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "Version restored successfully"})
}
