import 'dart:convert'; import 'package:http/http.dart' as http; import '../core/services/storage_service.dart'; import '../data/models/directorate_models.dart'; /// Authenticated API client for principals, supervisors, and directorates. /// Operational data always comes from the server; connection errors stay errors. class DirectorateApiService { static const String baseUrl = String.fromEnvironment( 'SAQEL_API_BASE_URL', defaultValue: 'https://saqel.intaleqapp.com', ); static final StorageService _storage = StorageService(); static Future> _headers() async { final token = await _storage.getToken(); if (token == null || token.isEmpty) { throw StateError('جلسة الإدارة غير متوفرة. يرجى تسجيل الدخول.'); } return { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', 'X-Device-Fingerprint': 'saqel_admin_flutter', }; } static Map _decode(http.Response response) { Map data = {}; if (response.bodyBytes.isNotEmpty) { final decoded = json.decode(utf8.decode(response.bodyBytes)); if (decoded is Map) data = Map.from(decoded); } if (response.statusCode < 200 || response.statusCode >= 300) { throw StateError( data['message']?.toString() ?? 'فشل طلب الخادم (${response.statusCode}).', ); } return data; } static Future requestOtp({ required String phoneNumber, required String role, }) async { final response = await http.post( Uri.parse('$baseUrl/api/auth/otp/request'), headers: const {'Accept': 'application/json', 'Content-Type': 'application/json'}, body: json.encode({'phone_number': phoneNumber, 'role': role}), ).timeout(const Duration(seconds: 30)); _decode(response); } static Future> verifyOtp({ required String phoneNumber, required String otp, required String role, }) async { final response = await http.post( Uri.parse('$baseUrl/api/auth/otp/verify'), headers: const {'Accept': 'application/json', 'Content-Type': 'application/json'}, body: json.encode({ 'phone_number': phoneNumber, 'otp': otp, 'role': role, 'device_fingerprint': 'saqel_admin_flutter', }), ).timeout(const Duration(seconds: 30)); final data = _decode(response); final payload = Map.from(data['data'] as Map? ?? const {}); final token = payload['token']?.toString() ?? ''; if (token.isEmpty) throw StateError('لم يُرجع الخادم جلسة إدارية صالحة.'); await _storage.saveToken(token); return payload; } static Future hasValidSession() async { try { final response = await http.get( Uri.parse('$baseUrl/api/auth/me'), headers: await _headers(), ).timeout(const Duration(seconds: 15)); _decode(response); return true; } catch (_) { return false; } } static Future> fetchDirectorateDashboard() async { final response = await http.get( Uri.parse('$baseUrl/api/directorate/dashboard'), headers: await _headers(), ).timeout(const Duration(seconds: 15)); return _decode(response); } static Future> fetchSchoolDashboard({int schoolId = 1}) async { final uri = Uri.parse('$baseUrl/api/supervisor/school-dashboard').replace( queryParameters: {'school_id': '$schoolId'}, ); final response = await http.get(uri, headers: await _headers()) .timeout(const Duration(seconds: 15)); return _decode(response); } static Future recordLesson({ required int schoolId, required int teacherId, required String subject, required String gradeLevel, required String lessonTitle, required int durationMinutes, String? filePath, List? fileBytes, required String fileName, }) async { final request = http.MultipartRequest('POST', Uri.parse('$baseUrl/api/supervisor/record-lesson')); final headers = await _headers(); headers.remove('Content-Type'); request.headers.addAll(headers); request.fields.addAll({ 'school_id': '$schoolId', 'teacher_id': '$teacherId', 'subject': subject, 'grade_level': gradeLevel, 'lesson_title': lessonTitle, 'duration_minutes': '$durationMinutes', }); if (fileBytes != null) { request.files.add(http.MultipartFile.fromBytes('video', fileBytes, filename: fileName)); } else if (filePath != null) { request.files.add(await http.MultipartFile.fromPath('video', filePath, filename: fileName)); } else { throw StateError('لم يتم اختيار ملف فيديو حقيقي.'); } final streamed = await request.send().timeout(const Duration(minutes: 10)); final response = await http.Response.fromStream(streamed); final data = _decode(response); return RecordedLessonResult.fromJson( Map.from(data['data'] as Map? ?? const {}), ); } static Future pushExamToLab({required int examId, required int schoolId}) async { final response = await http.post( Uri.parse('$baseUrl/api/supervisor/exam/push-to-lab'), headers: await _headers(), body: json.encode({'exam_id': examId, 'school_id': schoolId}), ).timeout(const Duration(seconds: 30)); return _decode(response)['status'] == 'success'; } static Future uploadPanoramicSample({ required int sessionId, String? filePath, List? fileBytes, required String fileName, }) async { final request = http.MultipartRequest('POST', Uri.parse('$baseUrl/api/supervisor/exam/upload-panoramic')); final headers = await _headers(); headers.remove('Content-Type'); request.headers.addAll(headers); request.fields['session_id'] = '$sessionId'; if (fileBytes != null) { request.files.add(http.MultipartFile.fromBytes('video', fileBytes, filename: fileName)); } else if (filePath != null) { request.files.add(await http.MultipartFile.fromPath('video', filePath, filename: fileName)); } else { throw StateError('لم يتم اختيار عينة فيديو حقيقية.'); } final response = await http.Response.fromStream( await request.send().timeout(const Duration(minutes: 10)), ); return _decode(response)['status'] == 'success'; } static Future> fetchDualForms({ String subject = 'الفيزياء', String gradeLevel = 'الصف العاشر الأساسي', }) async { final uri = Uri.parse('$baseUrl/api/unified-exams/dual-forms').replace( queryParameters: {'subject': subject, 'grade_level': gradeLevel}, ); final response = await http.get(uri, headers: await _headers()) .timeout(const Duration(seconds: 30)); final data = _decode(response); return Map.from(data['data'] as Map? ?? const {}); } static Future> evaluateExamSessionIntegrity({ List> submissions = const [], }) async { final response = await http.post( Uri.parse('$baseUrl/api/unified-exams/evaluate-integrity'), headers: await _headers(), body: json.encode({'submissions': submissions}), ).timeout(const Duration(seconds: 30)); final data = _decode(response); return Map.from(data['data'] as Map? ?? const {}); } static Future> dispatchParentReports({int schoolId = 1}) async { final response = await http.post( Uri.parse('$baseUrl/api/parent-reports/dispatch'), headers: await _headers(), body: json.encode({'school_id': schoolId}), ).timeout(const Duration(minutes: 2)); return _decode(response); } static Future> importSchoolRoster({ int schoolId = 1, required List> records, }) async { final response = await http.post( Uri.parse('$baseUrl/api/school-roster/import'), headers: await _headers(), body: json.encode({'school_id': schoolId, 'records': records}), ).timeout(const Duration(minutes: 2)); return _decode(response); } }