import Foundation enum BlockedUsersServiceError: LocalizedError { case unexpectedStatus(String) case decoding(debugDescription: String) case encoding(String) var errorDescription: String? { switch self { case .unexpectedStatus(let message): return message case .decoding(let debugDescription): return AppConfig.DEBUG ? debugDescription : NSLocalizedString("Не удалось загрузить список.", comment: "Blocked users service decoding error") case .encoding(let message): return message } } } struct BlockedUserPayload: Decodable { let userId: UUID let login: String let fullName: String? let customName: String? let createdAt: Date } final class BlockedUsersService { private let client: NetworkClient private let decoder: JSONDecoder init(client: NetworkClient = .shared) { self.client = client self.decoder = JSONDecoder() self.decoder.keyDecodingStrategy = .convertFromSnakeCase self.decoder.dateDecodingStrategy = .custom(Self.decodeDate) } func fetchBlockedUsers(completion: @escaping (Result<[BlockedUserPayload], Error>) -> Void) { client.request( path: "/v1/user/blacklist/list", method: .get, requiresAuth: true ) { [decoder] result in switch result { case .success(let response): do { let apiResponse = try decoder.decode(APIResponse<[BlockedUserPayload]>.self, from: response.data) guard apiResponse.status == "fine" else { let message = apiResponse.detail ?? NSLocalizedString("Не удалось загрузить список.", comment: "Blocked users service unexpected status") completion(.failure(BlockedUsersServiceError.unexpectedStatus(message))) return } completion(.success(apiResponse.data)) } catch { let debugMessage = Self.describeDecodingError(error: error, data: response.data) if AppConfig.DEBUG { print("[BlockedUsersService] decode blocked users failed: \(debugMessage)") } completion(.failure(BlockedUsersServiceError.decoding(debugDescription: debugMessage))) } case .failure(let error): if case let NetworkError.server(_, data) = error, let data, let message = Self.errorMessage(from: data) { completion(.failure(BlockedUsersServiceError.unexpectedStatus(message))) return } completion(.failure(error)) } } } func fetchBlockedUsers() async throws -> [BlockedUserPayload] { try await withCheckedThrowingContinuation { continuation in fetchBlockedUsers { result in continuation.resume(with: result) } } } func remove(userId: UUID, completion: @escaping (Result) -> Void) { let request = BlockedUserDeleteRequest(userId: userId) let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase guard let body = try? encoder.encode(request) else { let message = NSLocalizedString("Не удалось подготовить данные запроса.", comment: "Blocked users delete encoding error") completion(.failure(BlockedUsersServiceError.encoding(message))) return } client.request( path: "/v1/user/blacklist/remove", method: .delete, body: body, requiresAuth: true ) { [decoder] result in switch result { case .success(let response): do { let apiResponse = try decoder.decode(APIResponse.self, from: response.data) guard apiResponse.status == "fine" else { let message = apiResponse.detail ?? NSLocalizedString("Не удалось удалить пользователя из списка.", comment: "Blocked users delete unexpected status") completion(.failure(BlockedUsersServiceError.unexpectedStatus(message))) return } completion(.success(apiResponse.data.message)) } catch { let debugMessage = Self.describeDecodingError(error: error, data: response.data) if AppConfig.DEBUG { print("[BlockedUsersService] decode delete response failed: \(debugMessage)") } completion(.failure(BlockedUsersServiceError.decoding(debugDescription: debugMessage))) } case .failure(let error): if case let NetworkError.server(_, data) = error, let data, let message = Self.errorMessage(from: data) { completion(.failure(BlockedUsersServiceError.unexpectedStatus(message))) return } completion(.failure(error)) } } } func remove(userId: UUID) async throws -> String { try await withCheckedThrowingContinuation { continuation in remove(userId: userId) { result in continuation.resume(with: result) } } } private static func decodeDate(from decoder: Decoder) throws -> Date { let container = try decoder.singleValueContainer() let string = try container.decode(String.self) if let date = iso8601WithFractionalSeconds.date(from: string) { return date } if let date = iso8601Simple.date(from: string) { return date } throw DecodingError.dataCorruptedError( in: container, debugDescription: "Невозможно декодировать дату: \(string)" ) } private static func describeDecodingError(error: Error, data: Data) -> String { var parts: [String] = [] if let decodingError = error as? DecodingError { parts.append(decodingDescription(from: decodingError)) } else { parts.append(error.localizedDescription) } if let payload = truncatedPayload(from: data) { parts.append("payload=\(payload)") } return parts.joined(separator: "\n") } private static func decodingDescription(from error: DecodingError) -> String { switch error { case .typeMismatch(let type, let context): return "Type mismatch for \(type) at \(codingPath(from: context)): \(context.debugDescription)" case .valueNotFound(let type, let context): return "Value not found for \(type) at \(codingPath(from: context)): \(context.debugDescription)" case .keyNotFound(let key, let context): return "Missing key '\(key.stringValue)' at \(codingPath(from: context)): \(context.debugDescription)" case .dataCorrupted(let context): return "Corrupted data at \(codingPath(from: context)): \(context.debugDescription)" @unknown default: return error.localizedDescription } } private static func codingPath(from context: DecodingError.Context) -> String { let path = context.codingPath.map { $0.stringValue }.filter { !$0.isEmpty } return path.isEmpty ? "root" : path.joined(separator: ".") } private static func truncatedPayload(from data: Data, limit: Int = 512) -> String? { guard let string = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), !string.isEmpty else { return nil } if string.count <= limit { return string } let index = string.index(string.startIndex, offsetBy: limit) return String(string[string.startIndex.. String? { if let apiError = try? JSONDecoder().decode(ErrorResponse.self, from: data) { if let detail = apiError.detail, !detail.isEmpty { return detail } if let message = apiError.data?.message, !message.isEmpty { return message } } if let string = String(data: data, encoding: .utf8), !string.isEmpty { return string } return nil } private static let iso8601WithFractionalSeconds: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return formatter }() private static let iso8601Simple: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime] return formatter }() } private struct BlockedUserDeleteRequest: Encodable { let userId: UUID }