import Foundation import LCEssentials /// Explicit failure for callers that want to see it (`evaluateOrThrow`, the /// notifications client). The safe `evaluate(...)` path never surfaces this — /// it degrades to cache/defaults instead (FC-060 §4). public enum FeatureControlError: Error, Sendable, Equatable { /// 400 `FEATURE_CONTROL_CONTEXT_INVALID` case invalidContext(code: String) /// 401 `…MISSING_AUTH` / `…INVALID_MODULE_TOKEN` case unauthorized(code: String) /// 403 `…PANEL_ROLE_REQUIRED` / `…INSUFFICIENT_PERMISSIONS` case forbidden(code: String) /// 429 `FEATURE_CONTROL_RATE_LIMIT` case rateLimited(code: String) /// Any other non-2xx status. case server(code: String, status: Int) /// Decoding failure, or any error not raised by `LCEssentials.API`'s HTTP path. case transport(message: String) } enum FeatureControlErrorMapper { /// `LCEssentials.API` throws an `NSError` (domain `LCEssentials.DEFAULT_ERROR_DOMAIN`, /// `code` = HTTP status, `localizedFailureReason` = pretty-printed body) for non-2xx /// responses, or a bridged `DecodingError`/`URLError` for anything else. Only the /// former carries a real HTTP status to map. static func map(_ error: Error) -> FeatureControlError { let nsError = error as NSError guard nsError.domain == LCEssentials.DEFAULT_ERROR_DOMAIN else { return .transport(message: nsError.localizedDescription) } let status = nsError.code let bodyCode = extractCode(from: nsError.localizedFailureReason) ?? "UNKNOWN" switch status { case 400: return .invalidContext(code: bodyCode) case 401: return .unauthorized(code: bodyCode) case 403: return .forbidden(code: bodyCode) case 429: return .rateLimited(code: bodyCode) default: return .server(code: bodyCode, status: status) } } private static func extractCode(from prettyJSON: String?) -> String? { guard let prettyJSON, let data = prettyJSON.data(using: .utf8), let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let code = object["code"] as? String else { return nil } return code } }