package stats import ( "rideaware/internal/workout" "rideaware/pkg/database" "time" ) type Repository struct{} func NewRepository() *Repository { return &Repository{} } // Summary holds overall aggregated stats for a user. type Summary struct { TotalRides int `json:"total_rides"` TotalDistance float64 `json:"total_distance"` TotalDuration int `json:"total_duration"` TotalElevGain int `json:"total_elev_gain"` TotalCalories int `json:"total_calories"` AvgPower float64 `json:"avg_power"` AvgHR float64 `json:"avg_hr"` AvgDistance float64 `json:"avg_distance"` AvgDuration float64 `json:"avg_duration"` LongestRide int `json:"longest_ride"` FarthestRide float64 `json:"farthest_ride"` MaxPower int `json:"max_power"` MaxHR int `json:"max_hr"` MostElevGain int `json:"most_elev_gain"` } // PeriodStats holds aggregated stats for a time period. type PeriodStats struct { Period string `json:"period"` Year int `json:"year"` Rides int `json:"rides"` Distance float64 `json:"distance"` Duration int `json:"duration"` ElevGain int `json:"elev_gain"` Calories int `json:"calories"` AvgPower float64 `json:"avg_power"` AvgHR float64 `json:"avg_hr"` } // GetSummary returns overall ride statistics for completed workouts. func (r *Repository) GetSummary(userID uint) (*Summary, error) { var summary Summary err := database.DB.Model(&workout.Workout{}). Select(` COUNT(*) as total_rides, COALESCE(SUM(distance), 0) as total_distance, COALESCE(SUM(duration), 0) as total_duration, COALESCE(SUM(elev_gain), 0) as total_elev_gain, COALESCE(SUM(calories_burned), 0) as total_calories, COALESCE(AVG(NULLIF(avg_power, 0)), 0) as avg_power, COALESCE(AVG(NULLIF(avg_hr, 0)), 0) as avg_hr, COALESCE(AVG(distance), 0) as avg_distance, COALESCE(AVG(duration), 0) as avg_duration, COALESCE(MAX(duration), 0) as longest_ride, COALESCE(MAX(distance), 0) as farthest_ride, COALESCE(MAX(max_power), 0) as max_power, COALESCE(MAX(max_hr), 0) as max_hr, COALESCE(MAX(elev_gain), 0) as most_elev_gain `). Where("user_id = ? AND status = ?", userID, "completed"). Scan(&summary).Error return &summary, err } // GetWeeklyStats returns weekly aggregated stats for the last N weeks. func (r *Repository) GetWeeklyStats(userID uint, weeks int) ([]PeriodStats, error) { cutoff := time.Now().AddDate(0, 0, -weeks*7) var stats []PeriodStats err := database.DB.Model(&workout.Workout{}). Select(` TO_CHAR(scheduled_date, 'IYYY-IW') as period, EXTRACT(ISOYEAR FROM scheduled_date)::int as year, COUNT(*) as rides, COALESCE(SUM(distance), 0) as distance, COALESCE(SUM(duration), 0) as duration, COALESCE(SUM(elev_gain), 0) as elev_gain, COALESCE(SUM(calories_burned), 0) as calories, COALESCE(AVG(NULLIF(avg_power, 0)), 0) as avg_power, COALESCE(AVG(NULLIF(avg_hr, 0)), 0) as avg_hr `). Where("user_id = ? AND status = ? AND scheduled_date >= ?", userID, "completed", cutoff). Group("period, year"). Order("year ASC, period ASC"). Scan(&stats).Error return stats, err } // GetMonthlyStats returns monthly aggregated stats for the last N months. func (r *Repository) GetMonthlyStats(userID uint, months int) ([]PeriodStats, error) { cutoff := time.Now().AddDate(0, -months, 0) var stats []PeriodStats err := database.DB.Model(&workout.Workout{}). Select(` TO_CHAR(scheduled_date, 'YYYY-MM') as period, EXTRACT(YEAR FROM scheduled_date)::int as year, COUNT(*) as rides, COALESCE(SUM(distance), 0) as distance, COALESCE(SUM(duration), 0) as duration, COALESCE(SUM(elev_gain), 0) as elev_gain, COALESCE(SUM(calories_burned), 0) as calories, COALESCE(AVG(NULLIF(avg_power, 0)), 0) as avg_power, COALESCE(AVG(NULLIF(avg_hr, 0)), 0) as avg_hr `). Where("user_id = ? AND status = ? AND scheduled_date >= ?", userID, "completed", cutoff). Group("period, year"). Order("year ASC, period ASC"). Scan(&stats).Error return stats, err } // PersonalBest holds a single personal best record. type PersonalBest struct { Category string `json:"category"` Value interface{} `json:"value"` Unit string `json:"unit"` WorkoutID uint `json:"workout_id"` Date time.Time `json:"date"` Title string `json:"title"` } // GetPersonalBests returns personal best records across key metrics. func (r *Repository) GetPersonalBests(userID uint) ([]PersonalBest, error) { var pbs []PersonalBest type pbRow struct { ID uint Title string ScheduledDate time.Time MaxPower int MaxHR int Distance float64 Duration int ElevGain int AvgPower int } // Max Power var maxPower pbRow if err := database.DB.Model(&workout.Workout{}). Where("user_id = ? AND status = ? AND max_power > 0", userID, "completed"). Order("max_power DESC").Limit(1). Scan(&maxPower).Error; err == nil && maxPower.ID != 0 { pbs = append(pbs, PersonalBest{ Category: "max_power", Value: maxPower.MaxPower, Unit: "watts", WorkoutID: maxPower.ID, Date: maxPower.ScheduledDate, Title: maxPower.Title, }) } // Max HR var maxHR pbRow if err := database.DB.Model(&workout.Workout{}). Where("user_id = ? AND status = ? AND max_hr > 0", userID, "completed"). Order("max_hr DESC").Limit(1). Scan(&maxHR).Error; err == nil && maxHR.ID != 0 { pbs = append(pbs, PersonalBest{ Category: "max_hr", Value: maxHR.MaxHR, Unit: "bpm", WorkoutID: maxHR.ID, Date: maxHR.ScheduledDate, Title: maxHR.Title, }) } // Longest Ride (duration) var longest pbRow if err := database.DB.Model(&workout.Workout{}). Where("user_id = ? AND status = ? AND duration > 0", userID, "completed"). Order("duration DESC").Limit(1). Scan(&longest).Error; err == nil && longest.ID != 0 { pbs = append(pbs, PersonalBest{ Category: "longest_ride", Value: longest.Duration, Unit: "seconds", WorkoutID: longest.ID, Date: longest.ScheduledDate, Title: longest.Title, }) } // Farthest Ride (distance) var farthest pbRow if err := database.DB.Model(&workout.Workout{}). Where("user_id = ? AND status = ? AND distance > 0", userID, "completed"). Order("distance DESC").Limit(1). Scan(&farthest).Error; err == nil && farthest.ID != 0 { pbs = append(pbs, PersonalBest{ Category: "farthest_ride", Value: farthest.Distance, Unit: "km", WorkoutID: farthest.ID, Date: farthest.ScheduledDate, Title: farthest.Title, }) } // Most Elevation var mostElev pbRow if err := database.DB.Model(&workout.Workout{}). Where("user_id = ? AND status = ? AND elev_gain > 0", userID, "completed"). Order("elev_gain DESC").Limit(1). Scan(&mostElev).Error; err == nil && mostElev.ID != 0 { pbs = append(pbs, PersonalBest{ Category: "most_elevation", Value: mostElev.ElevGain, Unit: "meters", WorkoutID: mostElev.ID, Date: mostElev.ScheduledDate, Title: mostElev.Title, }) } // Best Avg Power var bestAvgPower pbRow if err := database.DB.Model(&workout.Workout{}). Where("user_id = ? AND status = ? AND avg_power > 0", userID, "completed"). Order("avg_power DESC").Limit(1). Scan(&bestAvgPower).Error; err == nil && bestAvgPower.ID != 0 { pbs = append(pbs, PersonalBest{ Category: "best_avg_power", Value: bestAvgPower.AvgPower, Unit: "watts", WorkoutID: bestAvgPower.ID, Date: bestAvgPower.ScheduledDate, Title: bestAvgPower.Title, }) } return pbs, nil }