package activity import ( "fmt" "path/filepath" "strings" "time" "rideaware/internal/workout" ) type Service struct { workoutRepo *workout.Repository } func NewService() *Service { return &Service{ workoutRepo: workout.NewRepository(), } } // ImportActivity parses an activity file and creates or updates a workout with the metrics. func (s *Service) ImportActivity(userID uint, fileData []byte, filename string, opts ImportOptions) (*workout.Workout, error) { ext := strings.ToLower(filepath.Ext(filename)) var parsed *ParsedActivity var err error switch ext { case ".fit": parsed, err = ParseFIT(fileData) case ".tcx": parsed, err = ParseTCX(fileData) case ".gpx": parsed, err = ParseGPX(fileData) default: return nil, fmt.Errorf("unsupported file type: %s (supported: .fit, .tcx, .gpx)", ext) } if err != nil { return nil, fmt.Errorf("failed to parse %s file: %w", ext, err) } // If updating an existing workout, apply metrics to it if opts.WorkoutID > 0 { return s.updateExistingWorkout(userID, opts.WorkoutID, parsed, opts) } // Create a new workout from parsed data return s.createNewWorkout(userID, parsed, ext, opts) } func (s *Service) updateExistingWorkout(userID uint, workoutID uint, parsed *ParsedActivity, opts ImportOptions) (*workout.Workout, error) { w, err := s.workoutRepo.GetWorkoutByID(workoutID, userID) if err != nil { return nil, err } w.Status = "completed" w.Duration = parsed.Duration w.Distance = parsed.Distance w.ElevGain = parsed.ElevGain w.AvgPower = parsed.AvgPower w.MaxPower = parsed.MaxPower w.AvgHR = parsed.AvgHR w.MaxHR = parsed.MaxHR w.CaloriesBurned = parsed.CaloriesBurned if opts.EquipmentID != nil { w.EquipmentID = opts.EquipmentID } if err := s.workoutRepo.UpdateWorkout(w); err != nil { return nil, err } return w, nil } func (s *Service) createNewWorkout(userID uint, parsed *ParsedActivity, fileExt string, opts ImportOptions) (*workout.Workout, error) { title := opts.Title if title == "" && parsed.Title != "" { title = parsed.Title } if title == "" { title = "Imported Ride" } scheduledDate := parsed.StartTime if scheduledDate.IsZero() { scheduledDate = time.Now() } w := &workout.Workout{ UserID: userID, Title: title, Type: "ride", Status: "completed", ScheduledDate: scheduledDate, Duration: parsed.Duration, Distance: parsed.Distance, ElevGain: parsed.ElevGain, AvgPower: parsed.AvgPower, MaxPower: parsed.MaxPower, AvgHR: parsed.AvgHR, MaxHR: parsed.MaxHR, CaloriesBurned: parsed.CaloriesBurned, FileType: strings.TrimPrefix(fileExt, "."), EquipmentID: opts.EquipmentID, Notes: opts.Notes, } if err := s.workoutRepo.CreateWorkout(w); err != nil { return nil, err } return w, nil } // ImportOptions holds optional parameters for activity import. type ImportOptions struct { WorkoutID uint // If set, update existing workout instead of creating new Title string // Custom title override EquipmentID *uint // Equipment to associate Notes string // Optional notes }