package education import ( "context" "fmt" "sync" "time" "github.com/google/uuid" "go.uber.org/zap" ) // Tutorial représente un tutoriel vidéo type Tutorial struct { ID string `json:"id"` Title string `json:"title"` Description string `json:"description"` Author string `json:"author"` Category string `json:"category"` Tags []string `json:"tags"` VideoURL string `json:"video_url"` Thumbnail string `json:"thumbnail"` Duration time.Duration `json:"duration"` Quality VideoQuality `json:"quality"` Language string `json:"language"` IsFree bool `json:"is_free"` IsPublished bool `json:"is_published"` Views int64 `json:"views"` Likes int64 `json:"likes"` Dislikes int64 `json:"dislikes"` Rating float64 `json:"rating"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` mu sync.RWMutex } // VideoQuality définit la qualité de la vidéo type VideoQuality string const ( VideoQualityHD VideoQuality = "hd" VideoQuality4K VideoQuality = "4k" VideoQuality8K VideoQuality = "8k" ) // TutorialStep représente une étape dans un tutoriel type TutorialStep struct { ID string `json:"id"` TutorialID string `json:"tutorial_id"` Title string `json:"title"` Description string `json:"description"` Content string `json:"content"` Order int `json:"order"` Timestamp time.Duration `json:"timestamp"` IsFree bool `json:"is_free"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // TutorialComment représente un commentaire sur un tutoriel type TutorialComment struct { ID string `json:"id"` TutorialID string `json:"tutorial_id"` UserID string `json:"user_id"` Username string `json:"username"` Content string `json:"content"` Rating int `json:"rating"` // 1-5 étoiles IsHelpful bool `json:"is_helpful"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // TutorialManager gère les tutoriels vidéo type TutorialManager struct { tutorials map[string]*Tutorial steps map[string][]*TutorialStep comments map[string][]*TutorialComment logger *zap.Logger mu sync.RWMutex } // NewTutorialManager crée un nouveau gestionnaire de tutoriels func NewTutorialManager(logger *zap.Logger) *TutorialManager { return &TutorialManager{ tutorials: make(map[string]*Tutorial), steps: make(map[string][]*TutorialStep), comments: make(map[string][]*TutorialComment), logger: logger, } } // CreateTutorial crée un nouveau tutoriel func (tm *TutorialManager) CreateTutorial(ctx context.Context, title, description, author, category, videoURL, thumbnail, language string, duration time.Duration, quality VideoQuality, isFree bool, tags []string) (*Tutorial, error) { tm.mu.Lock() defer tm.mu.Unlock() tutorialID := uuid.New().String() tutorial := &Tutorial{ ID: tutorialID, Title: title, Description: description, Author: author, Category: category, Tags: tags, VideoURL: videoURL, Thumbnail: thumbnail, Duration: duration, Quality: quality, Language: language, IsFree: isFree, IsPublished: false, Views: 0, Likes: 0, Dislikes: 0, Rating: 0.0, CreatedAt: time.Now(), UpdatedAt: time.Now(), } tm.tutorials[tutorialID] = tutorial tm.logger.Info("Tutoriel créé", zap.String("tutorial_id", tutorialID), zap.String("title", title), zap.String("author", author)) return tutorial, nil } // GetTutorial récupère un tutoriel par son ID func (tm *TutorialManager) GetTutorial(ctx context.Context, tutorialID string) (*Tutorial, error) { tm.mu.RLock() defer tm.mu.RUnlock() tutorial, exists := tm.tutorials[tutorialID] if !exists { return nil, fmt.Errorf("tutoriel non trouvé: %s", tutorialID) } return tutorial, nil } // ListTutorials liste tous les tutoriels disponibles func (tm *TutorialManager) ListTutorials(ctx context.Context, filters map[string]interface{}) ([]*Tutorial, error) { tm.mu.RLock() defer tm.mu.RUnlock() var tutorials []*Tutorial for _, tutorial := range tm.tutorials { // Appliquer les filtres si fournis if filters != nil { if category, ok := filters["category"].(string); ok && tutorial.Category != category { continue } if isPublished, ok := filters["is_published"].(bool); ok && tutorial.IsPublished != isPublished { continue } if isFree, ok := filters["is_free"].(bool); ok && tutorial.IsFree != isFree { continue } if language, ok := filters["language"].(string); ok && tutorial.Language != language { continue } if author, ok := filters["author"].(string); ok && tutorial.Author != author { continue } } tutorials = append(tutorials, tutorial) } return tutorials, nil } // UpdateTutorial met à jour un tutoriel func (tm *TutorialManager) UpdateTutorial(ctx context.Context, tutorialID string, updates map[string]interface{}) (*Tutorial, error) { tm.mu.Lock() defer tm.mu.Unlock() tutorial, exists := tm.tutorials[tutorialID] if !exists { return nil, fmt.Errorf("tutoriel non trouvé: %s", tutorialID) } // Appliquer les mises à jour if title, ok := updates["title"].(string); ok { tutorial.Title = title } if description, ok := updates["description"].(string); ok { tutorial.Description = description } if author, ok := updates["author"].(string); ok { tutorial.Author = author } if category, ok := updates["category"].(string); ok { tutorial.Category = category } if videoURL, ok := updates["video_url"].(string); ok { tutorial.VideoURL = videoURL } if thumbnail, ok := updates["thumbnail"].(string); ok { tutorial.Thumbnail = thumbnail } if duration, ok := updates["duration"].(time.Duration); ok { tutorial.Duration = duration } if quality, ok := updates["quality"].(VideoQuality); ok { tutorial.Quality = quality } if isPublished, ok := updates["is_published"].(bool); ok { tutorial.IsPublished = isPublished } if tags, ok := updates["tags"].([]string); ok { tutorial.Tags = tags } tutorial.UpdatedAt = time.Now() tm.logger.Info("Tutoriel mis à jour", zap.String("tutorial_id", tutorialID), zap.String("title", tutorial.Title)) return tutorial, nil } // DeleteTutorial supprime un tutoriel func (tm *TutorialManager) DeleteTutorial(ctx context.Context, tutorialID string) error { tm.mu.Lock() defer tm.mu.Unlock() if _, exists := tm.tutorials[tutorialID]; !exists { return fmt.Errorf("tutoriel non trouvé: %s", tutorialID) } delete(tm.tutorials, tutorialID) delete(tm.steps, tutorialID) delete(tm.comments, tutorialID) tm.logger.Info("Tutoriel supprimé", zap.String("tutorial_id", tutorialID)) return nil } // AddTutorialStep ajoute une étape à un tutoriel func (tm *TutorialManager) AddTutorialStep(ctx context.Context, tutorialID, title, description, content string, order int, timestamp time.Duration, isFree bool) (*TutorialStep, error) { tm.mu.Lock() defer tm.mu.Unlock() stepID := uuid.New().String() step := &TutorialStep{ ID: stepID, TutorialID: tutorialID, Title: title, Description: description, Content: content, Order: order, Timestamp: timestamp, IsFree: isFree, CreatedAt: time.Now(), UpdatedAt: time.Now(), } tm.steps[tutorialID] = append(tm.steps[tutorialID], step) tm.logger.Info("Étape de tutoriel ajoutée", zap.String("tutorial_id", tutorialID), zap.String("step_id", stepID), zap.String("title", title)) return step, nil } // GetTutorialSteps récupère toutes les étapes d'un tutoriel func (tm *TutorialManager) GetTutorialSteps(ctx context.Context, tutorialID string) ([]*TutorialStep, error) { tm.mu.RLock() defer tm.mu.RUnlock() steps, exists := tm.steps[tutorialID] if !exists { return []*TutorialStep{}, nil } return steps, nil } // AddTutorialComment ajoute un commentaire à un tutoriel func (tm *TutorialManager) AddTutorialComment(ctx context.Context, tutorialID, userID, username, content string, rating int) (*TutorialComment, error) { tm.mu.Lock() defer tm.mu.Unlock() commentID := uuid.New().String() comment := &TutorialComment{ ID: commentID, TutorialID: tutorialID, UserID: userID, Username: username, Content: content, Rating: rating, IsHelpful: false, CreatedAt: time.Now(), UpdatedAt: time.Now(), } tm.comments[tutorialID] = append(tm.comments[tutorialID], comment) // Mettre à jour la note moyenne du tutoriel tm.updateTutorialRating(tutorialID) tm.logger.Info("Commentaire ajouté", zap.String("tutorial_id", tutorialID), zap.String("comment_id", commentID), zap.String("username", username)) return comment, nil } // GetTutorialComments récupère tous les commentaires d'un tutoriel func (tm *TutorialManager) GetTutorialComments(ctx context.Context, tutorialID string) ([]*TutorialComment, error) { tm.mu.RLock() defer tm.mu.RUnlock() comments, exists := tm.comments[tutorialID] if !exists { return []*TutorialComment{}, nil } return comments, nil } // IncrementViews incrémente le nombre de vues d'un tutoriel func (tm *TutorialManager) IncrementViews(ctx context.Context, tutorialID string) error { tm.mu.Lock() defer tm.mu.Unlock() tutorial, exists := tm.tutorials[tutorialID] if !exists { return fmt.Errorf("tutoriel non trouvé: %s", tutorialID) } tutorial.Views++ tutorial.UpdatedAt = time.Now() tm.logger.Debug("Vues incrémentées", zap.String("tutorial_id", tutorialID), zap.Int64("views", tutorial.Views)) return nil } // LikeTutorial ajoute un like à un tutoriel func (tm *TutorialManager) LikeTutorial(ctx context.Context, tutorialID string) error { tm.mu.Lock() defer tm.mu.Unlock() tutorial, exists := tm.tutorials[tutorialID] if !exists { return fmt.Errorf("tutoriel non trouvé: %s", tutorialID) } tutorial.Likes++ tutorial.UpdatedAt = time.Now() tm.logger.Debug("Like ajouté", zap.String("tutorial_id", tutorialID), zap.Int64("likes", tutorial.Likes)) return nil } // DislikeTutorial ajoute un dislike à un tutoriel func (tm *TutorialManager) DislikeTutorial(ctx context.Context, tutorialID string) error { tm.mu.Lock() defer tm.mu.Unlock() tutorial, exists := tm.tutorials[tutorialID] if !exists { return fmt.Errorf("tutoriel non trouvé: %s", tutorialID) } tutorial.Dislikes++ tutorial.UpdatedAt = time.Now() tm.logger.Debug("Dislike ajouté", zap.String("tutorial_id", tutorialID), zap.Int64("dislikes", tutorial.Dislikes)) return nil } // updateTutorialRating met à jour la note moyenne d'un tutoriel func (tm *TutorialManager) updateTutorialRating(tutorialID string) { comments, exists := tm.comments[tutorialID] if !exists || len(comments) == 0 { return } var totalRating int var ratedComments int for _, comment := range comments { if comment.Rating > 0 { totalRating += comment.Rating ratedComments++ } } if ratedComments > 0 { tutorial, exists := tm.tutorials[tutorialID] if exists { tutorial.Rating = float64(totalRating) / float64(ratedComments) tutorial.UpdatedAt = time.Now() } } } // SearchTutorials recherche des tutoriels par mots-clés func (tm *TutorialManager) SearchTutorials(ctx context.Context, query string, filters map[string]interface{}) ([]*Tutorial, error) { tm.mu.RLock() defer tm.mu.RUnlock() var results []*Tutorial query = fmt.Sprintf("%%%s%%", query) // Recherche LIKE for _, tutorial := range tm.tutorials { // Vérifier si le tutoriel correspond à la recherche matches := false if contains(tutorial.Title, query) || contains(tutorial.Description, query) || contains(tutorial.Author, query) { matches = true } // Vérifier les tags for _, tag := range tutorial.Tags { if contains(tag, query) { matches = true break } } if !matches { continue } // Appliquer les filtres si fournis if filters != nil { if category, ok := filters["category"].(string); ok && tutorial.Category != category { continue } if isPublished, ok := filters["is_published"].(bool); ok && tutorial.IsPublished != isPublished { continue } if isFree, ok := filters["is_free"].(bool); ok && tutorial.IsFree != isFree { continue } } results = append(results, tutorial) } return results, nil } // contains vérifie si une chaîne contient une sous-chaîne (insensible à la casse) func contains(s, substr string) bool { return len(s) >= len(substr) && (s == substr || (len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || containsSubstring(s, substr)))) } // containsSubstring vérifie si une chaîne contient une sous-chaîne func containsSubstring(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }