package services import ( "context" "errors" "fmt" "github.com/google/uuid" "veza-backend-api/internal/models" "go.uber.org/zap" "gorm.io/gorm" ) // PlaylistFollowService gère les opérations sur les follows de playlists // T0489: Create Playlist Follow Feature type PlaylistFollowService struct { db *gorm.DB logger *zap.Logger } // NewPlaylistFollowService crée un nouveau service de follows de playlists func NewPlaylistFollowService(db *gorm.DB, logger *zap.Logger) *PlaylistFollowService { if logger == nil { logger = zap.NewNop() } return &PlaylistFollowService{ db: db, logger: logger, } } // FollowPlaylist ajoute un follow d'un utilisateur sur une playlist // MIGRATION UUID: Completée. userID et playlistID sont des UUIDs. func (s *PlaylistFollowService) FollowPlaylist(ctx context.Context, userID uuid.UUID, playlistID uuid.UUID) error { // Vérifier si la playlist existe var playlist models.Playlist if err := s.db.WithContext(ctx).First(&playlist, "id = ?", playlistID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("playlist not found") } return fmt.Errorf("failed to check playlist: %w", err) } // Vérifier si l'utilisateur est le propriétaire (ne peut pas suivre sa propre playlist) if playlist.UserID == userID { return errors.New("cannot follow own playlist") } // Vérifier si l'utilisateur suit déjà cette playlist var existing models.PlaylistFollow if err := s.db.WithContext(ctx).Where("user_id = ? AND playlist_id = ? AND deleted_at IS NULL", userID, playlistID).First(&existing).Error; err == nil { // Déjà suivi, retourner nil (idempotent) return nil } else if !errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("failed to check existing follow: %w", err) } // Créer le follow follow := models.PlaylistFollow{ UserID: userID, PlaylistID: playlistID, } if err := s.db.WithContext(ctx).Create(&follow).Error; err != nil { return fmt.Errorf("failed to create follow: %w", err) } // Mettre à jour le compteur de followers de la playlist if err := s.db.WithContext(ctx).Model(&playlist).UpdateColumn("follower_count", gorm.Expr("follower_count + ?", 1)).Error; err != nil { s.logger.Warn("Failed to update playlist follower_count", zap.String("playlist_id", playlistID.String()), zap.Error(err), ) // Ne pas retourner l'erreur, le follow a été créé avec succès } s.logger.Info("Playlist followed", zap.String("user_id", userID.String()), zap.String("playlist_id", playlistID.String()), ) return nil } // UnfollowPlaylist supprime un follow d'un utilisateur sur une playlist // MIGRATION UUID: Completée. userID et playlistID sont des UUIDs. func (s *PlaylistFollowService) UnfollowPlaylist(ctx context.Context, userID uuid.UUID, playlistID uuid.UUID) error { // Vérifier si le follow existe var follow models.PlaylistFollow if err := s.db.WithContext(ctx).Where("user_id = ? AND playlist_id = ? AND deleted_at IS NULL", userID, playlistID).First(&follow).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { // Pas de follow à supprimer, retourner nil (idempotent) return nil } return fmt.Errorf("failed to check follow: %w", err) } // Supprimer le follow (soft delete) if err := s.db.WithContext(ctx).Delete(&follow).Error; err != nil { return fmt.Errorf("failed to delete follow: %w", err) } // Mettre à jour le compteur de followers de la playlist var playlist models.Playlist if err := s.db.WithContext(ctx).First(&playlist, "id = ?", playlistID).Error; err == nil { // Use CASE expression for SQLite compatibility (GREATEST is not supported in SQLite) if err := s.db.WithContext(ctx).Model(&playlist).UpdateColumn("follower_count", gorm.Expr("CASE WHEN follower_count - 1 < 0 THEN 0 ELSE follower_count - 1 END")).Error; err != nil { s.logger.Warn("Failed to update playlist follower_count", zap.String("playlist_id", playlistID.String()), zap.Error(err), ) // Ne pas retourner l'erreur, le follow a été supprimé avec succès } } s.logger.Info("Playlist unfollowed", zap.String("user_id", userID.String()), zap.String("playlist_id", playlistID.String()), ) return nil } // IsFollowing vérifie si un utilisateur suit une playlist // MIGRATION UUID: Completée. userID et playlistID sont des UUIDs. func (s *PlaylistFollowService) IsFollowing(ctx context.Context, userID uuid.UUID, playlistID uuid.UUID) (bool, error) { var count int64 err := s.db.WithContext(ctx).Model(&models.PlaylistFollow{}). Where("user_id = ? AND playlist_id = ? AND deleted_at IS NULL", userID, playlistID). Count(&count).Error if err != nil { return false, fmt.Errorf("failed to check follow: %w", err) } return count > 0, nil } // GetPlaylistFollowersCount retourne le nombre de followers d'une playlist func (s *PlaylistFollowService) GetPlaylistFollowersCount(ctx context.Context, playlistID uuid.UUID) (int64, error) { var count int64 err := s.db.WithContext(ctx).Model(&models.PlaylistFollow{}). Where("playlist_id = ? AND deleted_at IS NULL", playlistID). Count(&count).Error if err != nil { return 0, fmt.Errorf("failed to get followers count: %w", err) } return count, nil } // GetFollowedPlaylists retourne toutes les playlists suivies par un utilisateur // T0498: Create Playlist Recommendations func (s *PlaylistFollowService) GetFollowedPlaylists(ctx context.Context, userID uuid.UUID) ([]*models.Playlist, error) { var playlists []*models.Playlist err := s.db.WithContext(ctx). Joins("INNER JOIN playlist_follows ON playlist_follows.playlist_id = playlists.id"). Where("playlist_follows.user_id = ? AND playlist_follows.deleted_at IS NULL", userID). Preload("User"). Preload("Tracks"). Preload("Tracks.Track"). Find(&playlists).Error if err != nil { return nil, fmt.Errorf("failed to get followed playlists: %w", err) } return playlists, nil }