// lib/services/marine_tarball_sampling_service.dart import 'dart:io'; import 'dart:convert'; import 'dart:async'; // Added for TimeoutException import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as p; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:provider/provider.dart'; import 'package:flutter/material.dart'; import 'package:environment_monitoring_app/models/tarball_data.dart'; import 'package:environment_monitoring_app/services/local_storage_service.dart'; import 'package:environment_monitoring_app/services/server_config_service.dart'; import 'package:environment_monitoring_app/services/zipping_service.dart'; import 'package:environment_monitoring_app/services/api_service.dart'; import 'package:environment_monitoring_app/services/submission_api_service.dart'; import 'package:environment_monitoring_app/services/submission_ftp_service.dart'; import 'package:environment_monitoring_app/services/telegram_service.dart'; import 'package:environment_monitoring_app/services/retry_service.dart'; import 'package:environment_monitoring_app/auth_provider.dart'; /// A dedicated service to handle all business logic for the Marine Tarball Sampling feature. class MarineTarballSamplingService { final SubmissionApiService _submissionApiService = SubmissionApiService(); final SubmissionFtpService _submissionFtpService = SubmissionFtpService(); final ZippingService _zippingService = ZippingService(); final LocalStorageService _localStorageService = LocalStorageService(); final ServerConfigService _serverConfigService = ServerConfigService(); final DatabaseHelper _dbHelper = DatabaseHelper(); final RetryService _retryService = RetryService(); final TelegramService _telegramService; MarineTarballSamplingService(this._telegramService); Future> submitTarballSample({ required TarballSamplingData data, required List>? appSettings, required BuildContext context, }) async { const String moduleName = 'marine_tarball'; final authProvider = Provider.of(context, listen: false); final connectivityResult = await Connectivity().checkConnectivity(); bool isOnline = connectivityResult != ConnectivityResult.none; bool isOfflineSession = authProvider.isLoggedIn && (authProvider.profileData?['token']?.startsWith("offline-session-") ?? false); if (isOnline && isOfflineSession) { debugPrint("Submission initiated online during an offline session. Attempting auto-relogin..."); final bool transitionSuccess = await authProvider.checkAndTransitionToOnlineSession(); if (transitionSuccess) { isOfflineSession = false; } else { isOnline = false; } } if (isOnline && !isOfflineSession) { debugPrint("Proceeding with direct ONLINE submission..."); return await _performOnlineSubmission( data: data, appSettings: appSettings, moduleName: moduleName, authProvider: authProvider, ); } else { debugPrint("Proceeding with OFFLINE queuing mechanism..."); return await _performOfflineQueuing( data: data, moduleName: moduleName, ); } } Future> _performOnlineSubmission({ required TarballSamplingData data, required List>? appSettings, required String moduleName, required AuthProvider authProvider, }) async { final serverName = (await _serverConfigService.getActiveApiConfig())?['config_name'] as String? ?? 'Default'; final imageFiles = data.toImageFiles()..removeWhere((key, value) => value == null); final finalImageFiles = imageFiles.cast(); bool anyApiSuccess = false; Map apiDataResult = {}; Map apiImageResult = {}; // --- START: MODIFICATION FOR GRANULAR ERROR HANDLING --- // Step 1: Attempt API Submission in its own try-catch block. try { apiDataResult = await _submissionApiService.submitPost( moduleName: moduleName, endpoint: 'marine/tarball/sample', body: data.toFormData(), ); if (apiDataResult['success'] == false && (apiDataResult['message'] as String?)?.contains('Unauthorized') == true) { debugPrint("API submission failed with Unauthorized. Attempting silent relogin..."); final bool reloginSuccess = await authProvider.attemptSilentRelogin(); if (reloginSuccess) { debugPrint("Silent relogin successful. Retrying data submission..."); apiDataResult = await _submissionApiService.submitPost( moduleName: moduleName, endpoint: 'marine/tarball/sample', body: data.toFormData(), ); } } if (apiDataResult['success'] == true) { anyApiSuccess = true; data.reportId = apiDataResult['data']?['autoid']?.toString(); if (data.reportId != null) { apiImageResult = await _submissionApiService.submitMultipart( moduleName: moduleName, endpoint: 'marine/tarball/images', fields: {'autoid': data.reportId!}, files: finalImageFiles, ); if (apiImageResult['success'] != true) { anyApiSuccess = false; // Downgrade success if images fail } } else { anyApiSuccess = false; apiDataResult['message'] = 'API Error: Submission succeeded but did not return a record ID.'; } } } on SocketException catch (e) { final errorMessage = "API submission failed with network error: $e"; debugPrint(errorMessage); anyApiSuccess = false; apiDataResult = {'success': false, 'message': errorMessage}; // Manually queue the failed API tasks since the service might not have been able to await _retryService.addApiToQueue(endpoint: 'marine/tarball/sample', method: 'POST', body: data.toFormData()); if(finalImageFiles.isNotEmpty && data.reportId != null) { await _retryService.addApiToQueue(endpoint: 'marine/tarball/images', method: 'POST_MULTIPART', fields: {'autoid': data.reportId!}, files: finalImageFiles); } } on TimeoutException catch (e) { final errorMessage = "API submission timed out: $e"; debugPrint(errorMessage); anyApiSuccess = false; apiDataResult = {'success': false, 'message': errorMessage}; await _retryService.addApiToQueue(endpoint: 'marine/tarball/sample', method: 'POST', body: data.toFormData()); } // Step 2: Attempt FTP Submission in its own try-catch block. // This code will now run even if the API submission above failed. Map ftpResults = {'statuses': []}; bool anyFtpSuccess = false; try { ftpResults = await _generateAndUploadFtpFiles(data, finalImageFiles, serverName, moduleName); anyFtpSuccess = !ftpResults['statuses'].any((status) => status['success'] == false); } on SocketException catch (e) { debugPrint("FTP submission failed with network error: $e"); anyFtpSuccess = false; // Note: The underlying SubmissionFtpService already queues failed uploads, // so we just need to catch the error to prevent a crash. } on TimeoutException catch (e) { debugPrint("FTP submission timed out: $e"); anyFtpSuccess = false; } // --- END: MODIFICATION FOR GRANULAR ERROR HANDLING --- // Step 3: Determine final status based on the outcomes of the independent steps. final bool overallSuccess = anyApiSuccess || anyFtpSuccess; String finalMessage; String finalStatus; if (anyApiSuccess && anyFtpSuccess) { finalMessage = 'Data submitted successfully to all destinations.'; finalStatus = 'S4'; } else if (anyApiSuccess && !anyFtpSuccess) { finalMessage = 'Data sent to API, but some FTP uploads failed and were queued.'; finalStatus = 'S3'; } else if (!anyApiSuccess && anyFtpSuccess) { finalMessage = 'API submission failed and was queued, but files were sent to FTP successfully.'; finalStatus = 'L4'; } else { finalMessage = 'All submission attempts failed and have been queued for retry.'; finalStatus = 'L1'; } await _logAndSave( data: data, status: finalStatus, message: finalMessage, apiResults: [apiDataResult, apiImageResult], ftpStatuses: ftpResults['statuses'], serverName: serverName, finalImageFiles: finalImageFiles, ); if (overallSuccess) { _handleTarballSuccessAlert(data, appSettings, isDataOnly: finalImageFiles.isEmpty); } return {'success': overallSuccess, 'message': finalMessage, 'reportId': data.reportId}; } Future> _performOfflineQueuing({ required TarballSamplingData data, required String moduleName, }) async { final serverConfig = await _serverConfigService.getActiveApiConfig(); final serverName = serverConfig?['config_name'] as String? ?? 'Default'; final String? localLogPath = await _localStorageService.saveTarballSamplingData(data, serverName: serverName); if (localLogPath == null) { const message = "Failed to save submission to local device storage."; await _logAndSave(data: data, status: 'Error', message: message, apiResults: [], ftpStatuses: [], serverName: serverName, finalImageFiles: {}); return {'success': false, 'message': message}; } await _retryService.queueTask( type: 'tarball_submission', payload: { 'module': moduleName, 'localLogPath': localLogPath, 'serverConfig': serverConfig, }, ); const successMessage = "No internet connection. Submission has been saved and queued for upload."; await _logAndSave(data: data, status: 'Queued', message: successMessage, apiResults: [], ftpStatuses: [], serverName: serverName, finalImageFiles: {}); return {'success': true, 'message': successMessage}; } Future> _generateAndUploadFtpFiles(TarballSamplingData data, Map imageFiles, String serverName, String moduleName) async { final stationCode = data.selectedStation?['tbl_station_code'] ?? 'NA'; final fileTimestamp = "${data.samplingDate}_${data.samplingTime}".replaceAll(':', '-').replaceAll(' ', '_'); final baseFileName = '${stationCode}_$fileTimestamp'; final Directory? logDirectory = await _localStorageService.getLogDirectory( serverName: serverName, module: 'marine', subModule: 'marine_tarball_sampling', ); final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, data.reportId ?? baseFileName)) : null; if (localSubmissionDir != null && !await localSubmissionDir.exists()) { await localSubmissionDir.create(recursive: true); } final dataZip = await _zippingService.createDataZip( jsonDataMap: {'data.json': jsonEncode(data.toDbJson())}, baseFileName: baseFileName, destinationDir: localSubmissionDir, ); Map ftpDataResult = {'success': true, 'statuses': []}; if (dataZip != null) { ftpDataResult = await _submissionFtpService.submit( moduleName: moduleName, fileToUpload: dataZip, remotePath: '/${p.basename(dataZip.path)}', ); } final imageZip = await _zippingService.createImageZip( imageFiles: imageFiles.values.toList(), baseFileName: baseFileName, destinationDir: localSubmissionDir, ); Map ftpImageResult = {'success': true, 'statuses': []}; if (imageZip != null) { ftpImageResult = await _submissionFtpService.submit( moduleName: moduleName, fileToUpload: imageZip, remotePath: '/${p.basename(imageZip.path)}', ); } return { 'statuses': >[ ...(ftpDataResult['statuses'] as List), ...(ftpImageResult['statuses'] as List), ], }; } Future _logAndSave({ required TarballSamplingData data, required String status, required String message, required List> apiResults, required List> ftpStatuses, required String serverName, required Map finalImageFiles, }) async { data.submissionStatus = status; data.submissionMessage = message; final fileTimestamp = "${data.samplingDate}_${data.samplingTime}".replaceAll(':', '-').replaceAll(' ', '_'); await _localStorageService.saveTarballSamplingData(data, serverName: serverName); final logData = { 'submission_id': data.reportId ?? fileTimestamp, 'module': 'marine', 'type': 'Tarball', 'status': status, 'message': message, 'report_id': data.reportId, 'created_at': DateTime.now().toIso8601String(), 'form_data': jsonEncode(data.toDbJson()), 'image_data': jsonEncode(finalImageFiles.values.map((f) => f.path).toList()), 'server_name': serverName, 'api_status': jsonEncode(apiResults), 'ftp_status': jsonEncode(ftpStatuses), }; await _dbHelper.saveSubmissionLog(logData); } Future _handleTarballSuccessAlert(TarballSamplingData data, List>? appSettings, {required bool isDataOnly}) async { try { final message = data.generateTelegramAlertMessage(isDataOnly: isDataOnly); final bool wasSent = await _telegramService.sendAlertImmediately('marine_tarball', message, appSettings); if (!wasSent) { await _telegramService.queueMessage('marine_tarball', message, appSettings); } } catch (e) { debugPrint("Failed to handle Tarball Telegram alert: $e"); } } }