import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import '../../core/config/app_config.dart'; import '../mock/mock_data.dart'; import 'dio_web_credentials_stub.dart' if (dart.library.html) 'dio_web_credentials_web.dart'; import 'video_encryptor_stub.dart' if (dart.library.html) 'video_encryptor_web.dart'; class ApiException implements Exception { final String message; ApiException(this.message); @override String toString() => message; } class ApiService { late final Dio _dio; String? _accessToken; String _tokenType = 'Bearer'; String? _userName; String? _userPhone; String? get currentUserName => _userName; String? get currentUserPhone => _userPhone; ApiService() { _dio = Dio( BaseOptions( baseUrl: AppConfig.baseUrl, connectTimeout: const Duration(seconds: 15), receiveTimeout: const Duration(seconds: 15), headers: {'Accept': 'application/json'}, ), ); configureWebCredentials(_dio); _dio.interceptors.add( LogInterceptor( request: true, requestHeader: true, requestBody: true, responseHeader: true, responseBody: true, error: true, logPrint: (obj) => debugPrint(obj.toString()), ), ); } Future> login( String phoneNumber, String password, ) async { if (AppConfig.isDebug) { await Future.delayed( const Duration(seconds: 1), ); // Simulate network delay // Simple mock validation if (phoneNumber.isNotEmpty && password.isNotEmpty) { final data = Map.from(MockData.loginSuccess); _bindAccessToken(data); return data; } else { throw ApiException('اطلاعات ورود نامعتبر است.'); } } else { try { final response = await _dio.post( '/auth/login', data: {'phone_number': phoneNumber, 'password': password}, ); if (response.data is Map) { final data = response.data as Map; _bindAccessToken(data); return data; } return {}; } on DioException catch (e) { throw ApiException(_formatDioError(e)); } catch (e) { throw ApiException(e.toString()); } } } void _bindAccessToken(Map payload) { final token = payload['access_token']; if (token is String && token.trim().isNotEmpty) { _accessToken = token.trim(); } final tokenType = payload['token_type']; if (tokenType is String && tokenType.trim().isNotEmpty) { _tokenType = tokenType.trim(); } if (_accessToken != null) { _dio.options.headers['Authorization'] = '$_tokenType $_accessToken'; } final user = payload['user']; if (user is Map) { final userName = user['user_name']; final userPhone = user['user_phone']; if (userName is String && userName.trim().isNotEmpty) { _userName = userName.trim(); } if (userPhone is String && userPhone.trim().isNotEmpty) { _userPhone = userPhone.trim(); } } } void clearSession() { _accessToken = null; _userName = null; _userPhone = null; _dio.options.headers.remove('Authorization'); } String _formatDioError(DioException e) { final request = e.requestOptions; final response = e.response; final uri = request.uri.toString(); final method = request.method; final statusCode = response?.statusCode; final responseData = response?.data; return 'DioException(type: ${e.type}, method: $method, uri: $uri, statusCode: $statusCode, message: ${e.message}, response: $responseData)'; } Future> getDashboardData() async { if (AppConfig.isDebug) { await Future.delayed(const Duration(seconds: 1)); return MockData.dashboardStats; } else { try { final response = await _dio.get('/dashboard'); return response.data; } catch (e) { throw ApiException(e.toString()); } } } Future> getCourses({int page = 1}) async { if (AppConfig.isDebug) { await Future.delayed(const Duration(milliseconds: 600)); return { 'data': [], 'links': { 'first': null, 'last': null, 'prev': null, 'next': null, }, 'meta': { 'current_page': 1, 'from': null, 'last_page': 1, 'path': '${AppConfig.baseUrl}/web/courses', 'per_page': 0, 'to': null, 'total': 0, 'links': [], }, }; } try { final response = await _dio.get( '/web/courses', queryParameters: {'page': page}, ); if (response.data is Map) { return response.data as Map; } return {}; } on DioException catch (e) { throw ApiException(_formatDioError(e)); } catch (e) { throw ApiException(e.toString()); } } Future> getCourseById(int courseId) async { if (AppConfig.isDebug) { await Future.delayed(const Duration(milliseconds: 500)); return { 'data': { 'id': courseId, 'teacher_name': { 'user_name': _userName ?? 'کاربر نمونه', }, 'title': 'دوره نمونه شماره $courseId', 'cover_url': null, 'description': 'توضیحات نمونه برای دوره شماره $courseId', 'price': 0, 'preview_video_url': null, }, }; } try { final response = await _dio.get('/web/courses/$courseId'); if (response.data is Map) { return response.data as Map; } return {}; } on DioException catch (e) { throw ApiException(_formatDioError(e)); } catch (e) { throw ApiException(e.toString()); } } Future> getCourseLicenses({ required int courseId, int page = 1, }) async { if (AppConfig.isDebug) { await Future.delayed(const Duration(milliseconds: 500)); return { 'data': >[ { 'activation_code': 'SPOT-$courseId-001', 'status': 'active', 'device_limits': >[ {'count': 2}, ], 'course': { 'id': courseId, 'title': 'دوره نمونه شماره $courseId', }, 'user': {'user_name': _userName ?? 'کاربر نمونه'}, }, ], 'links': { 'first': null, 'last': null, 'prev': null, 'next': null, }, 'meta': { 'current_page': 1, 'from': 1, 'last_page': 1, 'path': '${AppConfig.baseUrl}/web/courses/$courseId/licenses', 'per_page': 15, 'to': 1, 'total': 1, 'links': [], }, }; } try { final response = await _dio.get( '/web/courses/$courseId/licenses', queryParameters: {'page': page}, ); if (response.data is Map) { return response.data as Map; } return {}; } on DioException catch (e) { throw ApiException(_formatDioError(e)); } catch (e) { throw ApiException(e.toString()); } } Future> createCourseLicense({ required int courseId, required String studentPhone, String? studentName, String? studentEmail, required List> deviceLimits, }) async { if (AppConfig.isDebug) { await Future.delayed(const Duration(milliseconds: 500)); return { 'status': 'success', 'message': 'لایسنس با موفقیت ساخته شد.', 'data': { 'activation_code': 'NEW-$courseId-001', 'status': 'active', 'device_limits': deviceLimits, 'course': {'id': courseId}, 'user': { 'user_name': studentName ?? 'دانشجو', 'user_phone': studentPhone, }, }, }; } try { final response = await _dio.post( '/web/courses/$courseId/licenses', data: { 'student_phone': studentPhone, if (studentName != null && studentName.trim().isNotEmpty) 'student_name': studentName.trim(), if (studentEmail != null && studentEmail.trim().isNotEmpty) 'student_email': studentEmail.trim(), 'device_limits': deviceLimits, }, ); if (response.data is Map) { return response.data as Map; } return {}; } on DioException catch (e) { throw ApiException(_formatDioError(e)); } catch (e) { throw ApiException(e.toString()); } } Future> createCourse({ required String title, required String description, required num price, required Uint8List coverBytes, required String coverFileName, required Uint8List previewVideoBytes, required String previewVideoFileName, required String teacherPhone, required String alt, }) async { if (AppConfig.isDebug) { await Future.delayed(const Duration(milliseconds: 700)); return { 'status': 'success', 'message': 'دوره با موفقیت ساخته شد.', 'data': { 'id': 999, 'teacher_name': { 'user_name': _userName ?? 'کاربر نمونه', }, 'title': title, 'cover_url': null, 'description': description, 'price': price.toInt(), 'preview_video_url': null, }, }; } try { final formData = FormData.fromMap({ 'title': title, 'description': description, 'price': price, 'alt': alt, 'cover': MultipartFile.fromBytes(coverBytes, filename: coverFileName), 'preview_video': MultipartFile.fromBytes( previewVideoBytes, filename: previewVideoFileName, ), 'teacher_phone': teacherPhone, }); final response = await _dio.post('/web/courses', data: formData); if (response.data is Map) { return response.data as Map; } return {}; } on DioException catch (e) { throw ApiException(_formatDioError(e)); } catch (e) { throw ApiException(e.toString()); } } Future> uploadEncryptedCourseVideo({ required int courseId, required String title, required Uint8List sourceVideoBytes, void Function(int sent, int total)? onSendProgress, }) async { final encrypted = await encryptVideoForUpload(sourceVideoBytes); if (AppConfig.isDebug) { await Future.delayed(const Duration(milliseconds: 900)); return { 'status': 'success', 'message': 'ویدیوی رمزنگاری شده با موفقیت دریافت و ثبت شد.', 'data': { 'title': title, 'course_id': courseId, 'created_at': DateTime.now().toIso8601String(), 'updated_at': DateTime.now().toIso8601String(), 'content_id': encrypted.contentId, }, }; } try { final formData = FormData.fromMap({ 'title': title, 'content_id': encrypted.contentId, 'content_key': encrypted.contentKeyHex, 'video_file': MultipartFile.fromBytes( encrypted.encryptedFileBytes, filename: encrypted.encryptedFileName, ), }); final response = await _dio.post( '/web/courses/$courseId/videos', data: formData, onSendProgress: onSendProgress, ); if (response.data is Map) { return response.data as Map; } return {}; } on DioException catch (e) { throw ApiException(_formatDioError(e)); } catch (e) { throw ApiException(e.toString()); } } }