// lib/services/base_api_service.dart import 'dart:convert'; import 'dart:io'; // Import for SocketException check import 'dart:async'; // Import for TimeoutException check import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import 'package:path/path.dart' as path; import 'package:environment_monitoring_app/auth_provider.dart'; /// Custom exception thrown when the API returns a 401 Unauthorized status. class SessionExpiredException implements Exception { final String message; SessionExpiredException([this.message = "Your session has expired. Please log in again."]); @override String toString() => message; } /// A low-level service for making direct HTTP requests. /// This service is now "dumb" and only sends a request to the specific /// baseUrl provided. It no longer contains logic for server fallbacks. class BaseApiService { Future> _getHeaders() async { final prefs = await SharedPreferences.getInstance(); final String? token = prefs.getString(AuthProvider.tokenKey); return { if (token != null) 'Authorization': 'Bearer $token', }; } Future> _getJsonHeaders() async { final headers = await _getHeaders(); headers['Content-Type'] = 'application/json'; return headers; } /// Generic GET request handler. Future> get(String baseUrl, String endpoint) async { try { final url = Uri.parse('$baseUrl/$endpoint'); final response = await http.get(url, headers: await _getJsonHeaders()) .timeout(const Duration(seconds: 60)); return _handleResponse(response); } on SessionExpiredException { rethrow; // CRITICAL FIX: Allow SessionExpiredException to propagate up. } on SocketException catch (e) { debugPrint('BaseApiService GET network error: $e'); rethrow; // Re-throw network-related exceptions to be caught by higher-level logic (e.g., login screen) } on TimeoutException catch (e) { debugPrint('BaseApiService GET timeout error: $e'); rethrow; // Re-throw timeout exceptions } catch (e) { debugPrint('GET request to $baseUrl failed with general error: $e'); return {'success': false, 'message': 'An unexpected error occurred: $e'}; } } /// Generic POST request handler to a specific server. Future> post(String baseUrl, String endpoint, Map body) async { try { final url = Uri.parse('$baseUrl/$endpoint'); debugPrint('Attempting POST to: $url'); final response = await http.post( url, headers: await _getJsonHeaders(), body: jsonEncode(body), ).timeout(const Duration(seconds: 60)); // Note: login.dart applies its own shorter timeout over this. return _handleResponse(response); } on SessionExpiredException { rethrow; // CRITICAL FIX: Allow SessionExpiredException to propagate up. } on SocketException catch (e) { debugPrint('BaseApiService POST network error: $e'); rethrow; // Re-throw network-related exceptions to be caught by higher-level logic (e.g., login screen) } on TimeoutException catch (e) { debugPrint('BaseApiService POST timeout error: $e'); rethrow; // Re-throw timeout exceptions } catch (e) { debugPrint('POST to $baseUrl failed with general error: $e'); return {'success': false, 'message': 'API connection failed: $e'}; } } /// Generic multipart request handler to a specific server. Future> postMultipart({ required String baseUrl, required String endpoint, required Map fields, required Map files, }) async { try { final url = Uri.parse('$baseUrl/$endpoint'); debugPrint('Attempting multipart upload to: $url'); var request = http.MultipartRequest('POST', url); final headers = await _getHeaders(); request.headers.addAll(headers); if (fields.isNotEmpty) { request.fields.addAll(fields); } for (var entry in files.entries) { if (await entry.value.exists()) { request.files.add(await http.MultipartFile.fromPath( entry.key, entry.value.path, filename: path.basename(entry.value.path), )); } else { debugPrint('File does not exist: ${entry.value.path}. Skipping this file.'); } } var streamedResponse = await request.send().timeout(const Duration(seconds: 60)); final responseBody = await streamedResponse.stream.bytesToString(); return _handleResponse(http.Response(responseBody, streamedResponse.statusCode)); } on SessionExpiredException { rethrow; // CRITICAL FIX: Allow SessionExpiredException to propagate up. } on SocketException catch (e) { debugPrint('BaseApiService Multipart network error: $e'); rethrow; // Re-throw network-related exceptions } on TimeoutException catch (e) { debugPrint('BaseApiService Multipart timeout error: $e'); rethrow; // Re-throw timeout exceptions } catch (e, s) { debugPrint('Multipart upload to $baseUrl failed. Error: $e'); debugPrint('Stack trace: $s'); return {'success': false, 'message': 'API connection failed: $e'}; } } Map _handleResponse(http.Response response) { debugPrint('Handling response. Status: ${response.statusCode}, Body: ${response.body}'); // Check for 401 Unauthorized and throw the specific exception. if (response.statusCode == 401) { throw SessionExpiredException(); } try { // Try to parse the response body as JSON. final Map responseData = jsonDecode(response.body); if (response.statusCode >= 200 && response.statusCode < 300) { // Successful API call (2xx status code) // Check application-level success flag if one exists in your API standard. // Assuming your API returns {'status': 'success', 'data': ...} or {'success': true, 'data': ...} if (responseData.containsKey('success') && responseData['success'] == false) { return {'success': false, 'message': responseData['message'] ?? 'API indicated failure.'}; } // If no explicit failure flag, or if success=true, return success. // Adjust logic based on your API's specific response structure. return { 'success': true, 'data': responseData['data'], // Assumes data is nested under 'data' key 'message': responseData['message'] ?? 'Success' }; } else { // API returned an error code (4xx, 5xx). // Return the error message provided by the server. return {'success': false, 'message': responseData['message'] ?? 'Server error: ${response.statusCode}'}; } } catch (e) { debugPrint('Failed to parse server response: $e'); return {'success': false, 'message': 'Failed to parse server response. Body: ${response.body}'}; } } }