// // PhotoImportViewModel.swift // SportsTime // // ViewModel for photo import flow - orchestrates extraction, matching, and import. // import Foundation import SwiftUI import PhotosUI import SwiftData import Photos @MainActor @Observable final class PhotoImportViewModel { // State var showingPicker = false var isProcessing = false var processedCount = 0 var totalCount = 0 // Results var processedPhotos: [PhotoImportCandidate] = [] var confirmedImports: Set = [] var selectedMatches: [UUID: GameMatchCandidate] = [:] // Services private let metadataExtractor = PhotoMetadataExtractor.shared private let gameMatcher = GameMatcher.shared // MARK: - Computed var categorized: GameMatcher.CategorizedImports { gameMatcher.categorizeImports(processedPhotos) } var hasConfirmedImports: Bool { !confirmedImports.isEmpty } var confirmedCount: Int { confirmedImports.count } // MARK: - Photo Processing func processSelectedPhotos(_ items: [PhotosPickerItem]) async { guard !items.isEmpty else { return } isProcessing = true totalCount = items.count processedCount = 0 processedPhotos = [] confirmedImports = [] selectedMatches = [:] // Load PHAssets from PhotosPickerItems var assets: [PHAsset] = [] for item in items { if let assetId = item.itemIdentifier { let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil) if let asset = fetchResult.firstObject { assets.append(asset) } } processedCount += 1 } // Extract metadata from all assets let metadataList = await metadataExtractor.extractMetadata(from: assets) // Process each photo through game matcher processedCount = 0 for metadata in metadataList { let candidate = await gameMatcher.processPhotoForImport(metadata: metadata) processedPhotos.append(candidate) // Auto-confirm high-confidence matches if candidate.canAutoProcess { confirmedImports.insert(candidate.id) } processedCount += 1 } isProcessing = false } // MARK: - User Actions func toggleConfirmation(for candidateId: UUID) { if confirmedImports.contains(candidateId) { confirmedImports.remove(candidateId) } else { confirmedImports.insert(candidateId) } } func selectMatch(_ match: GameMatchCandidate, for candidateId: UUID) { selectedMatches[candidateId] = match confirmedImports.insert(candidateId) } func confirmAll() { for candidate in processedPhotos { if case .singleMatch = candidate.matchResult { confirmedImports.insert(candidate.id) } else if case .multipleMatches = candidate.matchResult, selectedMatches[candidate.id] != nil { confirmedImports.insert(candidate.id) } } } // MARK: - Import Creation func createVisits(modelContext: ModelContext) async { for candidate in processedPhotos { guard confirmedImports.contains(candidate.id) else { continue } // Get the match to use let matchToUse: GameMatchCandidate? switch candidate.matchResult { case .singleMatch(let match): matchToUse = match case .multipleMatches: matchToUse = selectedMatches[candidate.id] case .noMatches: matchToUse = nil } guard let match = matchToUse else { continue } // Create the visit let visit = StadiumVisit( canonicalStadiumId: match.stadium.id.uuidString, stadiumUUID: match.stadium.id, stadiumNameAtVisit: match.stadium.name, visitDate: match.game.dateTime, sport: match.game.sport, visitType: .game, homeTeamName: match.homeTeam.fullName, awayTeamName: match.awayTeam.fullName, finalScore: nil, scoreSource: nil, dataSource: .automatic, seatLocation: nil, notes: nil, photoLatitude: candidate.metadata.coordinates?.latitude, photoLongitude: candidate.metadata.coordinates?.longitude, photoCaptureDate: candidate.metadata.captureDate, source: .photoImport ) modelContext.insert(visit) } try? modelContext.save() } // MARK: - Reset func reset() { processedPhotos = [] confirmedImports = [] selectedMatches = [:] isProcessing = false processedCount = 0 totalCount = 0 } }