import 'package:dio/dio.dart'; import 'package:http_parser/http_parser.dart'; import '../models/api_error.dart'; import '../blocs/session/session_bloc.dart'; import '../blocs/session/session_event.dart'; import '../blocs/session/session_state.dart'; class ApiClient { late final Dio _dio; final SessionBloc _sessionBloc; ApiClient(this._sessionBloc, {String baseUrl = 'https://go.b0esche.cloud'}) { _dio = Dio( BaseOptions( baseUrl: baseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration( seconds: 120, ), // Increased for file uploads and org operations ), ); _dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) { // Add JWT if available final token = _getCurrentToken(); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } return handler.next(options); }, onError: (error, handler) async { if (error.response?.statusCode == 401) { final path = error.requestOptions.path; // Do not expire session for auth endpoints; show inline error instead final isAuthEndpoint = path.startsWith('/auth/'); if (!isAuthEndpoint) { // Session expired, trigger logout _sessionBloc.add(SessionExpired()); } } return handler.next(error); }, ), ); } String get baseUrl => _dio.options.baseUrl; String? get currentToken => _getCurrentToken(); String? _getCurrentToken() { // Get from SessionBloc state final state = _sessionBloc.state; if (state is SessionActive) { return state.token; } return null; } Future get( String path, { Map? queryParameters, required T Function(dynamic data) fromJson, }) async { try { final response = await _dio.get(path, queryParameters: queryParameters); return fromJson(response.data); } on DioException catch (e) { throw _handleError(e); } } Future> getRaw( String path, { Map? queryParameters, }) async { try { final response = await _dio.get(path, queryParameters: queryParameters); return response.data; } on DioException catch (e) { throw _handleError(e); } } Future post( String path, { dynamic data, required T Function(dynamic data) fromJson, }) async { try { final response = await _dio.post(path, data: data); return fromJson(response.data); } on DioException catch (e) { throw _handleError(e); } } Future> getBytes(String path) async { try { final response = await _dio.get( path, options: Options(responseType: ResponseType.bytes), ); return response.data; } on DioException catch (e) { throw _handleError(e); } } Future> postRaw(String path, {dynamic data}) async { try { final response = await _dio.post(path, data: data); return response.data; } on DioException catch (e) { throw _handleError(e); } } Future patch( String path, { dynamic data, required T Function(dynamic data) fromJson, }) async { try { final response = await _dio.patch(path, data: data); return fromJson(response.data); } on DioException catch (e) { throw _handleError(e); } } Future put( String path, { dynamic data, required T Function(dynamic data) fromJson, }) async { try { final response = await _dio.put(path, data: data); return fromJson(response.data); } on DioException catch (e) { throw _handleError(e); } } Future> putRaw(String path, {dynamic data}) async { try { final response = await _dio.put(path, data: data); return response.data; } on DioException catch (e) { throw _handleError(e); } } Future delete(String path) async { try { await _dio.delete(path); } on DioException catch (e) { throw _handleError(e); } } Future> getList( String path, { Map? queryParameters, required T Function(dynamic data) fromJson, }) async { try { final response = await _dio.get(path, queryParameters: queryParameters); final data = response.data; if (data == null) return []; return (data as List).map(fromJson).toList(); } on DioException catch (e) { throw _handleError(e); } } // User profile methods Future> getUserProfile() async { return getRaw('/user/profile'); } Future> updateUserProfile({ required String displayName, String? email, }) async { final data = {'displayName': displayName}; if (email != null) data['email'] = email; return putRaw('/user/profile', data: data); } Future> changePassword({ required String currentPassword, required String newPassword, }) async { return postRaw( '/user/change-password', data: {'currentPassword': currentPassword, 'newPassword': newPassword}, ); } // Avatar upload Future> uploadAvatar( List imageBytes, String filename, { ProgressCallback? onSendProgress, }) async { final formData = FormData.fromMap({ 'avatar': MultipartFile.fromBytes( imageBytes, filename: filename, contentType: MediaType('image', filename.split('.').last), ), }); try { final response = await _dio.post( '/user/avatar', data: formData, onSendProgress: onSendProgress, ); return response.data; } on DioException catch (e) { throw _handleError(e); } } Future> deleteAccount() async { try { final response = await _dio.delete('/user/account'); return response.data; } on DioException catch (e) { throw _handleError(e); } } ApiError _handleError(DioException e) { final status = e.response?.statusCode; final data = e.response?.data; // Handle network errors if (e.type == DioExceptionType.connectionError || e.type == DioExceptionType.connectionTimeout || e.type == DioExceptionType.receiveTimeout || e.type == DioExceptionType.sendTimeout) { return ApiError( code: 'NETWORK_ERROR', message: 'Network error. Please check your connection and try again.', status: status, ); } // Only try to extract code/message if data is a Map String code = 'UNKNOWN'; String message = 'Unknown error'; if (data is Map) { code = data['code'] ?? 'UNKNOWN'; message = data['message'] ?? 'Unknown error'; } else if (data != null) { message = data.toString(); } return ApiError(code: code, message: message, status: status); } }