import Foundation import UIKit class ExportService { static let shared = ExportService() private init() {} // MARK: - Export Formats enum ExportFormat: String, CaseIterable, Identifiable { case csv = "CSV" case json = "JSON" var id: String { rawValue } var fileExtension: String { switch self { case .csv: return "csv" case .json: return "json" } } var mimeType: String { switch self { case .csv: return "text/csv" case .json: return "application/json" } } } // MARK: - Export Data func exportToCSV( sources: [InvestmentSource], categories: [Category] ) -> String { let currencyCode = AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext).currency var csv = "Account,Category,Source,Date,Value (\(currencyCode)),Contribution (\(currencyCode)),Notes\n" for source in sources.sorted(by: { $0.name < $1.name }) { let accountName = source.account?.name ?? "Default" let categoryName = source.category?.name ?? "Uncategorized" for snapshot in source.snapshotsArray { let date = formatDate(snapshot.date) let value = formatDecimal(snapshot.decimalValue) let contribution = snapshot.contribution != nil ? formatDecimal(snapshot.decimalContribution) : "" let notes = escapeCSV(snapshot.notes ?? "") csv += "\(escapeCSV(accountName)),\(escapeCSV(categoryName)),\(escapeCSV(source.name)),\(date),\(value),\(contribution),\(notes)\n" } } return csv } func exportToJSON( sources: [InvestmentSource], categories: [Category] ) -> String { var exportData: [String: Any] = [:] exportData["exportDate"] = ISO8601DateFormatter().string(from: Date()) exportData["version"] = 2 exportData["currency"] = AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext).currency let accounts = Dictionary(grouping: sources) { $0.account?.id.uuidString ?? "default" } var accountsArray: [[String: Any]] = [] for (_, accountSources) in accounts { let account = accountSources.first?.account var accountDict: [String: Any] = [ "name": account?.name ?? "Default", "currency": account?.currency ?? exportData["currency"] as? String ?? "EUR", "inputMode": account?.inputMode ?? InputMode.simple.rawValue, "notificationFrequency": account?.notificationFrequency ?? NotificationFrequency.monthly.rawValue, "customFrequencyMonths": account?.customFrequencyMonths ?? 1 ] // Export categories for this account let categoriesById = Dictionary(uniqueKeysWithValues: categories.map { ($0.id, $0) }) let sourcesByCategory = Dictionary(grouping: accountSources) { $0.category?.id ?? UUID() } var categoriesArray: [[String: Any]] = [] for (categoryId, categorySources) in sourcesByCategory { let category = categoriesById[categoryId] var categoryDict: [String: Any] = [ "name": category?.name ?? "Uncategorized", "color": category?.colorHex ?? "#3B82F6", "icon": category?.icon ?? "chart.pie.fill" ] var sourcesArray: [[String: Any]] = [] for source in categorySources { var sourceDict: [String: Any] = [ "name": source.name, "isActive": source.isActive, "notificationFrequency": source.notificationFrequency ] var snapshotsArray: [[String: Any]] = [] for snapshot in source.snapshotsArray { var snapshotDict: [String: Any] = [ "date": ISO8601DateFormatter().string(from: snapshot.date), "value": NSDecimalNumber(decimal: snapshot.decimalValue).doubleValue ] if snapshot.contribution != nil { snapshotDict["contribution"] = NSDecimalNumber( decimal: snapshot.decimalContribution ).doubleValue } if let notes = snapshot.notes, !notes.isEmpty { snapshotDict["notes"] = notes } snapshotsArray.append(snapshotDict) } sourceDict["snapshots"] = snapshotsArray sourcesArray.append(sourceDict) } categoryDict["sources"] = sourcesArray categoriesArray.append(categoryDict) } accountDict["categories"] = categoriesArray accountsArray.append(accountDict) } exportData["accounts"] = accountsArray // Add summary let totalValue = sources.reduce(Decimal.zero) { $0 + $1.latestValue } exportData["summary"] = [ "totalSources": sources.count, "totalCategories": categories.count, "totalValue": NSDecimalNumber(decimal: totalValue).doubleValue, "totalSnapshots": sources.reduce(0) { $0 + $1.snapshotCount } ] // Convert to JSON do { let jsonData = try JSONSerialization.data( withJSONObject: exportData, options: [.prettyPrinted, .sortedKeys] ) return String(data: jsonData, encoding: .utf8) ?? "{}" } catch { print("JSON export error: \(error)") return "{}" } } // MARK: - Share func share( format: ExportFormat, sources: [InvestmentSource], categories: [Category], from viewController: UIViewController ) { let content: String let fileName: String switch format { case .csv: content = exportToCSV(sources: sources, categories: categories) fileName = "investment_tracker_export.csv" case .json: content = exportToJSON(sources: sources, categories: categories) fileName = "investment_tracker_export.json" } // Create temporary file let tempURL = FileManager.default.temporaryDirectory .appendingPathComponent(fileName) do { try content.write(to: tempURL, atomically: true, encoding: .utf8) let activityVC = UIActivityViewController( activityItems: [tempURL], applicationActivities: nil ) // iPad support if let popover = activityVC.popoverPresentationController { popover.sourceView = viewController.view popover.sourceRect = CGRect( x: viewController.view.bounds.midX, y: viewController.view.bounds.midY, width: 0, height: 0 ) } viewController.present(activityVC, animated: true) { FirebaseService.shared.logExportAttempt( format: format.rawValue, success: true ) } } catch { print("Export error: \(error)") FirebaseService.shared.logExportAttempt( format: format.rawValue, success: false ) } } // MARK: - Helpers private func formatDate(_ date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" return formatter.string(from: date) } private func formatDecimal(_ decimal: Decimal) -> String { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.minimumFractionDigits = 2 formatter.maximumFractionDigits = 2 formatter.decimalSeparator = "." formatter.groupingSeparator = "" return formatter.string(from: decimal as NSDecimalNumber) ?? "0.00" } private func escapeCSV(_ value: String) -> String { var escaped = value if escaped.contains("\"") || escaped.contains(",") || escaped.contains("\n") { escaped = escaped.replacingOccurrences(of: "\"", with: "\"\"") escaped = "\"\(escaped)\"" } return escaped } } // MARK: - Import Service (Future) extension ExportService { func importFromCSV(_ content: String) -> (sources: [ImportedSource], errors: [String]) { // Future implementation for importing data return ([], ["Import not yet implemented"]) } struct ImportedSource { let name: String let categoryName: String let snapshots: [(date: Date, value: Decimal, contribution: Decimal?, notes: String?)] } }