// lib/services/river_manual_triennial_sampling_service.dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart'; // Keep material import
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'package:image/image.dart' as img;
import 'package:geolocator/geolocator.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
import 'package:usb_serial/usb_serial.dart';
import 'dart:convert';
import 'package:intl/intl.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:provider/provider.dart'; // Keep provider import

import '../auth_provider.dart';
import 'location_service.dart';
import '../models/river_manual_triennial_sampling_data.dart';
import '../bluetooth/bluetooth_manager.dart';
import '../serial/serial_manager.dart';
import 'api_service.dart'; // Keep DatabaseHelper import
import 'package:environment_monitoring_app/services/database_helper.dart';

import 'local_storage_service.dart';
import 'server_config_service.dart';
import 'zipping_service.dart';
import 'submission_api_service.dart';
import 'submission_ftp_service.dart';
import 'telegram_service.dart';
import 'retry_service.dart';
import 'base_api_service.dart'; // Import for SessionExpiredException
import 'user_preferences_service.dart'; // ADDED


class RiverManualTriennialSamplingService {
  final LocationService _locationService = LocationService();
  final BluetoothManager _bluetoothManager = BluetoothManager();
  final SerialManager _serialManager = SerialManager();
  final SubmissionApiService _submissionApiService = SubmissionApiService();
  final SubmissionFtpService _submissionFtpService = SubmissionFtpService();
  final DatabaseHelper _dbHelper = DatabaseHelper();
  final LocalStorageService _localStorageService = LocalStorageService();
  final ServerConfigService _serverConfigService = ServerConfigService();
  final ZippingService _zippingService = ZippingService();
  final RetryService _retryService = RetryService();
  final TelegramService _telegramService;
  final UserPreferencesService _userPreferencesService = UserPreferencesService(); // ADDED
  final ImagePicker _picker = ImagePicker();

  static const platform = MethodChannel('com.example.environment_monitoring_app/usb');

  RiverManualTriennialSamplingService(this._telegramService);

  Future<Position> getCurrentLocation() => _locationService.getCurrentLocation();
  double calculateDistance(double lat1, double lon1, double lat2, double lon2) => _locationService.calculateDistance(lat1, lon1, lat2, lon2);

  Future<File?> pickAndProcessImage(ImageSource source, { required RiverManualTriennialSamplingData data, required String imageInfo, bool isRequired = false, String? stationCode}) async {
    try {
      final XFile? pickedFile = await _picker.pickImage(
        source: source,
        imageQuality: 85,
        maxWidth: 1024,
      );

      if (pickedFile == null) {
        return null;
      }

      final bytes = await pickedFile.readAsBytes();
      img.Image? originalImage = img.decodeImage(bytes);
      if (originalImage == null) {
        return null;
      }

      // ✅ FIX: Apply landscape check to ALL photos, not just required ones.
      if (originalImage.height > originalImage.width) {
        debugPrint("Image rejected: Must be in landscape orientation.");
        return null;
      }

      final String watermarkTimestamp = "${data.samplingDate} ${data.samplingTime}";
      final font = img.arial24;
      final textWidth = watermarkTimestamp.length * 12;
      img.fillRect(originalImage, x1: 5, y1: 5, x2: textWidth + 15, y2: 35, color: img.ColorRgb8(255, 255, 255),);
      img.drawString(originalImage, watermarkTimestamp, font: font, x: 10, y: 10, color: img.ColorRgb8(0, 0, 0));

      final tempDir = await getTemporaryDirectory();
      final finalStationCode = stationCode ?? 'NA';
      final fileTimestamp = "${data.samplingDate}-${data.samplingTime}".replaceAll(':', '-');
      final newFileName = "${finalStationCode}_${fileTimestamp}_${imageInfo.replaceAll(' ', '')}.jpg";
      final filePath = p.join(tempDir.path, newFileName);

      return File(filePath)..writeAsBytesSync(img.encodeJpg(originalImage));

    } catch (e) {
      debugPrint('Error in pickAndProcessImage: $e');
      return null;
    }
  }

  ValueNotifier<BluetoothConnectionState> get bluetoothConnectionState => _bluetoothManager.connectionState;
  ValueNotifier<SerialConnectionState> get serialConnectionState => _serialManager.connectionState;

  ValueNotifier<String?> get sondeId {
    if (_bluetoothManager.connectionState.value != BluetoothConnectionState.disconnected) {
      return _bluetoothManager.sondeId;
    }
    return _serialManager.sondeId;
  }

  Stream<Map<String, double>> get bluetoothDataStream => _bluetoothManager.dataStream;
  Stream<Map<String, double>> get serialDataStream => _serialManager.dataStream;
  String? get connectedBluetoothDeviceName => _bluetoothManager.connectedDeviceName.value;
  String? get connectedSerialDeviceName => _serialManager.connectedDeviceName.value;

  Future<bool> requestDevicePermissions() async {
    Map<Permission, PermissionStatus> statuses = await [
      Permission.bluetoothScan,
      Permission.bluetoothConnect,
      Permission.locationWhenInUse,
    ].request();

    if (statuses[Permission.bluetoothScan] == PermissionStatus.granted &&
        statuses[Permission.bluetoothConnect] == PermissionStatus.granted) {
      return true;
    } else {
      return false;
    }
  }

  Future<List<BluetoothDevice>> getPairedBluetoothDevices() => _bluetoothManager.getPairedDevices();
  Future<void> connectToBluetoothDevice(BluetoothDevice device) => _bluetoothManager.connect(device);
  void disconnectFromBluetooth() => _bluetoothManager.disconnect();
  void startBluetoothAutoReading({Duration? interval}) => _bluetoothManager.startAutoReading(interval: interval ?? const Duration(seconds: 2));
  void stopBluetoothAutoReading() => _bluetoothManager.stopAutoReading();
  Future<List<UsbDevice>> getAvailableSerialDevices() => _serialManager.getAvailableDevices();

  Future<bool> requestUsbPermission(UsbDevice device) async {
    try {
      return await platform.invokeMethod('requestUsbPermission', {'vid': device.vid, 'pid': device.pid}) ?? false;
    } on PlatformException catch (e) {
      debugPrint("Failed to request USB permission: '${e.message}'.");
      return false;
    }
  }

  Future<void> connectToSerialDevice(UsbDevice device) async {
    final bool permissionGranted = await requestUsbPermission(device);
    if (permissionGranted) {
      await _serialManager.connect(device);
    } else {
      throw Exception("USB permission was not granted.");
    }
  }

  void disconnectFromSerial() => _serialManager.disconnect();
  void startSerialAutoReading({Duration? interval}) => _serialManager.startAutoReading(interval: interval ?? const Duration(seconds: 2));
  void stopSerialAutoReading() => _serialManager.stopAutoReading();
  void dispose() {
    _bluetoothManager.dispose();
    _serialManager.dispose();
  }

  // --- START: NEW HELPER METHOD ---
  /// Generates a unique timestamp ID from the sampling date and time.
  String _generateTimestampId(String? date, String? time) {
    final String dateTimeString = "${date ?? ''} ${time ?? ''}";
    try {
      // Time format from model is HH:mm
      final DateTime samplingDateTime = DateFormat('yyyy-MM-dd HH:mm').parse(dateTimeString);
      return samplingDateTime.millisecondsSinceEpoch.toString();
    } catch (e) {
      // Fallback: if parsing fails, use the current time in milliseconds
      debugPrint("Could not parse '$dateTimeString' for timestamp ID, using current time. Error: $e");
      return DateTime.now().millisecondsSinceEpoch.toString();
    }
  }
  // --- END: NEW HELPER METHOD ---

  Future<Map<String, dynamic>> submitData({
    required RiverManualTriennialSamplingData data,
    required List<Map<String, dynamic>>? appSettings,
    required AuthProvider authProvider,
    String? logDirectory,
  }) async {
    const String moduleName = 'river_triennial'; // Correct module name

    // --- START: MODIFIED TO USE TIMESTAMP ID ---
    // Generate the unique timestamp ID and assign it immediately.
    // This ID will be used for filenames and as the primary submission ID.
    final String timestampId = _generateTimestampId(data.samplingDate, data.samplingTime);
    data.reportId = timestampId; // This is the primary ID now.
    // --- END: MODIFIED TO USE TIMESTAMP ID ---

    final connectivityResult = await Connectivity().checkConnectivity();
    bool isOnline = !connectivityResult.contains(ConnectivityResult.none);
    bool isOfflineSession = authProvider.isLoggedIn && (authProvider.profileData?['token']?.startsWith("offline-session-") ?? false);

    if (isOnline && isOfflineSession) {
      debugPrint("River Triennial submission online during offline session. Attempting auto-relogin...");
      try {
        final bool transitionSuccess = await authProvider.checkAndTransitionToOnlineSession();
        if (transitionSuccess) {
          isOfflineSession = false;
        } else {
          isOnline = false; // Auto-relogin failed, treat as offline
        }
      } on SessionExpiredException catch (_) {
        debugPrint("Session expired during auto-relogin check. Treating as offline.");
        isOnline = false;
      }
    }

    if (isOnline && !isOfflineSession) {
      debugPrint("Proceeding with direct ONLINE River Triennial submission...");
      return await _performOnlineSubmission(
        data: data,
        appSettings: appSettings,
        moduleName: moduleName,
        authProvider: authProvider,
        logDirectory: logDirectory,
      );
    } else {
      debugPrint("Proceeding with OFFLINE River Triennial queuing mechanism...");
      return await _performOfflineQueuing(
        data: data,
        moduleName: moduleName,
        logDirectory: logDirectory, // Pass for potential update
      );
    }
  }

  Future<Map<String, dynamic>> _performOnlineSubmission({
    required RiverManualTriennialSamplingData data,
    required List<Map<String, dynamic>>? appSettings,
    required String moduleName,
    required AuthProvider authProvider,
    String? logDirectory,
  }) async {
    // --- START FIX: Capture the status before attempting submission ---
    final String? previousStatus = data.submissionStatus;
    // --- END FIX ---

    final serverName = (await _serverConfigService.getActiveApiConfig())?['config_name'] as String? ?? 'Default';
    final imageFilesWithNulls = data.toApiImageFiles();
    imageFilesWithNulls.removeWhere((key, value) => value == null);
    final Map<String, File> finalImageFiles = imageFilesWithNulls.cast<String, File>();

    bool anyApiSuccess = false;
    Map<String, dynamic> apiDataResult = {};
    Map<String, dynamic> apiImageResult = {};
    String finalMessage = '';
    String finalStatus = '';
    bool isSessionKnownToBeExpired = false;

    // --- START: MODIFIED TO USE TIMESTAMP ID ---
    String? apiRecordId; // Will hold the DB ID (e.g., 102) from the server
    // data.reportId already contains the timestamp ID
    // --- END: MODIFIED TO USE TIMESTAMP ID ---

    // 1. Check module preferences for API
    final pref = await _userPreferencesService.getModulePreference(moduleName);
    bool isApiEnabled = pref?['is_api_enabled'] ?? true;
    bool isFtpEnabled = pref?['is_ftp_enabled'] ?? true; // --- MODIFIED: Check FTP pref early ---

    if (isApiEnabled) {
      try {
        // 1. Submit Form Data
        apiDataResult = await _submissionApiService.submitPost(
          moduleName: moduleName,
          endpoint: 'river/triennial/sample', // Correct endpoint
          body: data.toApiFormData(),
        );

        if (apiDataResult['success'] == true) {
          anyApiSuccess = true;
          // --- START: MODIFIED TO USE TIMESTAMP ID ---
          // Store the server's database ID in a separate variable.
          apiRecordId = apiDataResult['data']?['r_tri_id']?.toString(); // Correct ID key
          // --- END: MODIFIED TO USE TIMESTAMP ID ---

          if (apiRecordId != null) {
            if (finalImageFiles.isNotEmpty) {
              // 2. Submit Images
              apiImageResult = await _submissionApiService.submitMultipart(
                moduleName: moduleName,
                endpoint: 'river/triennial/images', // Correct endpoint
                // --- START: MODIFIED TO USE TIMESTAMP ID ---
                fields: {'r_tri_id': apiRecordId}, // Correct field key
                // --- END: MODIFIED TO USE TIMESTAMP ID ---
                files: finalImageFiles,
              );
              if (apiImageResult['success'] != true) {
                anyApiSuccess = false;
              }
            }
          } else {
            anyApiSuccess = false;
            // --- START: MODIFIED TO USE TIMESTAMP ID ---
            apiDataResult['message'] = 'API Error: Submission succeeded but did not return a server record ID.';
            // --- END: MODIFIED TO USE TIMESTAMP ID ---
          }
        }
        // If apiDataResult['success'] is false, SubmissionApiService queued it.

      } on SessionExpiredException catch (_) {
        debugPrint("Online submission failed due to session expiry that could not be refreshed.");
        isSessionKnownToBeExpired = true;
        anyApiSuccess = false;
        apiDataResult = {'success': false, 'message': 'Session expired and re-login failed. API submission queued.'};
        // Manually queue API calls
        await _retryService.addApiToQueue(endpoint: 'river/triennial/sample', method: 'POST', body: data.toApiFormData());
        if (finalImageFiles.isNotEmpty && apiRecordId != null) {
          // Also queue images if data call might have partially succeeded before expiry
          // --- START: MODIFIED TO USE TIMESTAMP ID ---
          await _retryService.addApiToQueue(endpoint: 'river/triennial/images', method: 'POST_MULTIPART', fields: {'r_tri_id': apiRecordId}, files: finalImageFiles);
          // --- END: MODIFIED TO USE TIMESTAMP ID ---
        }

        // --- START FIX: Queue all four JSON files ---
        // Get all potential FTP configs
        final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];

        final dataZip = await _zippingService.createDataZip(
          jsonDataMap: { // Use specific JSON structures for River Triennial FTP
            'db.json': data.toDbJson(), // Assuming similar structure is needed, adjust if different
            'river_triennial_basic_form.json': data.toBasicFormJson(),
            'river_triennial_reading.json': data.toReadingJson(),
            'river_triennial_manual_info.json': data.toManualInfoJson(),
          },
          baseFileName: _generateBaseFileName(data),
          destinationDir: null,
        );
        if (dataZip != null) {
          // Queue for each config separately
          for (final config in ftpConfigs) {
            final configId = config['ftp_config_id'];
            if (configId != null) {
              await _retryService.addFtpToQueue(
                  localFilePath: dataZip.path,
                  remotePath: '/${p.basename(dataZip.path)}',
                  ftpConfigId: configId // Provide the specific config ID
              );
            }
          }
        }
        // --- END FIX ---
      }
    } else {
      debugPrint("API submission disabled for $moduleName by user preference.");
      apiDataResult = {'success': true, 'message': 'API submission disabled by user.'};
      anyApiSuccess = true; // Treated as success since it was intentional
    }

    // 3. Submit FTP Files
    Map<String, dynamic> ftpResults = {'statuses': []};
    bool anyFtpSuccess = false;

    // --- START FIX: Check if FTP is enabled AND if it was already successful ---
    // 'L4' status means API Failed but FTP Succeeded. If re-submitting an L4 record, we skip FTP.
    // 'S4' means everything succeeded.
    bool previousFtpSuccess = data.submissionStatus == 'L4' || data.submissionStatus == 'S4';

    if (!isFtpEnabled) {
      debugPrint("FTP submission disabled for $moduleName by user preference. Skipping FTP.");
      ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'FTP disabled by user preference.', 'success': true}]};
      anyFtpSuccess = true;
    } else if (previousFtpSuccess) {
      debugPrint("FTP submission skipped because it was already successful (Status: ${data.submissionStatus}).");
      ftpResults = {'statuses': [{'status': 'Skipped', 'message': 'Already successful in previous attempt.', 'success': true}]};
      anyFtpSuccess = true;
    } else {
      // Proceed with FTP logic only if enabled AND not previously successful
      if (isSessionKnownToBeExpired) {
        debugPrint("Skipping FTP attempt due to known expired session. Manually queuing FTP tasks.");
        // --- START: MODIFIED TO USE TIMESTAMP ID ---
        final baseFileNameForQueue = _generateBaseFileName(data); // Use helper
        // --- END: MODIFIED TO USE TIMESTAMP ID ---

        // --- START FIX: Add ftpConfigId when queuing ---
        // Get all potential FTP configs
        final ftpConfigs = await _dbHelper.loadFtpConfigs() ?? [];

        final dataZip = await _zippingService.createDataZip(
          jsonDataMap: { // Use specific JSON structures for River Triennial FTP
            'db.json': data.toDbJson(), // Assuming similar structure is needed, adjust if different
            'river_triennial_basic_form.json': data.toBasicFormJson(), // ADDED
            'river_triennial_reading.json': data.toReadingJson(),       // ADDED
            'river_triennial_manual_info.json': data.toManualInfoJson(), // ADDED
          },
          baseFileName: baseFileNameForQueue,
          destinationDir: null,
        );
        if (dataZip != null) {
          // Queue for each config separately
          for (final config in ftpConfigs) {
            final configId = config['ftp_config_id'];
            if (configId != null) {
              await _retryService.addFtpToQueue(
                  localFilePath: dataZip.path,
                  remotePath: '/${p.basename(dataZip.path)}',
                  ftpConfigId: configId // Provide the specific config ID
              );
            }
          }
        }

        if (finalImageFiles.isNotEmpty) {
          // Note: For the session expired case, renaming logic would ideally be here too,
          // but requires complex reconstruction of the map. Following the previous pattern,
          // we attempt to respect the rename if possible.
          final Map<String, File> retryImages = {};
          final String dateStr = (data.samplingDate ?? '').replaceAll('-', '');
          final String timeStr = (data.samplingTime ?? '').replaceAll(':', '');
          final String timestampId = "$dateStr$timeStr";

          void addRetryMap(File? file, String prefix) {
            if(file != null) retryImages['${prefix}_$timestampId.jpg'] = file;
          }
          addRetryMap(data.backgroundStationImage, 'background');
          addRetryMap(data.upstreamRiverImage, 'upstream');
          addRetryMap(data.downstreamRiverImage, 'downstream');
          addRetryMap(data.sampleTurbidityImage, 'sample_turbidity');
          addRetryMap(data.optionalImage1, 'optional_1');
          addRetryMap(data.optionalImage2, 'optional_2');
          addRetryMap(data.optionalImage3, 'optional_3');
          addRetryMap(data.optionalImage4, 'optional_4');

          final retryImageZip = await _zippingService.createRenamedImageZip(
            imageFiles: retryImages,
            baseFileName: baseFileNameForQueue,
            destinationDir: null,
          );
          if (retryImageZip != null) {
            // Queue for each config separately
            for (final config in ftpConfigs) {
              final configId = config['ftp_config_id'];
              if (configId != null) {
                await _retryService.addFtpToQueue(
                    localFilePath: retryImageZip.path,
                    remotePath: '/${p.basename(retryImageZip.path)}',
                    ftpConfigId: configId // Provide the specific config ID
                );
              }
            }
          }
        }
        // --- END FIX ---
        ftpResults = {'statuses': [{'status': 'Queued', 'message': 'FTP upload queued due to API session issue.', 'success': false}]};
        anyFtpSuccess = false;
      } else {
        try {
          ftpResults = await _generateAndUploadFtpFiles(data, finalImageFiles, serverName, moduleName);
          anyFtpSuccess = !(ftpResults['statuses'] as List).any((status) => status['success'] == false && status['status'] != 'Not Configured');
        } catch (e) {
          debugPrint("Unexpected FTP submission error: $e");
          anyFtpSuccess = false;
        }
      }
    }

    // 4. Determine Final Status
    final bool overallSuccess = anyApiSuccess || anyFtpSuccess;

    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 or 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 = apiDataResult['message'] ?? 'All submission attempts failed and have been queued for retry.';
      finalStatus = 'L1';
    }

    // 5. Log Locally
    await _logAndSave(
      data: data,
      status: finalStatus,
      message: finalMessage,
      apiResults: [apiDataResult, apiImageResult],
      ftpStatuses: ftpResults['statuses'],
      serverName: serverName,
      // --- START: MODIFIED TO USE TIMESTAMP ID ---
      apiRecordId: apiRecordId, // Pass the server DB ID
      // --- END: MODIFIED TO USE TIMESTAMP ID ---
      logDirectory: logDirectory,
    );

    // 6. Send Alert
    // --- START FIX: Check if log was already successful before sending alert ---
    final bool wasAlreadySuccessful = previousStatus == 'S4' || previousStatus == 'S3' || previousStatus == 'L4';
    if (overallSuccess && !wasAlreadySuccessful) {
      _handleSuccessAlert(data, appSettings, isDataOnly: finalImageFiles.isEmpty, isSessionExpired: isSessionKnownToBeExpired);
    }
    // --- END FIX ---

    // Return consistent format
    return {
      'status': finalStatus,
      'success': overallSuccess,
      'message': finalMessage,
      // --- START: MODIFIED TO USE TIMESTAMP ID ---
      'reportId': data.reportId // This is now the timestamp ID
      // --- END: MODIFIED TO USE TIMESTAMP ID ---
    };
  }

  /// Handles queuing the submission data when the device is offline.
  Future<Map<String, dynamic>> _performOfflineQueuing({
    required RiverManualTriennialSamplingData data,
    required String moduleName,
    String? logDirectory, // Added for potential update
  }) async {
    final serverConfig = await _serverConfigService.getActiveApiConfig();
    final serverName = serverConfig?['config_name'] as String? ?? 'Default';

    data.submissionStatus = 'L1';
    data.submissionMessage = 'Submission queued for later retry.';

    String? savedLogPath = logDirectory; // Use existing path if provided

    // Save/Update local log first
    if (savedLogPath != null && savedLogPath.isNotEmpty) {
      await _localStorageService.updateRiverManualTriennialLog(data.toMap()..['logDirectory'] = savedLogPath);
      debugPrint("Updated existing River Triennial log for queuing: $savedLogPath");
    } else {
      savedLogPath = await _localStorageService.saveRiverManualTriennialSamplingData(data, serverName: serverName);
      debugPrint("Saved new River Triennial log for queuing: $savedLogPath");
    }


    if (savedLogPath == null) {
      const message = "Failed to save submission to local device storage.";
      // --- START: MODIFIED TO USE TIMESTAMP ID ---
      await _logAndSave(data: data, status: 'Error', message: message, apiResults: [], ftpStatuses: [], serverName: serverName, apiRecordId: null, logDirectory: logDirectory);
      // --- END: MODIFIED TO USE TIMESTAMP ID ---
      return {'status': 'Error', 'success': false, 'message': message};
    }

    await _retryService.queueTask(
      type: 'river_triennial_submission', // Correct type
      payload: {
        'module': moduleName,
        'localLogPath': p.join(savedLogPath, 'data.json'), // Point to the json file
        'serverConfig': serverConfig,
      },
    );

    const successMessage = "Device offline. Submission has been saved locally and queued for automatic retry when connection is restored.";
    // Log final queued state to central DB
    // await _logAndSave(data: data, status: 'Queued', message: successMessage, apiResults: [], ftpStatuses: [], serverName: serverName, apiRecordId: null, logDirectory: savedLogPath);

    return {'status': 'Queued', 'success': true, 'message': successMessage, 'reportId': data.reportId}; // Return timestamp ID
  }

  // --- START: MODIFIED _generateBaseFileName ---
  /// Helper to generate the base filename for ZIP files.
  String _generateBaseFileName(RiverManualTriennialSamplingData data) {
    final stationCode = data.selectedStation?['sampling_station_code'] ?? 'UNKNOWN';

    // We now always use data.reportId, which we set as the timestamp.
    if (data.reportId == null || data.reportId!.isEmpty) {
      // This is a safety fallback, but should not happen if submitData is used.
      debugPrint("Warning: reportId is null in _generateBaseFileName. Using current timestamp.");
      return '${stationCode}_${DateTime.now().millisecondsSinceEpoch.toString()}';
    }
    return '${stationCode}_${data.reportId}';
  }
  // --- END: MODIFIED _generateBaseFileName ---


  /// Generates data and image ZIP files and uploads them using SubmissionFtpService.
  Future<Map<String, dynamic>> _generateAndUploadFtpFiles(RiverManualTriennialSamplingData data, Map<String, File> imageFiles, String serverName, String moduleName) async {

    // 1. GENERATE TIMESTAMP FOR IMAGE RENAMING
    // e.g., "2025-09-30" and "14:34:19" -> "20250930143419"
    final String dateStr = (data.samplingDate ?? '').replaceAll('-', '');
    final String timeStr = (data.samplingTime ?? '').replaceAll(':', '');
    final String zipImageTimestamp = "$dateStr$timeStr";

    // 2. USE ORIGINAL BASE FILENAME (Report ID / Milliseconds) for Folder/Zip
    final baseFileName = _generateBaseFileName(data);

    // 3. SETUP DIRECTORIES
    final Directory? logDirectory = await _localStorageService.getLogDirectory( // Use generic getter
      serverName: serverName,
      module: 'river',
      subModule: 'river_triennial_sampling', // Correct sub-module path
    );

    // Use baseFileName for the folder
    final Directory? localSubmissionDir = logDirectory != null ? Directory(p.join(logDirectory.path, baseFileName)) : null;
    if (localSubmissionDir != null && !await localSubmissionDir.exists()) {
      await localSubmissionDir.create(recursive: true);
    }

    // 4. CREATE DATA ZIP
    final dataZip = await _zippingService.createDataZip(
      // --- START FIX: Include all four JSON files ---
      jsonDataMap: {
        'db.json': data.toDbJson(),
        'river_insitu_basic_form.json': data.toBasicFormJson(), // ADDED
        'river_sampling_reading.json': data.toReadingJson(),       // ADDED
        'river_manual_info.json': data.toManualInfoJson(), // ADDED
      },
      // --- END FIX ---
      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)}');
    }

    // 5. CREATE IMAGE ZIP (RENAMING LOGIC)
    Map<String, dynamic> ftpImageResult = {'success': true, 'statuses': []};

    // Create map: "New Name Inside Zip" -> "Original File on Phone"
    final Map<String, File> imagesForZip = {};

    void mapImage(File? file, String prefix) {
      if (file != null && file.existsSync()) {
        // Rename inside zip: prefix_20250930143419.jpg
        imagesForZip['${prefix}_$zipImageTimestamp.jpg'] = file;
      }
    }

    // Map the specific fields to their short prefixes
    mapImage(data.backgroundStationImage, 'background');
    mapImage(data.upstreamRiverImage, 'upstream');
    mapImage(data.downstreamRiverImage, 'downstream');
    mapImage(data.sampleTurbidityImage, 'turbidity');
    mapImage(data.optionalImage1, 'optional_1');
    mapImage(data.optionalImage2, 'optional_2');
    mapImage(data.optionalImage3, 'optional_3');
    mapImage(data.optionalImage4, 'optional_4');

    if (imagesForZip.isNotEmpty) {
      // Call the NEW function: createRenamedImageZip
      final imageZip = await _zippingService.createRenamedImageZip(
        imageFiles: imagesForZip,
        baseFileName: baseFileName,
        destinationDir: localSubmissionDir,
      );

      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? ?? []), // Use null-aware spread
        ...(ftpImageResult['statuses'] as List? ?? []), // Use null-aware spread
      ],
    };
  }

  /// Saves or updates the local log file and saves a record to the central DB log.
  Future<void> _logAndSave({
    required RiverManualTriennialSamplingData data,
    required String status,
    required String message,
    required List<Map<String, dynamic>> apiResults,
    required List<Map<String, dynamic>> ftpStatuses,
    required String serverName,
    // --- START: MODIFIED TO USE TIMESTAMP ID ---
    String? apiRecordId, // This is the server DB ID (e.g., 102)
    // --- END: MODIFIED TO USE TIMESTAMP ID ---
    String? logDirectory,
  }) async {
    data.submissionStatus = status;
    data.submissionMessage = message;
    final baseFileName = _generateBaseFileName(data); // This now uses the timestamp ID

    // Prepare log data map using toMap()
    final Map<String, dynamic> logMapData = data.toMap();
    // Add submission metadata
    logMapData['submissionStatus'] = status;
    logMapData['submissionMessage'] = message;
    // --- START: MODIFIED TO USE TIMESTAMP ID ---
    // data.reportId (which is the timestamp) is already in the map from toMap()
    // We add the server's DB ID separately for the log file
    logMapData['apiRecordId'] = apiRecordId; // (e.g., 102)
    // --- END: MODIFIED TO USE TIMESTAMP ID ---
    logMapData['serverConfigName'] = serverName;
    logMapData['api_status'] = jsonEncode(apiResults.where((r) => r.isNotEmpty).toList());
    logMapData['ftp_status'] = jsonEncode(ftpStatuses);


    if (logDirectory != null && logDirectory.isNotEmpty) {
      // Update existing log
      logMapData['logDirectory'] = logDirectory; // Ensure logDirectory path is in the map
      await _localStorageService.updateRiverManualTriennialLog(logMapData); // Use specific update method
    } else {
      // Save new log
      await _localStorageService.saveRiverManualTriennialSamplingData(data, serverName: serverName); // Use specific save method
    }

    // Save to central DB log
    final imagePaths = data.toApiImageFiles().values.whereType<File>().map((f) => f.path).toList();
    final centralLogData = {
      // --- START: MODIFIED TO USE TIMESTAMP ID ---
      'submission_id': data.reportId ?? baseFileName, // This is the timestamp ID
      'module': 'river',
      'type': data.samplingType ?? 'Triennial', // Correct type
      'status': status,
      'message': message,
      'report_id': apiRecordId, // This is the server DB ID (e.g., 102)
      // --- END: MODIFIED TO USE TIMESTAMP ID ---
      'created_at': DateTime.now().toIso8601String(),
      'form_data': jsonEncode(logMapData), // Log the comprehensive map
      'image_data': jsonEncode(imagePaths),
      'server_name': serverName,
      'api_status': jsonEncode(apiResults),
      'ftp_status': jsonEncode(ftpStatuses),
    };
    try {
      await _dbHelper.saveSubmissionLog(centralLogData);
    } catch (e) {
      debugPrint("Error saving River Triennial submission log to DB: $e");
    }
  }


  /// Handles sending or queuing the Telegram alert for River Triennial submissions.
  Future<void> _handleSuccessAlert(RiverManualTriennialSamplingData data, List<Map<String, dynamic>>? appSettings, {required bool isDataOnly, bool isSessionExpired = false}) async {
    try {
      final message = await _generateSuccessAlertMessage(data, isDataOnly: isDataOnly); // Call local helper
      final alertKey = 'river_triennial'; // Correct key

      if (isSessionExpired) {
        debugPrint("Session is expired; queuing Telegram alert directly for $alertKey.");
        await _telegramService.queueMessage(alertKey, message, appSettings);
      } else {
        final bool wasSent = await _telegramService.sendAlertImmediately(alertKey, message, appSettings);
        if (!wasSent) {
          await _telegramService.queueMessage(alertKey, message, appSettings);
        }
      }
    } catch (e) {
      debugPrint("Failed to handle River Triennial Telegram alert: $e");
    }
  }

  /// Generates the specific Telegram alert message content for River Triennial.
  Future<String> _generateSuccessAlertMessage(RiverManualTriennialSamplingData data, {required bool isDataOnly}) async {
    final submissionType = isDataOnly ? "(Data Only)" : "(Data & Images)";
    final stationName = data.selectedStation?['sampling_river'] ?? 'N/A';
    final stationCode = data.selectedStation?['sampling_station_code'] ?? 'N/A';
    final submissionDate = data.samplingDate ?? DateFormat('yyyy-MM-dd').format(DateTime.now());
    final submitter = data.firstSamplerName ?? 'N/A';
    final sondeID = data.sondeId ?? 'N/A';
    final distanceKm = data.distanceDifferenceInKm ?? 0;
    final distanceMeters = (distanceKm * 1000).toStringAsFixed(0);
    final distanceRemarks = data.distanceDifferenceRemarks ?? 'N/A';

    final buffer = StringBuffer()
      ..writeln('✅ *River Triennial Sample ${submissionType} Submitted:*')
      ..writeln()
      ..writeln('*Station Name & Code:* $stationName ($stationCode)')
      ..writeln('*Date of Submitted:* $submissionDate')
      ..writeln('*Submitted by User:* $submitter')
      ..writeln('*Sonde ID:* $sondeID')
      ..writeln('*Status of Submission:* Successful');

    if (distanceKm * 1000 > 50 || (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A')) { // Check if distance > 50m
      buffer
        ..writeln()
        ..writeln('🔔 *Distance Alert:*')
        ..writeln('*Distance from station:* $distanceMeters meters');
      if (distanceRemarks.isNotEmpty && distanceRemarks != 'N/A') {
        buffer.writeln('*Remarks for distance:* $distanceRemarks');
      }
    }

    // Note: Parameter limit checks are not typically done for Triennial in the same way as In-Situ.
    // If needed, similar logic to _getOutOfBoundsAlertSection in RiverInSituSamplingService
    // would need to be adapted here, potentially using riverParameterLimits from the DB.

    // --- START FIX: Add parameter limit check section ---
    final outOfBoundsAlert = await _getOutOfBoundsAlertSection(data);
    if (outOfBoundsAlert.isNotEmpty) {
      buffer.write(outOfBoundsAlert);
    }
    // --- END FIX ---

    return buffer.toString();
  }

  /// Helper to generate the parameter limit alert section for Telegram.
  Future<String> _getOutOfBoundsAlertSection(RiverManualTriennialSamplingData data) async {
    // Define mapping from data model keys to parameter names used in limits table
    const Map<String, String> _parameterKeyToLimitName = {
      'oxygenConcentration': 'Oxygen Conc', 'oxygenSaturation': 'Oxygen Sat', 'ph': 'pH',
      'salinity': 'Salinity', 'electricalConductivity': 'Conductivity', 'temperature': 'Temperature',
      'tds': 'TDS', 'turbidity': 'Turbidity', 'ammonia': 'Ammonia', 'batteryVoltage': 'Battery',
    };

    final allLimits = await _dbHelper.loadRiverParameterLimits() ?? []; // Load river limits
    if (allLimits.isEmpty) return "";

    final readings = {
      'oxygenConcentration': data.oxygenConcentration, 'oxygenSaturation': data.oxygenSaturation,
      'ph': data.ph, 'salinity': data.salinity, 'electricalConductivity': data.electricalConductivity,
      'temperature': data.temperature, 'tds': data.tds, 'turbidity': data.turbidity,
      'ammonia': data.ammonia, 'batteryVoltage': data.batteryVoltage,
    };

    final List<String> outOfBoundsMessages = [];

    double? parseLimitValue(dynamic value) {
      if (value == null) return null;
      if (value is num) return value.toDouble();
      if (value is String) return double.tryParse(value);
      return null;
    }

    readings.forEach((key, value) {
      if (value == null || value == -999.0) return;

      final limitName = _parameterKeyToLimitName[key];
      if (limitName == null) return;

      // Find the limit data for this parameter (river limits are not station-specific in the current DB structure)
      final limitData = allLimits.firstWhere(
            (l) => l['param_parameter_list'] == limitName,
        orElse: () => <String, dynamic>{}, // Use explicit type
      );

      if (limitData.isNotEmpty) {
        final lowerLimit = parseLimitValue(limitData['param_lower_limit']);
        final upperLimit = parseLimitValue(limitData['param_upper_limit']);

        if ((lowerLimit != null && value < lowerLimit) || (upperLimit != null && value > upperLimit)) {
          final valueStr = value.toStringAsFixed(5);
          final lowerStr = lowerLimit?.toStringAsFixed(5) ?? 'N/A';
          final upperStr = upperLimit?.toStringAsFixed(5) ?? 'N/A';
          outOfBoundsMessages.add('- *$limitName*: `$valueStr` (Limit: `$lowerStr` - `$upperStr`)');
        }
      }
    });

    if (outOfBoundsMessages.isEmpty) {
      return "";
    }

    final buffer = StringBuffer()
      ..writeln()
      ..writeln('⚠️ *Parameter Limit Alert:*')
      ..writeln('The following parameters were outside their defined limits:');
    buffer.writeAll(outOfBoundsMessages, '\n');

    return buffer.toString();
  }
}