package nutrition import ( "math" "time" "rideaware/internal/stats" "rideaware/internal/user" "rideaware/internal/workout" "rideaware/pkg/database" ) type Service struct { userRepo *user.Repository statsRepo *stats.Repository workoutRepo *workout.Repository } func NewService() *Service { return &Service{ userRepo: user.NewRepository(), statsRepo: stats.NewRepository(), workoutRepo: workout.NewRepository(), } } // NutritionTargets contains calculated daily nutrition targets type NutritionTargets struct { BMR int `json:"bmr"` TDEE int `json:"tdee"` DailyCalories int `json:"daily_calories"` GoalAdjustment int `json:"goal_adjustment"` Protein int `json:"protein_g"` Carbs int `json:"carbs_g"` Fat int `json:"fat_g"` NutritionGoal string `json:"nutrition_goal"` DietaryPref string `json:"dietary_preference"` CurrentWeight float64 `json:"current_weight"` TargetWeight float64 `json:"target_weight"` IsConfigured bool `json:"is_configured"` } // DailyNutrition represents one day's nutrition data type DailyNutrition struct { Date string `json:"date"` BaseCalories int `json:"base_calories"` WorkoutCalories int `json:"workout_calories"` TotalTarget int `json:"total_target"` Protein int `json:"protein_g"` Carbs int `json:"carbs_g"` Fat int `json:"fat_g"` IsTrainingDay bool `json:"is_training_day"` WorkoutTitle string `json:"workout_title,omitempty"` } // WeeklyNutrition contains a week of daily nutrition data type WeeklyNutrition struct { Days []DailyNutrition `json:"days"` AvgCalories int `json:"avg_calories"` AvgProtein int `json:"avg_protein_g"` AvgCarbs int `json:"avg_carbs_g"` AvgFat int `json:"avg_fat_g"` TotalWorkoutCal int `json:"total_workout_calories"` } // CalculateBMR uses Mifflin-St Jeor equation func CalculateBMR(weight, height float64, age int, gender string) int { if weight <= 0 || height <= 0 || age <= 0 { return 0 } bmr := 10.0*weight + 6.25*height - 5.0*float64(age) if gender == "female" { bmr -= 161 } else { bmr += 5 } return int(math.Round(bmr)) } // CalculateTDEE applies activity multiplier to BMR func CalculateTDEE(bmr int, activityLevel string) int { multipliers := map[string]float64{ "sedentary": 1.2, "lightly_active": 1.375, "active": 1.55, "very_active": 1.725, } mult, ok := multipliers[activityLevel] if !ok { mult = 1.375 // default to lightly active } return int(math.Round(float64(bmr) * mult)) } // goalAdjustment returns calorie adjustment for the goal func goalAdjustment(goal string) int { switch goal { case "weight_loss": return -500 case "performance": return 300 default: // maintenance return 0 } } // macroSplit returns protein/carbs/fat percentages based on dietary preference func macroSplit(pref string) (protPct, carbPct, fatPct float64) { switch pref { case "high_carb": return 0.20, 0.60, 0.20 case "high_protein": return 0.35, 0.40, 0.25 case "keto": return 0.30, 0.10, 0.60 default: // balanced return 0.25, 0.50, 0.25 } } // calculateMacros converts calories to grams of each macro func calculateMacros(calories int, pref string) (protein, carbs, fat int) { protPct, carbPct, fatPct := macroSplit(pref) protein = int(math.Round(float64(calories) * protPct / 4.0)) // 4 cal/g carbs = int(math.Round(float64(calories) * carbPct / 4.0)) // 4 cal/g fat = int(math.Round(float64(calories) * fatPct / 9.0)) // 9 cal/g return } // GetTargets calculates nutrition targets for a user func (s *Service) GetTargets(userID uint) (*NutritionTargets, error) { userObj, err := s.userRepo.GetUserByID(userID) if err != nil { return nil, err } profile := userObj.Profile if profile == nil { return &NutritionTargets{IsConfigured: false}, nil } // Check if nutrition is configured if profile.Height <= 0 || profile.Age <= 0 || profile.Weight <= 0 || profile.Gender == "" { return &NutritionTargets{ IsConfigured: false, CurrentWeight: profile.Weight, NutritionGoal: profile.NutritionGoal, }, nil } bmr := CalculateBMR(profile.Weight, profile.Height, profile.Age, profile.Gender) activityLevel := profile.ActivityLevel if activityLevel == "" { activityLevel = "lightly_active" } tdee := CalculateTDEE(bmr, activityLevel) goal := profile.NutritionGoal if goal == "" { goal = "maintenance" } adj := goalAdjustment(goal) dailyCal := tdee + adj dietPref := profile.DietaryPref if dietPref == "" { dietPref = "balanced" } protein, carbs, fat := calculateMacros(dailyCal, dietPref) return &NutritionTargets{ BMR: bmr, TDEE: tdee, DailyCalories: dailyCal, GoalAdjustment: adj, Protein: protein, Carbs: carbs, Fat: fat, NutritionGoal: goal, DietaryPref: dietPref, CurrentWeight: profile.Weight, TargetWeight: profile.TargetWeight, IsConfigured: true, }, nil } // GetWeeklyNutrition returns 7 days of nutrition data with workout adjustments func (s *Service) GetWeeklyNutrition(userID uint) (*WeeklyNutrition, error) { targets, err := s.GetTargets(userID) if err != nil { return nil, err } if !targets.IsConfigured { return &WeeklyNutrition{Days: []DailyNutrition{}}, nil } // Get workouts for the next 7 days now := time.Now() today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) endDate := today.AddDate(0, 0, 7) workouts, err := s.workoutRepo.GetWorkoutsByDateRange(userID, today, endDate) if err != nil { workouts = []workout.Workout{} } // Also get past 7 days for completed workout calories pastStart := today.AddDate(0, 0, -7) pastWorkouts, err := s.workoutRepo.GetWorkoutsByDateRange(userID, pastStart, today) if err != nil { pastWorkouts = []workout.Workout{} } allWorkouts := append(pastWorkouts, workouts...) // Build workout map by date workoutMap := make(map[string][]workout.Workout) for _, w := range allWorkouts { dateStr := w.ScheduledDate.Format("2006-01-02") workoutMap[dateStr] = append(workoutMap[dateStr], w) } // Get user FTP for calorie estimation var ftp int database.DB.Table("user_profiles").Select("ftp").Where("user_id = ?", userID).Scan(&ftp) days := make([]DailyNutrition, 0, 7) totalWorkoutCal := 0 for i := 0; i < 7; i++ { date := today.AddDate(0, 0, i) dateStr := date.Format("2006-01-02") dayWorkouts := workoutMap[dateStr] workoutCal := 0 isTraining := false title := "" for _, w := range dayWorkouts { if w.CaloriesBurned > 0 { workoutCal += w.CaloriesBurned } else if w.Duration > 0 && ftp > 0 && w.AvgPower > 0 { // Estimate: ~4 cal per kJ, kJ = watts * seconds / 1000 kj := float64(w.AvgPower) * float64(w.Duration) / 1000.0 workoutCal += int(kj * 4.0) } else if w.Duration > 0 { // Rough estimate: ~8 cal/min for cycling workoutCal += (w.Duration / 60) * 8 } isTraining = true if title == "" { title = w.Title } } totalTarget := targets.DailyCalories + workoutCal protein, carbs, fat := calculateMacros(totalTarget, targets.DietaryPref) days = append(days, DailyNutrition{ Date: dateStr, BaseCalories: targets.DailyCalories, WorkoutCalories: workoutCal, TotalTarget: totalTarget, Protein: protein, Carbs: carbs, Fat: fat, IsTrainingDay: isTraining, WorkoutTitle: title, }) totalWorkoutCal += workoutCal } // Calculate averages avgCal, avgProt, avgCarb, avgFat := 0, 0, 0, 0 for _, d := range days { avgCal += d.TotalTarget avgProt += d.Protein avgCarb += d.Carbs avgFat += d.Fat } return &WeeklyNutrition{ Days: days, AvgCalories: avgCal / 7, AvgProtein: avgProt / 7, AvgCarbs: avgCarb / 7, AvgFat: avgFat / 7, TotalWorkoutCal: totalWorkoutCal, }, nil }