environment_monitoring_app/lib/services/marine_tarball_sampling_service.dart

328 lines
14 KiB
Dart

// 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<Map<String, dynamic>> submitTarballSample({
required TarballSamplingData data,
required List<Map<String, dynamic>>? appSettings,
required BuildContext context,
}) async {
const String moduleName = 'marine_tarball';
final authProvider = Provider.of<AuthProvider>(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<Map<String, dynamic>> _performOnlineSubmission({
required TarballSamplingData data,
required List<Map<String, dynamic>>? 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<String, File>();
bool anyApiSuccess = false;
Map<String, dynamic> apiDataResult = {};
Map<String, dynamic> 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<String, dynamic> 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<Map<String, dynamic>> _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<Map<String, dynamic>> _generateAndUploadFtpFiles(TarballSamplingData data, Map<String, File> 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<String, dynamic> 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<String, dynamic> ftpImageResult = {'success': true, 'statuses': []};
if (imageZip != null) {
ftpImageResult = await _submissionFtpService.submit(
moduleName: moduleName,
fileToUpload: imageZip,
remotePath: '/${p.basename(imageZip.path)}',
);
}
return {
'statuses': <Map<String, dynamic>>[
...(ftpDataResult['statuses'] as List),
...(ftpImageResult['statuses'] as List),
],
};
}
Future<void> _logAndSave({
required TarballSamplingData data,
required String status,
required String message,
required List<Map<String, dynamic>> apiResults,
required List<Map<String, dynamic>> ftpStatuses,
required String serverName,
required Map<String, File> 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<void> _handleTarballSuccessAlert(TarballSamplingData data, List<Map<String, dynamic>>? 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");
}
}
}