import Foundation struct ManifestEntry: Codable { let localIdentifier: String // PHAsset.localIdentifier — primary key let filename: String let creationDate: Date? let fileSize: Int64 let remotePath: String let uploadedAt: Date } struct BackupManifest: Codable { static let remoteFilename = ".kisani.json" var version: Int = 1 var lastUpdated: Date = Date() var entries: [ManifestEntry] = [] // Primary match: stable PHAsset localIdentifier func contains(localIdentifier id: String) -> Bool { guard !id.isEmpty else { return false } return entries.contains { $0.localIdentifier == id } } // Fallback: case-insensitive filename (for assets backed up before manifest existed) func containsByFilename(_ name: String) -> Bool { let lower = name.lowercased() return entries.contains { $0.filename.lowercased() == lower } } // Merge new entries, replacing any existing record for the same localIdentifier mutating func merge(entries newEntries: [ManifestEntry]) { let newIds = Set(newEntries.map { $0.localIdentifier }.filter { !$0.isEmpty }) entries.removeAll { !$0.localIdentifier.isEmpty && newIds.contains($0.localIdentifier) } entries.append(contentsOf: newEntries) lastUpdated = Date() } // Bootstrap from a raw NAS directory listing (no localIdentifiers known) static func buildFromNASListing(_ items: [NASItem]) -> BackupManifest { var manifest = BackupManifest() manifest.entries = items .filter { !$0.isDirectory && $0.name != remoteFilename } .map { item in ManifestEntry( localIdentifier: "", filename: item.name, creationDate: item.modifiedDate, fileSize: item.size, remotePath: item.path, uploadedAt: item.modifiedDate ?? Date() ) } return manifest } }