package linguistics import ( "context" "fmt" "tercul/internal/models" "time" "tercul/internal/platform/log" ) // WorkAnalysisService defines the interface for work-specific analysis operations type WorkAnalysisService interface { // AnalyzeWork performs linguistic analysis on a work AnalyzeWork(ctx context.Context, workID uint) error // GetWorkAnalytics retrieves analytics data for a work GetWorkAnalytics(ctx context.Context, workID uint) (*WorkAnalytics, error) } // WorkAnalytics contains analytics data for a work type WorkAnalytics struct { WorkID uint ViewCount int64 LikeCount int64 CommentCount int64 BookmarkCount int64 TranslationCount int64 ReadabilityScore float64 SentimentScore float64 TopKeywords []string PopularTranslations []TranslationAnalytics } // TranslationAnalytics contains analytics data for a translation type TranslationAnalytics struct { TranslationID uint Language string ViewCount int64 LikeCount int64 } // workAnalysisService implements the WorkAnalysisService interface type workAnalysisService struct { textAnalyzer TextAnalyzer analysisCache AnalysisCache analysisRepo AnalysisRepository concurrency int cacheEnabled bool } // NewWorkAnalysisService creates a new WorkAnalysisService func NewWorkAnalysisService( textAnalyzer TextAnalyzer, analysisCache AnalysisCache, analysisRepo AnalysisRepository, concurrency int, cacheEnabled bool, ) WorkAnalysisService { return &workAnalysisService{ textAnalyzer: textAnalyzer, analysisCache: analysisCache, analysisRepo: analysisRepo, concurrency: concurrency, cacheEnabled: cacheEnabled, } } // AnalyzeWork performs linguistic analysis on a work and stores the results func (s *workAnalysisService) AnalyzeWork(ctx context.Context, workID uint) error { if workID == 0 { return fmt.Errorf("invalid work ID") } // Check cache first if enabled if s.cacheEnabled && s.analysisCache.IsEnabled() { cacheKey := fmt.Sprintf("work_analysis:%d", workID) if result, err := s.analysisCache.Get(ctx, cacheKey); err == nil { log.LogInfo("Cache hit for work analysis", log.F("workID", workID)) // Store directly to database return s.analysisRepo.StoreAnalysisResults(ctx, workID, result) } } // Get work content from database content, err := s.analysisRepo.GetWorkContent(ctx, workID, "") if err != nil { log.LogError("Failed to get work content for analysis", log.F("workID", workID), log.F("error", err)) return fmt.Errorf("failed to get work content: %w", err) } // Skip analysis if content is empty if content == "" { log.LogWarn("Skipping analysis for work with empty content", log.F("workID", workID)) return nil } // Get work to determine language (via repository to avoid leaking GORM) work, err := s.analysisRepo.GetWorkByID(ctx, workID) if err != nil { log.LogError("Failed to fetch work for analysis", log.F("workID", workID), log.F("error", err)) return fmt.Errorf("failed to fetch work: %w", err) } // Analyze the text start := time.Now() log.LogInfo("Analyzing work", log.F("workID", workID), log.F("language", work.Language), log.F("contentLength", len(content))) var result *AnalysisResult // Use concurrent processing for large texts if len(content) > 10000 && s.concurrency > 1 { result, err = s.textAnalyzer.AnalyzeTextConcurrently(ctx, content, work.Language, s.concurrency) } else { result, err = s.textAnalyzer.AnalyzeText(ctx, content, work.Language) } if err != nil { log.LogError("Failed to analyze work text", log.F("workID", workID), log.F("error", err)) return fmt.Errorf("failed to analyze work text: %w", err) } // Store results in database if err := s.analysisRepo.StoreAnalysisResults(ctx, workID, result); err != nil { log.LogError("Failed to store analysis results", log.F("workID", workID), log.F("error", err)) return fmt.Errorf("failed to store analysis results: %w", err) } // Cache the result if caching is enabled if s.cacheEnabled && s.analysisCache.IsEnabled() { cacheKey := fmt.Sprintf("work_analysis:%d", workID) if err := s.analysisCache.Set(ctx, cacheKey, result); err != nil { log.LogWarn("Failed to cache work analysis result", log.F("workID", workID), log.F("error", err)) } } log.LogInfo("Successfully analyzed work", log.F("workID", workID), log.F("wordCount", result.WordCount), log.F("readabilityScore", result.ReadabilityScore), log.F("sentiment", result.Sentiment), log.F("durationMs", time.Since(start).Milliseconds())) return nil } // GetWorkAnalytics retrieves analytics data for a work func (s *workAnalysisService) GetWorkAnalytics(ctx context.Context, workID uint) (*WorkAnalytics, error) { if workID == 0 { return nil, fmt.Errorf("invalid work ID") } // Get the work to ensure it exists work, err := s.analysisRepo.GetWorkByID(ctx, workID) if err != nil { return nil, fmt.Errorf("work not found: %w", err) } // Get analysis results from database _, readabilityScore, languageAnalysis, _ := s.analysisRepo.GetAnalysisData(ctx, workID) // Extract keywords from JSONB var keywords []string if languageAnalysis.Analysis != nil { if keywordsData, ok := languageAnalysis.Analysis["keywords"].([]interface{}); ok { for _, kw := range keywordsData { if keywordMap, ok := kw.(map[string]interface{}); ok { if text, ok := keywordMap["text"].(string); ok { keywords = append(keywords, text) } } } } } // For now, return placeholder analytics with actual analysis data return &WorkAnalytics{ WorkID: work.ID, ViewCount: 0, // TODO: Implement view counting LikeCount: 0, // TODO: Implement like counting CommentCount: 0, // TODO: Implement comment counting BookmarkCount: 0, // TODO: Implement bookmark counting TranslationCount: 0, // TODO: Implement translation counting ReadabilityScore: readabilityScore.Score, SentimentScore: extractSentimentFromAnalysis(languageAnalysis.Analysis), TopKeywords: keywords, PopularTranslations: []TranslationAnalytics{}, // TODO: Implement translation analytics }, nil } // extractSentimentFromAnalysis extracts sentiment from the Analysis JSONB field func extractSentimentFromAnalysis(analysis models.JSONB) float64 { if analysis == nil { return 0.0 } if sentiment, ok := analysis["sentiment"].(float64); ok { return sentiment } return 0.0 }