package repository import ( "context" "fmt" "github.com/prabhupadasays/spsays-bff/internal/model" ) // maxMentionsPerPost caps how many users a post can tag. const maxMentionsPerPost = 30 // MentionRepo handles post_mentions membership + mention suggestions. type MentionRepo struct{ pg *Postgres } func NewMentionRepo(pg *Postgres) *MentionRepo { return &MentionRepo{pg: pg} } // Suggest returns up to `limit` users to mention, prioritised: people the // viewer follows (rank 0), then people who follow the viewer (rank 1), then // everyone else (rank 2). Excludes the viewer. Optional name/handle search. // Offset paginates for the infinite-scroll popover. func (r *MentionRepo) Suggest(ctx context.Context, viewerID, q string, limit, offset int) ([]model.UserSummary, error) { like := "%" + q + "%" rows, err := r.pg.Pool.Query(ctx, ` SELECT u.id, u.handle, u.name, u.avatar_url, CASE WHEN EXISTS (SELECT 1 FROM follows f WHERE f.follower_id = $1 AND f.following_id = u.id AND f.status = 'accepted') THEN 0 WHEN EXISTS (SELECT 1 FROM follows f WHERE f.follower_id = u.id AND f.following_id = $1 AND f.status = 'accepted') THEN 1 ELSE 2 END AS rank FROM users u WHERE u.id <> $1 AND ($2 = '' OR u.name ILIKE $3 OR u.handle ILIKE $3) ORDER BY rank, u.name, u.id LIMIT $4 OFFSET $5`, viewerID, q, like, limit, offset, ) if err != nil { return nil, fmt.Errorf("suggest mentions: %w", err) } defer rows.Close() out := make([]model.UserSummary, 0, limit) for rows.Next() { var s model.UserSummary var rank int if err := rows.Scan(&s.ID, &s.Handle, &s.Name, &s.AvatarURL, &rank); err != nil { return nil, fmt.Errorf("scan suggestion: %w", err) } out = append(out, s) } return out, rows.Err() } // AddMany tags the given users on a post (idempotent, capped). Used on create. func (r *MentionRepo) AddMany(ctx context.Context, postID string, userIDs []string) error { if len(userIDs) > maxMentionsPerPost { userIDs = userIDs[:maxMentionsPerPost] } for _, uid := range userIDs { if _, err := r.pg.Pool.Exec(ctx, `INSERT INTO post_mentions (post_id, user_id) VALUES ($1, $2) ON CONFLICT (post_id, user_id) DO NOTHING`, postID, uid, ); err != nil { return fmt.Errorf("add mention: %w", err) } } return nil } // Add tags a single user (idempotent), enforcing the per-post cap. func (r *MentionRepo) Add(ctx context.Context, postID, userID string) error { var n int if err := r.pg.Pool.QueryRow(ctx, `SELECT count(*) FROM post_mentions WHERE post_id = $1`, postID, ).Scan(&n); err != nil { return fmt.Errorf("count mentions: %w", err) } if n >= maxMentionsPerPost { return fmt.Errorf("a post can mention at most %d people", maxMentionsPerPost) } if _, err := r.pg.Pool.Exec(ctx, `INSERT INTO post_mentions (post_id, user_id) VALUES ($1, $2) ON CONFLICT (post_id, user_id) DO NOTHING`, postID, userID, ); err != nil { return fmt.Errorf("add mention: %w", err) } return nil } // Remove untags a user from a post. func (r *MentionRepo) Remove(ctx context.Context, postID, userID string) error { if _, err := r.pg.Pool.Exec(ctx, `DELETE FROM post_mentions WHERE post_id = $1 AND user_id = $2`, postID, userID, ); err != nil { return fmt.Errorf("remove mention: %w", err) } return nil } // ListForPost returns the users tagged on a post (insertion order). func (r *MentionRepo) ListForPost(ctx context.Context, postID string) ([]model.UserSummary, error) { rows, err := r.pg.Pool.Query(ctx, ` SELECT u.id, u.handle, u.name, u.avatar_url FROM post_mentions m JOIN users u ON u.id = m.user_id WHERE m.post_id = $1 ORDER BY m.created_at ASC`, postID) if err != nil { return nil, fmt.Errorf("list mentions: %w", err) } defer rows.Close() out := []model.UserSummary{} for rows.Next() { var s model.UserSummary if err := rows.Scan(&s.ID, &s.Handle, &s.Name, &s.AvatarURL); err != nil { return nil, fmt.Errorf("scan mention: %w", err) } out = append(out, s) } return out, rows.Err() } // HydrateCounts fills MentionsCount on the given posts (one grouped query). func (r *MentionRepo) HydrateCounts(ctx context.Context, posts []*model.Post) error { ids := make([]string, 0, len(posts)) for _, p := range posts { if p != nil { ids = append(ids, p.ID) } } if len(ids) == 0 { return nil } rows, err := r.pg.Pool.Query(ctx, `SELECT post_id, count(*) FROM post_mentions WHERE post_id = ANY($1) GROUP BY post_id`, ids) if err != nil { return fmt.Errorf("hydrate mention counts: %w", err) } counts := make(map[string]int, len(ids)) for rows.Next() { var id string var c int if err := rows.Scan(&id, &c); err != nil { rows.Close() return fmt.Errorf("scan mention count: %w", err) } counts[id] = c } rows.Close() if err := rows.Err(); err != nil { return err } for _, p := range posts { if p != nil { p.MentionsCount = counts[p.ID] } } return nil }