/ BalanceKit / UserProfile.swift
UserProfile.swift
  1  //
  2  //  UserProfile.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 PersistedUserProfile: Codable {
 12      let version: Int
 13      let profile: UserProfileData
 14      
 15      static let currentVersion = 1
 16  }
 17  
 18  struct UserProfileData: Codable {
 19      var isOnboardingCompleted: Bool
 20      var age: Int
 21      var gender: Gender
 22      var height: Double
 23      var weight: Double
 24      var targetWeight: Double
 25      var activityLevel: ActivityLevel
 26      var weightGoal: WeightGoal
 27  }
 28  
 29  // MARK: - Enums
 30  enum Gender: String, CaseIterable, Identifiable, Codable {
 31      case male = "Männlich"
 32      case female = "Weiblich"
 33      
 34      var id: String { self.rawValue }
 35  }
 36  
 37  enum ActivityLevel: String, CaseIterable, Identifiable, Codable {
 38      case sedentary = "Inaktiv (Bürojob, kaum Bewegung)"
 39      case lightlyActive = "Leicht aktiv (1-3x Sport pro Woche)"
 40      case moderatelyActive = "Moderat aktiv (3-5x Sport pro Woche)"
 41      case veryActive = "Sehr aktiv (6-7x Sport pro Woche)"
 42      case extremelyActive = "Extrem aktiv (Profi-Sportler, mehrmals täglich Training)"
 43      
 44      var id: String { self.rawValue }
 45      
 46      var factor: Double {
 47          switch self {
 48          case .sedentary:
 49              return 1.2
 50          case .lightlyActive:
 51              return 1.375
 52          case .moderatelyActive:
 53              return 1.55
 54          case .veryActive:
 55              return 1.725
 56          case .extremelyActive:
 57              return 1.9
 58          }
 59      }
 60  }
 61  
 62  enum WeightGoal: String, CaseIterable, Identifiable, Codable {
 63      case lose = "Abnehmen"
 64      case maintain = "Gewicht halten"
 65      case gain = "Zunehmen"
 66      
 67      var id: String { self.rawValue }
 68      
 69      var calorieAdjustment: Double {
 70          switch self {
 71          case .lose:
 72              return 0.8  // 20% Kaloriendefizit
 73          case .maintain:
 74              return 1.0  // Keine Anpassung
 75          case .gain:
 76              return 1.1  // 10% Kalorienüberschuss
 77          }
 78      }
 79      
 80      // Makronährstoff-Verteilungen basierend auf dem Ziel
 81      var macroRatio: (protein: Double, carbs: Double, fat: Double) {
 82          switch self {
 83          case .lose:
 84              return (0.35, 0.4, 0.25)  // Höheres Protein für Muskelerhalt
 85          case .maintain:
 86              return (0.3, 0.45, 0.25)  // Ausgewogen
 87          case .gain:
 88              return (0.25, 0.5, 0.25)  // Höhere Kohlenhydrate für Energie
 89          }
 90      }
 91  }
 92  
 93  class UserProfile: ObservableObject, Codable {
 94      @Published var isOnboardingCompleted: Bool = false
 95      @Published var age: Int = 30
 96      @Published var gender: Gender = .male
 97      @Published var height: Double = 175  // in cm
 98      @Published var weight: Double = 75   // in kg
 99      @Published var targetWeight: Double = 75  // in kg
100      @Published var activityLevel: ActivityLevel = .lightlyActive
101      @Published var weightGoal: WeightGoal = .maintain
102      
103      // Error tracking
104      @Published var lastError: String?
105      
106      private enum CodingKeys: String, CodingKey {
107          case isOnboardingCompleted, age, gender, height, weight, targetWeight, activityLevel, weightGoal
108      }
109      
110      init() {
111          load()
112      }
113      
114      // Berechnung des Grundumsatzes nach Harris-Benedict-Formel
115      var basalMetabolicRate: Double {
116          if gender == .male {
117              // Männer: BMR = 66 + (13.7 × Gewicht in kg) + (5 × Größe in cm) - (6.8 × Alter in Jahren)
118              return 66 + (13.7 * weight) + (5 * height) - (6.8 * Double(age))
119          } else {
120              // Frauen: BMR = 655 + (9.6 × Gewicht in kg) + (1.8 × Größe in cm) - (4.7 × Alter in Jahren)
121              return 655 + (9.6 * weight) + (1.8 * height) - (4.7 * Double(age))
122          }
123      }
124      
125      // Berechnung des täglichen Kalorienbedarfs
126      var dailyCalorieNeeds: Int {
127          let bmrWithActivity = basalMetabolicRate * activityLevel.factor
128          let adjustedCalories = bmrWithActivity * weightGoal.calorieAdjustment
129          return Int(adjustedCalories)
130      }
131      
132      // Berechnung der empfohlenen Makronährstoffe
133      var recommendedMacros: (protein: Double, carbs: Double, fat: Double) {
134          let ratio = weightGoal.macroRatio
135          let caloriesFromProtein = Double(dailyCalorieNeeds) * ratio.protein
136          let caloriesFromCarbs = Double(dailyCalorieNeeds) * ratio.carbs
137          let caloriesFromFat = Double(dailyCalorieNeeds) * ratio.fat
138          
139          // Umrechnung von Kalorien in Gramm: Protein 4 kcal/g, Kohlenhydrate 4 kcal/g, Fett 9 kcal/g
140          let protein = caloriesFromProtein / 4
141          let carbs = caloriesFromCarbs / 4
142          let fat = caloriesFromFat / 9
143          
144          return (protein, carbs, fat)
145      }
146      
147      // MARK: - Codable Konformität
148      required init(from decoder: Decoder) throws {
149          let container = try decoder.container(keyedBy: CodingKeys.self)
150          isOnboardingCompleted = try container.decode(Bool.self, forKey: .isOnboardingCompleted)
151          age = try container.decode(Int.self, forKey: .age)
152          gender = try container.decode(Gender.self, forKey: .gender)
153          height = try container.decode(Double.self, forKey: .height)
154          weight = try container.decode(Double.self, forKey: .weight)
155          targetWeight = try container.decode(Double.self, forKey: .targetWeight)
156          activityLevel = try container.decode(ActivityLevel.self, forKey: .activityLevel)
157          weightGoal = try container.decode(WeightGoal.self, forKey: .weightGoal)
158      }
159      
160      func encode(to encoder: Encoder) throws {
161          var container = encoder.container(keyedBy: CodingKeys.self)
162          try container.encode(isOnboardingCompleted, forKey: .isOnboardingCompleted)
163          try container.encode(age, forKey: .age)
164          try container.encode(gender, forKey: .gender)
165          try container.encode(height, forKey: .height)
166          try container.encode(weight, forKey: .weight)
167          try container.encode(targetWeight, forKey: .targetWeight)
168          try container.encode(activityLevel, forKey: .activityLevel)
169          try container.encode(weightGoal, forKey: .weightGoal)
170      }
171      
172      // MARK: - Speichern und Laden mit Versioning
173      private let saveKey = "UserProfileData"
174      
175      func save() {
176          do {
177              let profileData = UserProfileData(
178                  isOnboardingCompleted: isOnboardingCompleted,
179                  age: age,
180                  gender: gender,
181                  height: height,
182                  weight: weight,
183                  targetWeight: targetWeight,
184                  activityLevel: activityLevel,
185                  weightGoal: weightGoal
186              )
187              
188              let persistedProfile = PersistedUserProfile(version: PersistedUserProfile.currentVersion, profile: profileData)
189              let encoded = try JSONEncoder().encode(persistedProfile)
190              UserDefaults.standard.set(encoded, forKey: saveKey)
191              lastError = nil
192              print("✅ UserProfile erfolgreich gespeichert (Version \(PersistedUserProfile.currentVersion))")
193          } catch {
194              lastError = "Fehler beim Speichern des Profils: \(error.localizedDescription)"
195              print("❌ Speicherfehler UserProfile: \(error)")
196          }
197      }
198      
199      private func load() {
200          guard let savedData = UserDefaults.standard.data(forKey: saveKey) else {
201              print("ℹ️ Kein gespeichertes Profil gefunden - verwende Defaults")
202              return
203          }
204          
205          do {
206              // Versuche zuerst, versioned data zu laden
207              let persistedProfile = try JSONDecoder().decode(PersistedUserProfile.self, from: savedData)
208              
209              // Validiere und lade Profil
210              let profile = validateProfile(persistedProfile.profile)
211              applyProfile(profile)
212              
213              print("✅ UserProfile geladen (Version \(persistedProfile.version))")
214              lastError = nil
215              
216          } catch {
217              print("⚠️ Versioned load fehlgeschlagen, versuche legacy format...")
218              
219              // Fallback: Versuche altes Format zu laden
220              do {
221                  let legacyProfile = try JSONDecoder().decode(UserProfile.self, from: savedData)
222                  
223                  // Kopiere Werte vom Legacy-Profil
224                  self.isOnboardingCompleted = legacyProfile.isOnboardingCompleted
225                  self.age = max(10, min(120, legacyProfile.age))
226                  self.gender = legacyProfile.gender
227                  self.height = max(50, min(250, legacyProfile.height))
228                  self.weight = max(20, min(300, legacyProfile.weight))
229                  self.targetWeight = max(20, min(300, legacyProfile.targetWeight))
230                  self.activityLevel = legacyProfile.activityLevel
231                  self.weightGoal = legacyProfile.weightGoal
232                  
233                  // Speichere im neuen Format
234                  save()
235                  print("✅ Legacy UserProfile erfolgreich migriert")
236                  
237              } catch {
238                  lastError = "Fehler beim Laden des Profils: \(error.localizedDescription)"
239                  print("❌ Kritischer Fehler beim Laden von UserProfile: \(error)")
240              }
241          }
242      }
243      
244      // Validierung des Profils
245      private func validateProfile(_ profile: UserProfileData) -> UserProfileData {
246          var validated = profile
247          
248          // Validiere Wertebereiche
249          validated.age = max(10, min(120, profile.age))
250          validated.height = max(50, min(250, profile.height))
251          validated.weight = max(20, min(300, profile.weight))
252          validated.targetWeight = max(20, min(300, profile.targetWeight))
253          
254          return validated
255      }
256      
257      // Wende validierte Profil-Daten an
258      private func applyProfile(_ profile: UserProfileData) {
259          self.isOnboardingCompleted = profile.isOnboardingCompleted
260          self.age = profile.age
261          self.gender = profile.gender
262          self.height = profile.height
263          self.weight = profile.weight
264          self.targetWeight = profile.targetWeight
265          self.activityLevel = profile.activityLevel
266          self.weightGoal = profile.weightGoal
267      }
268  }