import Foundation // MARK: - API Error enum APIError: LocalizedError { case invalidResponse case httpError(statusCode: Int, message: String) case serverError(String) case decodingError(String) case unauthorized case noData case invalidURL var errorDescription: String? { switch self { case .invalidResponse: return "Invalid response from server" case .httpError(let code, let msg): return "HTTP \(code): \(msg)" case .serverError(let msg): return msg case .decodingError(let msg): return "Failed to decode: \(msg)" case .unauthorized: return "Please log in to continue" case .noData: return "No data received" case .invalidURL: return "Invalid URL" } } } // MARK: - API Service actor APIService { static let shared = APIService() static let baseURL = "https://food.payfrit.com/api" private let session: URLSession private var userToken: String? init() { let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 30 config.timeoutIntervalForResource = 60 self.session = URLSession(configuration: config) } // MARK: - Token Management func setToken(_ token: String) { self.userToken = token } func clearToken() { self.userToken = nil } // MARK: - HTTP Methods private func request(_ endpoint: String, method: String = "GET", body: [String: Any]? = nil, includeAuth: Bool = true) async throws -> [String: Any] { guard let url = URL(string: "\(Self.baseURL)/\(endpoint)") else { throw APIError.invalidURL } var request = URLRequest(url: url) request.httpMethod = method request.setValue("application/json", forHTTPHeaderField: "Content-Type") if includeAuth, let token = userToken { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } if let body = body { request.httpBody = try JSONSerialization.data(withJSONObject: body) } let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw APIError.invalidResponse } if httpResponse.statusCode == 401 { throw APIError.unauthorized } guard (200...299).contains(httpResponse.statusCode) else { let message = String(data: data, encoding: .utf8) ?? "Unknown error" throw APIError.httpError(statusCode: httpResponse.statusCode, message: message) } guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { throw APIError.decodingError("Invalid JSON response") } // Check for API-level errors if let ok = json["ok"] as? Bool, !ok { let message = json["error"] as? String ?? json["message"] as? String ?? "Unknown error" throw APIError.serverError(message) } return json } private func get(_ endpoint: String, includeAuth: Bool = true) async throws -> [String: Any] { return try await request(endpoint, method: "GET", includeAuth: includeAuth) } private func post(_ endpoint: String, body: [String: Any], includeAuth: Bool = true) async throws -> [String: Any] { return try await request(endpoint, method: "POST", body: body, includeAuth: includeAuth) } private func delete(_ endpoint: String) async throws -> [String: Any] { return try await request(endpoint, method: "DELETE") } // MARK: - Auth Endpoints func login(email: String, password: String) async throws -> (UserProfile, String) { let json = try await post("user/login.php", body: ["email": email, "password": password], includeAuth: false) guard let token = json["token"] as? String, let userData = json["user"] as? [String: Any] else { throw APIError.decodingError("Missing token or user data") } let profile = UserProfile(json: userData) return (profile, token) } func register(email: String, password: String, name: String, zipCode: String) async throws -> (UserProfile, String) { let body: [String: Any] = [ "email": email, "password": password, "name": name, "zipCode": zipCode ] let json = try await post("user/register.php", body: body, includeAuth: false) guard let token = json["token"] as? String, let userData = json["user"] as? [String: Any] else { throw APIError.decodingError("Missing token or user data") } let profile = UserProfile(json: userData) return (profile, token) } func logout() async throws { _ = try await post("user/account.php", body: ["action": "logout"]) } func deleteAccount() async throws { _ = try await post("user/account.php", body: ["action": "delete"]) } // MARK: - User Endpoints func getProfile() async throws -> UserProfile { let json = try await get("user/account.php") guard let userData = json["user"] as? [String: Any] else { throw APIError.decodingError("Missing user data") } return UserProfile(json: userData) } func exportData() async throws -> URL { let json = try await post("user/account.php", body: ["action": "export"]) guard let urlString = json["downloadUrl"] as? String, let url = URL(string: urlString) else { throw APIError.decodingError("Missing download URL") } return url } // MARK: - Product Endpoints func lookupProduct(barcode: String) async throws -> Product { let json = try await get("scan.php?barcode=\(barcode)", includeAuth: false) // API returns product data at root level or nested under "product" let productData = (json["product"] as? [String: Any]) ?? json return Product(json: productData) } func getProduct(id: Int) async throws -> Product { let json = try await get("scan.php?id=\(id)", includeAuth: false) // API returns product data at root level or nested under "product" let productData = (json["product"] as? [String: Any]) ?? json return Product(json: productData) } func getAlternatives(productId: Int, sort: String? = nil, filters: [String]? = nil) async throws -> [Alternative] { var endpoint = "alternatives.php?productId=\(productId)" if let sort = sort { endpoint += "&sort=\(sort)" } if let filters = filters, !filters.isEmpty { endpoint += "&filters=\(filters.joined(separator: ","))" } let json = try await get(endpoint, includeAuth: false) guard let alternativesData = json["alternatives"] as? [[String: Any]] else { return [] } return alternativesData.map { Alternative(json: $0) } } // MARK: - Favorites Endpoints func getFavorites() async throws -> [Product] { let json = try await get("user/favorites.php") guard let productsData = json["products"] as? [[String: Any]] else { return [] } return productsData.map { Product(json: $0) } } func addFavorite(productId: Int) async throws { _ = try await post("user/favorites.php", body: ["action": "add", "productId": productId]) } func removeFavorite(productId: Int) async throws { _ = try await post("user/favorites.php", body: ["action": "remove", "productId": productId]) } // MARK: - History Endpoints func getHistory() async throws -> [ScanHistoryItem] { let json = try await get("user/scans.php") guard let itemsData = json["items"] as? [[String: Any]] else { return [] } return itemsData.map { ScanHistoryItem(json: $0) } } func addToHistory(productId: Int) async throws { _ = try await post("user/scans.php", body: ["productId": productId]) } } // MARK: - JSON Helpers enum JSON { static func parseInt(_ value: Any?) -> Int { if let i = value as? Int { return i } if let s = value as? String, let i = Int(s) { return i } if let d = value as? Double { return Int(d) } return 0 } static func parseDouble(_ value: Any?) -> Double { if let d = value as? Double { return d } if let i = value as? Int { return Double(i) } if let s = value as? String, let d = Double(s) { return d } return 0 } static func parseString(_ value: Any?) -> String { if let s = value as? String { return s } if let i = value as? Int { return String(i) } if let d = value as? Double { return String(d) } return "" } static func parseBool(_ value: Any?) -> Bool { if let b = value as? Bool { return b } if let i = value as? Int { return i != 0 } if let s = value as? String { return s.lowercased() == "true" || s == "1" } return false } static func parseOptionalString(_ value: Any?) -> String? { if let s = value as? String, !s.isEmpty { return s } return nil } static func parseStringArray(_ value: Any?) -> [String] { if let arr = value as? [String] { return arr } if let s = value as? String { return s.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) } } return [] } }