import Alamofire import Foundation import LilithAgentCore import LilithLogging import MacSyncShared import SwiftyJSON private let log = AppLogger.logger(for: "INote.API") // MARK: - Payload Types public struct SyncNotePayload { public let noteIdentifier: String public let folder: String? public let name: String public let body: String public let modifiedAt: String? public init( noteIdentifier: String, folder: String?, name: String, body: String, modifiedAt: String? ) { self.noteIdentifier = noteIdentifier self.folder = folder self.name = name self.body = body self.modifiedAt = modifiedAt } var dictionary: [String: Any?] { [ "noteIdentifier": noteIdentifier, "folder": folder, "name": name, "body": body, "modifiedAt": modifiedAt, ] } } public struct INoteStatsResponse { public let folderCount: Int public let noteCount: Int public let lastSyncAt: Date? public init(folderCount: Int, noteCount: Int, lastSyncAt: Date?) { self.folderCount = folderCount self.noteCount = noteCount self.lastSyncAt = lastSyncAt } } // MARK: - Protocol public protocol INoteAPIClientProtocol: AnyObject, Sendable { var isAuthenticated: Bool { get } func syncNotes(_ payloads: [SyncNotePayload]) async throws -> Int func getStats() async throws -> INoteStatsResponse func getPendingSends() async throws -> [PendingNoteSend] func reportSendResult(id: String, status: String, error: String?) async throws } // MARK: - APIClient public final class APIClient: BaseAPIClient, INoteAPIClientProtocol, @unchecked Sendable { public static let shared: APIClient = { APIClient(baseURL: macSyncResolveServerURL(), keychain: macSyncSharedKeychain) }() public var isAuthenticated: Bool { (try? getAuthToken()) != nil } public func syncNotes(_ payloads: [SyncNotePayload]) async throws -> Int { let params: [String: Any] = ["notes": payloads.map { $0.dictionary }] let data = try await authenticatedRequest("/client/inotes/sync", method: .post, parameters: params) let json = JSON(data) guard json["success"].boolValue else { let msg = json["error"]["message"].stringValue throw APIError.serverError( statusCode: json["statusCode"].intValue, message: msg.isEmpty ? "Server error" : msg ) } let synced = json["data"]["synced"].intValue log.info("syncNotes synced=\(synced)") return synced } public func getStats() async throws -> INoteStatsResponse { let data = try await authenticatedRequest("/client/inotes/stats", method: .get) let json = JSON(data) guard json["success"].boolValue else { throw APIError.serverError(statusCode: 0, message: json["error"]["message"].stringValue) } return INoteStatsResponse( folderCount: json["data"]["folderCount"].intValue, noteCount: json["data"]["noteCount"].intValue, lastSyncAt: json["data"]["lastSyncAt"].string.flatMap { ISO8601DateFormatter().date(from: $0) } ) } // MARK: - Send Queue public func getPendingSends() async throws -> [PendingNoteSend] { let data = try await authenticatedRequest("/client/inotes/send-queue/pending", method: .get) let json = JSON(data) guard json["success"].boolValue else { throw APIError.serverError(statusCode: 0, message: json["error"]["message"].stringValue) } return json["data"]["items"].arrayValue.map { item in PendingNoteSend( id: item["id"].stringValue, action: item["action"].stringValue, payload: NoteSendPayload( noteIdentifier: item["payload"]["noteIdentifier"].string, folder: item["payload"]["folder"].string, name: item["payload"]["name"].string, body: item["payload"]["body"].string ), createdAt: item["createdAt"].stringValue ) } } public func reportSendResult(id: String, status: String, error: String?) async throws { var params: [String: Any] = ["status": status] if let err = error { params["error"] = err } let data = try await authenticatedRequest("/client/inotes/send-queue/\(id)/result", method: .post, parameters: params) let json = JSON(data) guard json["success"].boolValue else { throw APIError.serverError(statusCode: 0, message: json["error"]["message"].stringValue) } } }