/ BalanceKit / WorkoutTracker.swift
WorkoutTracker.swift
  1  //
  2  //  WorkoutTracker.swift
  3  //  BalanceKit
  4  //
  5  //  Created by Alexander Kunau on 12.07.25.
  6  //
  7  
  8  import Foundation
  9  
 10  // MARK: - Data Versioning
 11  struct PersistedWorkoutData: Codable {
 12      let version: Int
 13      let workouts: [Workout]
 14      
 15      static let currentVersion = 1
 16  }
 17  
 18  // MARK: - Workout Types
 19  enum WorkoutType: String, CaseIterable, Identifiable, Codable {
 20      case walking = "Gehen"
 21      case running = "Laufen"
 22      case cycling = "Radfahren"
 23      case swimming = "Schwimmen"
 24      case weightLifting = "Krafttraining"
 25      case yoga = "Yoga"
 26      case hiit = "HIIT-Training"
 27      case other = "Sonstiges"
 28      
 29      var id: String { self.rawValue }
 30      
 31      // Durchschnittlicher Kalorienverbrauch pro Stunde für eine 70kg Person
 32      // Tatsächlicher Verbrauch variiert je nach Gewicht, Alter, Geschlecht, Intensität
 33      var caloriesPerHourBase: Int {
 34          switch self {
 35          case .walking: return 280
 36          case .running: return 600
 37          case .cycling: return 450
 38          case .swimming: return 500
 39          case .weightLifting: return 350
 40          case .yoga: return 250
 41          case .hiit: return 700
 42          case .other: return 400
 43          }
 44      }
 45      
 46      // Icon für die Anzeige
 47      var icon: String {
 48          switch self {
 49          case .walking: return "figure.walk"
 50          case .running: return "figure.run"
 51          case .cycling: return "bicycle"
 52          case .swimming: return "figure.pool.swim"
 53          case .weightLifting: return "dumbbell"
 54          case .yoga: return "figure.mind.and.body"
 55          case .hiit: return "figure.highintensity.intervaltraining"
 56          case .other: return "figure.mixed.cardio"
 57          }
 58      }
 59  }
 60  
 61  // Struktur für eine Trainingseinheit
 62  struct Workout: Identifiable, Codable {
 63      var id = UUID()
 64      var type: WorkoutType
 65      var duration: TimeInterval // in Sekunden
 66      var date: Date
 67      var caloriesBurned: Int
 68      var notes: String = ""
 69      
 70      // Berechnung der verbrannten Kalorien basierend auf Aktivität, Dauer und Gewicht
 71      static func calculateCalories(type: WorkoutType, durationInMinutes: Double, weightInKg: Double) -> Int {
 72          let durationInHours = durationInMinutes / 60.0
 73          
 74          // Gewichtsanpassungsfaktor (Referenzgewicht ist 70kg)
 75          let weightFactor = weightInKg / 70.0
 76          
 77          // Berechne Kalorien basierend auf Aktivität, Dauer und angepasst an das Gewicht
 78          let baseCalories = Double(type.caloriesPerHourBase) * durationInHours
 79          let adjustedCalories = baseCalories * weightFactor
 80          
 81          return Int(adjustedCalories)
 82      }
 83  }
 84  
 85  // Manager für Trainingseinheiten
 86  class WorkoutManager: ObservableObject {
 87      @Published var workouts: [Workout] = [] {
 88          didSet {
 89              save()
 90          }
 91      }
 92      
 93      // Error tracking
 94      @Published var lastError: String?
 95      
 96      private let saveKey = "SavedWorkouts"
 97      
 98      init() {
 99          load()
100      }
101      
102      // Neue Trainingseinheit hinzufügen
103      func addWorkout(type: WorkoutType, durationInMinutes: Double, date: Date, userWeight: Double, notes: String = "") {
104          let caloriesBurned = Workout.calculateCalories(type: type, durationInMinutes: durationInMinutes, weightInKg: userWeight)
105          
106          let newWorkout = Workout(
107              type: type,
108              duration: durationInMinutes * 60, // Umrechnung in Sekunden
109              date: date,
110              caloriesBurned: caloriesBurned,
111              notes: notes
112          )
113          
114          workouts.append(newWorkout)
115      }
116      
117      // Trainingseinheit löschen
118      func deleteWorkout(at indexSet: IndexSet) {
119          workouts.remove(atOffsets: indexSet)
120      }
121      
122      // Trainingseinheiten für einen bestimmten Tag
123      func workoutsForDay(_ date: Date) -> [Workout] {
124          let calendar = Calendar.current
125          let startOfDay = calendar.startOfDay(for: date)
126          guard let endOfDay = calendar.date(byAdding: .day, value: 1, to: startOfDay) else {
127              return []
128          }
129          
130          return workouts.filter { $0.date >= startOfDay && $0.date < endOfDay }
131      }
132      
133      // Gesamtzahl der verbrannten Kalorien für einen bestimmten Tag (nur manuell eingegebene Workouts)
134      func totalCaloriesBurnedForDay(_ date: Date) -> Int {
135          let dayWorkouts = workoutsForDay(date)
136          return dayWorkouts.reduce(0) { $0 + $1.caloriesBurned }
137      }
138      
139      // Gesamtzahl der verbrannten Kalorien für einen bestimmten Tag inkl. HealthKit-Daten
140      func totalCaloriesBurnedWithHealthKit(date: Date, healthManager: HealthManager?) -> Int {
141          let workoutCalories = totalCaloriesBurnedForDay(date)
142          
143          guard let healthManager = healthManager else {
144              return workoutCalories
145          }
146          
147          // Für heute können wir direkt den aktuellen Wert verwenden
148          let isToday = Calendar.current.isDateInToday(date)
149          
150          if isToday {
151              let total = workoutCalories + Int(healthManager.activeCaloriesBurned)
152              print("DEBUG WorkoutTracker (heute): workouts=\(workoutCalories), healthKit=\(Int(healthManager.activeCaloriesBurned)), total=\(total)")
153              return total
154          } else {
155              // Für andere Tage versuchen wir gecachte Daten zu verwenden
156              if let cachedData = healthManager.getCachedHealthData(for: date) {
157                  let total = workoutCalories + Int(cachedData.activeCalories)
158                  print("DEBUG WorkoutTracker (cache): workouts=\(workoutCalories), healthKit=\(Int(cachedData.activeCalories)), total=\(total)")
159                  return total
160              } else {
161                  // Wenn keine gecachten Daten vorhanden sind, versuche sie zu laden
162                  healthManager.fetchActiveCaloriesForDate(date) { calories in
163                      print("Aktive Kalorien für \(date): \(calories)")
164                  }
165                  print("DEBUG WorkoutTracker (no cache): workouts=\(workoutCalories), healthKit=0, total=\(workoutCalories)")
166                  return workoutCalories
167              }
168          }
169      }
170      
171      // Gesamtzahl der verbrannten Kalorien für eine bestimmte Woche
172      func totalCaloriesBurnedForWeek(startingFrom date: Date) -> Int {
173          let calendar = Calendar.current
174          let startOfDay = calendar.startOfDay(for: date)
175          guard let endDate = calendar.date(byAdding: .day, value: 7, to: startOfDay) else {
176              return 0
177          }
178          
179          let weekWorkouts = workouts.filter { $0.date >= startOfDay && $0.date < endDate }
180          return weekWorkouts.reduce(0) { $0 + $1.caloriesBurned }
181      }
182      
183      // Speichern der Daten in UserDefaults mit Versioning
184      private func save() {
185          do {
186              let persistedData = PersistedWorkoutData(version: PersistedWorkoutData.currentVersion, workouts: workouts)
187              let encoded = try JSONEncoder().encode(persistedData)
188              UserDefaults.standard.set(encoded, forKey: saveKey)
189              lastError = nil
190              print("✅ Workouts erfolgreich gespeichert (Version \(PersistedWorkoutData.currentVersion), \(workouts.count) Workouts)")
191          } catch {
192              lastError = "Fehler beim Speichern der Workouts: \(error.localizedDescription)"
193              print("❌ Speicherfehler Workouts: \(error)")
194          }
195      }
196      
197      // Laden der Daten aus UserDefaults mit Migration
198      private func load() {
199          guard let savedWorkouts = UserDefaults.standard.data(forKey: saveKey) else {
200              print("ℹ️ Keine gespeicherten Workouts gefunden")
201              workouts = []
202              return
203          }
204          
205          do {
206              // Versuche zuerst, versioned data zu laden
207              let persistedData = try JSONDecoder().decode(PersistedWorkoutData.self, from: savedWorkouts)
208              
209              // Validiere und lade Workouts
210              workouts = validateAndCleanWorkouts(persistedData.workouts)
211              print("✅ Workouts geladen (Version \(persistedData.version), \(workouts.count) Workouts)")
212              lastError = nil
213              
214          } catch {
215              print("⚠️ Versioned load fehlgeschlagen, versuche legacy format...")
216              
217              // Fallback: Versuche altes Format zu laden
218              do {
219                  let legacyWorkouts = try JSONDecoder().decode([Workout].self, from: savedWorkouts)
220                  
221                  // Validiere und migriere Legacy-Daten
222                  workouts = validateAndCleanWorkouts(legacyWorkouts)
223                  
224                  // Speichere sofort im neuen Format
225                  save()
226                  print("✅ Legacy Workouts erfolgreich migriert (\(workouts.count) Workouts)")
227                  
228              } catch {
229                  lastError = "Fehler beim Laden der Workouts: \(error.localizedDescription)"
230                  print("❌ Kritischer Fehler beim Laden von Workouts: \(error)")
231                  workouts = []
232              }
233          }
234      }
235      
236      // Validierung und Bereinigung von Workouts
237      private func validateAndCleanWorkouts(_ workouts: [Workout]) -> [Workout] {
238          return workouts.filter { workout in
239              // Validierungskriterien
240              let isValid = workout.duration > 0 &&
241                            workout.caloriesBurned >= 0 &&
242                            workout.date <= Date()
243              
244              if !isValid {
245                  print("⚠️ Invalides Workout gefiltert: \(workout.type.rawValue)")
246              }
247              
248              return isValid
249          }
250      }
251  }