package valueobjects import ( "fmt" "time" "knowfoolery/backend/shared/types" "knowfoolery/backend/shared/errors" ) // QuestionStatistics tracks usage and performance statistics for a question type QuestionStatistics struct { // Usage counts totalAttempts int64 correctAttempts int64 incorrectAttempts int64 timeoutAttempts int64 // Hint usage hintsRequested int64 // Difficulty tracking difficultyVotes map[types.DifficultyLevel]int64 averageDifficulty float64 // Performance metrics averageTimeToAnswer time.Duration fastestAnswer time.Duration slowestAnswer time.Duration // Session tracking uniqueSessions int64 uniqueUsers int64 // Time tracking firstUsed *types.Timestamp lastUsed *types.Timestamp // Trends (last 30 days) recentAttempts int64 recentCorrectRate float64 // Quality indicators skipRate float64 // How often players skip this question reportCount int64 // How many times question was reported // Calculated fields correctRate float64 popularityScore float64 // Update tracking lastCalculated types.Timestamp } // NewQuestionStatistics creates new question statistics func NewQuestionStatistics() *QuestionStatistics { now := types.NewTimestamp() return &QuestionStatistics{ totalAttempts: 0, correctAttempts: 0, incorrectAttempts: 0, timeoutAttempts: 0, hintsRequested: 0, difficultyVotes: make(map[types.DifficultyLevel]int64), averageDifficulty: 0.0, averageTimeToAnswer: 0, fastestAnswer: time.Duration(0), slowestAnswer: time.Duration(0), uniqueSessions: 0, uniqueUsers: 0, firstUsed: nil, lastUsed: nil, recentAttempts: 0, recentCorrectRate: 0.0, skipRate: 0.0, reportCount: 0, correctRate: 0.0, popularityScore: 0.0, lastCalculated: now, } } // NewQuestionStatisticsWithData creates statistics with existing data func NewQuestionStatisticsWithData( totalAttempts int64, correctAttempts int64, incorrectAttempts int64, timeoutAttempts int64, hintsRequested int64, uniqueSessions int64, uniqueUsers int64, firstUsed *types.Timestamp, lastUsed *types.Timestamp, ) *QuestionStatistics { stats := NewQuestionStatistics() stats.totalAttempts = totalAttempts stats.correctAttempts = correctAttempts stats.incorrectAttempts = incorrectAttempts stats.timeoutAttempts = timeoutAttempts stats.hintsRequested = hintsRequested stats.uniqueSessions = uniqueSessions stats.uniqueUsers = uniqueUsers stats.firstUsed = firstUsed stats.lastUsed = lastUsed stats.recalculate() return stats } // Getters // TotalAttempts returns the total number of attempts func (qs *QuestionStatistics) TotalAttempts() int64 { return qs.totalAttempts } // CorrectAttempts returns the number of correct attempts func (qs *QuestionStatistics) CorrectAttempts() int64 { return qs.correctAttempts } // IncorrectAttempts returns the number of incorrect attempts func (qs *QuestionStatistics) IncorrectAttempts() int64 { return qs.incorrectAttempts } // TimeoutAttempts returns the number of timeout attempts func (qs *QuestionStatistics) TimeoutAttempts() int64 { return qs.timeoutAttempts } // HintsRequested returns the number of hints requested func (qs *QuestionStatistics) HintsRequested() int64 { return qs.hintsRequested } // DifficultyVotes returns the difficulty votes func (qs *QuestionStatistics) DifficultyVotes() map[types.DifficultyLevel]int64 { result := make(map[types.DifficultyLevel]int64) for k, v := range qs.difficultyVotes { result[k] = v } return result } // AverageDifficulty returns the average perceived difficulty func (qs *QuestionStatistics) AverageDifficulty() float64 { return qs.averageDifficulty } // AverageTimeToAnswer returns the average time to answer func (qs *QuestionStatistics) AverageTimeToAnswer() time.Duration { return qs.averageTimeToAnswer } // FastestAnswer returns the fastest answer time func (qs *QuestionStatistics) FastestAnswer() time.Duration { return qs.fastestAnswer } // SlowestAnswer returns the slowest answer time func (qs *QuestionStatistics) SlowestAnswer() time.Duration { return qs.slowestAnswer } // UniqueSessions returns the number of unique sessions func (qs *QuestionStatistics) UniqueSessions() int64 { return qs.uniqueSessions } // UniqueUsers returns the number of unique users func (qs *QuestionStatistics) UniqueUsers() int64 { return qs.uniqueUsers } // FirstUsed returns when the question was first used func (qs *QuestionStatistics) FirstUsed() *types.Timestamp { return qs.firstUsed } // LastUsed returns when the question was last used func (qs *QuestionStatistics) LastUsed() *types.Timestamp { return qs.lastUsed } // RecentAttempts returns recent attempts count func (qs *QuestionStatistics) RecentAttempts() int64 { return qs.recentAttempts } // RecentCorrectRate returns recent correct rate func (qs *QuestionStatistics) RecentCorrectRate() float64 { return qs.recentCorrectRate } // SkipRate returns the skip rate func (qs *QuestionStatistics) SkipRate() float64 { return qs.skipRate } // ReportCount returns the number of reports func (qs *QuestionStatistics) ReportCount() int64 { return qs.reportCount } // CorrectRate returns the overall correct rate func (qs *QuestionStatistics) CorrectRate() float64 { return qs.correctRate } // PopularityScore returns the popularity score func (qs *QuestionStatistics) PopularityScore() float64 { return qs.popularityScore } // LastCalculated returns when statistics were last calculated func (qs *QuestionStatistics) LastCalculated() types.Timestamp { return qs.lastCalculated } // Business methods // RecordUsage records a usage event for the question func (qs *QuestionStatistics) RecordUsage(wasCorrect bool, difficulty types.DifficultyLevel) error { now := types.NewTimestamp() // Update usage counts qs.totalAttempts++ if wasCorrect { qs.correctAttempts++ } else { qs.incorrectAttempts++ } // Record difficulty vote if difficulty != "" { qs.difficultyVotes[difficulty]++ } // Update timestamps if qs.firstUsed == nil { qs.firstUsed = &now } qs.lastUsed = &now // Recalculate derived statistics qs.recalculate() return nil } // RecordTimeout records a timeout event func (qs *QuestionStatistics) RecordTimeout() { qs.totalAttempts++ qs.timeoutAttempts++ now := types.NewTimestamp() if qs.firstUsed == nil { qs.firstUsed = &now } qs.lastUsed = &now qs.recalculate() } // RecordHintRequest records a hint request func (qs *QuestionStatistics) RecordHintRequest() { qs.hintsRequested++ qs.recalculate() } // RecordAnswerTime records time taken to answer func (qs *QuestionStatistics) RecordAnswerTime(duration time.Duration) error { if duration < 0 { return errors.ErrValidationFailed("duration", "duration cannot be negative") } // Update fastest/slowest if this is first record or a new extreme if qs.fastestAnswer == 0 || duration < qs.fastestAnswer { qs.fastestAnswer = duration } if duration > qs.slowestAnswer { qs.slowestAnswer = duration } // Recalculate average (simplified - in real implementation would track all times) if qs.totalAttempts > 0 { currentTotal := qs.averageTimeToAnswer * time.Duration(qs.totalAttempts-1) qs.averageTimeToAnswer = (currentTotal + duration) / time.Duration(qs.totalAttempts) } else { qs.averageTimeToAnswer = duration } qs.recalculate() return nil } // RecordSessionUsage records usage by a new session func (qs *QuestionStatistics) RecordSessionUsage() { qs.uniqueSessions++ qs.recalculate() } // RecordUserUsage records usage by a new user func (qs *QuestionStatistics) RecordUserUsage() { qs.uniqueUsers++ qs.recalculate() } // RecordSkip records that the question was skipped func (qs *QuestionStatistics) RecordSkip() { // This would affect skip rate calculation qs.recalculate() } // RecordReport records a report for inappropriate content func (qs *QuestionStatistics) RecordReport() { qs.reportCount++ qs.recalculate() } // UpdateRecentStatistics updates recent (30-day) statistics func (qs *QuestionStatistics) UpdateRecentStatistics(attempts int64, correctRate float64) { qs.recentAttempts = attempts qs.recentCorrectRate = correctRate qs.recalculate() } // Analysis methods // IsPopular returns true if the question is popular func (qs *QuestionStatistics) IsPopular() bool { return qs.popularityScore >= types.PopularityThreshold } // IsDifficult returns true if the question is difficult based on statistics func (qs *QuestionStatistics) IsDifficult() bool { return qs.correctRate < types.DifficultQuestionThreshold } // IsEasy returns true if the question is easy based on statistics func (qs *QuestionStatistics) IsEasy() bool { return qs.correctRate > types.EasyQuestionThreshold } // IsProblemQuestion returns true if the question has quality issues func (qs *QuestionStatistics) IsProblemQuestion() bool { return qs.reportCount >= types.ProblemQuestionReportThreshold || qs.skipRate >= types.ProblemQuestionSkipThreshold } // GetPerformanceCategory returns the performance category func (qs *QuestionStatistics) GetPerformanceCategory() string { switch { case qs.correctRate >= 0.8: return "excellent" case qs.correctRate >= 0.6: return "good" case qs.correctRate >= 0.4: return "average" case qs.correctRate >= 0.2: return "challenging" default: return "very_difficult" } } // GetUsageCategory returns the usage category func (qs *QuestionStatistics) GetUsageCategory() string { switch { case qs.totalAttempts >= 1000: return "heavily_used" case qs.totalAttempts >= 500: return "frequently_used" case qs.totalAttempts >= 100: return "moderately_used" case qs.totalAttempts >= 10: return "lightly_used" default: return "rarely_used" } } // GetHintUsageRate returns the hint usage rate func (qs *QuestionStatistics) GetHintUsageRate() float64 { if qs.totalAttempts == 0 { return 0.0 } return float64(qs.hintsRequested) / float64(qs.totalAttempts) } // GetTimeoutRate returns the timeout rate func (qs *QuestionStatistics) GetTimeoutRate() float64 { if qs.totalAttempts == 0 { return 0.0 } return float64(qs.timeoutAttempts) / float64(qs.totalAttempts) } // GetEngagementScore returns an engagement score func (qs *QuestionStatistics) GetEngagementScore() float64 { // Combination of popularity, correct rate, and hint usage baseScore := qs.popularityScore * 0.4 correctRateBonus := qs.correctRate * 0.3 hintUsageBonus := (1.0 - qs.GetHintUsageRate()) * 0.2 // Less hints = more engaging timeoutPenalty := qs.GetTimeoutRate() * -0.1 return baseScore + correctRateBonus + hintUsageBonus + timeoutPenalty } // Validation // Validate performs validation on the statistics func (qs *QuestionStatistics) Validate() *types.ValidationResult { result := types.NewValidationResult() // Check consistency calculatedTotal := qs.correctAttempts + qs.incorrectAttempts + qs.timeoutAttempts if calculatedTotal != qs.totalAttempts { result.AddError("total attempts does not match sum of individual attempt types") } // Check bounds if qs.correctRate < 0.0 || qs.correctRate > 1.0 { result.AddError("correct rate must be between 0.0 and 1.0") } if qs.skipRate < 0.0 || qs.skipRate > 1.0 { result.AddError("skip rate must be between 0.0 and 1.0") } if qs.averageDifficulty < 0.0 || qs.averageDifficulty > 5.0 { result.AddError("average difficulty must be between 0.0 and 5.0") } // Check negative values if qs.totalAttempts < 0 || qs.correctAttempts < 0 || qs.incorrectAttempts < 0 || qs.timeoutAttempts < 0 || qs.hintsRequested < 0 || qs.uniqueSessions < 0 || qs.uniqueUsers < 0 || qs.reportCount < 0 { result.AddError("counts cannot be negative") } // Check time consistency if qs.firstUsed != nil && qs.lastUsed != nil { if qs.lastUsed.Before(*qs.firstUsed) { result.AddError("last used cannot be before first used") } } return result } // Helper methods // recalculate recalculates derived statistics func (qs *QuestionStatistics) recalculate() { // Calculate correct rate if qs.totalAttempts > 0 { qs.correctRate = float64(qs.correctAttempts) / float64(qs.totalAttempts) } else { qs.correctRate = 0.0 } // Calculate average difficulty from votes if len(qs.difficultyVotes) > 0 { var totalWeightedScore float64 var totalVotes int64 for difficulty, votes := range qs.difficultyVotes { weight := difficultyToWeight(difficulty) totalWeightedScore += weight * float64(votes) totalVotes += votes } if totalVotes > 0 { qs.averageDifficulty = totalWeightedScore / float64(totalVotes) } } // Calculate popularity score qs.popularityScore = calculatePopularityScore( qs.totalAttempts, qs.uniqueSessions, qs.uniqueUsers, qs.correctRate, qs.recentAttempts, ) qs.lastCalculated = types.NewTimestamp() } // difficultyToWeight converts difficulty level to numerical weight func difficultyToWeight(difficulty types.DifficultyLevel) float64 { switch difficulty { case types.DifficultyEasy: return 1.0 case types.DifficultyMedium: return 2.0 case types.DifficultyHard: return 3.0 case types.DifficultyExpert: return 4.0 case types.DifficultyImpossible: return 5.0 default: return 2.5 // Default to medium } } // calculatePopularityScore calculates a popularity score based on various metrics func calculatePopularityScore(totalAttempts, uniqueSessions, uniqueUsers int64, correctRate float64, recentAttempts int64) float64 { // Base score from usage usageScore := float64(totalAttempts) * 0.1 // Bonus for unique sessions and users uniquenessScore := float64(uniqueSessions)*0.2 + float64(uniqueUsers)*0.3 // Bonus for good correct rate (not too easy, not too hard) correctRateScore := 0.0 if correctRate >= 0.3 && correctRate <= 0.8 { correctRateScore = 100.0 * (1.0 - abs(correctRate-0.55)) // Peak at 55% correct } // Recent activity bonus recentActivityScore := float64(recentAttempts) * 0.5 return usageScore + uniquenessScore + correctRateScore + recentActivityScore } // abs returns absolute value of float64 func abs(x float64) float64 { if x < 0 { return -x } return x } // ToSnapshot creates a read-only snapshot of the statistics func (qs *QuestionStatistics) ToSnapshot() *QuestionStatisticsSnapshot { difficultyVotesCopy := make(map[types.DifficultyLevel]int64) for k, v := range qs.difficultyVotes { difficultyVotesCopy[k] = v } return &QuestionStatisticsSnapshot{ TotalAttempts: qs.totalAttempts, CorrectAttempts: qs.correctAttempts, IncorrectAttempts: qs.incorrectAttempts, TimeoutAttempts: qs.timeoutAttempts, HintsRequested: qs.hintsRequested, DifficultyVotes: difficultyVotesCopy, AverageDifficulty: qs.averageDifficulty, AverageTimeToAnswer: qs.averageTimeToAnswer, FastestAnswer: qs.fastestAnswer, SlowestAnswer: qs.slowestAnswer, UniqueSessions: qs.uniqueSessions, UniqueUsers: qs.uniqueUsers, FirstUsed: qs.firstUsed, LastUsed: qs.lastUsed, RecentAttempts: qs.recentAttempts, RecentCorrectRate: qs.recentCorrectRate, SkipRate: qs.skipRate, ReportCount: qs.reportCount, CorrectRate: qs.correctRate, PopularityScore: qs.popularityScore, LastCalculated: qs.lastCalculated, } } // GetSummary returns a human-readable summary func (qs *QuestionStatistics) GetSummary() string { return fmt.Sprintf("Usage: %d attempts (%.1f%% correct) | Popularity: %.0f | Category: %s", qs.totalAttempts, qs.correctRate*100, qs.popularityScore, qs.GetPerformanceCategory(), ) } // QuestionStatisticsSnapshot represents a read-only view of question statistics type QuestionStatisticsSnapshot struct { TotalAttempts int64 `json:"total_attempts"` CorrectAttempts int64 `json:"correct_attempts"` IncorrectAttempts int64 `json:"incorrect_attempts"` TimeoutAttempts int64 `json:"timeout_attempts"` HintsRequested int64 `json:"hints_requested"` DifficultyVotes map[types.DifficultyLevel]int64 `json:"difficulty_votes"` AverageDifficulty float64 `json:"average_difficulty"` AverageTimeToAnswer time.Duration `json:"average_time_to_answer"` FastestAnswer time.Duration `json:"fastest_answer"` SlowestAnswer time.Duration `json:"slowest_answer"` UniqueSessions int64 `json:"unique_sessions"` UniqueUsers int64 `json:"unique_users"` FirstUsed *types.Timestamp `json:"first_used,omitempty"` LastUsed *types.Timestamp `json:"last_used,omitempty"` RecentAttempts int64 `json:"recent_attempts"` RecentCorrectRate float64 `json:"recent_correct_rate"` SkipRate float64 `json:"skip_rate"` ReportCount int64 `json:"report_count"` CorrectRate float64 `json:"correct_rate"` PopularityScore float64 `json:"popularity_score"` LastCalculated types.Timestamp `json:"last_calculated"` } // GetSummary returns a summary of the snapshot func (qss *QuestionStatisticsSnapshot) GetSummary() string { return fmt.Sprintf("Usage: %d attempts (%.1f%% correct) | Popularity: %.0f", qss.TotalAttempts, qss.CorrectRate*100, qss.PopularityScore, ) }