/ BalanceKit / FoodItem.swift
FoodItem.swift
  1  //
  2  //  FoodItem.swift
  3  //  BalanceKit
  4  //
  5  //  Created by Alexander Kunau on 12.07.25.
  6  //
  7  
  8  import Foundation
  9  import HealthKit
 10  import OSLog
 11  
 12  // MARK: - Data Versioning
 13  struct PersistedFoodData: Codable {
 14      let version: Int
 15      let foodItems: [FoodItem]
 16      
 17      static let currentVersion = 1
 18  }
 19  
 20  struct PersistedPresetsData: Codable {
 21      let version: Int
 22      let presets: [MealPreset]
 23      
 24      static let currentVersion = 1
 25  }
 26  
 27  struct PersistedGoalsData: Codable {
 28      let version: Int
 29      let calorieGoal: Int
 30      let proteinGoal: Double
 31      let carbsGoal: Double
 32      let fatGoal: Double
 33      
 34      static let currentVersion = 1
 35  }
 36  
 37  // MARK: - Meal Types
 38  enum MealType: String, Codable, CaseIterable {
 39      case breakfast = "Frühstück"
 40      case lunch = "Mittagessen"
 41      case dinner = "Abendessen"
 42      case snack = "Snack"
 43      
 44      var icon: String {
 45          switch self {
 46          case .breakfast: return "sunrise"
 47          case .lunch: return "sun.max"
 48          case .dinner: return "sunset"
 49          case .snack: return "cup.and.saucer"
 50          }
 51      }
 52  }
 53  
 54  // Struktur für ein einzelnes Nahrungsmittel
 55  struct FoodItem: Identifiable, Codable {
 56      var id = UUID()
 57      var name: String
 58      var calories: Int
 59      var protein: Double
 60      var carbs: Double
 61      var fat: Double
 62      var date: Date
 63      var mealType: MealType
 64      
 65      // Hilfsfunktionen für Filter und Gruppierung
 66      var day: Date {
 67          let calendar = Calendar.current
 68          return calendar.startOfDay(for: date)
 69      }
 70      
 71      var month: Date {
 72          let calendar = Calendar.current
 73          let components = calendar.dateComponents([.year, .month], from: date)
 74          return calendar.date(from: components)!
 75      }
 76  }
 77  
 78  // Struktur für eine Mahlzeiten-Vorlage
 79  struct MealPreset: Identifiable, Codable {
 80      var id = UUID()
 81      var name: String
 82      var mealType: MealType
 83      var foods: [PresetFoodItem]
 84      
 85      var totalCalories: Int {
 86          foods.reduce(0) { $0 + $1.calories }
 87      }
 88      
 89      var totalProtein: Double {
 90          foods.reduce(0) { $0 + $1.protein }
 91      }
 92      
 93      var totalCarbs: Double {
 94          foods.reduce(0) { $0 + $1.carbs }
 95      }
 96      
 97      var totalFat: Double {
 98          foods.reduce(0) { $0 + $1.fat }
 99      }
100  }
101  
102  // Vereinfachte Nahrungsmittel-Struktur für Vorlagen
103  struct PresetFoodItem: Identifiable, Codable {
104      var id = UUID()
105      var name: String
106      var calories: Int
107      var protein: Double
108      var carbs: Double
109      var fat: Double
110  }
111  
112  // Datenmanager für die Nahrungsmittel
113  class FoodDataManager: ObservableObject {
114      @Published var foodItems: [FoodItem] = [] {
115          didSet {
116              save()
117          }
118      }
119      
120      @Published var mealPresets: [MealPreset] = [] {
121          didSet {
122              savePresets()
123          }
124      }
125      
126      // HealthKit Integration
127      @Published var healthManager: HealthManager?
128      
129      // Neue Zielwerte
130      @Published var dailyCalorieGoal: Int = 2000 {
131          didSet {
132              saveGoals()
133          }
134      }
135      
136      @Published var dailyProteinGoal: Double = 100.0 {
137          didSet {
138              saveGoals()
139          }
140      }
141      
142      @Published var dailyCarbsGoal: Double = 250.0 {
143          didSet {
144              saveGoals()
145          }
146      }
147      
148      @Published var dailyFatGoal: Double = 70.0 {
149          didSet {
150              saveGoals()
151          }
152      }
153      
154      private let saveKey = "SavedFoodItems"
155      private let presetsSaveKey = "SavedMealPresets"
156      private let goalsSaveKey = "SavedNutritionGoals"
157      
158      // Error tracking
159      @Published var lastError: String?
160      
161      init() {
162          load()
163          loadPresets()
164          loadGoals()
165          
166          // Initialisiere HealthManager immer (auch im Simulator für UI-Tests)
167          #if DEBUG
168          Logger.persistence.debug("FoodDataManager - Initialisiere HealthManager")
169          #endif
170          healthManager = HealthManager()
171      }
172      
173      // Nahrungsmittel zur Liste hinzufügen
174      func addFoodItem(name: String, calories: Int, protein: Double, carbs: Double, fat: Double, date: Date, mealType: MealType) {
175          let newItem = FoodItem(name: name, calories: calories, protein: protein, carbs: carbs, fat: fat, date: date, mealType: mealType)
176          foodItems.append(newItem)
177          
178          // In HealthKit speichern, wenn verfügbar
179          healthManager?.saveFoodItem(name: name, calories: Double(calories), protein: protein, carbs: carbs, fat: fat)
180      }
181      
182      // Nahrungsmittel aktualisieren
183      func updateFoodItem(id: UUID, name: String, calories: Int, protein: Double, carbs: Double, fat: Double, date: Date, mealType: MealType) {
184          if let index = foodItems.firstIndex(where: { $0.id == id }) {
185              var updatedItem = foodItems[index]
186              updatedItem.name = name
187              updatedItem.calories = calories
188              updatedItem.protein = protein
189              updatedItem.carbs = carbs
190              updatedItem.fat = fat
191              updatedItem.date = date
192              updatedItem.mealType = mealType
193              
194              foodItems[index] = updatedItem
195          }
196      }
197      
198      // Nahrungsmittel aus Preset zur Liste hinzufügen
199      func addFoodItemsFromPreset(_ preset: MealPreset, date: Date) {
200          for presetItem in preset.foods {
201              let newItem = FoodItem(
202                  name: presetItem.name,
203                  calories: presetItem.calories,
204                  protein: presetItem.protein,
205                  carbs: presetItem.carbs,
206                  fat: presetItem.fat,
207                  date: date,
208                  mealType: preset.mealType
209              )
210              foodItems.append(newItem)
211          }
212      }
213      
214      // Neues Preset erstellen
215      func createPreset(name: String, mealType: MealType, foods: [PresetFoodItem]) {
216          let newPreset = MealPreset(name: name, mealType: mealType, foods: foods)
217          mealPresets.append(newPreset)
218      }
219      
220      // Preset löschen
221      func deletePreset(at index: Int) {
222          mealPresets.remove(at: index)
223      }
224      
225      // Preset löschen mit ID
226      func deletePreset(withId id: UUID) {
227          if let index = mealPresets.firstIndex(where: { $0.id == id }) {
228              mealPresets.remove(at: index)
229          }
230      }
231      
232      // Presets nach Mahlzeitentyp filtern
233      func presets(for mealType: MealType?) -> [MealPreset] {
234          if let type = mealType {
235              return mealPresets.filter { $0.mealType == type }
236          } else {
237              return mealPresets
238          }
239      }
240      
241      // Nahrungsmittel aus der Liste entfernen
242      func deleteFoodItem(at indexSet: IndexSet) {
243          foodItems.remove(atOffsets: indexSet)
244      }
245      
246      // MARK: - Helper für Datumsfilterung
247      private func foodItemsInRange(from startDate: Date, to endDate: Date) -> [FoodItem] {
248          return foodItems.filter { $0.date >= startDate && $0.date < endDate }
249      }
250      
251      // MARK: - Tageswerte
252      func totalCaloriesForDay(_ date: Date) -> Int {
253          let startOfDay = Calendar.current.startOfDay(for: date)
254          let endOfDay = Calendar.current.date(byAdding: .day, value: 1, to: startOfDay)!
255          return foodItemsInRange(from: startOfDay, to: endOfDay).reduce(0) { $0 + $1.calories }
256      }
257      
258      func totalProteinForDay(_ date: Date) -> Double {
259          let startOfDay = Calendar.current.startOfDay(for: date)
260          let endOfDay = Calendar.current.date(byAdding: .day, value: 1, to: startOfDay)!
261          return foodItemsInRange(from: startOfDay, to: endOfDay).reduce(0) { $0 + $1.protein }
262      }
263      
264      func totalCarbsForDay(_ date: Date) -> Double {
265          let startOfDay = Calendar.current.startOfDay(for: date)
266          let endOfDay = Calendar.current.date(byAdding: .day, value: 1, to: startOfDay)!
267          return foodItemsInRange(from: startOfDay, to: endOfDay).reduce(0) { $0 + $1.carbs }
268      }
269      
270      func totalFatForDay(_ date: Date) -> Double {
271          let startOfDay = Calendar.current.startOfDay(for: date)
272          let endOfDay = Calendar.current.date(byAdding: .day, value: 1, to: startOfDay)!
273          return foodItemsInRange(from: startOfDay, to: endOfDay).reduce(0) { $0 + $1.fat }
274      }
275      
276      // MARK: - Wochenwerte
277      func totalCaloriesForWeek(startingFrom date: Date) -> Int {
278          let calendar = Calendar.current
279          let startOfDay = calendar.startOfDay(for: date)
280          let endDate = calendar.date(byAdding: .day, value: 7, to: startOfDay)!
281          return foodItemsInRange(from: startOfDay, to: endDate).reduce(0) { $0 + $1.calories }
282      }
283      
284      func totalProteinForWeek(startingFrom date: Date) -> Double {
285          let calendar = Calendar.current
286          let startOfDay = calendar.startOfDay(for: date)
287          let endDate = calendar.date(byAdding: .day, value: 7, to: startOfDay)!
288          return foodItemsInRange(from: startOfDay, to: endDate).reduce(0) { $0 + $1.protein }
289      }
290      
291      func totalCarbsForWeek(startingFrom date: Date) -> Double {
292          let calendar = Calendar.current
293          let startOfDay = calendar.startOfDay(for: date)
294          let endDate = calendar.date(byAdding: .day, value: 7, to: startOfDay)!
295          return foodItemsInRange(from: startOfDay, to: endDate).reduce(0) { $0 + $1.carbs }
296      }
297      
298      func totalFatForWeek(startingFrom date: Date) -> Double {
299          let calendar = Calendar.current
300          let startOfDay = calendar.startOfDay(for: date)
301          let endDate = calendar.date(byAdding: .day, value: 7, to: startOfDay)!
302          return foodItemsInRange(from: startOfDay, to: endDate).reduce(0) { $0 + $1.fat }
303      }
304      
305      // MARK: - Monatswerte
306      func totalCaloriesForMonth(_ date: Date) -> Int {
307          let calendar = Calendar.current
308          let components = calendar.dateComponents([.year, .month], from: date)
309          guard let startOfMonth = calendar.date(from: components),
310                let nextMonth = calendar.date(byAdding: .month, value: 1, to: startOfMonth) else { return 0 }
311          return foodItemsInRange(from: startOfMonth, to: nextMonth).reduce(0) { $0 + $1.calories }
312      }
313      
314      func totalProteinForMonth(_ date: Date) -> Double {
315          let calendar = Calendar.current
316          let components = calendar.dateComponents([.year, .month], from: date)
317          guard let startOfMonth = calendar.date(from: components),
318                let nextMonth = calendar.date(byAdding: .month, value: 1, to: startOfMonth) else { return 0 }
319          return foodItemsInRange(from: startOfMonth, to: nextMonth).reduce(0) { $0 + $1.protein }
320      }
321      
322      func totalCarbsForMonth(_ date: Date) -> Double {
323          let calendar = Calendar.current
324          let components = calendar.dateComponents([.year, .month], from: date)
325          guard let startOfMonth = calendar.date(from: components),
326                let nextMonth = calendar.date(byAdding: .month, value: 1, to: startOfMonth) else { return 0 }
327          return foodItemsInRange(from: startOfMonth, to: nextMonth).reduce(0) { $0 + $1.carbs }
328      }
329      
330      func totalFatForMonth(_ date: Date) -> Double {
331          let calendar = Calendar.current
332          let components = calendar.dateComponents([.year, .month], from: date)
333          guard let startOfMonth = calendar.date(from: components),
334                let nextMonth = calendar.date(byAdding: .month, value: 1, to: startOfMonth) else { return 0 }
335          return foodItemsInRange(from: startOfMonth, to: nextMonth).reduce(0) { $0 + $1.fat }
336      }
337      
338      // MARK: - Food Items Filterung
339      func foodItemsForDay(_ date: Date, mealType: MealType? = nil) -> [FoodItem] {
340          let startOfDay = Calendar.current.startOfDay(for: date)
341          let endOfDay = Calendar.current.date(byAdding: .day, value: 1, to: startOfDay)!
342          let dayItems = foodItemsInRange(from: startOfDay, to: endOfDay)
343          
344          if let type = mealType {
345              return dayItems.filter { $0.mealType == type }
346          }
347          return dayItems
348      }
349      
350      // MARK: - Persistenz mit Versioning und Error Handling
351      private func save() {
352          do {
353              let persistedData = PersistedFoodData(version: PersistedFoodData.currentVersion, foodItems: foodItems)
354              let encoded = try JSONEncoder().encode(persistedData)
355              UserDefaults.standard.set(encoded, forKey: saveKey)
356              lastError = nil
357              Logger.persistence.info("✅ FoodItems erfolgreich gespeichert (Version \(PersistedFoodData.currentVersion))")
358          } catch {
359              lastError = "Fehler beim Speichern der Nahrungsmittel: \(error.localizedDescription)"
360              Logger.persistence.error("❌ Speicherfehler FoodItems: \(error)")
361          }
362      }
363      
364      // Laden der Daten aus UserDefaults mit Migration
365      private func load() {
366          guard let savedItems = UserDefaults.standard.data(forKey: saveKey) else {
367              Logger.persistence.info("ℹ️ Keine gespeicherten FoodItems gefunden - starte mit leerer Liste")
368              foodItems = []
369              return
370          }
371          
372          do {
373              // Versuche zuerst, versioned data zu laden
374              let persistedData = try JSONDecoder().decode(PersistedFoodData.self, from: savedItems)
375              
376              // Führe Migration durch, falls notwendig
377              foodItems = migrateFoodItems(from: persistedData.version, items: persistedData.foodItems)
378              Logger.persistence.info("✅ FoodItems geladen (Version \(persistedData.version), \(self.foodItems.count) Items)")
379              lastError = nil
380              
381          } catch {
382              Logger.persistence.warning("⚠️ Versioned load fehlgeschlagen, versuche legacy format...")
383              
384              // Fallback: Versuche altes Format zu laden (ohne Versioning)
385              do {
386                  let legacyItems = try JSONDecoder().decode([FoodItem].self, from: savedItems)
387                  
388                  // Validiere und migriere Legacy-Daten
389                  foodItems = validateAndCleanFoodItems(legacyItems)
390                  
391                  // Speichere sofort im neuen Format
392                  save()
393                  Logger.persistence.info("✅ Legacy FoodItems erfolgreich migriert (\(self.foodItems.count) Items)")
394                  
395              } catch {
396                  lastError = "Fehler beim Laden der Nahrungsmittel: \(error.localizedDescription)"
397                  Logger.persistence.error("❌ Kritischer Fehler beim Laden von FoodItems: \(error)")
398                  foodItems = []
399              }
400          }
401      }
402      
403      // Migration zwischen Versionen
404      private func migrateFoodItems(from version: Int, items: [FoodItem]) -> [FoodItem] {
405          let migratedItems = items
406          
407          // Aktuell Version 1, keine Migration nötig
408          // Bei zukünftigen Versionen hier Migrationslogik hinzufügen:
409          // if version < 2 {
410          //     migratedItems = migrateV1ToV2(migratedItems)
411          // }
412          
413          return validateAndCleanFoodItems(migratedItems)
414      }
415      
416      // Validierung und Bereinigung von FoodItems
417      private func validateAndCleanFoodItems(_ items: [FoodItem]) -> [FoodItem] {
418          return items.filter { item in
419              // Validierungskriterien
420              let isValid = item.calories >= 0 &&
421                            item.protein >= 0 &&
422                            item.carbs >= 0 &&
423                            item.fat >= 0 &&
424                            !item.name.isEmpty &&
425                            item.date <= Date()
426              
427              if !isValid {
428                  print("⚠️ Invalides FoodItem gefiltert: \(item.name)")
429              }
430              
431              return isValid
432          }
433      }
434      
435      // Speichern der Presets in UserDefaults mit Versioning
436      private func savePresets() {
437          do {
438              let persistedData = PersistedPresetsData(version: PersistedPresetsData.currentVersion, presets: mealPresets)
439              let encoded = try JSONEncoder().encode(persistedData)
440              UserDefaults.standard.set(encoded, forKey: presetsSaveKey)
441              print("✅ Presets erfolgreich gespeichert (Version \(PersistedPresetsData.currentVersion))")
442          } catch {
443              lastError = "Fehler beim Speichern der Presets: \(error.localizedDescription)"
444              print("❌ Speicherfehler Presets: \(error)")
445          }
446      }
447      
448      // Laden der Presets aus UserDefaults mit Migration
449      private func loadPresets() {
450          guard let savedPresets = UserDefaults.standard.data(forKey: presetsSaveKey) else {
451              print("ℹ️ Keine gespeicherten Presets gefunden")
452              mealPresets = []
453              return
454          }
455          
456          do {
457              let persistedData = try JSONDecoder().decode(PersistedPresetsData.self, from: savedPresets)
458              mealPresets = validateAndCleanPresets(persistedData.presets)
459              print("✅ Presets geladen (Version \(persistedData.version), \(mealPresets.count) Presets)")
460          } catch {
461              print("⚠️ Versioned load fehlgeschlagen, versuche legacy format...")
462              
463              do {
464                  let legacyPresets = try JSONDecoder().decode([MealPreset].self, from: savedPresets)
465                  mealPresets = validateAndCleanPresets(legacyPresets)
466                  savePresets() // Speichere im neuen Format
467                  print("✅ Legacy Presets erfolgreich migriert (\(mealPresets.count) Presets)")
468              } catch {
469                  lastError = "Fehler beim Laden der Presets: \(error.localizedDescription)"
470                  print("❌ Kritischer Fehler beim Laden von Presets: \(error)")
471                  mealPresets = []
472              }
473          }
474      }
475      
476      // Validierung von Presets
477      private func validateAndCleanPresets(_ presets: [MealPreset]) -> [MealPreset] {
478          return presets.filter { preset in
479              let isValid = !preset.name.isEmpty &&
480                            !preset.foods.isEmpty &&
481                            preset.foods.allSatisfy { food in
482                                food.calories >= 0 && food.protein >= 0 && food.carbs >= 0 && food.fat >= 0
483                            }
484              
485              if !isValid {
486                  print("⚠️ Invalides Preset gefiltert: \(preset.name)")
487              }
488              
489              return isValid
490          }
491      }
492      
493      // MARK: - Ziele Speichern mit Versioning
494      private func saveGoals() {
495          do {
496              let persistedData = PersistedGoalsData(
497                  version: PersistedGoalsData.currentVersion,
498                  calorieGoal: dailyCalorieGoal,
499                  proteinGoal: dailyProteinGoal,
500                  carbsGoal: dailyCarbsGoal,
501                  fatGoal: dailyFatGoal
502              )
503              let encoded = try JSONEncoder().encode(persistedData)
504              UserDefaults.standard.set(encoded, forKey: goalsSaveKey)
505              print("✅ Goals erfolgreich gespeichert")
506          } catch {
507              lastError = "Fehler beim Speichern der Ziele: \(error.localizedDescription)"
508              print("❌ Speicherfehler Goals: \(error)")
509          }
510      }
511      
512      // Laden der Zielwerte mit Migration
513      private func loadGoals() {
514          guard let savedData = UserDefaults.standard.data(forKey: goalsSaveKey) else {
515              // Legacy: Versuche altes Dictionary-Format
516              if let savedGoals = UserDefaults.standard.dictionary(forKey: goalsSaveKey) {
517                  dailyCalorieGoal = savedGoals["calories"] as? Int ?? 2000
518                  dailyProteinGoal = savedGoals["protein"] as? Double ?? 100.0
519                  dailyCarbsGoal = savedGoals["carbs"] as? Double ?? 250.0
520                  dailyFatGoal = savedGoals["fat"] as? Double ?? 70.0
521                  print("ℹ️ Legacy Goals geladen")
522                  saveGoals() // Migriere zu neuem Format
523              } else {
524                  print("ℹ️ Keine gespeicherten Goals gefunden - verwende Defaults")
525              }
526              return
527          }
528          
529          do {
530              let persistedData = try JSONDecoder().decode(PersistedGoalsData.self, from: savedData)
531              
532              // Validiere Goals
533              dailyCalorieGoal = max(0, persistedData.calorieGoal)
534              dailyProteinGoal = max(0, persistedData.proteinGoal)
535              dailyCarbsGoal = max(0, persistedData.carbsGoal)
536              dailyFatGoal = max(0, persistedData.fatGoal)
537              
538              print("✅ Goals geladen (Version \(persistedData.version))")
539          } catch {
540              lastError = "Fehler beim Laden der Ziele: \(error.localizedDescription)"
541              print("❌ Fehler beim Laden von Goals: \(error)")
542              // Behalte Defaults
543          }
544      }
545  }