import Foundation import PDFKit import UIKit @MainActor final class DefaultLeadDetailRepository: LeadDetailRepository { private struct FileKey: Hashable { let reference: LeadFileReference let leadID: String let salesCode: String } private let remoteService: any LeadDetailRemoteService private let cacheDirectory: URL private var cachedFiles: [FileKey: URL] = [:] private var downloads: [FileKey: Task] = [:] init(remoteService: any LeadDetailRemoteService, cacheDirectory: URL = FileManager.default.temporaryDirectory.appendingPathComponent("LeadFiles")) { self.remoteService = remoteService self.cacheDirectory = cacheDirectory } func detail(id: String, salesCode: String) async throws -> LeadDetail { do { return try await remoteService.fetchDetail(id: id, salesCode: salesCode).toDomain(expectedID: id) } catch { throw DefaultLeadRepository.map(error, leadID: id) } } func file(_ reference: LeadFileReference, leadID: String, salesCode: String) async throws -> URL { try Task.checkCancellation() let key = FileKey(reference: reference, leadID: leadID, salesCode: salesCode) if let url = cachedFiles[key], FileManager.default.fileExists(atPath: url.path) { return url } if let download = downloads[key] { let url = try await download.value try Task.checkCancellation() return url } let directory = cacheDirectory let service = remoteService // A shared download survives one viewer closing; that viewer checks cancellation below. let download = Task { let data: Data do { data = try await service.download(reference, leadID: leadID, salesCode: salesCode) } catch LeadRemoteError.http(404) { throw LeadFileError.notFound } catch { throw DefaultLeadRepository.map(error) } return try await Task.detached { try Self.persist(data, kind: reference.kind, directory: directory) }.value } downloads[key] = download do { let url = try await download.value downloads[key] = nil cachedFiles[key] = url try Task.checkCancellation() return url } catch { downloads[key] = nil throw error } } nonisolated private static func persist(_ data: Data, kind: LeadFileKind, directory: URL) throws -> URL { switch kind { case .pdf: guard let document = PDFDocument(data: data), document.pageCount > 0, !document.isLocked else { throw LeadFileError.invalidFile } case .image: guard UIImage(data: data) != nil else { throw LeadFileError.invalidFile } } do { try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) let url = directory.appendingPathComponent(UUID().uuidString) .appendingPathExtension(kind == .pdf ? "pdf" : "png") try data.write(to: url, options: .atomic) return url } catch { throw LeadFileError.storageUnavailable } } }