// // 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 } print("📷 [PhotoImport] ════════════════════════════════════════════════") print("📷 [PhotoImport] Starting photo import with \(items.count) items") isProcessing = true totalCount = items.count processedCount = 0 processedPhotos = [] confirmedImports = [] selectedMatches = [:] // Load PHAssets from PhotosPickerItems var assets: [PHAsset] = [] for (index, item) in items.enumerated() { print("📷 [PhotoImport] ────────────────────────────────────────────────") print("📷 [PhotoImport] Processing item \(index + 1)/\(items.count)") print("📷 [PhotoImport] Item identifier: \(item.itemIdentifier ?? "nil")") print("📷 [PhotoImport] Item supportedContentTypes: \(item.supportedContentTypes)") if let assetId = item.itemIdentifier { let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil) print("📷 [PhotoImport] PHAsset fetch result count: \(fetchResult.count)") if let asset = fetchResult.firstObject { print("📷 [PhotoImport] ✅ Found PHAsset") print("📷 [PhotoImport] - localIdentifier: \(asset.localIdentifier)") print("📷 [PhotoImport] - mediaType: \(asset.mediaType.rawValue)") print("📷 [PhotoImport] - creationDate: \(asset.creationDate?.description ?? "nil")") print("📷 [PhotoImport] - location: \(asset.location?.description ?? "nil")") print("📷 [PhotoImport] - sourceType: \(asset.sourceType.rawValue)") print("📷 [PhotoImport] - pixelWidth: \(asset.pixelWidth)") print("📷 [PhotoImport] - pixelHeight: \(asset.pixelHeight)") assets.append(asset) } else { print("📷 [PhotoImport] ⚠️ No PHAsset found for identifier") } } else { print("📷 [PhotoImport] ⚠️ No itemIdentifier on PhotosPickerItem") } processedCount += 1 } print("📷 [PhotoImport] ────────────────────────────────────────────────") print("📷 [PhotoImport] Loaded \(assets.count) PHAssets, extracting metadata...") // Extract metadata from all assets let metadataList = await metadataExtractor.extractMetadata(from: assets) print("📷 [PhotoImport] ────────────────────────────────────────────────") print("📷 [PhotoImport] Extracted \(metadataList.count) metadata records") // Summarize metadata extraction results let withLocation = metadataList.filter { $0.hasValidLocation }.count let withDate = metadataList.filter { $0.hasValidDate }.count print("📷 [PhotoImport] Photos with location: \(withLocation)/\(metadataList.count)") print("📷 [PhotoImport] Photos with date: \(withDate)/\(metadataList.count)") // Process each photo through game matcher processedCount = 0 for (index, metadata) in metadataList.enumerated() { print("📷 [PhotoImport] Matching photo \(index + 1): date=\(metadata.captureDate?.description ?? "nil"), location=\(metadata.hasValidLocation)") let candidate = await gameMatcher.processPhotoForImport(metadata: metadata) processedPhotos.append(candidate) // Auto-confirm high-confidence matches if candidate.canAutoProcess { confirmedImports.insert(candidate.id) print("📷 [PhotoImport] ✅ Auto-confirmed match") } processedCount += 1 } print("📷 [PhotoImport] ════════════════════════════════════════════════") print("📷 [PhotoImport] Import complete: \(processedPhotos.count) photos, \(confirmedImports.count) auto-confirmed") 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( stadiumId: match.stadium.id, stadiumNameAtVisit: match.stadium.name, visitDate: match.game.dateTime, sport: match.game.sport, visitType: .game, gameId: match.game.id, homeTeamId: match.homeTeam.id, awayTeamId: match.awayTeam.id, homeTeamName: match.homeTeam.fullName, awayTeamName: match.awayTeam.fullName, finalScore: match.formattedFinalScore, scoreSource: match.formattedFinalScore != nil ? .scraped : 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 } }