package services import ( "context" "encoding/json" "errors" "fmt" "github.com/google/uuid" "veza-backend-api/internal/models" "veza-backend-api/internal/repositories" "go.uber.org/zap" "gorm.io/gorm" ) // PlaylistVersionService gère les versions de playlists // T0509: Create Playlist Version History type PlaylistVersionService struct { versionRepo repositories.PlaylistVersionRepository playlistRepo repositories.PlaylistRepository playlistTrackRepo repositories.PlaylistTrackRepository logger *zap.Logger } // NewPlaylistVersionService crée un nouveau service de versions de playlists func NewPlaylistVersionService( versionRepo repositories.PlaylistVersionRepository, playlistRepo repositories.PlaylistRepository, playlistTrackRepo repositories.PlaylistTrackRepository, logger *zap.Logger, ) *PlaylistVersionService { if logger == nil { logger = zap.NewNop() } return &PlaylistVersionService{ versionRepo: versionRepo, playlistRepo: playlistRepo, playlistTrackRepo: playlistTrackRepo, logger: logger, } } // SaveVersion sauvegarde une version de la playlist // T0509: Create Playlist Version History // MIGRATION UUID: userID en uuid.UUID, playlistID en uuid.UUID func (s *PlaylistVersionService) SaveVersion(ctx context.Context, playlistID uuid.UUID, userID uuid.UUID, action models.PlaylistVersionAction) (*models.PlaylistVersion, error) { // Récupérer la playlist avec ses tracks playlist, err := s.playlistRepo.GetByIDWithTracks(ctx, playlistID) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New("playlist not found") } return nil, fmt.Errorf("failed to get playlist: %w", err) } // Obtenir le prochain numéro de version versionNumber, err := s.versionRepo.GetNextVersionNumber(ctx, playlistID) if err != nil { return nil, fmt.Errorf("failed to get next version number: %w", err) } // Créer un snapshot des tracks tracksSnapshot, err := s.createTracksSnapshot(ctx, playlistID) if err != nil { s.logger.Warn("Failed to create tracks snapshot", zap.Error(err)) // Continuer même si le snapshot échoue tracksSnapshot = "[]" } // Créer la version (PlaylistVersion utilise UUID) version := &models.PlaylistVersion{ PlaylistID: playlistID, UserID: userID, Version: versionNumber, Action: action, Title: playlist.Title, Description: playlist.Description, IsPublic: playlist.IsPublic, CoverURL: playlist.CoverURL, TracksSnapshot: tracksSnapshot, } if err := s.versionRepo.Create(ctx, version); err != nil { return nil, fmt.Errorf("failed to create version: %w", err) } s.logger.Info("Playlist version saved", zap.String("playlist_id", playlistID.String()), zap.String("user_id", userID.String()), zap.Int("version", versionNumber), zap.String("action", string(action)), ) return version, nil } // createTracksSnapshot crée un snapshot JSON des tracks de la playlist func (s *PlaylistVersionService) createTracksSnapshot(ctx context.Context, playlistID uuid.UUID) (string, error) { // Récupérer les tracks de la playlist playlist, err := s.playlistRepo.GetByIDWithTracks(ctx, playlistID) if err != nil { return "", err } // Créer un snapshot simple avec les IDs et positions type TrackSnapshot struct { TrackID uuid.UUID `json:"track_id"` Position int `json:"position"` } snapshots := make([]TrackSnapshot, 0, len(playlist.Tracks)) for _, track := range playlist.Tracks { snapshots = append(snapshots, TrackSnapshot{ TrackID: track.TrackID, Position: track.Position, }) } // Sérialiser en JSON data, err := json.Marshal(snapshots) if err != nil { return "", fmt.Errorf("failed to marshal tracks snapshot: %w", err) } return string(data), nil } // GetVersions récupère l'historique des versions d'une playlist // T0509: Create Playlist Version History func (s *PlaylistVersionService) GetVersions(ctx context.Context, playlistID uuid.UUID, limit, offset int) ([]*models.PlaylistVersion, int64, error) { return s.versionRepo.GetByPlaylistID(ctx, playlistID, limit, offset) } // GetVersion récupère une version spécifique // T0509: Create Playlist Version History func (s *PlaylistVersionService) GetVersion(ctx context.Context, playlistID uuid.UUID, version int) (*models.PlaylistVersion, error) { return s.versionRepo.GetByVersion(ctx, playlistID, version) } // RestoreVersion restaure une playlist à une version spécifique // T0509: Create Playlist Version History // MIGRATION UUID: userID en uuid.UUID, playlistID en uuid.UUID func (s *PlaylistVersionService) RestoreVersion(ctx context.Context, playlistID uuid.UUID, userID uuid.UUID, version int) (*models.PlaylistVersion, error) { // Récupérer la version à restaurer versionToRestore, err := s.versionRepo.GetByVersion(ctx, playlistID, version) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New("version not found") } return nil, fmt.Errorf("failed to get version: %w", err) } // Récupérer la playlist actuelle playlist, err := s.playlistRepo.GetByID(ctx, playlistID) if err != nil { return nil, fmt.Errorf("failed to get playlist: %w", err) } // Restaurer les propriétés de la playlist playlist.Title = versionToRestore.Title playlist.Description = versionToRestore.Description playlist.IsPublic = versionToRestore.IsPublic playlist.CoverURL = versionToRestore.CoverURL if err := s.playlistRepo.Update(ctx, playlist); err != nil { return nil, fmt.Errorf("failed to update playlist: %w", err) } // Restaurer les tracks si le snapshot existe if versionToRestore.TracksSnapshot != "" { if err := s.restoreTracksFromSnapshot(ctx, playlistID, versionToRestore.TracksSnapshot); err != nil { s.logger.Warn("Failed to restore tracks from snapshot", zap.Error(err)) // Ne pas échouer la restauration si les tracks ne peuvent pas être restaurés } } // Créer une nouvelle version pour la restauration restoredVersion, err := s.SaveVersion(ctx, playlistID, userID, models.PlaylistVersionActionRestored) if err != nil { s.logger.Warn("Failed to save restored version", zap.Error(err)) // Retourner quand même la version restaurée return versionToRestore, nil } s.logger.Info("Playlist version restored", zap.String("playlist_id", playlistID.String()), zap.String("user_id", userID.String()), zap.Int("restored_version", version), zap.Int("new_version", restoredVersion.Version), ) return restoredVersion, nil } // restoreTracksFromSnapshot restaure les tracks depuis un snapshot func (s *PlaylistVersionService) restoreTracksFromSnapshot(_ context.Context, playlistID uuid.UUID, snapshot string) error { type TrackSnapshot struct { TrackID uuid.UUID `json:"track_id"` Position int `json:"position"` } var snapshots []TrackSnapshot if err := json.Unmarshal([]byte(snapshot), &snapshots); err != nil { return fmt.Errorf("failed to unmarshal tracks snapshot: %w", err) } // Supprimer tous les tracks actuels // Note: Cette opération peut être coûteuse, mais nécessaire pour une restauration complète // Dans une implémentation optimisée, on pourrait comparer et ne modifier que ce qui a changé // Pour l'instant, on ne restaure pas automatiquement les tracks car cela nécessite // de supprimer tous les tracks existants et de les recréer, ce qui peut être risqué // Cette fonctionnalité peut être ajoutée plus tard si nécessaire s.logger.Info("Tracks snapshot restoration skipped (not implemented)", zap.String("playlist_id", playlistID.String()), zap.Int("tracks_count", len(snapshots)), ) return nil }