import Alamofire import Foundation import LilithAgentCore import LilithLogging import MacSyncShared import SwiftyJSON private let log = AppLogger.logger(for: "ICal.API") // MARK: - Payload Types public struct SyncCalendarPayload { public let calendarIdentifier: String public let title: String public let calendarType: String public let color: String public let source: String var dictionary: [String: Any?] { [ "calendarIdentifier": calendarIdentifier, "title": title, "calendarType": calendarType, "color": color, "source": source, ] } } public struct SyncEventPayload { public let eventIdentifier: String public let calendarIdentifier: String public let title: String public let notes: String? public let location: String? public let url: String? public let isAllDay: Bool public let startDate: String public let endDate: String public let createdAt: String? public let modifiedAt: String? public let status: String public let availability: String public let isDetached: Bool public let hasAlarms: Bool public let hasRecurrenceRules: Bool public let recurrenceRule: String? public let organizerEmail: String? public let organizerName: String? public let attendeeCount: Int var dictionary: [String: Any?] { [ "eventIdentifier": eventIdentifier, "calendarIdentifier": calendarIdentifier, "title": title, "notes": notes, "location": location, "url": url, "isAllDay": isAllDay, "startDate": startDate, "endDate": endDate, "createdAt": createdAt, "modifiedAt": modifiedAt, "status": status, "availability": availability, "isDetached": isDetached, "hasAlarms": hasAlarms, "hasRecurrenceRules": hasRecurrenceRules, "recurrenceRule": recurrenceRule, "organizerEmail": organizerEmail, "organizerName": organizerName, "attendeeCount": attendeeCount, ] } } public struct ICalStatsResponse { public let totalCalendars: Int public let totalEvents: Int public let lastSyncAt: Date? } // MARK: - Protocol public protocol ICalAPIClientProtocol: AnyObject, Sendable { var isAuthenticated: Bool { get } func syncCalendars(_ payloads: [SyncCalendarPayload]) async throws -> Int func syncEvents(_ payloads: [SyncEventPayload]) async throws -> Int func getStats() async throws -> ICalStatsResponse func getPendingSends() async throws -> [PendingCalendarSend] func reportSendResult(id: String, status: String, error: String?) async throws } // MARK: - APIClient public final class APIClient: BaseAPIClient, ICalAPIClientProtocol, @unchecked Sendable { public static let shared: APIClient = { APIClient(baseURL: macSyncResolveServerURL(), keychain: macSyncSharedKeychain) }() public var isAuthenticated: Bool { (try? getAuthToken()) != nil } public func syncCalendars(_ payloads: [SyncCalendarPayload]) async throws -> Int { let params: [String: Any] = ["calendars": payloads.map { $0.dictionary }] let data = try await authenticatedRequest("/client/ical/calendars", 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("syncCalendars synced=\(synced)") return synced } public func syncEvents(_ payloads: [SyncEventPayload]) async throws -> Int { let params: [String: Any] = ["events": payloads.map { $0.dictionary }] let data = try await authenticatedRequest("/client/ical/events", 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("syncEvents synced=\(synced)") return synced } // MARK: - Send Queue public func getPendingSends() async throws -> [PendingCalendarSend] { let data = try await authenticatedRequest("/client/ical/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 PendingCalendarSend( id: item["id"].stringValue, action: item["action"].stringValue, payload: CalendarSendPayload( eventIdentifier: item["payload"]["eventIdentifier"].string, calendarIdentifier: item["payload"]["calendarIdentifier"].string, title: item["payload"]["title"].string, notes: item["payload"]["notes"].string, location: item["payload"]["location"].string, startDate: item["payload"]["startDate"].string, endDate: item["payload"]["endDate"].string, isAllDay: item["payload"]["isAllDay"].bool, url: item["payload"]["url"].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/ical/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) } } public func getStats() async throws -> ICalStatsResponse { let data = try await authenticatedRequest("/client/ical/stats", method: .get) let json = JSON(data) guard json["success"].boolValue else { throw APIError.serverError(statusCode: 0, message: json["error"]["message"].stringValue) } return ICalStatsResponse( totalCalendars: json["data"]["totalCalendars"].intValue, totalEvents: json["data"]["totalEvents"].intValue, lastSyncAt: json["data"]["lastSyncAt"].string.flatMap { ISO8601DateFormatter().date(from: $0) } ) } }