package services import ( "context" "database/sql" "fmt" "github.com/google/uuid" "veza-backend-api/internal/database" "go.uber.org/zap" ) // SocialService handles social features (follows, likes, comments) type SocialService struct { db *database.Database logger *zap.Logger } // Comment represents a comment on a track type Comment struct { ID uuid.UUID `json:"id" db:"id"` UserID uuid.UUID `json:"user_id" db:"user_id"` TrackID uuid.UUID `json:"track_id" db:"track_id"` ParentID *uuid.UUID `json:"parent_id" db:"parent_id"` Content string `json:"content" db:"content"` CreatedAt string `json:"created_at" db:"created_at"` UpdatedAt string `json:"updated_at" db:"updated_at"` } // NewSocialService creates a new social service func NewSocialService(db *database.Database, logger *zap.Logger) *SocialService { return &SocialService{ db: db, logger: logger, } } // FollowUser creates a follow relationship func (ss *SocialService) FollowUser(ctx context.Context, followerID, followedID uuid.UUID) error { _, err := ss.db.ExecContext(ctx, ` INSERT INTO follows (follower_id, followed_id) VALUES ($1, $2) ON CONFLICT (follower_id, followed_id) DO NOTHING `, followerID, followedID) if err != nil { return fmt.Errorf("failed to follow user: %w", err) } ss.logger.Info("User followed", zap.String("follower_id", followerID.String()), zap.String("followed_id", followedID.String()), ) return nil } // UnfollowUser removes a follow relationship func (ss *SocialService) UnfollowUser(ctx context.Context, followerID, followedID uuid.UUID) error { _, err := ss.db.ExecContext(ctx, ` DELETE FROM follows WHERE follower_id = $1 AND followed_id = $2 `, followerID, followedID) if err != nil { return fmt.Errorf("failed to unfollow user: %w", err) } return nil } // LikeTrack creates a like on a track func (ss *SocialService) LikeTrack(userID, trackID uuid.UUID) error { ctx := context.Background() _, err := ss.db.ExecContext(ctx, ` INSERT INTO likes (user_id, track_id) VALUES ($1, $2) ON CONFLICT (user_id, track_id) DO NOTHING `, userID, trackID) if err != nil { return fmt.Errorf("failed to like track: %w", err) } return nil } // UnlikeTrack removes a like from a track func (ss *SocialService) UnlikeTrack(userID, trackID uuid.UUID) error { ctx := context.Background() _, err := ss.db.ExecContext(ctx, ` DELETE FROM likes WHERE user_id = $1 AND track_id = $2 `, userID, trackID) if err != nil { return fmt.Errorf("failed to unlike track: %w", err) } return nil } // CreateComment creates a comment on a track func (ss *SocialService) CreateComment(userID, trackID uuid.UUID, content string, parentID *uuid.UUID) (*Comment, error) { ctx := context.Background() var commentID uuid.UUID err := ss.db.QueryRowContext(ctx, ` INSERT INTO comments (id, user_id, track_id, parent_id, content) VALUES (gen_random_uuid(), $1, $2, $3, $4) RETURNING id `, userID, trackID, parentID, content).Scan(&commentID) if err != nil { return nil, fmt.Errorf("failed to create comment: %w", err) } // Fetch and return the created comment var comment Comment err = ss.db.QueryRowContext(ctx, ` SELECT id, user_id, track_id, parent_id, content, created_at, updated_at FROM comments WHERE id = $1 `, commentID).Scan( &comment.ID, &comment.UserID, &comment.TrackID, &comment.ParentID, &comment.Content, &comment.CreatedAt, &comment.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("failed to fetch comment: %w", err) } return &comment, nil } // GetFollowersCount returns the number of followers for a user func (ss *SocialService) GetFollowersCount(userID uuid.UUID) (int, error) { ctx := context.Background() var count int err := ss.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM follows WHERE followed_id = $1 `, userID).Scan(&count) if err != nil { return 0, fmt.Errorf("failed to get followers count: %w", err) } return count, nil } // GetFollowingCount returns the number of users being followed func (ss *SocialService) GetFollowingCount(userID uuid.UUID) (int, error) { ctx := context.Background() var count int err := ss.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM follows WHERE follower_id = $1 `, userID).Scan(&count) if err != nil { return 0, fmt.Errorf("failed to get following count: %w", err) } return count, nil } // GetLikesCount returns the number of likes for a track func (ss *SocialService) GetLikesCount(trackID uuid.UUID) (int, error) { ctx := context.Background() var count int err := ss.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM likes WHERE track_id = $1 `, trackID).Scan(&count) if err != nil { return 0, fmt.Errorf("failed to get likes count: %w", err) } return count, nil } // IsFollowing checks if a user is following another user func (ss *SocialService) IsFollowing(followerID, followedID uuid.UUID) (bool, error) { ctx := context.Background() var exists bool err := ss.db.QueryRowContext(ctx, ` SELECT EXISTS( SELECT 1 FROM follows WHERE follower_id = $1 AND followed_id = $2 ) `, followerID, followedID).Scan(&exists) if err != nil { if err == sql.ErrNoRows { return false, nil } return false, fmt.Errorf("failed to check follow status: %w", err) } return exists, nil } // IsTrackLiked checks if a user has liked a track func (ss *SocialService) IsTrackLiked(userID, trackID uuid.UUID) (bool, error) { ctx := context.Background() var exists bool err := ss.db.QueryRowContext(ctx, ` SELECT EXISTS( SELECT 1 FROM likes WHERE user_id = $1 AND track_id = $2 ) `, userID, trackID).Scan(&exists) if err != nil { if err == sql.ErrNoRows { return false, nil } return false, fmt.Errorf("failed to check like status: %w", err) } return exists, nil } // BlockUser creates a block relationship between users // BE-API-018: Implement user block/unblock endpoints func (ss *SocialService) BlockUser(blockerID, blockedID uuid.UUID) error { ctx := context.Background() // Vérifier qu'on ne peut pas se bloquer soi-même if blockerID == blockedID { return fmt.Errorf("cannot block yourself") } _, err := ss.db.ExecContext(ctx, ` INSERT INTO user_blocks (blocker_id, blocked_id) VALUES ($1, $2) ON CONFLICT (blocker_id, blocked_id) DO NOTHING `, blockerID, blockedID) if err != nil { return fmt.Errorf("failed to block user: %w", err) } ss.logger.Info("User blocked", zap.String("blocker_id", blockerID.String()), zap.String("blocked_id", blockedID.String()), ) return nil } // UnblockUser removes a block relationship between users // BE-API-018: Implement user block/unblock endpoints func (ss *SocialService) UnblockUser(blockerID, blockedID uuid.UUID) error { ctx := context.Background() _, err := ss.db.ExecContext(ctx, ` DELETE FROM user_blocks WHERE blocker_id = $1 AND blocked_id = $2 `, blockerID, blockedID) if err != nil { return fmt.Errorf("failed to unblock user: %w", err) } ss.logger.Info("User unblocked", zap.String("blocker_id", blockerID.String()), zap.String("blocked_id", blockedID.String()), ) return nil } // IsBlocked checks if a user is blocked by another user // BE-API-018: Helper method to check block status func (ss *SocialService) IsBlocked(blockerID, blockedID uuid.UUID) (bool, error) { ctx := context.Background() var exists bool err := ss.db.QueryRowContext(ctx, ` SELECT EXISTS( SELECT 1 FROM user_blocks WHERE blocker_id = $1 AND blocked_id = $2 ) `, blockerID, blockedID).Scan(&exists) if err != nil { if err == sql.ErrNoRows { return false, nil } return false, fmt.Errorf("failed to check block status: %w", err) } return exists, nil } // SuggestionUser is a minimal user for follow suggestions (v0.10.0 F211) type SuggestionUser struct { ID uuid.UUID `json:"id"` Username string `json:"username"` AvatarURL string `json:"avatar_url"` FollowersCount int `json:"followers_count"` } // GetFollowSuggestions returns users to follow based on "friends of friends" (v0.10.0 F211). // No ML - simple 2-hop: users followed by people the current user follows. func (ss *SocialService) GetFollowSuggestions(ctx context.Context, userID uuid.UUID, limit int) ([]SuggestionUser, error) { if limit <= 0 { limit = 10 } if limit > 20 { limit = 20 } rows, err := ss.db.QueryContext(ctx, ` SELECT DISTINCT u.id, u.username, COALESCE(u.avatar, '') as avatar, COALESCE(up.follower_count, 0) as follower_count FROM follows f1 JOIN follows f2 ON f2.follower_id = f1.followed_id JOIN users u ON u.id = f2.followed_id AND u.deleted_at IS NULL LEFT JOIN user_profiles up ON up.user_id = u.id WHERE f1.follower_id = $1 AND f2.followed_id != $1 AND f2.followed_id NOT IN (SELECT followed_id FROM follows WHERE follower_id = $1) LIMIT $2 `, userID, userID, limit) if err != nil { return nil, fmt.Errorf("failed to get follow suggestions: %w", err) } defer rows.Close() var result []SuggestionUser for rows.Next() { var u SuggestionUser var avatar string if err := rows.Scan(&u.ID, &u.Username, &avatar, &u.FollowersCount); err != nil { continue } u.AvatarURL = avatar result = append(result, u) } return result, nil }