// // ImageNamingUtils.swift import CryptoKit import Foundation /// Utility for creating consistent image filenames across platforms class ImageNamingUtils { private static let imageExtension = ".jpg" private static let hashLength = 12 /// Generates a deterministic filename for a problem image static func generateImageFilename(problemId: String, imageIndex: Int) -> String { let input = "\(problemId)_\(imageIndex)" let hash = createHash(from: input) return "problem_\(hash)_\(imageIndex)\(imageExtension)" } /// Legacy method for backward compatibility static func generateImageFilename(problemId: String, timestamp: String, imageIndex: Int) -> String { return generateImageFilename(problemId: problemId, imageIndex: imageIndex) } /// Extracts problem ID from an image filename static func extractProblemIdFromFilename(_ filename: String) -> String? { guard filename.hasPrefix("problem_") && filename.hasSuffix(imageExtension) else { return nil } let nameWithoutExtension = String(filename.dropLast(imageExtension.count)) let parts = nameWithoutExtension.components(separatedBy: "_") guard parts.count == 3 && parts[0] == "problem" else { return nil } return parts[1] } /// Validates if a filename follows our naming convention static func isValidImageFilename(_ filename: String) -> Bool { guard filename.hasPrefix("problem_") && filename.hasSuffix(imageExtension) else { return false } let nameWithoutExtension = String(filename.dropLast(imageExtension.count)) let parts = nameWithoutExtension.components(separatedBy: "_") return parts.count == 3 && parts[0] == "problem" && parts[1].count == hashLength && Int(parts[2]) != nil } /// Migrates an existing filename to our naming convention static func migrateFilename(oldFilename: String, problemId: String, imageIndex: Int) -> String { if isValidImageFilename(oldFilename) { return oldFilename } return generateImageFilename(problemId: problemId, imageIndex: imageIndex) } /// Creates a deterministic hash from input string private static func createHash(from input: String) -> String { let inputData = Data(input.utf8) let hashed = SHA256.hash(data: inputData) let hashString = hashed.compactMap { String(format: "%02x", $0) }.joined() return String(hashString.prefix(hashLength)) } /// Batch renames images for a problem to use our naming convention static func batchRenameForProblem(problemId: String, existingFilenames: [String]) -> [String: String] { var renameMap: [String: String] = [:] for (index, oldFilename) in existingFilenames.enumerated() { let newFilename = generateImageFilename(problemId: problemId, imageIndex: index) if newFilename != oldFilename { renameMap[oldFilename] = newFilename } } return renameMap } /// Validates that a collection of filenames follow our naming convention static func validateFilenames(_ filenames: [String]) -> ImageValidationResult { var validImages: [String] = [] var invalidImages: [String] = [] for filename in filenames { if isValidImageFilename(filename) { validImages.append(filename) } else { invalidImages.append(filename) } } return ImageValidationResult( totalImages: filenames.count, validImages: validImages, invalidImages: invalidImages ) } /// Generates the canonical filename that should be used for a problem image static func getCanonicalImageFilename(problemId: String, imageIndex: Int) -> String { return generateImageFilename(problemId: problemId, imageIndex: imageIndex) } /// Creates a mapping of existing server filenames to canonical filenames static func createServerMigrationMap( problemId: String, serverImageFilenames: [String], localImageCount: Int ) -> [String: String] { var migrationMap: [String: String] = [:] for imageIndex in 0.. 0 else { return 100.0 } return (Double(validImages.count) / Double(totalImages)) * 100.0 } }