package analytics import ( "context" "errors" "strings" "tercul/internal/domain" "tercul/internal/jobs/linguistics" "tercul/internal/platform/log" "time" ) type Service interface { IncrementWorkViews(ctx context.Context, workID uint) error IncrementWorkLikes(ctx context.Context, workID uint) error IncrementWorkComments(ctx context.Context, workID uint) error IncrementWorkBookmarks(ctx context.Context, workID uint) error IncrementWorkShares(ctx context.Context, workID uint) error IncrementWorkTranslationCount(ctx context.Context, workID uint) error IncrementTranslationViews(ctx context.Context, translationID uint) error IncrementTranslationLikes(ctx context.Context, translationID uint) error IncrementTranslationComments(ctx context.Context, translationID uint) error IncrementTranslationShares(ctx context.Context, translationID uint) error GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) UpdateWorkReadingTime(ctx context.Context, workID uint) error UpdateWorkComplexity(ctx context.Context, workID uint) error UpdateWorkSentiment(ctx context.Context, workID uint) error UpdateTranslationReadingTime(ctx context.Context, translationID uint) error UpdateTranslationSentiment(ctx context.Context, translationID uint) error UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error UpdateTrending(ctx context.Context) error } type service struct { repo domain.AnalyticsRepository analysisRepo linguistics.AnalysisRepository translationRepo domain.TranslationRepository sentimentProvider linguistics.SentimentProvider } func NewService(repo domain.AnalyticsRepository, analysisRepo linguistics.AnalysisRepository, translationRepo domain.TranslationRepository, sentimentProvider linguistics.SentimentProvider) Service { return &service{ repo: repo, analysisRepo: analysisRepo, translationRepo: translationRepo, sentimentProvider: sentimentProvider, } } func (s *service) IncrementWorkViews(ctx context.Context, workID uint) error { return s.repo.IncrementWorkCounter(ctx, workID, "views", 1) } func (s *service) IncrementWorkLikes(ctx context.Context, workID uint) error { return s.repo.IncrementWorkCounter(ctx, workID, "likes", 1) } func (s *service) IncrementWorkComments(ctx context.Context, workID uint) error { return s.repo.IncrementWorkCounter(ctx, workID, "comments", 1) } func (s *service) IncrementWorkBookmarks(ctx context.Context, workID uint) error { return s.repo.IncrementWorkCounter(ctx, workID, "bookmarks", 1) } func (s *service) IncrementWorkShares(ctx context.Context, workID uint) error { return s.repo.IncrementWorkCounter(ctx, workID, "shares", 1) } func (s *service) IncrementWorkTranslationCount(ctx context.Context, workID uint) error { return s.repo.IncrementWorkCounter(ctx, workID, "translation_count", 1) } func (s *service) IncrementTranslationViews(ctx context.Context, translationID uint) error { return s.repo.IncrementTranslationCounter(ctx, translationID, "views", 1) } func (s *service) IncrementTranslationLikes(ctx context.Context, translationID uint) error { return s.repo.IncrementTranslationCounter(ctx, translationID, "likes", 1) } func (s *service) IncrementTranslationComments(ctx context.Context, translationID uint) error { return s.repo.IncrementTranslationCounter(ctx, translationID, "comments", 1) } func (s *service) IncrementTranslationShares(ctx context.Context, translationID uint) error { return s.repo.IncrementTranslationCounter(ctx, translationID, "shares", 1) } func (s *service) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) { return s.repo.GetOrCreateWorkStats(ctx, workID) } func (s *service) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) { return s.repo.GetOrCreateTranslationStats(ctx, translationID) } func (s *service) UpdateWorkReadingTime(ctx context.Context, workID uint) error { stats, err := s.repo.GetOrCreateWorkStats(ctx, workID) if err != nil { return err } textMetadata, _, _, err := s.analysisRepo.GetAnalysisData(ctx, workID) if err != nil { return err } if textMetadata == nil { return errors.New("text metadata not found") } readingTime := 0 if textMetadata.WordCount > 0 { readingTime = (textMetadata.WordCount + 199) / 200 // Ceil division } stats.ReadingTime = readingTime return s.repo.UpdateWorkStats(ctx, workID, *stats) } func (s *service) UpdateWorkComplexity(ctx context.Context, workID uint) error { stats, err := s.repo.GetOrCreateWorkStats(ctx, workID) if err != nil { return err } _, readabilityScore, _, err := s.analysisRepo.GetAnalysisData(ctx, workID) if err != nil { log.LogWarn("could not get readability score for work", log.F("workID", workID), log.F("error", err)) return nil } if readabilityScore == nil { return errors.New("readability score not found") } stats.Complexity = readabilityScore.Score return s.repo.UpdateWorkStats(ctx, workID, *stats) } func (s *service) UpdateWorkSentiment(ctx context.Context, workID uint) error { stats, err := s.repo.GetOrCreateWorkStats(ctx, workID) if err != nil { return err } _, _, languageAnalysis, err := s.analysisRepo.GetAnalysisData(ctx, workID) if err != nil { log.LogWarn("could not get language analysis for work", log.F("workID", workID), log.F("error", err)) return nil } if languageAnalysis == nil { return errors.New("language analysis not found") } sentiment, ok := languageAnalysis.Analysis["sentiment"].(float64) if !ok { return errors.New("sentiment score not found in language analysis") } stats.Sentiment = sentiment return s.repo.UpdateWorkStats(ctx, workID, *stats) } func (s *service) UpdateTranslationReadingTime(ctx context.Context, translationID uint) error { stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID) if err != nil { return err } translation, err := s.translationRepo.GetByID(ctx, translationID) if err != nil { return err } if translation == nil { return errors.New("translation not found") } wordCount := len(strings.Fields(translation.Content)) readingTime := 0 if wordCount > 0 { readingTime = (wordCount + 199) / 200 // Ceil division } stats.ReadingTime = readingTime return s.repo.UpdateTranslationStats(ctx, translationID, *stats) } func (s *service) UpdateTranslationSentiment(ctx context.Context, translationID uint) error { stats, err := s.repo.GetOrCreateTranslationStats(ctx, translationID) if err != nil { return err } translation, err := s.translationRepo.GetByID(ctx, translationID) if err != nil { return err } if translation == nil { return errors.New("translation not found") } sentiment, err := s.sentimentProvider.Score(translation.Content, translation.Language) if err != nil { return err } stats.Sentiment = sentiment return s.repo.UpdateTranslationStats(ctx, translationID, *stats) } func (s *service) UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error { today := time.Now().UTC().Truncate(24 * time.Hour) engagement, err := s.repo.GetOrCreateUserEngagement(ctx, userID, today) if err != nil { return err } switch eventType { case "work_read": engagement.WorksRead++ case "comment_made": engagement.CommentsMade++ case "like_given": engagement.LikesGiven++ case "bookmark_made": engagement.BookmarksMade++ case "translation_made": engagement.TranslationsMade++ default: return errors.New("invalid engagement event type") } return s.repo.UpdateUserEngagement(ctx, engagement) } func (s *service) UpdateTrending(ctx context.Context) error { // TODO: Implement trending update return nil }