// lib/services/local_storage_service.dart import 'dart:io'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:path/path.dart' as p; import '../models/air_installation_data.dart'; import '../models/tarball_data.dart'; import '../models/in_situ_sampling_data.dart'; import '../models/river_in_situ_sampling_data.dart'; /// A comprehensive service for handling all local data storage for offline submissions. class LocalStorageService { // ======================================================================= // Part 1: Public Storage Setup // ======================================================================= Future _requestPermissions() async { var status = await Permission.manageExternalStorage.request(); return status.isGranted; } Future _getPublicMMSV4Directory() async { if (await _requestPermissions()) { final Directory? externalDir = await getExternalStorageDirectory(); if (externalDir != null) { final publicRootPath = externalDir.path.split('/Android/')[0]; final mmsv4Dir = Directory(p.join(publicRootPath, 'MMSV4')); if (!await mmsv4Dir.exists()) { await mmsv4Dir.create(recursive: true); } return mmsv4Dir; } } debugPrint("LocalStorageService: Manage External Storage permission was not granted."); return null; } // ======================================================================= // --- UPDATED: Part 2: Air Manual Sampling Methods --- // ======================================================================= Future _getAirManualBaseDir() async { final mmsv4Dir = await _getPublicMMSV4Directory(); if (mmsv4Dir == null) return null; final airDir = Directory(p.join(mmsv4Dir.path, 'air', 'air_manual_sampling')); if (!await airDir.exists()) { await airDir.create(recursive: true); } return airDir; } /// Saves or updates an air sampling record to a local JSON file within a folder named by its refID. /// CORRECTED: This now accepts a generic Map, which is what the service layer provides. Future saveAirSamplingRecord(Map data, String refID) async { final baseDir = await _getAirManualBaseDir(); if (baseDir == null) { debugPrint("Could not get public storage directory for Air Manual. Check permissions."); return null; } try { final eventDir = Directory(p.join(baseDir.path, refID)); if (!await eventDir.exists()) { await eventDir.create(recursive: true); } // Helper function to copy a file and return its new path Future copyImageToLocal(File? imageFile) async { if (imageFile == null) return null; try { final String fileName = p.basename(imageFile.path); final File newFile = await imageFile.copy(p.join(eventDir.path, fileName)); return newFile.path; } catch (e) { debugPrint("Error copying file ${imageFile.path}: $e"); return null; } } // This logic is now more robust; it checks for File objects and copies them if they exist. final AirInstallationData tempInstallationData = AirInstallationData.fromJson(data); data['imageFrontPath'] = await copyImageToLocal(tempInstallationData.imageFront); data['imageBackPath'] = await copyImageToLocal(tempInstallationData.imageBack); data['imageLeftPath'] = await copyImageToLocal(tempInstallationData.imageLeft); data['imageRightPath'] = await copyImageToLocal(tempInstallationData.imageRight); data['optionalImage1Path'] = await copyImageToLocal(tempInstallationData.optionalImage1); data['optionalImage2Path'] = await copyImageToLocal(tempInstallationData.optionalImage2); data['optionalImage3Path'] = await copyImageToLocal(tempInstallationData.optionalImage3); data['optionalImage4Path'] = await copyImageToLocal(tempInstallationData.optionalImage4); final jsonFile = File(p.join(eventDir.path, 'data.json')); await jsonFile.writeAsString(jsonEncode(data)); debugPrint("Air sampling log and images saved to: ${eventDir.path}"); return eventDir.path; } catch (e) { debugPrint("Error saving air sampling log to local storage: $e"); return null; } } Future>> getAllAirSamplingLogs() async { final baseDir = await _getAirManualBaseDir(); if (baseDir == null || !await baseDir.exists()) return []; try { final List> logs = []; final entities = baseDir.listSync(); for (var entity in entities) { if (entity is Directory) { final jsonFile = File(p.join(entity.path, 'data.json')); if (await jsonFile.exists()) { final content = await jsonFile.readAsString(); final data = jsonDecode(content) as Map; data['logDirectory'] = entity.path; logs.add(data); } } } return logs; } catch (e) { debugPrint("Error getting all air sampling logs: $e"); return []; } } // ======================================================================= // Part 3: Tarball Specific Methods // ======================================================================= Future _getTarballBaseDir() async { final mmsv4Dir = await _getPublicMMSV4Directory(); if (mmsv4Dir == null) return null; final tarballDir = Directory(p.join(mmsv4Dir.path, 'marine', 'marine_tarball_sampling')); if (!await tarballDir.exists()) { await tarballDir.create(recursive: true); } return tarballDir; } Future saveTarballSamplingData(TarballSamplingData data) async { final baseDir = await _getTarballBaseDir(); if (baseDir == null) { debugPrint("Could not get public storage directory. Check permissions."); return null; } try { final stationCode = data.selectedStation?['tbl_station_code'] ?? 'UNKNOWN_STATION'; final timestamp = "${data.samplingDate}_${data.samplingTime?.replaceAll(':', '-')}"; final eventFolderName = "${stationCode}_$timestamp"; final eventDir = Directory(p.join(baseDir.path, eventFolderName)); if (!await eventDir.exists()) { await eventDir.create(recursive: true); } final Map jsonData = { ...data.toFormData(), 'submissionStatus': data.submissionStatus, 'submissionMessage': data.submissionMessage, 'reportId': data.reportId }; jsonData['selectedStation'] = data.selectedStation; final imageFiles = data.toImageFiles(); for (var entry in imageFiles.entries) { final File? imageFile = entry.value; if (imageFile != null) { final String originalFileName = p.basename(imageFile.path); final File newFile = await imageFile.copy(p.join(eventDir.path, originalFileName)); jsonData[entry.key] = newFile.path; } } final jsonFile = File(p.join(eventDir.path, 'data.json')); await jsonFile.writeAsString(jsonEncode(jsonData)); debugPrint("Tarball log saved to: ${jsonFile.path}"); return eventDir.path; } catch (e) { debugPrint("Error saving tarball log to local storage: $e"); return null; } } Future>> getAllTarballLogs() async { final baseDir = await _getTarballBaseDir(); if (baseDir == null || !await baseDir.exists()) return []; try { final List> logs = []; final entities = baseDir.listSync(); for (var entity in entities) { if (entity is Directory) { final jsonFile = File(p.join(entity.path, 'data.json')); if (await jsonFile.exists()) { final content = await jsonFile.readAsString(); final data = jsonDecode(content) as Map; data['logDirectory'] = entity.path; logs.add(data); } } } return logs; } catch (e) { debugPrint("Error getting all tarball logs: $e"); return []; } } Future updateTarballLog(Map updatedLogData) async { final logDir = updatedLogData['logDirectory']; if (logDir == null) { debugPrint("Cannot update log: logDirectory key is missing."); return; } try { final jsonFile = File(p.join(logDir, 'data.json')); if (await jsonFile.exists()) { updatedLogData.remove('isResubmitting'); await jsonFile.writeAsString(jsonEncode(updatedLogData)); debugPrint("Log updated successfully at: ${jsonFile.path}"); } } catch (e) { debugPrint("Error updating tarball log: $e"); } } // ======================================================================= // Part 4: Marine In-Situ Specific Methods // ======================================================================= Future _getInSituBaseDir() async { final mmsv4Dir = await _getPublicMMSV4Directory(); if (mmsv4Dir == null) return null; final inSituDir = Directory(p.join(mmsv4Dir.path, 'marine', 'marine_in_situ_sampling')); if (!await inSituDir.exists()) { await inSituDir.create(recursive: true); } return inSituDir; } Future saveInSituSamplingData(InSituSamplingData data) async { final baseDir = await _getInSituBaseDir(); if (baseDir == null) { debugPrint("Could not get public storage directory for In-Situ. Check permissions."); return null; } try { final stationCode = data.selectedStation?['man_station_code'] ?? 'UNKNOWN_STATION'; final timestamp = "${data.samplingDate}_${data.samplingTime?.replaceAll(':', '-')}"; final eventFolderName = "${stationCode}_$timestamp"; final eventDir = Directory(p.join(baseDir.path, eventFolderName)); if (!await eventDir.exists()) { await eventDir.create(recursive: true); } final Map jsonData = { ...data.toApiFormData(), 'submissionStatus': data.submissionStatus, 'submissionMessage': data.submissionMessage, 'reportId': data.reportId }; jsonData['selectedStation'] = data.selectedStation; final imageFiles = data.toApiImageFiles(); for (var entry in imageFiles.entries) { final File? imageFile = entry.value; if (imageFile != null) { final String originalFileName = p.basename(imageFile.path); final File newFile = await imageFile.copy(p.join(eventDir.path, originalFileName)); jsonData[entry.key] = newFile.path; } } final jsonFile = File(p.join(eventDir.path, 'data.json')); await jsonFile.writeAsString(jsonEncode(jsonData)); debugPrint("In-Situ log saved to: ${jsonFile.path}"); return eventDir.path; } catch (e) { debugPrint("Error saving In-Situ log to local storage: $e"); return null; } } Future>> getAllInSituLogs() async { final baseDir = await _getInSituBaseDir(); if (baseDir == null || !await baseDir.exists()) return []; try { final List> logs = []; final entities = baseDir.listSync(); for (var entity in entities) { if (entity is Directory) { final jsonFile = File(p.join(entity.path, 'data.json')); if (await jsonFile.exists()) { final content = await jsonFile.readAsString(); final data = jsonDecode(content) as Map; data['logDirectory'] = entity.path; logs.add(data); } } } return logs; } catch (e) { debugPrint("Error getting all in-situ logs: $e"); return []; } } Future updateInSituLog(Map updatedLogData) async { final logDir = updatedLogData['logDirectory']; if (logDir == null) { debugPrint("Cannot update log: logDirectory key is missing."); return; } try { final jsonFile = File(p.join(logDir, 'data.json')); if (await jsonFile.exists()) { updatedLogData.remove('isResubmitting'); await jsonFile.writeAsString(jsonEncode(updatedLogData)); debugPrint("Log updated successfully at: ${jsonFile.path}"); } } catch (e) { debugPrint("Error updating in-situ log: $e"); } } // ======================================================================= // UPDATED: Part 5: River In-Situ Specific Methods // ======================================================================= Future _getRiverInSituBaseDir(String? samplingType) async { final mmsv4Dir = await _getPublicMMSV4Directory(); if (mmsv4Dir == null) return null; String subfolderName; if (samplingType == 'Schedule' || samplingType == 'Triennial') { subfolderName = samplingType!; } else { subfolderName = 'Others'; } final inSituDir = Directory(p.join(mmsv4Dir.path, 'river', 'river_in_situ_sampling', subfolderName)); if (!await inSituDir.exists()) { await inSituDir.create(recursive: true); } return inSituDir; } Future saveRiverInSituSamplingData(RiverInSituSamplingData data) async { final baseDir = await _getRiverInSituBaseDir(data.samplingType); if (baseDir == null) { debugPrint("Could not get public storage directory for River In-Situ. Check permissions."); return null; } try { final stationCode = data.selectedStation?['sampling_station_code'] ?? 'UNKNOWN_STATION'; final timestamp = "${data.samplingDate}_${data.samplingTime?.replaceAll(':', '-')}"; final eventFolderName = "${stationCode}_$timestamp"; final eventDir = Directory(p.join(baseDir.path, eventFolderName)); if (!await eventDir.exists()) { await eventDir.create(recursive: true); } final Map jsonData = { ...data.toApiFormData(), 'submissionStatus': data.submissionStatus, 'submissionMessage': data.submissionMessage, 'reportId': data.reportId }; jsonData['selectedStation'] = data.selectedStation; final imageFiles = data.toApiImageFiles(); for (var entry in imageFiles.entries) { final File? imageFile = entry.value; if (imageFile != null) { final String originalFileName = p.basename(imageFile.path); final File newFile = await imageFile.copy(p.join(eventDir.path, originalFileName)); jsonData[entry.key] = newFile.path; } } final jsonFile = File(p.join(eventDir.path, 'data.json')); await jsonFile.writeAsString(jsonEncode(jsonData)); debugPrint("River In-Situ log saved to: ${jsonFile.path}"); return eventDir.path; } catch (e) { debugPrint("Error saving River In-Situ log to local storage: $e"); return null; } } Future>> getAllRiverInSituLogs() async { final mmsv4Dir = await _getPublicMMSV4Directory(); if (mmsv4Dir == null) return []; final topLevelDir = Directory(p.join(mmsv4Dir.path, 'river', 'river_in_situ_sampling')); if (!await topLevelDir.exists()) return []; try { final List> logs = []; final typeSubfolders = topLevelDir.listSync(); for (var typeSubfolder in typeSubfolders) { if (typeSubfolder is Directory) { final eventFolders = typeSubfolder.listSync(); for (var eventFolder in eventFolders) { if (eventFolder is Directory) { final jsonFile = File(p.join(eventFolder.path, 'data.json')); if (await jsonFile.exists()) { final content = await jsonFile.readAsString(); final data = jsonDecode(content) as Map; data['logDirectory'] = eventFolder.path; logs.add(data); } } } } } return logs; } catch (e) { debugPrint("Error getting all river in-situ logs: $e"); return []; } } Future updateRiverInSituLog(Map updatedLogData) async { final logDir = updatedLogData['logDirectory']; if (logDir == null) { debugPrint("Cannot update log: logDirectory key is missing."); return; } try { final jsonFile = File(p.join(logDir, 'data.json')); if (await jsonFile.exists()) { updatedLogData.remove('isResubmitting'); await jsonFile.writeAsString(jsonEncode(updatedLogData)); debugPrint("Log updated successfully at: ${jsonFile.path}"); } } catch (e) { debugPrint("Error updating river in-situ log: $e"); } } }