package ai import ( "fmt" "math" "time" ) // validateWorkouts ensures AI-generated workouts meet quality standards func validateWorkouts(workouts []AIWorkout) error { if len(workouts) == 0 { return fmt.Errorf("no workouts generated") } for i, w := range workouts { if err := validateWorkout(w, i); err != nil { return err } } return nil } func validateWorkout(w AIWorkout, index int) error { // Required fields if w.Title == "" { return fmt.Errorf("workout %d: title is required", index) } if w.Duration <= 0 { return fmt.Errorf("workout %d: invalid duration %d", index, w.Duration) } if w.Duration > 86400 { return fmt.Errorf("workout %d: duration exceeds 24 hours", index) } // Validate date format if _, err := time.Parse("2006-01-02", w.ScheduledDate); err != nil { return fmt.Errorf("workout %d: invalid date format %s", index, w.ScheduledDate) } // Validate segments if len(w.Segments) == 0 { return fmt.Errorf("workout %d: must have at least one segment", index) } if len(w.Segments) > 100 { return fmt.Errorf("workout %d: too many segments (%d), maximum 100", index, len(w.Segments)) } totalDuration := 0 for j, seg := range w.Segments { if seg.Duration <= 0 { return fmt.Errorf("workout %d, segment %d: invalid duration %d", index, j, seg.Duration) } // Validate power values are reasonable (0-300% FTP) if seg.Power < 0 || seg.Power > 3.0 { return fmt.Errorf("workout %d, segment %d: power %.2f out of range (0-3.0)", index, j, seg.Power) } if seg.PowerLow < 0 || seg.PowerLow > 3.0 { return fmt.Errorf("workout %d, segment %d: power_low %.2f out of range (0-3.0)", index, j, seg.PowerLow) } if seg.PowerHigh < 0 || seg.PowerHigh > 3.0 { return fmt.Errorf("workout %d, segment %d: power_high %.2f out of range (0-3.0)", index, j, seg.PowerHigh) } // Validate that power_low <= power_high (except for ramps/cooldowns where power descends) // For warmup, cooldown, and ramp types, power can go from high to low if seg.PowerLow > 0 && seg.PowerHigh > 0 && seg.PowerLow > seg.PowerHigh { // Allow descending power for warmup, cooldown, and ramp segments if seg.Type != "warmup" && seg.Type != "cooldown" && seg.Type != "ramp" { return fmt.Errorf("workout %d, segment %d: power_low (%.2f) > power_high (%.2f)", index, j, seg.PowerLow, seg.PowerHigh) } } // Validate segment type validTypes := map[string]bool{ "warmup": true, "cooldown": true, "steadystate": true, "interval": true, "ramp": true, "rest": true, "freeride": true, } if !validTypes[seg.Type] { return fmt.Errorf("workout %d, segment %d: invalid type %s", index, j, seg.Type) } totalDuration += seg.Duration } // Verify total duration matches sum of segments (within 5% tolerance) tolerance := float64(w.Duration) * 0.05 diff := math.Abs(float64(totalDuration - w.Duration)) if diff > tolerance { return fmt.Errorf("workout %d: duration mismatch (declared %d, segments total %d)", index, w.Duration, totalDuration) } // Validate TSS is reasonable (0-500) if w.EstimatedTSS < 0 || w.EstimatedTSS > 500 { return fmt.Errorf("workout %d: unrealistic TSS %.1f (expected 0-500)", index, w.EstimatedTSS) } return nil } // validateGenerateRequest validates user input func validateGenerateRequest(req GenerateRequest) error { if req.PlanDuration < 1 || req.PlanDuration > 28 { return fmt.Errorf("plan_duration must be between 1 and 28 days") } if req.WeeklyHours < 1 || req.WeeklyHours > 40 { return fmt.Errorf("weekly_hours must be between 1 and 40") } validIntensities := map[string]bool{"easy": true, "moderate": true, "hard": true} if !validIntensities[req.IntensityLevel] { return fmt.Errorf("intensity_level must be 'easy', 'moderate', or 'hard'") } if len(req.FocusAreas) == 0 { return fmt.Errorf("at least one focus area is required") } if len(req.FocusAreas) > 3 { return fmt.Errorf("maximum 3 focus areas allowed") } validFocusAreas := map[string]bool{ "endurance": true, "threshold": true, "vo2max": true, "recovery": true, "sprint": true, "sweet_spot": true, } for _, focus := range req.FocusAreas { if !validFocusAreas[focus] { return fmt.Errorf("invalid focus area: %s", focus) } } // Validate start date format if _, err := time.Parse("2006-01-02", req.StartDate); err != nil { return fmt.Errorf("invalid start_date format, use YYYY-MM-DD") } return nil }