// lib/serial/serial_manager.dart

import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/foundation.dart'; // For debugPrint
import 'package:usb_serial/usb_serial.dart';

import 'utils/converter.dart';
import 'utils/crc_calculator.dart';
import 'utils/parameter_helper.dart';

enum SerialConnectionState { disconnected, connecting, connected }

/// Manages the connection and data communication with a USB Serial device.
class SerialManager {
  UsbPort? _port;
  // Using ValueNotifier to easily notify UI about connection state changes.
  final ValueNotifier<SerialConnectionState> connectionState = ValueNotifier(
    SerialConnectionState.disconnected,
  );
  // ValueNotifier to expose the connected device's name.
  final ValueNotifier<String?> connectedDeviceName = ValueNotifier(null);

  // --- ADDED: ValueNotifier for Sonde ID ---
  // This will be updated when the Sonde ID is parsed from Level 0 data.
  final ValueNotifier<String?> sondeId = ValueNotifier<String?>(null);

  // --- ADDED: Flag to prevent updates after disposal ---
  bool _isDisposed = false;

  // StreamController to broadcast parsed data readings to multiple listeners.
  final StreamController<Map<String, double>> _dataStreamController =
  StreamController<Map<String, double>>.broadcast();
  Stream<Map<String, double>> get dataStream => _dataStreamController.stream;

  // --- Protocol State Variables ---
  int _runningCounter = 0; // Sequence number for commands
  int _communicationLevel = 0; // Tracks the current step in the 3-level protocol
  String? _parentAddress; // Address parsed from Level 0 response
  String? _serialNumber; // Serial number parsed from Level 0 response (used internally for now)
  List<String> _parameterList = []; // List of parameters parsed from Level 1 response

  Timer? _dataRequestTimer; // Timer for periodic auto-reading
  Timer? _responseTimeoutTimer; // Timer to detect if a response is not received in time

  // Buffer to accumulate incoming serial data, as messages may arrive in chunks.
  final StringBuffer _responseBuffer = StringBuffer();
  // String to look for at the start of an expected response (e.g., '7E02 + seqNo')
  String? _lookupString;
  // Flag to prevent sending new commands before the current read cycle is complete.
  bool _isReading = false;

  /// Fetches a list of available USB devices connected to the Android device.
  Future<List<UsbDevice>> getAvailableDevices() async {
    return UsbSerial.listDevices();
  }

  /// Connects to the specified USB device.
  /// Handles opening the port, setting parameters, and starting the input stream listener.
  Future<void> connect(UsbDevice device) async {
    // Prevent connecting if already connected
    if (connectionState.value == SerialConnectionState.connected) {
      debugPrint("SerialManager: Already connected. Disconnecting existing connection.");
      // Now that disconnect() is async and returns Future<void>, awaiting it is correct.
      await disconnect(); // Disconnect cleanly before new connection
    }

    try {
      _isDisposed = false; // Reset disposed flag on new connection
      connectionState.value = SerialConnectionState.connecting;
      connectedDeviceName.value = device.productName ?? 'Unknown Device'; // Update device name early

      _port = await device.create();
      if (_port == null) {
        debugPrint("SerialManager: Failed to create USB serial port for device: ${device.productName}.");
        // No need to call disconnect, as nothing was connected yet.
        connectionState.value = SerialConnectionState.disconnected; // Reset state
        connectedDeviceName.value = null; // Clear device name
        throw Exception("Failed to create USB serial port.");
      }

      bool openResult = await _port!.open();
      if (!openResult) {
        debugPrint("SerialManager: Failed to open USB serial port for device: ${device.productName}.");
        _port = null; // Clear port reference
        connectionState.value = SerialConnectionState.disconnected; // Reset state
        connectedDeviceName.value = null; // Clear device name
        throw Exception("Failed to open USB serial port.");
      }

      await _port!.setDTR(true);
      await _port!.setRTS(true);

      _port!.setPortParameters(
        115200, // Baud Rate
        UsbPort.DATABITS_8,
        UsbPort.STOPBITS_1,
        UsbPort.PARITY_NONE,
      );

      // Listen for incoming data from the USB serial port.
      _port!.inputStream!.listen(
        _onDataReceived,
        onError: (e) {
          debugPrint("SerialManager: Stream Error: $e");
          // Since disconnect() is now async, calling it directly here without await is fine
          // because we don't need to wait for it within the error handler.
          disconnect();
        },
        onDone: () {
          debugPrint("SerialManager: Stream Closed");
          // Same here, no need to await within onDone.
          disconnect();
        },
      );

      // --- REVISED: Added null check before updating state ---
      if (!_isDisposed) {
        connectionState.value = SerialConnectionState.connected;
        connectedDeviceName.value = device.productName;
      }
      debugPrint("SerialManager: Connected to ${connectedDeviceName.value}");

      // Start the live reading cycle immediately after connecting
      startLiveReading();

    } catch (e) {
      debugPrint("SerialManager: Connection Error: $e");
      // Ensure all states are reset on error during connection
      if (!_isDisposed) {
        connectionState.value = SerialConnectionState.disconnected;
        connectedDeviceName.value = null;
        sondeId.value = null; // Clear Sonde ID
      }
      _port = null;
      _responseBuffer.clear();
      _isReading = false;
      _cancelTimeout();
      rethrow; // Re-throw the exception for UI handling
    }
  }

  /// Disconnects from the currently connected USB device.
  /// Cleans up resources: cancels timers, closes port, resets state.
  // MODIFIED: Changed from void to Future<void> and added async.
  Future<void> disconnect() async {
    if (connectionState.value != SerialConnectionState.disconnected) {
      debugPrint("SerialManager: Disconnecting...");
      stopAutoReading(); // Stop any active auto-reading timer and timeout
      // FIX: Update ValueNotifiers before closing the stream and port.
      connectionState.value = SerialConnectionState.disconnected;
      connectedDeviceName.value = null;
      sondeId.value = null; // Clear Sonde ID on disconnect
      await _port?.close(); // Now correctly awaiting the close operation
      _port = null; // Clear port reference
      _responseBuffer.clear(); // Clear any buffered data
      _isReading = false; // Reset reading flag
      _communicationLevel = 0; // Reset communication level
      _parentAddress = null;
      _serialNumber = null;
      _parameterList.clear();
      debugPrint("SerialManager: Disconnected.");
    }
  }

  /// Starts a periodic timer to automatically request data from the device.
  void startAutoReading({Duration interval = const Duration(seconds: 2)}) {
    stopAutoReading(); // Stop any existing auto-reading timer first
    if (connectionState.value == SerialConnectionState.connected) {
      //startLiveReading(); // Initiate the first reading immediately
      // Set up a periodic timer to call startLiveReading at the specified interval.
      _dataRequestTimer = Timer.periodic(interval, (_) => startLiveReading());
      debugPrint("SerialManager: Auto reading started with interval ${interval.inSeconds}s.");
    } else {
      debugPrint("SerialManager: Cannot start auto reading, not connected.");
    }
  }

  /// Stops the automatic data refresh timer.
  void stopAutoReading() {
    _dataRequestTimer?.cancel(); // Cancel the periodic timer
    _dataRequestTimer = null; // Clear timer reference
    _cancelTimeout(); // Also cancel any pending response timeout
    _isReading = false; // Ensure the reading flag is reset
    debugPrint("SerialManager: Auto reading stopped.");
  }

  /// Initiates a full 3-step data reading sequence (Level 0, Level 1, Level 2).
  /// This function prevents overlapping read cycles using the `_isReading` flag.
  void startLiveReading() {
    // Only proceed if connected and not already in a reading cycle
    if (connectionState.value != SerialConnectionState.connected || _isReading) {
      debugPrint("SerialManager: Cannot start live reading. Connected: ${connectionState.value == SerialConnectionState.connected}, Is Reading: $_isReading");
      return;
    }
    _isReading = true; // Mark as busy to prevent re-entry
    debugPrint("--- SerialManager: Starting New Read Cycle ---");
    _communicationLevel = 0; // Start at communication level 0
    _responseBuffer.clear(); // Clear the buffer for a fresh start
    _lookupString = null; // Reset lookup string until a new command is sent
    _sendCommand(0); // Send the Level 0 command
  }

  /// Callback function for the serial port's input stream.
  /// Appends incoming data chunks to a buffer and attempts to process it.
  void _onDataReceived(Uint8List data) {
    if (data.isEmpty) return;
    String responseHex = Converter.byteArrayToHexString(data);
    _responseBuffer.write(responseHex); // Append to the shared buffer
    // debugPrint("SerialManager: Received Chunk (Buffer: ${_responseBuffer.length} chars): $responseHex"); // Too verbose normally
    _processBuffer(); // Attempt to process the buffer for complete messages
  }

  /// Attempts to extract and process complete messages from the `_responseBuffer`.
  /// Handles partial messages, garbage data, and CRC validation.
  void _processBuffer() {
    String buffer = _responseBuffer.toString();
    // Cannot process without an expected lookup string (set after sending a command)
    if (_lookupString == null) {
      // debugPrint("SerialManager: _processBuffer: No lookup string set. Waiting for command."); // Too verbose
      return;
    }

    int startIndex = buffer.indexOf(_lookupString!); // Find the start of the expected message
    if (startIndex == -1) {
      // debugPrint("SerialManager: _processBuffer: Lookup string not found in buffer. Waiting for more data."); // Too verbose
      return; // Expected start of message not found, wait for more data
    }

    // Discard any data before the expected start of the message (garbage or old data)
    if (startIndex > 0) {
      debugPrint("SerialManager: _processBuffer: Discarding ${startIndex} garbage characters from buffer.");
      buffer = buffer.substring(startIndex); // Remove leading garbage
      _responseBuffer.clear(); // Clear and rewrite the buffer to contain only valid data from startIndex
      _responseBuffer.write(buffer);
    }

    // A minimum of 34 hex characters are needed to parse the dataBlockLength (offset 30, length 4)
    if (buffer.length < 34) {
      // debugPrint("SerialManager: _processBuffer: Buffer too short for header (${buffer.length} < 34). Waiting for more data."); // Too verbose
      return; // Not enough data to even read the length field
    }

    try {
      // Parse the data block length from the message header
      int dataBlockLength = int.parse(buffer.substring(30, 34), radix: 16);
      // Calculate the total expected length of the complete message (Header + Data Block + CRC)
      int totalMessageLength = 34 + (dataBlockLength * 2) + 4; // 34 chars for header, dataBlockLength bytes * 2 chars/byte, 4 chars for CRC

      // If the buffer contains a complete message
      if (buffer.length >= totalMessageLength) {
        _cancelTimeout(); // Message received, cancel the response timeout timer
        String completeResponse = buffer.substring(0, totalMessageLength);

        // --- CRC Validation ---
        Uint8List responseBytes = Converter.hexStringToByteArray(completeResponse);
        // CRC is calculated over the entire message *excluding* the final 2 CRC bytes themselves
        Uint8List dataBytesForCrc = responseBytes.sublist(0, responseBytes.length - 2);
        String calculatedCrc = Crc16Ccitt.computeCrc16Ccitt(dataBytesForCrc);
        String receivedCrc = completeResponse.substring(completeResponse.length - 4); // Last 4 hex chars are the CRC

        if (calculatedCrc.toUpperCase() != receivedCrc.toUpperCase()) {
          debugPrint("SerialManager: CRC Mismatch! Calculated: $calculatedCrc, Received: $receivedCrc for response: $completeResponse. Discarding message.");
          // Discard the invalid message from the buffer and attempt to process the rest
          _responseBuffer.clear();
          _responseBuffer.write(buffer.substring(totalMessageLength));
          _isReading = false; // Consider ending the cycle or signaling an error that might require a retry.
          if(_responseBuffer.isNotEmpty) _processBuffer(); // Check if more messages are in the buffer
          return;
        }

        // Message is valid. Remove it from the buffer.
        _responseBuffer.clear();
        _responseBuffer.write(buffer.substring(totalMessageLength));

        debugPrint("SerialManager: Processing Full Valid Response (Lvl: $_communicationLevel): $completeResponse");
        _handleResponse(completeResponse); // Route the complete message for parsing

        // Recursively call _processBuffer in case multiple messages arrived in one chunk,
        // or a new message started immediately after the processed one.
        if(_responseBuffer.isNotEmpty) _processBuffer();
      }
    } catch (e) {
      debugPrint("SerialManager: Buffer processing error: $e. Resetting cycle and clearing buffer.");
      _responseBuffer.clear();
      _isReading = false; // End this cycle on error
    }
  }

  /// Dispatches the complete and validated response string to the appropriate handler
  /// based on the current communication level.
  void _handleResponse(String response) {
    // --- REVISED: Added null check before dispatching ---
    if (_isDisposed) {
      debugPrint("SerialManager: Ignoring response on disposed manager.");
      return;
    }

    switch (_communicationLevel) {
      case 0:
        _handleResponseLevel0(response);
        break;
      case 1:
        _handleResponseLevel1(response);
        break;
      case 2:
        _handleResponseLevel2(response);
        break;
      default:
        debugPrint("SerialManager: Unknown communication level: $_communicationLevel for response: $response");
        _isReading = false; // Ensure reading lock is released if level is invalid
    }
  }

  /// Handles and parses the response for communication Level 0.
  /// Extracts parent address and serial number.
  void _handleResponseLevel0(String responseHex) {
    try {
      // Minimum length check to ensure substrings are safe
      // 34 (header) + 40 (data block for L0) + 4 (CRC) = 78 expected min length for full response
      // It looks like your previous code assumed data block starts at 34 and contains 4 bytes (8 chars)
      // for parent address at offset 86, and 9 bytes (18 chars) for serial at 68.
      // Let's refine based on typical structure.
      // If `responseHex.substring(68, 86)` and `responseHex.substring(86, 94)` are correct from your sensor,
      // then total length for serial number (18) + parent address (8) + other header/footer must be respected.
      // 18 chars for serial, 8 chars for parent. Total 26 chars / 2 = 13 bytes data length.
      // So, total expected response length = 34 + (13*2) + 4 = 34 + 26 + 4 = 64.
      // If your sensor gives 94 chars: 34 (header) + (94-34-4=56 data chars) + 4 (CRC) -> 28 bytes data.
      // This means offsets 68-86 and 86-94 might be within a larger data block.
      // This response example is for a specific sensor protocol. Adjust offsets if yours differs.
      if (responseHex.length < 94) { // Assuming 94 is the correct full length including all L0 data.
        debugPrint("SerialManager: Error: Level 0 response too short. Length: ${responseHex.length} (expected at least 94)");
        _isReading = false;
        return;
      }

      // Re-evaluate based on your specific protocol:
      // The offsets 68-86 and 86-94 suggest a specific structure within the *data block* of the response.
      // The `dataBlockLength` field (at 30-34) specifies the length of the data portion *after* the header.
      // Let's assume these substrings are correct as per your sensor's documentation:
      _parentAddress = responseHex.substring(86, 94); // Extract parent address (8 hex chars = 4 bytes)
      _serialNumber = Converter.hexToAscii(responseHex.substring(68, 86)); // Extract serial number (18 hex chars = 9 bytes, then convert to ASCII)

      debugPrint("SerialManager: Parsed L0 -> Address: $_parentAddress, Serial: $_serialNumber");

      // --- Update sondeId ValueNotifier here ---
      // --- REVISED: Added null check before updating ValueNotifier ---
      if (!_isDisposed) {
        if (_serialNumber != null && _serialNumber!.isNotEmpty) {
          sondeId.value = _serialNumber;
          debugPrint("SerialManager: Updated Sonde ID: ${sondeId.value}");
        } else {
          sondeId.value = "N/A"; // Or handle empty/null serial number appropriately
        }
      }


      if (_parentAddress != "00000000") { // Check for valid parent address
        _communicationLevel = 1; // Advance to Level 1
        // Delay before sending next command to allow device time to process
        Future.delayed(const Duration(milliseconds: 200), () => _sendCommand(1));
      } else {
        debugPrint("SerialManager: Parent address is 00000000, not proceeding to Level 1. Ending cycle.");
        _isReading = false; // End the cycle if parent address is invalid
      }
    } catch (e) {
      debugPrint("SerialManager: Error parsing L0: $e");
      _isReading = false; // End this cycle on error
    }
  }

  /// Handles and parses the response for communication Level 1.
  /// Extracts the list of available parameters.
  void _handleResponseLevel1(String responseHex) {
    try {
      if (responseHex.length < 34) {
        debugPrint("SerialManager: Error: Level 1 response too short for dataBlockLength. Length: ${responseHex.length}");
        _isReading = false;
        return;
      }
      int dataBlockLength = int.parse(responseHex.substring(30, 34), radix: 16);
      int expectedParamsBlockEnd = 34 + (dataBlockLength * 2);

      if (expectedParamsBlockEnd > responseHex.length) {
        debugPrint("SerialManager: Error: Level 1 parameters block calculated end index ($expectedParamsBlockEnd) exceeds received data length (${responseHex.length}). dataBlockLength was: $dataBlockLength (0x${dataBlockLength.toRadixString(16).padLeft(4, '0')})");
        _isReading = false;
        return;
      }

      String paramsBlock = responseHex.substring(34, expectedParamsBlockEnd);
      _parameterList.clear();

      // Each parameter entry is 6 hex characters (3 bytes).
      // Parameter code (2 bytes) is typically at index 2-5 within each 6-char block.
      // Example: '005A00' -> code '5A00'
      for (int i = 0; i <= paramsBlock.length - 6; i += 6) {
        String parameterCode = paramsBlock.substring(i + 2, i + 6);
        _parameterList.add(ParameterHelper.getDescription(parameterCode));
      }
      debugPrint("SerialManager: Parsed L1 -> Parameters: $_parameterList");
      _communicationLevel = 2; // Advance to Level 2
      Future.delayed(const Duration(milliseconds: 200), () => _sendCommand(2));
    } catch (e) {
      debugPrint("SerialManager: Error parsing L1: $e");
      _isReading = false;
    }
  }

  /// Handles and parses the response for communication Level 2.
  /// Extracts the actual values for the previously identified parameters.
  void _handleResponseLevel2(String responseHex) {
    try {
      if (responseHex.length < 34) {
        debugPrint("SerialManager: Error: Level 2 response too short for dataBlockLength. Length: ${responseHex.length}");
        _isReading = false;
        return;
      }
      int dataBlockLength = int.parse(responseHex.substring(30, 34), radix: 16);
      int expectedValuesBlockEnd = 34 + (dataBlockLength * 2);

      if (expectedValuesBlockEnd > responseHex.length) {
        debugPrint("SerialManager: Error: Level 2 values block calculated end index ($expectedValuesBlockEnd) exceeds received data length (${responseHex.length}). dataBlockLength was: $dataBlockLength (0x${dataBlockLength.toRadixString(16).padLeft(4, '0')})");
        _isReading = false;
        return;
      }

      String valuesBlock = responseHex.substring(34, expectedValuesBlockEnd);
      List<double> values = [];

      // Each value is 8 hex characters (4 bytes) representing a float
      for (int i = 0; i <= valuesBlock.length - 8; i += 8) {
        String valueHex = valuesBlock.substring(i, i + 8);
        try {
          values.add(Converter.hexToFloat(valueHex));
        } catch (e) {
          debugPrint("SerialManager: Error converting hex '$valueHex' to float in Level 2: $e");
          values.add(double.nan); // Add NaN or a default error value if conversion fails
        }
      }

      // Ensure the number of parsed parameters matches the number of parsed values.
      if (_parameterList.length == values.length) {
        Map<String, double> finalReadings = Map.fromIterables(_parameterList, values);
        // --- REVISED: Added null check before adding to the stream controller ---
        if (!_isDisposed) {
          _dataStreamController.add(finalReadings); // Broadcast the final parsed readings
        }
        debugPrint("SerialManager: Final Parsed Readings: $finalReadings");
      } else {
        debugPrint("SerialManager: L2 Data Mismatch: ${values.length} values for ${_parameterList.length} parameters. Parameter list: $_parameterList, Values: $values");
      }
    } catch (e) {
      debugPrint("SerialManager: Error parsing L2: $e");
    } finally {
      _isReading = false; // Mark the read cycle as complete, allowing the next one to start
      debugPrint("--- SerialManager: Read Cycle Complete ---");
    }
  }

  /// Constructs and sends the appropriate command based on the current communication level.
  void _sendCommand(int level) async {
    // Generate a sequence number (0-255), padded to 2 hex characters.
    String seqNo = (_runningCounter++ & 255).toRadixString(16).padLeft(2, '0').toUpperCase();

    // Set the lookup string for the expected response. This is the start of the message
    // that the `_processBuffer` will look for to identify a complete response.
    _lookupString = '7E02${seqNo}'; // Assuming responses start with 7E02 and the same sequence number.

    String commandBody;
    switch (level) {
      case 0:
        commandBody = '0000000002000000200000010000'; // Command for Level 0
        break;
      case 1:
        if (_parentAddress == null) {
          debugPrint("SerialManager: Error: _parentAddress is null for Level 1 command. Cannot send. Ending cycle.");
          _isReading = false; // Release the reading lock
          // Call disconnect (now async void) without await here as we don't need to block
          disconnect(); // Disconnect due to invalid state
          return;
        }
        commandBody = '${_parentAddress}02000000200000180000'; // Command for Level 1, includes parent address
        break;
      case 2:
        if (_parentAddress == null) {
          debugPrint("SerialManager: Error: _parentAddress is null for Level 2 command. Cannot send. Ending cycle.");
          _isReading = false; // Release the reading lock
          // Call disconnect (now async void) without await here as we don't need to block
          disconnect(); // Disconnect due to invalid state
          return;
        }
        commandBody = '${_parentAddress}02000000200000190000'; // Command for Level 2, includes parent address
        break;
      default:
        debugPrint("SerialManager: Attempted to send command for unknown level: $level. Resetting cycle.");
        _isReading = false;
        return;
    }

    String commandHex = '7E02$seqNo$commandBody'; // Full command without CRC
    Uint8List commandBytes = Converter.hexStringToByteArray(commandHex); // Convert command hex to bytes
    String crc = Crc16Ccitt.computeCrc16Ccitt(commandBytes); // Compute CRC for the command bytes
    String finalPacket = commandHex + crc; // Append CRC to the command

    try {
      await _port?.write(Converter.hexStringToByteArray(finalPacket)); // Send the full packet over serial
      debugPrint("SerialManager: Sent (Lvl: $level): $finalPacket (Expecting lookup: $_lookupString)");

      // Set a timeout. If no valid response is received within this duration,
      // it's assumed the command failed, and the reading cycle is reset.
      _cancelTimeout(); // Cancel any previous timeout
      _responseTimeoutTimer = Timer(const Duration(seconds: 3), () {
        debugPrint("SerialManager: Response timeout for Level $level. Unlocking for next cycle.");
        _isReading = false; // Allow the next read cycle to start
        _responseBuffer.clear(); // Clear buffer on timeout to avoid stale or partial data confusing future reads
        // Optionally, you might want to trigger a retry for the current level here
        // or just let the auto-reading timer trigger the next full cycle.
      });
    } catch (e) {
      debugPrint("SerialManager: Error sending command (Lvl: $level): $e. Disconnecting.");
      _isReading = false; // Release the reading lock on send error
      // Call disconnect (now async void) without await here as we don't need to block
      disconnect(); // Disconnect if sending fails
    }
  }

  /// Cancels the current response timeout timer if it's active.
  void _cancelTimeout() {
    _responseTimeoutTimer?.cancel();
    _responseTimeoutTimer = null;
  }

  /// Cleans up all resources held by the SerialManager when it's no longer needed.
  void dispose() {
    debugPrint("SerialManager: Disposing.");
    _isDisposed = true; // Set the flag immediately
    disconnect(); // Ensure full disconnection and cleanup
    _dataStreamController.close(); // Close the data stream controller
    // FIX: Dispose of all ValueNotifiers to prevent "used after dispose" errors
    connectionState.dispose();
    connectedDeviceName.dispose();
    sondeId.dispose();
  }
}