import 'dart:async';
import 'dart:convert';
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:meshcore_client/meshcore_client.dart' show MeshCoreConstants;
import 'package:provider/provider.dart';
import 'package:geolocator/geolocator.dart';
import '../providers/messages_provider.dart';
import '../providers/contacts_provider.dart';
import '../providers/map_provider.dart';
import '../providers/connection_provider.dart';
import '../providers/drawing_provider.dart';
import '../providers/voice_provider.dart';
import '../providers/app_provider.dart';
import '../models/message.dart';
import '../models/contact.dart';
import '../widgets/messages/sar_update_sheet.dart';
import '../widgets/messages/recipient_selector_sheet.dart';
import '../widgets/messages/messages_composer.dart';
import '../widgets/messages/messages_content.dart';
import '../widgets/common/contact_avatar.dart';
import '../services/message_destination_preferences.dart';
import '../services/voice_bitrate_preferences.dart';
import '../services/voice_recorder_service.dart';
import '../services/voice_codec_service.dart';
import '../utils/toast_logger.dart';
import '../utils/key_comparison.dart';
import '../utils/contact_sorting.dart';
import '../utils/voice_message_parser.dart';
import '../utils/image_message_parser.dart';
import '../utils/tictactoe_message_parser.dart';
import '../providers/image_provider.dart' as ip;
import '../services/image_codec_service.dart';
import '../services/image_preferences.dart';
import '../services/region_scope_preferences.dart';
import 'package:image_picker/image_picker.dart';
import '../l10n/app_localizations.dart';

class MessagesTab extends StatefulWidget {
  final VoidCallback? onNavigateToMap;
  final bool isActive;

  const MessagesTab({super.key, this.onNavigateToMap, this.isActive = true});

  @override
  State<MessagesTab> createState() => _MessagesTabState();
}

class _ComposerActionTile extends StatelessWidget {
  final IconData icon;
  final String title;
  final Color color;
  final bool enabled;
  final bool busy;
  final VoidCallback? onTap;

  const _ComposerActionTile({
    required this.icon,
    required this.title,
    required this.color,
    this.enabled = true,
    this.busy = false,
    this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final colorScheme = theme.colorScheme;
    final effectiveColor = enabled
        ? color
        : colorScheme.onSurfaceVariant.withValues(alpha: 0.45);

    return Material(
      color: enabled
          ? colorScheme.surfaceContainerLow
          : colorScheme.surfaceContainerLowest,
      borderRadius: BorderRadius.circular(20),
      child: ListTile(
        contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
        enabled: enabled,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
        onTap: enabled ? onTap : null,
        leading: Container(
          width: 42,
          height: 42,
          decoration: BoxDecoration(
            color: effectiveColor.withValues(alpha: enabled ? 0.14 : 0.10),
            borderRadius: BorderRadius.circular(14),
            border: Border.all(
              color: enabled
                  ? effectiveColor.withValues(alpha: 0.16)
                  : colorScheme.outlineVariant.withValues(alpha: 0.16),
            ),
          ),
          alignment: Alignment.center,
          child: Icon(icon, color: effectiveColor, size: 20),
        ),
        title: Text(
          title,
          maxLines: 2,
          overflow: TextOverflow.ellipsis,
          style: theme.textTheme.titleSmall?.copyWith(
            fontWeight: FontWeight.w700,
            letterSpacing: -0.2,
            color: enabled
                ? colorScheme.onSurface
                : colorScheme.onSurfaceVariant,
          ),
        ),
        trailing: busy
            ? SizedBox(
                width: 20,
                height: 20,
                child: CircularProgressIndicator(
                  strokeWidth: 2.2,
                  color: effectiveColor,
                ),
              )
            : Icon(
                Icons.chevron_right_rounded,
                color: colorScheme.onSurfaceVariant,
              ),
      ),
    );
  }
}

class _MessagesTabState extends State<MessagesTab> {
  static const int _maxContactMessageBytes = 156;
  static const int _maxChannelMessageBytes = 127;
  static const double _composerOverlayHeight = 148;
  static const Duration _channelAutoReadDelay = Duration(seconds: 5);

  final TextEditingController _textController = TextEditingController();
  final FocusNode _focusNode = FocusNode();
  final ScrollController _scrollController = ScrollController();
  int _messageByteCount = 0;
  String? _highlightedMessageId;
  Timer? _highlightTimer; // Timer for clearing message highlight
  Timer? _channelReadTimer;
  String? _pendingChannelReadKey;
  bool _suppressMentionTrigger = false;
  TextRange? _activeMentionRange;
  String _mentionQuery = '';
  List<Contact> _mentionSuggestions = const [];
  ContactsProvider? _contactsProvider;

  // Message destination state
  String _destinationType =
      MessageDestinationPreferences.destinationTypeChannel;
  Contact? _selectedRecipient;
  bool _isDestinationLocked = false;

  // Region scope state
  String? _channelRegionScopeName;
  Uint8List? _channelRegionScopeKey;
  Map<int, String> _channelRegionScopes = {};

  // Image sending state
  bool _isSendingImage = false;
  final ImagePicker _imagePicker = ImagePicker();

  // Voice recording state
  final VoiceRecorderService _voiceRecorder = VoiceRecorderService();
  bool _isRecording = false;
  bool _isSendingVoice = false;
  static const Duration _maxVoiceRecordingDuration = Duration(seconds: 30);
  static const double _silenceRmsThreshold = 500.0;
  static const double _silencePeakThreshold = 1400.0;
  static const int _maxInteriorSilentChunks = 2;
  bool get _voiceSupported => !kIsWeb && (Platform.isIOS || Platform.isAndroid);
  StreamSubscription<Int16List>? _voiceStreamSub;
  String? _currentVoiceSessionId;
  final List<Int16List> _recordedChunks = [];
  VoicePacketMode? _activeVoiceMode;
  int _selectedVoiceBitrate = VoiceBitratePreferences.defaultBitrate;

  @override
  void initState() {
    super.initState();
    _textController.addListener(_handleComposerChanged);
    _focusNode.addListener(_handleFocusChanged);
    _loadVoiceSettings();
    _loadAllChannelRegionScopes();
    _scheduleDestinationSync();
  }

  Future<void> _loadVoiceSettings() async {
    final bitrate = await VoiceBitratePreferences.getBitrate();
    if (!mounted) return;
    setState(() {
      _selectedVoiceBitrate = bitrate;
    });
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final contactsProvider = context.read<ContactsProvider>();
    if (!identical(_contactsProvider, contactsProvider)) {
      _contactsProvider?.removeListener(_handleContactsChanged);
      _contactsProvider = contactsProvider;
      _contactsProvider?.addListener(_handleContactsChanged);
    }
    _scheduleDestinationSync();
  }

  @override
  void dispose() {
    _highlightTimer?.cancel();
    _channelReadTimer?.cancel();
    _voiceStreamSub?.cancel();
    _voiceRecorder.dispose();
    _contactsProvider?.removeListener(_handleContactsChanged);
    _focusNode.removeListener(_handleFocusChanged);
    _textController.dispose();
    _focusNode.dispose();
    _scrollController.dispose();
    super.dispose();
  }

  @override
  void didUpdateWidget(covariant MessagesTab oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.isActive != widget.isActive) {
      _syncChannelAutoReadTimer(context.read<MessagesProvider>());
      if (widget.isActive) {
        _scheduleDestinationSync();
      }
    }
  }

  void _scheduleDestinationSync() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _synchronizeDestinationState();
    });
  }

  void _handleContactsChanged() {
    if (!mounted) return;
    _scheduleDestinationSync();
  }

  Future<void> _synchronizeDestinationState() async {
    if (!mounted) return;
    final messagesProvider = context.read<MessagesProvider>();
    final targetMessageId = messagesProvider.targetMessageId;
    final targetDestinationType = messagesProvider.targetDestinationType;
    final targetRecipientPublicKeyHex =
        messagesProvider.targetRecipientPublicKeyHex;

    await _restoreDestinationState(
      overrideType: targetDestinationType,
      overrideRecipientPublicKeyHex: targetRecipientPublicKeyHex,
    );

    if (!mounted) return;

    if (targetMessageId != null) {
      _scrollToMessage(targetMessageId);
      messagesProvider.clearMessageNavigation();
    }

    if (targetDestinationType != null) {
      messagesProvider.clearDestinationNavigation();
      _focusNode.requestFocus();
    }
  }

  void _scrollToMessage(String messageId) {
    final messagesProvider = context.read<MessagesProvider>();
    final messages = messagesProvider.buildDisplayMessages(
      _getFilteredMessages(messagesProvider),
    );

    final messageIndex = messages.indexWhere(
      (entry) => entry.message.id == messageId,
    );

    if (messageIndex != -1 && _scrollController.hasClients) {
      // Calculate position - accounting for reverse list
      final itemHeight = 80.0; // Approximate height of a message bubble
      final targetOffset = messageIndex * itemHeight;

      // Scroll to the message
      _scrollController.animateTo(
        targetOffset,
        duration: const Duration(milliseconds: 500),
        curve: Curves.easeInOut,
      );

      // Highlight the message briefly
      setState(() {
        _highlightedMessageId = messageId;
      });

      // Clear highlight after 2 seconds using a properly managed Timer
      _highlightTimer?.cancel();
      _highlightTimer = Timer(const Duration(seconds: 2), () {
        if (mounted) {
          setState(() {
            _highlightedMessageId = null;
          });
        }
      });
    }
  }

  void _handleComposerChanged() {
    _updateCharacterCount();

    if (_suppressMentionTrigger) {
      return;
    }

    _updateMentionSuggestions(_textController.value);
  }

  void _updateCharacterCount() {
    setState(() {
      _messageByteCount = utf8.encode(_textController.text).length;
    });
  }

  void _handleFocusChanged() {
    if (!_focusNode.hasFocus) {
      _clearMentionSuggestions();
      return;
    }

    if (_suppressMentionTrigger) {
      return;
    }

    _updateMentionSuggestions(_textController.value);
  }

  void _updateMentionSuggestions(TextEditingValue value) {
    final triggerRange = _getMentionTriggerRange(value);
    if (triggerRange == null) {
      _clearMentionSuggestions();
      return;
    }

    final query = value.text.substring(
      triggerRange.start + 1,
      triggerRange.end,
    );
    final contacts = _buildMentionSuggestions(query);
    if (!mounted) {
      return;
    }

    setState(() {
      _activeMentionRange = triggerRange;
      _mentionQuery = query;
      _mentionSuggestions = contacts;
    });
  }

  void _clearMentionSuggestions() {
    if (_activeMentionRange == null &&
        _mentionQuery.isEmpty &&
        _mentionSuggestions.isEmpty) {
      return;
    }

    setState(() {
      _activeMentionRange = null;
      _mentionQuery = '';
      _mentionSuggestions = const [];
    });
  }

  TextRange? _getMentionTriggerRange(TextEditingValue value) {
    if (!value.selection.isValid || !value.selection.isCollapsed) {
      return null;
    }

    final cursorOffset = value.selection.baseOffset;
    if (cursorOffset <= 0) {
      return null;
    }

    final text = value.text;
    final triggerStart = text.lastIndexOf('@', cursorOffset - 1);
    if (triggerStart == -1) {
      return null;
    }

    final leadingChar = triggerStart > 0 ? text[triggerStart - 1] : null;
    if (leadingChar != null && !RegExp(r'[\s\(\[\{]').hasMatch(leadingChar)) {
      return null;
    }

    final query = text.substring(triggerStart + 1, cursorOffset);
    if (query.contains(RegExp(r'[\s@\[\]\n\r]'))) {
      return null;
    }

    return TextRange(start: triggerStart, end: cursorOffset);
  }

  List<Contact> _buildMentionSuggestions(String query) {
    final contactsProvider = context.read<ContactsProvider>();
    final normalizedQuery = query.trim().toLowerCase();
    final contacts =
        contactsProvider.chatContacts.where((contact) {
          if (normalizedQuery.isEmpty) return true;
          return contact.displayName.toLowerCase().contains(normalizedQuery);
        }).toList()..sort((a, b) {
          final primary = compareContactsByFavouriteThenLastSeen(a, b);
          if (primary != 0) {
            return primary;
          }

          final aName = a.displayName.toLowerCase();
          final bName = b.displayName.toLowerCase();
          final aStarts =
              normalizedQuery.isNotEmpty && aName.startsWith(normalizedQuery);
          final bStarts =
              normalizedQuery.isNotEmpty && bName.startsWith(normalizedQuery);
          if (aStarts != bStarts) {
            return aStarts ? -1 : 1;
          }

          return compareContactsByDisplayName(a, b);
        });

    return contacts.take(8).toList(growable: false);
  }

  int get _maxMessageBytes =>
      _destinationType == MessageDestinationPreferences.destinationTypeChannel
      ? _maxChannelMessageBytes
      : _maxContactMessageBytes;

  TextInputFormatter get _messageByteLimiter =>
      TextInputFormatter.withFunction((oldValue, newValue) {
        if (utf8.encode(newValue.text).length <= _maxMessageBytes) {
          return newValue;
        }
        return oldValue;
      });

  void _enforceMessageByteLimit() {
    final currentText = _textController.text;
    if (utf8.encode(currentText).length <= _maxMessageBytes) {
      _updateCharacterCount();
      return;
    }

    var truncated = currentText;
    while (truncated.isNotEmpty &&
        utf8.encode(truncated).length > _maxMessageBytes) {
      truncated = truncated.substring(0, truncated.length - 1);
    }

    _textController.value = _textController.value.copyWith(
      text: truncated,
      selection: TextSelection.collapsed(offset: truncated.length),
      composing: TextRange.empty,
    );
    _updateCharacterCount();
  }

  Future<void> _restoreDestinationState({
    String? overrideType,
    String? overrideRecipientPublicKeyHex,
  }) async {
    final lockedDestination =
        await MessageDestinationPreferences.getLockedDestination();
    final savedDestination =
        await MessageDestinationPreferences.getDestination();
    final effectiveType =
        overrideType ??
        lockedDestination?['type'] ??
        savedDestination?['type'] ??
        MessageDestinationPreferences.destinationTypeChannel;
    final effectivePublicKeyHex =
        overrideRecipientPublicKeyHex ??
        lockedDestination?['publicKey'] ??
        savedDestination?['publicKey'];
    if (!mounted) return;
    final contactsProvider = context.read<ContactsProvider>();
    final recipient = _resolveDestinationRecipient(
      contactsProvider,
      effectiveType,
      effectivePublicKeyHex,
    );
    final allowsEmptyRecipient =
        effectiveType == MessageDestinationPreferences.destinationTypeAll ||
        (effectiveType == MessageDestinationPreferences.destinationTypeChannel &&
            effectivePublicKeyHex == null);
    final shouldFallbackToPublicChannel =
        recipient == null && !allowsEmptyRecipient;
    final destinationType = shouldFallbackToPublicChannel
        ? MessageDestinationPreferences.destinationTypeChannel
        : effectiveType;
    final selectedRecipient = shouldFallbackToPublicChannel ? null : recipient;
    final shouldClearSavedDestination =
        lockedDestination == null &&
        overrideType == null &&
        shouldFallbackToPublicChannel &&
        savedDestination != null;

    if (!mounted) return;

    setState(() {
      _isDestinationLocked = lockedDestination != null;
      _destinationType = destinationType;
      _selectedRecipient = selectedRecipient;
    });

    if (shouldClearSavedDestination) {
      await MessageDestinationPreferences.clearDestination();
    }

    _enforceMessageByteLimit();
    await _loadRegionScope();
  }

  Contact? _resolveDestinationRecipient(
    ContactsProvider contactsProvider,
    String type,
    String? publicKeyHex,
  ) {
    if (publicKeyHex == null) {
      return null;
    }

    final candidates = switch (type) {
      MessageDestinationPreferences.destinationTypeChannel =>
        contactsProvider.channels,
      MessageDestinationPreferences.destinationTypeRoom => contactsProvider.rooms,
      MessageDestinationPreferences.destinationTypeContact =>
        contactsProvider.chatContacts,
      _ => contactsProvider.contacts,
    };

    return candidates.where((contact) {
      return contact.publicKeyHex == publicKeyHex;
    }).firstOrNull;
  }

  /// Show recipient selector bottom sheet
  void _showRecipientSelector() {
    if (_isDestinationLocked) {
      return;
    }

    final contactsProvider = context.read<ContactsProvider>();
    final messagesProvider = context.read<MessagesProvider>();

    final contacts = List<Contact>.from(contactsProvider.chatContacts)
      ..sort((a, b) {
        final primary = compareContactsByFavouriteThenLastSeen(a, b);
        if (primary != 0) {
          return primary;
        }

        return compareContactsByDisplayName(a, b);
      });
    final rooms = List<Contact>.from(contactsProvider.rooms)
      ..sort((a, b) {
        final primary = compareContactsByLastSeen(a, b);
        if (primary != 0) {
          return primary;
        }

        return compareContactsByDisplayName(a, b);
      });
    final channels = List<Contact>.from(contactsProvider.channels)
      ..sort((a, b) {
        final primary = compareContactsByLastSeen(a, b);
        if (primary != 0) {
          return primary;
        }

        return compareContactsByDisplayName(a, b);
      });

    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      backgroundColor: Colors.transparent,
      builder: (context) => RecipientSelectorSheet(
        contacts: contacts,
        rooms: rooms,
        channels: channels,
        unreadCount: messagesProvider.unreadCount,
        unreadCountsByPublicKey: {
          for (final contact in [...contacts, ...rooms, ...channels])
            contact.publicKeyHex: messagesProvider.getUnreadCountForDestination(
              contact,
            ),
        },
        currentDestinationType: _destinationType,
        currentRecipientPublicKey: _selectedRecipient?.publicKeyHex,
        onSelect: _onRecipientSelected,
        channelRegionScopes: _channelRegionScopes,
      ),
    );
  }

  /// Handle recipient selection
  Future<void> _onRecipientSelected(
    String type,
    Contact? recipient, {
    bool persistSelection = true,
  }) async {
    setState(() {
      _destinationType = type;
      _selectedRecipient = recipient;
    });

    _markCurrentDestinationAsRead();

    _enforceMessageByteLimit();

    // Load region scope for channel destinations
    await _loadRegionScope();

    if (persistSelection) {
      await MessageDestinationPreferences.setDestination(
        type,
        recipientPublicKey: recipient?.publicKeyHex,
      );
    }

    // Show confirmation toast
    if (!mounted) return;
  }

  Future<void> _loadRegionScope() async {
    if (_destinationType !=
        MessageDestinationPreferences.destinationTypeChannel) {
      if (_channelRegionScopeName != null) {
        setState(() {
          _channelRegionScopeName = null;
          _channelRegionScopeKey = null;
        });
      }
      return;
    }
    final channelIdx = _selectedRecipient?.publicKey[1] ?? 0;
    final scope = await RegionScopePreferences.getScope(channelIdx);
    if (!mounted) return;
    setState(() {
      _channelRegionScopeName = scope?.name;
      _channelRegionScopeKey = scope?.key;
      if (scope != null) {
        _channelRegionScopes[channelIdx] = scope.name;
      } else {
        _channelRegionScopes.remove(channelIdx);
      }
    });
  }

  Future<void> _loadAllChannelRegionScopes() async {
    final contactsProvider = context.read<ContactsProvider>();
    final channels = contactsProvider.chatContacts
        .where((c) => c.publicKey.length > 1 && c.publicKey[0] == 0)
        .toList();
    final scopes = <int, String>{};
    // Always check channel 0 (public)
    final scope0 = await RegionScopePreferences.getScope(0);
    if (scope0 != null) scopes[0] = scope0.name;
    for (final ch in channels) {
      final idx = ch.publicKey[1];
      final scope = await RegionScopePreferences.getScope(idx);
      if (scope != null) scopes[idx] = scope.name;
    }
    if (!mounted) return;
    setState(() {
      _channelRegionScopes = scopes;
    });
  }

  void _insertReplyMention(String displayName, {TextRange? replacementRange}) {
    final trimmedName = displayName.trim();
    if (trimmedName.isEmpty) return;

    final mention = '@[$trimmedName] ';
    final value = _textController.value;
    final selection = replacementRange ?? value.selection;
    final hasSelection =
        selection.isValid &&
        selection.start >= 0 &&
        selection.end >= selection.start;

    final start = hasSelection ? selection.start : value.text.length;
    final end = hasSelection ? selection.end : value.text.length;
    final nextText = value.text.replaceRange(start, end, mention);
    final nextOffset = start + mention.length;

    _suppressMentionTrigger = true;
    _textController.value = value.copyWith(
      text: nextText,
      selection: TextSelection.collapsed(offset: nextOffset),
      composing: TextRange.empty,
    );
    _suppressMentionTrigger = false;
    _clearMentionSuggestions();
    _enforceMessageByteLimit();
  }

  void _selectMention(Contact contact) {
    _insertReplyMention(
      contact.displayName,
      replacementRange: _activeMentionRange,
    );
    _focusNode.requestFocus();
  }

  Future<void> _replyToMessage(Message message) async {
    final contactsProvider = context.read<ContactsProvider>();
    Contact? senderContact;
    String? senderDisplayName;

    String destinationType;
    Contact? recipient;

    if (message.isChannelMessage) {
      final channelIdx = message.channelIdx ?? 0;
      destinationType = MessageDestinationPreferences.destinationTypeChannel;

      if (channelIdx != 0) {
        recipient = contactsProvider.channels.where((contact) {
          return contact.publicKey.length > 1 &&
              contact.publicKey[1] == channelIdx;
        }).firstOrNull;

        if (recipient == null) {
          ToastLogger.error(
            context,
            AppLocalizations.of(context)!.cannotReplyContactNotFound,
          );
          return;
        }
      }

      final senderPrefix = message.senderPublicKeyPrefix;
      if (senderPrefix != null && senderPrefix.length >= 6) {
        senderContact = contactsProvider.findContactByPrefix(senderPrefix);
      }
      senderDisplayName =
          senderContact?.displayName ?? message.senderName?.trim();
    } else {
      final roomRecipient = message.recipientPublicKey == null
          ? null
          : contactsProvider.findContactByKey(message.recipientPublicKey!);

      if (roomRecipient?.isRoom == true) {
        destinationType = MessageDestinationPreferences.destinationTypeRoom;
        recipient = roomRecipient;

        final senderPrefix = message.senderPublicKeyPrefix;
        if (senderPrefix != null && senderPrefix.length >= 6) {
          senderContact = contactsProvider.findContactByPrefix(senderPrefix);
        }
        senderDisplayName =
            senderContact?.displayName ?? message.senderName?.trim();
      } else {
        final senderPrefix = message.senderPublicKeyPrefix;
        if (senderPrefix == null || senderPrefix.length < 6) {
          ToastLogger.error(
            context,
            AppLocalizations.of(context)!.cannotReplySenderMissing,
          );
          return;
        }

        recipient = contactsProvider.findContactByPrefix(senderPrefix);
        if (recipient == null) {
          ToastLogger.error(
            context,
            AppLocalizations.of(context)!.cannotReplyContactNotFound,
          );
          return;
        }

        destinationType = recipient.isRoom
            ? MessageDestinationPreferences.destinationTypeRoom
            : MessageDestinationPreferences.destinationTypeContact;
      }
    }

    await _onRecipientSelected(
      destinationType,
      recipient,
      persistSelection: !_isDestinationLocked,
    );
    if (!mounted) return;
    if ((message.isChannelMessage || recipient?.isRoom == true) &&
        senderDisplayName != null &&
        senderDisplayName.isNotEmpty) {
      _insertReplyMention(senderDisplayName);
    }
    _focusNode.requestFocus();
  }

  /// Get icon for current destination type
  IconData _getDestinationIcon() {
    if (_destinationType == MessageDestinationPreferences.destinationTypeAll) {
      return Icons.all_inbox;
    } else if (_destinationType ==
        MessageDestinationPreferences.destinationTypeChannel) {
      return Icons.public;
    } else if (_destinationType ==
        MessageDestinationPreferences.destinationTypeRoom) {
      return Icons.meeting_room;
    } else {
      return Icons.person;
    }
  }

  String _getDestinationLabel() {
    if (_destinationType == MessageDestinationPreferences.destinationTypeAll) {
      return AppLocalizations.of(context)!.showAll;
    }
    if (_destinationType ==
            MessageDestinationPreferences.destinationTypeChannel &&
        _selectedRecipient != null) {
      return _selectedRecipient!.getLocalizedDisplayName(context);
    }
    if (_selectedRecipient != null) {
      return _selectedRecipient!.displayName;
    }
    return 'Public Channel';
  }

  Future<void> _sendMessage() async {
    final text = _textController.text.trim();
    if (text.isEmpty) return;

    final connectionProvider = context.read<ConnectionProvider>();
    final messagesProvider = context.read<MessagesProvider>();
    final contactsProvider = context.read<ContactsProvider>();

    if (!connectionProvider.deviceInfo.isConnected) {
      if (!mounted) return;
      ToastLogger.error(context, 'Not connected to device');
      return;
    }

    if (_destinationType == MessageDestinationPreferences.destinationTypeAll) {
      if (!mounted) return;
      ToastLogger.error(context, 'Select a channel, contact, or room first');
      return;
    }

    _textController.clear();

    try {
      // Check destination type and send accordingly
      if (_destinationType ==
          MessageDestinationPreferences.destinationTypeChannel) {
        // Send to selected channel (or public channel if none selected)
        final channelIdx =
            _selectedRecipient?.publicKey[1] ??
            0; // Extract channel index from pseudo public key
        await _sendToChannel(
          text,
          connectionProvider,
          messagesProvider,
          channelIdx,
        );
      } else if (_selectedRecipient != null) {
        // Send to contact or room
        await _sendToRecipient(
          text,
          connectionProvider,
          messagesProvider,
          contactsProvider,
        );
      } else {
        // Fallback to public channel if no recipient selected
        debugPrint(
          '⚠️ [MessagesTab] No recipient selected, falling back to public channel',
        );
        await _sendToChannel(text, connectionProvider, messagesProvider, 0);
      }

      _markCurrentDestinationAsRead();

      if (!mounted) return;
    } catch (e) {
      if (!mounted) return;
      if (_textController.text.isEmpty) {
        _textController.text = text;
        _textController.selection = TextSelection.collapsed(
          offset: _textController.text.length,
        );
        _focusNode.requestFocus();
      }
      ToastLogger.error(context, 'Failed to send: $e');
    }
  }

  /// Send message to channel
  Future<void> _sendToChannel(
    String text,
    ConnectionProvider connectionProvider,
    MessagesProvider messagesProvider,
    int channelIdx,
  ) async {
    // Create message ID
    final messageId = '${DateTime.now().millisecondsSinceEpoch}_channel_sent';
    final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;

    // Get current device's public key (first 6 bytes)
    final devicePublicKey = connectionProvider.deviceInfo.publicKey;
    final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6);

    // Create sent message object
    final sentMessage = Message(
      id: messageId,
      messageType: MessageType.channel,
      senderPublicKeyPrefix: senderPublicKeyPrefix,
      pathLen: 0,
      textType: MessageTextType.plain,
      senderTimestamp: timestamp,
      text: text,
      receivedAt: DateTime.now(),
      deliveryStatus: MessageDeliveryStatus.sending,
      channelIdx: channelIdx,
    );

    // Add to messages list with "sending" status
    messagesProvider.addSentMessage(sentMessage, contact: _selectedRecipient);

    // Send to selected channel (with region scope if set)
    await connectionProvider.sendChannelMessage(
      channelIdx: channelIdx,
      text: text,
      messageId: messageId,
      floodScopeKey: _channelRegionScopeKey,
    );
  }

  /// Send message to contact or room
  Future<void> _sendToRecipient(
    String text,
    ConnectionProvider connectionProvider,
    MessagesProvider messagesProvider,
    ContactsProvider contactsProvider,
  ) async {
    if (_selectedRecipient == null) return;

    // Create message ID
    final messageId = '${DateTime.now().millisecondsSinceEpoch}_sent';
    final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;

    // Get current device's public key (first 6 bytes)
    final devicePublicKey = connectionProvider.deviceInfo.publicKey;
    final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6);

    // Create sent message object with recipient public key for retry support
    final sentMessage = Message(
      id: messageId,
      messageType: MessageType.contact,
      senderPublicKeyPrefix: senderPublicKeyPrefix,
      pathLen: 0,
      textType: MessageTextType.plain,
      senderTimestamp: timestamp,
      text: text,
      receivedAt: DateTime.now(),
      deliveryStatus: MessageDeliveryStatus.sending,
      recipientPublicKey: _selectedRecipient!.publicKey,
    );

    // Add to messages list with "sending" status
    messagesProvider.addSentMessage(sentMessage, contact: _selectedRecipient);

    // Send message to selected recipient
    final sentSuccessfully = await connectionProvider.sendTextMessage(
      contactPublicKey: _selectedRecipient!.publicKey,
      text: text,
      messageId: messageId,
      contact: _selectedRecipient,
    );

    if (!sentSuccessfully) {
      // Mark message as failed if sending failed
      messagesProvider.markMessageFailed(messageId);
    }
  }

  bool _isContactDestination() {
    return _destinationType ==
            MessageDestinationPreferences.destinationTypeContact &&
        _selectedRecipient != null;
  }

  void _showSendModeSheet() {
    if (!mounted) return;
    final l10n = AppLocalizations.of(context)!;

    showModalBottomSheet<void>(
      context: context,
      builder: (sheetContext) {
        return SafeArea(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              ListTile(
                leading: const Icon(Icons.route),
                title: Text(l10n.autoSend),
                subtitle: Text(l10n.autoSendDescription),
                onTap: () {
                  Navigator.pop(sheetContext);
                  _sendMessage();
                },
              ),
              ListTile(
                leading: const Icon(Icons.near_me),
                title: Text(l10n.sendDirect),
                subtitle: Text(l10n.sendDirectDescription),
                onTap: () async {
                  Navigator.pop(sheetContext);
                  final connectionProvider = context.read<ConnectionProvider>();
                  if (_selectedRecipient != null &&
                      connectionProvider.deviceInfo.isConnected) {
                    await connectionProvider
                        .setContactDirect(_selectedRecipient!);
                  }
                  await _sendMessage();
                },
              ),
              ListTile(
                leading: const Icon(Icons.cell_tower),
                title: Text(l10n.sendFlood),
                subtitle: Text(l10n.sendFloodDescription),
                onTap: () async {
                  Navigator.pop(sheetContext);
                  final connectionProvider = context.read<ConnectionProvider>();
                  if (_selectedRecipient != null &&
                      connectionProvider.deviceInfo.isConnected) {
                    await connectionProvider
                        .resetPath(_selectedRecipient!.publicKey);
                  }
                  await _sendMessage();
                },
              ),
            ],
          ),
        );
      },
    );
  }

  Future<void> _startTicTacToeGame() async {
    if (!mounted) return;
    if (_destinationType !=
            MessageDestinationPreferences.destinationTypeContact ||
        _selectedRecipient == null) {
      ToastLogger.warning(
        context,
        'Tic-Tac-Toe works only in direct messages. Choose a contact first.',
      );
      return;
    }

    final connectionProvider = context.read<ConnectionProvider>();
    final messagesProvider = context.read<MessagesProvider>();
    final contactsProvider = context.read<ContactsProvider>();
    if (!connectionProvider.deviceInfo.isConnected) {
      ToastLogger.error(context, 'Not connected to device');
      return;
    }

    final devicePublicKey = connectionProvider.deviceInfo.publicKey;
    if (devicePublicKey == null || devicePublicKey.length < 6) {
      ToastLogger.error(context, 'Device key unavailable');
      return;
    }

    final gameId = List.generate(
      8,
      (_) => math.Random.secure().nextInt(16).toRadixString(16),
    ).join();
    final starterKey6 = devicePublicKey
        .sublist(0, 6)
        .map((b) => b.toRadixString(16).padLeft(2, '0'))
        .join('');
    final startMessage = TicTacToeMessageParser.encodeStart(
      gameId: gameId,
      starterKey6: starterKey6,
      timestampSec: DateTime.now().millisecondsSinceEpoch ~/ 1000,
    );

    await _sendToRecipient(
      startMessage,
      connectionProvider,
      messagesProvider,
      contactsProvider,
    );
  }

  // ── Image sending ───────────────────────────────────────────────────────────

  Future<void> _pickAndSendImage({
    ImageSource source = ImageSource.gallery,
  }) async {
    if (_isSendingImage) return;
    final shouldContinue = await _confirmPublicChannelMediaSend('image');
    if (!shouldContinue) return;
    if (!mounted) return;
    final connectionProvider = context.read<ConnectionProvider>();
    final messagesProvider = context.read<MessagesProvider>();
    if (!connectionProvider.deviceInfo.isConnected) {
      ToastLogger.error(context, 'Not connected to device');
      return;
    }

    // Pick image.
    final picked = await _imagePicker.pickImage(source: source);
    if (picked == null) return;
    final rawBytes = await picked.readAsBytes();

    setState(() => _isSendingImage = true);
    String? failedMessageId;
    try {
      // Compress AVIF using user-selected size, compression and color mode.
      final maxSize = await ImagePreferences.getMaxSize();
      final compression = await ImagePreferences.getCompression();
      final grayscale = await ImagePreferences.getGrayscale();
      final ultraMode = await ImagePreferences.getUltraMode();
      final effectiveMaxSize = ImagePreferences.effectiveMaxSize(
        maxSize,
        ultraMode: ultraMode,
      );
      final result = await ImageCodecService.compress(
        rawBytes,
        maxDimension: effectiveMaxSize,
        compression: compression,
        grayscale: grayscale,
        ultraMode: ultraMode,
      );
      if (result == null) {
        if (!mounted) return;
        ToastLogger.error(context, 'Image compression failed');
        return;
      }
      final compressed = result.bytes;

      // Generate session ID (4 random bytes → 8 hex chars).
      final sessionId = List.generate(
        8,
        (_) => math.Random().nextInt(16).toRadixString(16),
      ).join();

      // Fragment.
      var imageDataBytesPerFragment = ImagePacket.maxDataBytes;
      if (_destinationType ==
              MessageDestinationPreferences.destinationTypeContact &&
          _selectedRecipient != null &&
          _selectedRecipient!.routeHasPath) {
        imageDataBytesPerFragment = safeImageDataBytesForPath(
          _selectedRecipient!.routeHopCount,
        );
      }

      final fragments = fragmentImage(
        sessionId: sessionId,
        format: ImageFormat.avif,
        bytes: compressed,
        maxDataBytes: imageDataBytesPerFragment,
      );

      if (fragments.isEmpty) {
        if (!mounted) return;
        ToastLogger.error(context, 'Image fragmentation failed');
        return;
      }

      // Build envelope.
      final deviceKey = connectionProvider.deviceInfo.publicKey;
      if (deviceKey == null || deviceKey.length < 6) {
        if (!mounted) return;
        ToastLogger.error(context, 'Device key unavailable');
        return;
      }
      final envelope = ImageEnvelope(
        sessionId: sessionId,
        format: ImageFormat.avif,
        total: fragments.length,
        width: result.width,
        height: result.height,
        sizeBytes: compressed.length,
      );

      if (!mounted) return;

      // Cache for deferred serving.
      final imageProvider = context.read<ip.ImageProvider>();
      imageProvider.cacheOutgoingSession(sessionId, fragments, envelope);

      // Add local placeholder message.
      final msgId = 'img_${sessionId}_sent';
      failedMessageId = msgId;
      final isChannel =
          _destinationType ==
          MessageDestinationPreferences.destinationTypeChannel;
      final channelIdx = isChannel
          ? (_selectedRecipient?.publicKey[1] ?? 0)
          : null;
      final recipient = _selectedRecipient;
      final placeholder = Message(
        id: msgId,
        messageType: isChannel ? MessageType.channel : MessageType.contact,
        channelIdx: channelIdx,
        senderPublicKeyPrefix: deviceKey.sublist(0, 6),
        pathLen: 0,
        textType: MessageTextType.plain,
        senderTimestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
        text: envelope.encode(),
        receivedAt: DateTime.now(),
        deliveryStatus: MessageDeliveryStatus.sending,
        recipientPublicKey: isChannel ? null : recipient?.publicKey,
      );
      messagesProvider.addSentMessage(placeholder, contact: recipient);

      // Send IE1 envelope via normal message path.
      final envelopeText = envelope.encode();

      if (isChannel) {
        await connectionProvider.sendChannelMessage(
          channelIdx: channelIdx ?? 0,
          text: envelopeText,
          messageId: msgId,
          floodScopeKey: _channelRegionScopeKey,
        );
      } else if (recipient != null) {
        final sent = await connectionProvider.sendTextMessage(
          contactPublicKey: recipient.publicKey,
          text: envelopeText,
          messageId: msgId,
          contact: recipient,
        );
        if (!sent) {
          messagesProvider.markMessageFailed(msgId);
          if (!mounted) return;
          ToastLogger.error(context, 'Failed to announce image');
          return;
        }
      } else {
        messagesProvider.markMessageFailed(msgId);
        if (!mounted) return;
        ToastLogger.error(context, 'No recipient selected');
        return;
      }

      debugPrint(
        '📷 [Image] Sent IE1 for session $sessionId: '
        '${fragments.length} fragments, ${compressed.length}B, '
        'chunk=${imageDataBytesPerFragment}B',
      );

      if (isChannel) {
        for (final fragment in fragments) {
          await connectionProvider.sendChannelData(
            channelIdx: channelIdx ?? 0,
            dataType: MeshCoreConstants.dataTypeDev,
            payload: fragment.encodeBinary(),
            floodScopeKey: _channelRegionScopeKey,
          );
        }
      }

      // Image fragments are always served on demand after an explicit IR2
      // fetch request, including direct contacts.
    } catch (e, st) {
      debugPrint('❌ [Image] _pickAndSendImage: $e\n$st');
      if (failedMessageId != null) {
        messagesProvider.markMessageFailed(failedMessageId);
      }
      if (!mounted) return;
      ToastLogger.error(context, 'Image send failed');
    } finally {
      if (mounted) setState(() => _isSendingImage = false);
    }
  }

  // ── Voice recording ────────────────────────────────────────────────────────

  Future<void> _startVoiceRecording() async {
    if (_isSendingVoice || _isRecording) return;
    debugPrint('🎙️ [Voice] _startVoiceRecording called');
    // Read fresh voice preferences so settings changes apply immediately.
    final selectedBitrate = await VoiceBitratePreferences.getBitrate();
    if (mounted) {
      setState(() {
        _selectedVoiceBitrate = selectedBitrate;
      });
    } else {
      _selectedVoiceBitrate = selectedBitrate;
    }
    final hasPermission = await _voiceRecorder.requestPermission();
    debugPrint('🎙️ [Voice] hasPermission=$hasPermission');
    if (!hasPermission) {
      if (!mounted) return;
      ToastLogger.error(context, 'Microphone permission required for voice');
      return;
    }

    if (!mounted) return;
    final appProvider = context.read<AppProvider>();
    final connectionProvider = context.read<ConnectionProvider>();
    debugPrint(
      '🎙️ [Voice] isConnected=${connectionProvider.deviceInfo.isConnected}',
    );
    if (!connectionProvider.deviceInfo.isConnected) {
      ToastLogger.error(context, 'Not connected to device');
      return;
    }

    // Generate a new session ID (4 random bytes → 8 hex chars)
    final rng = math.Random.secure();
    _currentVoiceSessionId = List.generate(
      4,
      (_) => rng.nextInt(256),
    ).map((b) => b.toRadixString(16).padLeft(2, '0')).join();

    _activeVoiceMode = VoiceBitratePreferences.toVoiceMode(
      _selectedVoiceBitrate,
    );
    final packetDuration = Duration(
      milliseconds: _activeVoiceMode!.packetDurationMs,
    );
    final maxVoicePackets = _maxVoicePacketsForMode(_activeVoiceMode!);

    debugPrint(
      '🎙️ [Voice] session=$_currentVoiceSessionId mode=$_activeVoiceMode chunkDuration=${packetDuration.inMilliseconds}ms maxPackets=$maxVoicePackets',
    );

    _recordedChunks.clear();
    setState(() => _isRecording = true);

    try {
      final stream = _voiceRecorder.startCapture(
        chunkDuration: packetDuration,
        sampleRateHz: _activeVoiceMode!.sampleRateHz,
        enableBandPassFilter: appProvider.isVoiceBandPassFilterEnabled,
        enableCompressor: appProvider.isVoiceCompressorEnabled,
        enableLimiter: appProvider.isVoiceLimiterEnabled,
        enableAutoGain: appProvider.isVoiceAutoGainEnabled,
        enableEchoCancellation: appProvider.isVoiceEchoCancellationEnabled,
        enableNoiseSuppression: appProvider.isVoiceNoiseSuppressionEnabled,
      );
      debugPrint('🎙️ [Voice] capture started, listening for chunks...');
      _voiceStreamSub = stream.listen(
        (pcmChunk) {
          if (!_isRecording) return;
          _recordedChunks.add(pcmChunk);
          debugPrint(
            '🎙️ [Voice] chunk #${_recordedChunks.length} received: ${pcmChunk.length} samples',
          );
          setState(() {});
          if (_recordedChunks.length >= maxVoicePackets) {
            debugPrint('🎙️ [Voice] max packets reached, stopping');
            _stopAndSendVoice();
          }
        },
        onError: (e) {
          debugPrint('❌ [Voice] stream error: $e');
          _stopAndSendVoice();
        },
        onDone: () => debugPrint('🎙️ [Voice] stream done'),
      );
    } catch (e) {
      debugPrint('❌ [Voice] startCapture threw: $e');
      if (mounted) setState(() => _isRecording = false);
    }
  }

  int _maxVoicePacketsForMode(VoicePacketMode mode) {
    final packets =
        _maxVoiceRecordingDuration.inMilliseconds ~/ mode.packetDurationMs;
    return packets < 1 ? 1 : packets;
  }

  Future<void> _stopAndSendVoice() async {
    if (!_isRecording) return;
    final trimSilenceEnabled = context
        .read<AppProvider>()
        .isVoiceSilenceTrimmingEnabled;
    debugPrint(
      '🎙️ [Voice] _stopAndSendVoice: ${_recordedChunks.length} chunks buffered',
    );

    await _voiceStreamSub?.cancel();
    _voiceStreamSub = null;
    await _voiceRecorder.stopCapture();

    final rawChunks = List<Int16List>.from(_recordedChunks);
    final trimmedChunks = trimSilenceEnabled
        ? _trimSilence(rawChunks)
        : rawChunks;
    final sessionId = _currentVoiceSessionId;
    final mode = _activeVoiceMode;
    final chunks = mode == null
        ? trimmedChunks
        : _prepareChunksForSending(trimmedChunks, mode);
    _recordedChunks.clear();

    debugPrint(
      '🎙️ [Voice] silence trim enabled=$trimSilenceEnabled: raw=${rawChunks.length} chunks -> kept=${chunks.length} chunks',
    );

    if (mounted) {
      setState(() {
        _isRecording = false;
        _isSendingVoice = chunks.isNotEmpty && sessionId != null;
      });
    }

    if (chunks.isEmpty || sessionId == null || mode == null || !mounted) {
      if (mounted) {
        setState(() {
          _isSendingVoice = false;
          _currentVoiceSessionId = null;
        });
      }
      return;
    }

    final shouldContinue = await _confirmPublicChannelMediaSend('voice');
    if (!shouldContinue) {
      if (mounted) {
        setState(() {
          _isSendingVoice = false;
          _currentVoiceSessionId = null;
        });
      }
      return;
    }

    try {
      await _encodeAndSendAllPackets(
        chunks: chunks,
        sessionId: sessionId,
        mode: mode,
      );
    } catch (e, st) {
      debugPrint('❌ [Voice] _encodeAndSendAllPackets threw: $e\n$st');
    } finally {
      debugPrint(
        '🎙️ [Voice] _stopAndSendVoice finally: resetting _isSendingVoice',
      );
      if (mounted) {
        setState(() {
          _isSendingVoice = false;
          _currentVoiceSessionId = null;
        });
      }
    }
  }

  Future<void> _encodeAndSendAllPackets({
    required List<Int16List> chunks,
    required String sessionId,
    required VoicePacketMode mode,
  }) async {
    final total = chunks.length;
    final codec = VoiceCodecService();
    final connectionProvider = context.read<ConnectionProvider>();
    final messagesProvider = context.read<MessagesProvider>();
    final voiceProvider = context.read<VoiceProvider>();
    final msgId = 'voice_${sessionId}_sent';
    final devicePublicKey = connectionProvider.deviceInfo.publicKey;
    final senderPublicKeyPrefix =
        devicePublicKey != null && devicePublicKey.length >= 6
        ? devicePublicKey.sublist(0, 6)
        : null;
    final isChannel =
        _destinationType ==
        MessageDestinationPreferences.destinationTypeChannel;
    final recipient = _selectedRecipient;
    final channelIdx = isChannel ? (recipient?.publicKey[1] ?? 0) : null;

    final encodedPackets = <VoicePacket>[];
    debugPrint(
      '🎙️ [Voice] encoding $total packets for deferred voice fetch, mode=${mode.label}, session=$sessionId',
    );
    for (var i = 0; i < total; i++) {
      if (!mounted) return;
      try {
        final codec2Data = await codec.encode(chunks[i], mode);
        debugPrint(
          '🎙️ [Voice] packet $i/$total encoded: ${codec2Data.length} bytes',
        );
        final packet = VoicePacket(
          sessionId: sessionId,
          mode: mode,
          index: i,
          total: total,
          codec2Data: codec2Data,
        );

        encodedPackets.add(packet);
        voiceProvider.addPacket(packet);
      } catch (e, st) {
        debugPrint('❌ [Voice] packet $i encode error: $e\n$st');
      }
    }

    if (encodedPackets.isEmpty) {
      debugPrint('❌ [Voice] No packets encoded for session $sessionId');
      messagesProvider.markMessageFailed(msgId);
      return;
    }

    voiceProvider.cacheOutgoingSession(sessionId, encodedPackets);

    if (senderPublicKeyPrefix == null || senderPublicKeyPrefix.length < 6) {
      debugPrint('❌ [Voice] Missing device public key prefix for envelope');
      messagesProvider.markMessageFailed(msgId);
      return;
    }

    final durationMs = encodedPackets.fold<int>(
      0,
      (sum, p) => sum + p.durationMs,
    );
    final envelope = VoiceEnvelope(
      sessionId: sessionId,
      mode: mode,
      total: encodedPackets.length,
      durationMs: durationMs,
      version: 3,
    );
    final envelopeText = envelope.encodeText();

    // Insert placeholder with real VE1 envelope text so technical details are populated.
    final sentMsg = Message(
      id: msgId,
      messageType: (!isChannel && recipient != null)
          ? MessageType.contact
          : MessageType.channel,
      senderPublicKeyPrefix: senderPublicKeyPrefix,
      pathLen: 0,
      textType: MessageTextType.plain,
      senderTimestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
      text: envelopeText,
      receivedAt: DateTime.now(),
      deliveryStatus: MessageDeliveryStatus.sent,
      isVoice: true,
      voiceId: sessionId,
      channelIdx: channelIdx,
      recipientPublicKey: isChannel ? null : recipient?.publicKey,
    );
    messagesProvider.addSentMessage(sentMsg, contact: recipient);

    try {
      if (isChannel) {
        await connectionProvider.sendChannelMessage(
          channelIdx: channelIdx ?? 0,
          text: envelopeText,
          messageId: msgId,
          floodScopeKey: _channelRegionScopeKey,
        );
      } else if (recipient != null) {
        final sentSuccessfully = await connectionProvider.sendTextMessage(
          contactPublicKey: recipient.publicKey,
          text: envelopeText,
          messageId: msgId,
          contact: recipient,
        );
        if (!sentSuccessfully) {
          messagesProvider.markMessageFailed(msgId);
          return;
        }
      } else {
        messagesProvider.markMessageFailed(msgId);
        if (!mounted) return;
        ToastLogger.error(context, 'No recipient selected');
        return;
      }
    } catch (e, st) {
      debugPrint('❌ [Voice] envelope send error: $e\n$st');
      messagesProvider.markMessageFailed(msgId);
      return;
    }

    debugPrint('🎙️ [Voice] envelope sent for session $sessionId');
    if (isChannel) {
      for (final packet in encodedPackets) {
        await connectionProvider.sendChannelData(
          channelIdx: channelIdx ?? 0,
          dataType: MeshCoreConstants.dataTypeDev,
          payload: packet.encodeBinary(),
          floodScopeKey: _channelRegionScopeKey,
        );
      }
    }
    // Mark the placeholder message as "sent" (ackTag=0, timeout=0 = no ACK tracking).
    // addSentMessage() forces deliveryStatus.sending; we upgrade it here so the
    // bubble shows "Sent" instead of "Sending" once all packets are on the wire.
    // For channels this is also set by the onMessageSent callback, but this is harmless.
    messagesProvider.markMessageSent(msgId, 0, 0);
  }

  List<Int16List> _prepareChunksForSending(
    List<Int16List> chunks,
    VoicePacketMode mode,
  ) {
    return chunks;
  }

  List<Int16List> _trimSilence(List<Int16List> chunks) {
    if (chunks.isEmpty) return chunks;

    final isSilent = chunks.map(_isSilentChunk).toList();
    final firstVoice = isSilent.indexWhere((silent) => !silent);
    if (firstVoice == -1) return const [];

    final lastVoice = isSilent.lastIndexWhere((silent) => !silent);
    if (lastVoice < firstVoice) return const [];

    final trimmed = <Int16List>[];
    var interiorSilentRun = 0;
    for (var i = firstVoice; i <= lastVoice; i++) {
      if (isSilent[i]) {
        interiorSilentRun++;
        if (interiorSilentRun <= _maxInteriorSilentChunks) {
          trimmed.add(chunks[i]);
        }
      } else {
        interiorSilentRun = 0;
        trimmed.add(chunks[i]);
      }
    }
    return trimmed;
  }

  bool _isSilentChunk(Int16List chunk) {
    if (chunk.isEmpty) return true;

    final rms = _chunkRms(chunk);
    final peak = _chunkPeak(chunk);
    return rms < _silenceRmsThreshold && peak < _silencePeakThreshold;
  }

  double _chunkRms(Int16List chunk) {
    var sumSquares = 0.0;
    for (final sample in chunk) {
      sumSquares += sample * sample;
    }
    return math.sqrt(sumSquares / chunk.length);
  }

  int _chunkPeak(Int16List chunk) {
    var peak = 0;
    for (final sample in chunk) {
      final absSample = sample.abs();
      if (absSample > peak) {
        peak = absSample;
      }
    }
    return peak;
  }

  bool _isPublicChannelSelected() {
    if (_destinationType !=
        MessageDestinationPreferences.destinationTypeChannel) {
      return false;
    }
    final channelIdx = _selectedRecipient?.publicKey[1] ?? 0;
    return channelIdx == 0;
  }

  Future<bool> _confirmPublicChannelMediaSend(String mediaType) async {
    if (!_isPublicChannelSelected() || !mounted) return true;

    final decision = await showDialog<bool>(
      context: context,
      builder: (dialogContext) => AlertDialog(
        title: Text(AppLocalizations.of(context)!.sendToPublicChannel),
        content: Text(
          'You are about to send $mediaType to the Public Channel. '
          'This is not advised because everyone on the mesh may receive it. '
          'Choose a private or tagged channel unless this is what you want.',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(dialogContext).pop(false),
            child: const Text('Cancel'),
          ),
          FilledButton(
            onPressed: () => Navigator.of(dialogContext).pop(true),
            child: Text(AppLocalizations.of(context)!.sendAnyway),
          ),
        ],
      ),
    );

    return decision ?? false;
  }

  // ── SAR dialog ─────────────────────────────────────────────────────────────

  void _showSarDialog() {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      backgroundColor: Colors.transparent,
      builder: (context) => SarUpdateSheet(
        onSend:
            (
              emoji,
              name,
              position,
              roomPublicKey,
              sendToChannel,
              sendToAllContacts,
              colorIndex,
            ) async {
              await _sendSarMessage(
                emoji,
                name,
                position,
                roomPublicKey,
                sendToChannel,
                sendToAllContacts,
                colorIndex,
              );
            },
      ),
    );
  }

  Future<void> _runAfterSheetDismissal(
    BuildContext sheetContext,
    Future<void> Function() action,
  ) async {
    Navigator.pop(sheetContext);
    await Future<void>.delayed(const Duration(milliseconds: 180));
    if (!mounted) return;
    await action();
  }

  void _showComposerActions() {
    showModalBottomSheet(
      context: context,
      showDragHandle: true,
      isScrollControlled: true,
      builder: (sheetContext) {
        final theme = Theme.of(sheetContext);
        final l10n = AppLocalizations.of(context)!;

        Future<void> runAction(Future<void> Function() action) async {
          await _runAfterSheetDismissal(sheetContext, action);
        }

        final actions = <Widget>[
          _ComposerActionTile(
            icon: Icons.search_rounded,
            title: l10n.searchMessages,
            color: const Color(0xFF2B6CB0),
            onTap: () => runAction(() async {
              _showFilteredMessageSearch();
            }),
          ),
          _ComposerActionTile(
            icon: Icons.add_location_alt_rounded,
            title: l10n.sendSarMarker,
            color: const Color(0xFFB45309),
            onTap: () => runAction(() async {
              _showSarDialog();
            }),
          ),
          if (_voiceSupported)
            _ComposerActionTile(
              icon: _isRecording ? Icons.stop_rounded : Icons.mic_rounded,
              title: _isRecording ? 'Stop recording' : 'Record voice',
              color: const Color(0xFF7C3AED),
              enabled: !_isSendingVoice,
              busy: _isSendingVoice,
              onTap: !_isSendingVoice
                  ? () => runAction(() async {
                      if (_isRecording) {
                        await _stopAndSendVoice();
                      } else {
                        await _startVoiceRecording();
                      }
                    })
                  : null,
            ),
          _ComposerActionTile(
            icon: Icons.photo_library_rounded,
            title: l10n.sendImageFromGallery,
            color: const Color(0xFF0F766E),
            enabled: !_isSendingImage,
            busy: _isSendingImage,
            onTap: !_isSendingImage
                ? () => runAction(() async {
                    await _pickAndSendImage(source: ImageSource.gallery);
                  })
                : null,
          ),
          _ComposerActionTile(
            icon: Icons.camera_alt_rounded,
            title: l10n.takePhoto,
            color: const Color(0xFF2563EB),
            enabled: !_isSendingImage,
            busy: _isSendingImage,
            onTap: !_isSendingImage
                ? () => runAction(() async {
                    await _pickAndSendImage(source: ImageSource.camera);
                  })
                : null,
          ),
          _ComposerActionTile(
            icon: Icons.grid_3x3_rounded,
            title: l10n.startTictactoe,
            color: const Color(0xFFBE185D),
            onTap: () => runAction(() async {
              await _startTicTacToeGame();
            }),
          ),
          if (_destinationType ==
              MessageDestinationPreferences.destinationTypeChannel)
            _ComposerActionTile(
              icon: _channelRegionScopeName != null
                  ? Icons.language_rounded
                  : Icons.public_rounded,
              title: _channelRegionScopeName != null
                  ? '${l10n.regionScope}: $_channelRegionScopeName'
                  : l10n.setRegionScope,
              color: const Color(0xFF7C3AED),
              onTap: () => runAction(() async {
                _showRegionScopeSheet();
              }),
            ),
        ];

        return SafeArea(
          child: ConstrainedBox(
            constraints: BoxConstraints(
              maxHeight: MediaQuery.of(sheetContext).size.height * 0.72,
            ),
            child: SingleChildScrollView(
              padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'More actions',
                    style: theme.textTheme.titleLarge?.copyWith(
                      fontWeight: FontWeight.w700,
                    ),
                  ),
                  const SizedBox(height: 16),
                  for (var index = 0; index < actions.length; index++) ...[
                    actions[index],
                    if (index != actions.length - 1) const SizedBox(height: 8),
                  ],
                ],
              ),
            ),
          ),
        );
      },
    );
  }

  void _showRegionScopeSheet() {
    final channelIdx = _selectedRecipient?.publicKey[1] ?? 0;
    final l10n = AppLocalizations.of(context)!;

    showModalBottomSheet(
      context: context,
      showDragHandle: true,
      isScrollControlled: true,
      builder: (sheetContext) {
        return _RegionScopeSheet(
          currentScopeName: _channelRegionScopeName,
          l10n: l10n,
          onScopeSelected: (String? name) async {
            Navigator.of(sheetContext).pop();
            if (name == null) {
              // Clear scope
              await RegionScopePreferences.clearScope(channelIdx);
              if (!mounted) return;
              setState(() {
                _channelRegionScopeName = null;
                _channelRegionScopeKey = null;
                _channelRegionScopes.remove(channelIdx);
              });
              ToastLogger.success(context, l10n.regionScopeCleared);
            } else {
              // Set scope
              final key = RegionScopePreferences.deriveRegionKey(name);
              await RegionScopePreferences.setScope(channelIdx, name, key);
              if (!mounted) return;
              setState(() {
                _channelRegionScopeName = name;
                _channelRegionScopeKey = key;
                _channelRegionScopes[channelIdx] = name;
              });
              ToastLogger.success(context, l10n.regionScopeSet(name));
            }
          },
        );
      },
    );
  }

  void _showFilteredMessageSearch() {
    final messagesProvider = context.read<MessagesProvider>();
    final scopedMessages = _getFilteredMessages(messagesProvider);
    final searchController = TextEditingController();

    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      backgroundColor: Theme.of(context).colorScheme.surface,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
      ),
      builder: (sheetContext) {
        return StatefulBuilder(
          builder: (context, setModalState) {
            final query = searchController.text.trim().toLowerCase();
            final matches = query.isEmpty
                ? scopedMessages
                : scopedMessages.where((message) {
                    return message.text.toLowerCase().contains(query) ||
                        (message.senderName?.toLowerCase().contains(query) ??
                            false);
                  }).toList();

            return SafeArea(
              child: Padding(
                padding: EdgeInsets.only(
                  bottom: MediaQuery.of(sheetContext).viewInsets.bottom,
                ),
                child: SizedBox(
                  height: MediaQuery.of(sheetContext).size.height * 0.72,
                  child: Column(
                    children: [
                      Padding(
                        padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
                        child: TextField(
                          controller: searchController,
                          autofocus: true,
                          decoration: InputDecoration(
                            hintText: 'Search in current filter',
                            prefixIcon: const Icon(Icons.search),
                            suffixIcon: searchController.text.isEmpty
                                ? null
                                : IconButton(
                                    icon: const Icon(Icons.clear),
                                    onPressed: () {
                                      searchController.clear();
                                      setModalState(() {});
                                    },
                                  ),
                            border: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(14),
                            ),
                          ),
                          onChanged: (_) => setModalState(() {}),
                        ),
                      ),
                      Expanded(
                        child: matches.isEmpty
                            ? Center(
                                child: Text(
                                  query.isEmpty
                                      ? 'No messages in this filter'
                                      : 'No matches in this filter',
                                ),
                              )
                            : ListView.separated(
                                itemCount: matches.length,
                                separatorBuilder: (_, _) =>
                                    const Divider(height: 1),
                                itemBuilder: (context, index) {
                                  final message = matches[index];
                                  final title =
                                      message.senderName?.trim().isNotEmpty ==
                                          true
                                      ? message.senderName!
                                      : message.isSentMessage
                                      ? 'You'
                                      : _getDestinationLabel();

                                  return ListTile(
                                    title: Text(
                                      title,
                                      maxLines: 1,
                                      overflow: TextOverflow.ellipsis,
                                    ),
                                    subtitle: Text(
                                      message.text,
                                      maxLines: 2,
                                      overflow: TextOverflow.ellipsis,
                                    ),
                                    onTap: () {
                                      Navigator.pop(sheetContext);
                                      _scrollToMessage(message.id);
                                    },
                                  );
                                },
                              ),
                      ),
                    ],
                  ),
                ),
              ),
            );
          },
        );
      },
    ).whenComplete(searchController.dispose);
  }

  Widget _buildDestinationAvatar(BuildContext context) {
    final recipient = _selectedRecipient;
    if (recipient != null) {
      return ContactAvatar(
        contact: recipient,
        radius: 14,
        displayName: _getDestinationLabel(),
      );
    }

    return Icon(
      _getDestinationIcon(),
      size: 17,
      color: Theme.of(context).colorScheme.onSurfaceVariant,
    );
  }

  Future<void> _sendSarMessage(
    String emoji,
    String name,
    Position position,
    Uint8List? roomPublicKey,
    bool sendToChannel,
    bool sendToAllContacts,
    int colorIndex,
  ) async {
    final connectionProvider = context.read<ConnectionProvider>();
    final messagesProvider = context.read<MessagesProvider>();

    if (!connectionProvider.deviceInfo.isConnected) {
      if (!mounted) return;
      ToastLogger.error(context, 'Not connected to device');
      return;
    }

    if (!sendToChannel && !sendToAllContacts && roomPublicKey == null) {
      if (!mounted) return;
      ToastLogger.error(
        context,
        'Please select a destination to send SAR marker',
      );
      return;
    }

    try {
      // New format: S:<emoji>:<colorIndex>:<latitude>,<longitude>:<name>
      // Round coordinates to 5 decimal places (~1m accuracy) since most GPS is only that accurate
      final sarMessage =
          'S:$emoji:${colorIndex.toString()}:${position.latitude.toStringAsFixed(5)},${position.longitude.toStringAsFixed(5)}:$name';

      if (sendToAllContacts) {
        // Send to all chat contacts (ContactType.chat)
        final contactsProvider = context.read<ContactsProvider>();
        final chatContacts = contactsProvider.chatContacts;

        if (chatContacts.isEmpty) {
          if (!mounted) return;
          ToastLogger.error(
            context,
            AppLocalizations.of(context)!.noContactsAvailable,
          );
          return;
        }

        // Create a single grouped message instead of multiple individual messages
        final groupId = '${DateTime.now().millisecondsSinceEpoch}_group';
        final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
        final devicePublicKey = connectionProvider.deviceInfo.publicKey;
        final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6);

        // Create recipient list
        final recipients = chatContacts.map((contact) {
          return MessageRecipient(
            publicKey: contact.publicKey,
            displayName: contact.displayName,
            deliveryStatus: MessageDeliveryStatus.sending,
            sentAt: DateTime.now(),
          );
        }).toList();

        // Create single grouped message
        final groupedMessage = Message(
          id: groupId,
          messageType: MessageType.contact,
          senderPublicKeyPrefix: senderPublicKeyPrefix,
          pathLen: 0,
          textType: MessageTextType.plain,
          senderTimestamp: timestamp,
          text: sarMessage,
          receivedAt: DateTime.now(),
          deliveryStatus: MessageDeliveryStatus.sending,
          groupId: groupId,
          recipients: recipients,
        );

        // Add the grouped message to the list
        messagesProvider.addSentMessage(groupedMessage);

        // Send to each contact and track status
        int successCount = 0;
        for (final contact in chatContacts) {
          final individualMessageId = '${groupId}_${contact.publicKeyShort}';

          // Register this individual send as part of the grouped message
          messagesProvider.registerGroupedMessageSend(
            individualMessageId,
            groupId,
            contact.publicKey,
          );

          // Send SAR message to contact (with ACK tracking)
          final sentSuccessfully = await connectionProvider.sendTextMessage(
            contactPublicKey: contact.publicKey,
            text: sarMessage,
            messageId: individualMessageId,
            contact: contact,
          );

          if (sentSuccessfully) {
            successCount++;
          } else {
            // Update recipient status in grouped message
            messagesProvider.updateGroupedMessageRecipientStatus(
              groupId,
              contact.publicKey,
              MessageDeliveryStatus.failed,
            );
          }

          // Add 1 second delay between sends to ensure:
          // 1. Different timestamps (messages sent in different seconds)
          // 2. Radio has time to fully process previous message and assign ACK tag
          // This ensures each message gets a unique ACK tag from the radio
          if (contact != chatContacts.last) {
            await Future.delayed(const Duration(seconds: 1));
          }
        }

        if (!mounted) return;
        ToastLogger.success(
          context,
          AppLocalizations.of(context)!.sarMarkerSentToContacts(successCount),
        );
      } else if (sendToChannel) {
        // Create message ID
        final messageId =
            '${DateTime.now().millisecondsSinceEpoch}_channel_sent';
        final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;

        // Get current device's public key (first 6 bytes)
        final devicePublicKey = connectionProvider.deviceInfo.publicKey;
        final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6);

        // Create sent message object
        final sentMessage = Message(
          id: messageId,
          messageType: MessageType.channel,
          senderPublicKeyPrefix: senderPublicKeyPrefix,
          pathLen: 0,
          textType: MessageTextType.plain,
          senderTimestamp: timestamp,
          text: sarMessage,
          receivedAt: DateTime.now(),
          deliveryStatus: MessageDeliveryStatus.sending,
          channelIdx: 0,
          // SAR marker data is automatically added by SarMessageParser.enhanceMessage in MessagesProvider
        );

        // Add to messages list with "sending" status
        messagesProvider.addSentMessage(sentMessage);

        // Send to public channel (ephemeral, over-the-air only)
        await connectionProvider.sendChannelMessage(
          channelIdx: 0,
          text: sarMessage,
          messageId: messageId,
          floodScopeKey: _channelRegionScopeKey,
        );

        if (!mounted) return;
      } else {
        // Create message ID
        final messageId = '${DateTime.now().millisecondsSinceEpoch}_sent';
        final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;

        // Get current device's public key (first 6 bytes)
        final devicePublicKey = connectionProvider.deviceInfo.publicKey;
        final senderPublicKeyPrefix = devicePublicKey?.sublist(0, 6);

        // Create sent message object with recipient public key for retry support
        final sentMessage = Message(
          id: messageId,
          messageType: MessageType.contact,
          senderPublicKeyPrefix: senderPublicKeyPrefix,
          pathLen: 0,
          textType: MessageTextType.plain,
          senderTimestamp: timestamp,
          text: sarMessage,
          receivedAt: DateTime.now(),
          deliveryStatus: MessageDeliveryStatus.sending,
          recipientPublicKey: roomPublicKey, // Store recipient for retry
          // SAR marker data is automatically added by SarMessageParser.enhanceMessage in MessagesProvider
        );

        // Look up the room contact for path logging
        final contactsProvider = context.read<ContactsProvider>();
        final roomContact = contactsProvider.contacts.where((c) {
          return c.publicKey.length >= roomPublicKey!.length &&
              c.publicKey.matches(roomPublicKey);
        }).firstOrNull;

        // Add to messages list with "sending" status
        messagesProvider.addSentMessage(sentMessage, contact: roomContact);

        // Send SAR message to selected room (persisted and immutable)
        final sentSuccessfully = await connectionProvider.sendTextMessage(
          contactPublicKey: roomPublicKey!,
          text: sarMessage,
          messageId: messageId, // Pass message ID so it can be tracked
          contact: roomContact, // Include contact for path status logging
        );

        if (!sentSuccessfully) {
          // Mark message as failed if sending failed
          messagesProvider.markMessageFailed(messageId);
        }

        if (!mounted) return;
        ToastLogger.success(context, 'SAR marker sent to room');
      }
    } catch (e) {
      if (!mounted) return;
      ToastLogger.error(context, 'Failed to send SAR marker: $e');
    }
  }

  /// Handle pull-to-refresh for manual message sync
  /// This is a FALLBACK mechanism - messages are normally synced automatically via PUSH_CODE_MSG_WAITING
  Future<void> _handleRefresh() async {
    final connectionProvider = context.read<ConnectionProvider>();

    if (!connectionProvider.deviceInfo.isConnected) {
      if (!mounted) return;
      ToastLogger.warning(context, 'Not connected - cannot sync messages');
      return;
    }

    try {
      debugPrint(
        '🔄 [MessagesTab] Manual refresh triggered - syncing messages',
      );
      if (!mounted) return;
    } catch (e) {
      debugPrint('❌ [MessagesTab] Sync error: $e');
      if (!mounted) return;
      ToastLogger.error(context, 'Sync failed: $e');
    }
  }

  void _markCurrentDestinationAsRead() {
    _channelReadTimer?.cancel();
    _pendingChannelReadKey = null;
    context.read<MessagesProvider>().markDestinationAsRead(
      destinationType: _destinationType,
      contact: _selectedRecipient,
    );
  }

  void _syncChannelAutoReadTimer(MessagesProvider messagesProvider) {
    if (!widget.isActive ||
        _destinationType !=
            MessageDestinationPreferences.destinationTypeChannel) {
      _channelReadTimer?.cancel();
      _pendingChannelReadKey = null;
      return;
    }

    final channelIdx = _selectedRecipient?.publicKey[1] ?? 0;
    final unreadCount = messagesProvider.getUnreadCountForChannel(channelIdx);

    if (unreadCount <= 0) {
      _channelReadTimer?.cancel();
      _pendingChannelReadKey = null;
      return;
    }

    final nextKey = '$channelIdx:$unreadCount';
    if (_pendingChannelReadKey == nextKey &&
        _channelReadTimer?.isActive == true) {
      return;
    }

    _channelReadTimer?.cancel();
    _pendingChannelReadKey = nextKey;
    _channelReadTimer = Timer(_channelAutoReadDelay, () {
      if (!mounted || !widget.isActive) {
        return;
      }

      if (_destinationType !=
          MessageDestinationPreferences.destinationTypeChannel) {
        return;
      }

      final currentChannelIdx = _selectedRecipient?.publicKey[1] ?? 0;
      if (currentChannelIdx != channelIdx) {
        return;
      }

      final latestProvider = context.read<MessagesProvider>();
      if (latestProvider.getUnreadCountForChannel(channelIdx) <= 0) {
        return;
      }

      _markCurrentDestinationAsRead();
    });
  }

  List<Message> _getFilteredMessages(MessagesProvider messagesProvider) {
    // Get all recent messages
    final allMessages = messagesProvider.getRecentMessages(count: 100);

    List<Message> filteredMessages;

    // If channel destination is selected, filter by selected channel.
    if (_destinationType == MessageDestinationPreferences.destinationTypeAll) {
      filteredMessages = allMessages;
    } else if (_destinationType ==
        MessageDestinationPreferences.destinationTypeChannel) {
      final selectedChannelIdx = _selectedRecipient?.publicKey[1] ?? 0;
      filteredMessages = allMessages
          .where(
            (message) =>
                message.isChannelMessage &&
                (message.channelIdx ?? 0) == selectedChannelIdx,
          )
          .toList();
    }
    // If a contact or room is selected, filter by recipient/sender prefixes.
    else if ((_destinationType ==
                MessageDestinationPreferences.destinationTypeContact ||
            _destinationType ==
                MessageDestinationPreferences.destinationTypeRoom) &&
        _selectedRecipient != null) {
      filteredMessages = allMessages.where((message) {
        if (!message.isContactMessage) {
          return false;
        }

        // Include messages sent TO this recipient
        if (message.recipientPublicKey != null &&
            message.recipientPublicKey!.length >= 6 &&
            _selectedRecipient!.publicKey.length >= 6) {
          // Compare first 6 bytes (public key prefix)
          final recipientPrefix = message.recipientPublicKey!.sublist(0, 6);
          final selectedPrefix = _selectedRecipient!.publicKey.sublist(0, 6);
          if (recipientPrefix.matches(selectedPrefix)) {
            return true;
          }
        }

        // Include messages received FROM this recipient
        if (message.senderPublicKeyPrefix != null &&
            message.senderPublicKeyPrefix!.length >= 6 &&
            _selectedRecipient!.publicKey.length >= 6) {
          final senderPrefix = message.senderPublicKeyPrefix!.sublist(0, 6);
          final selectedPrefix = _selectedRecipient!.publicKey.sublist(0, 6);
          if (senderPrefix.matches(selectedPrefix)) {
            return true;
          }
        }

        return false;
      }).toList();
    } else {
      // Default: show all messages (fallback case)
      filteredMessages = allMessages;
    }

    return filteredMessages
        .where((message) => !message.isSystemMessage)
        .toList();
  }

  void _handleMessageTap(Message message) {
    if (widget.onNavigateToMap == null) return;

    if (message.isSarMarker) {
      final mapProvider = context.read<MapProvider>();
      final marker = message.toSarMarker();
      if (marker == null) {
        return;
      }
      final error = mapProvider.navigateToSarMarker(marker);
      if (error != null && mounted) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text(error)));
        return;
      }
      widget.onNavigateToMap?.call();
      return;
    }

    if (message.isDrawing && message.drawingId != null) {
      debugPrint('🗺️ [MessagesTab] Drawing tapped! ID: ${message.drawingId}');
      final mapProvider = context.read<MapProvider>();
      final drawingProvider = context.read<DrawingProvider>();
      final error = mapProvider.navigateToDrawing(
        message.drawingId!,
        drawingProvider,
      );
      if (error != null && mounted) {
        ScaffoldMessenger.of(
          context,
        ).showSnackBar(SnackBar(content: Text(error)));
        return;
      }
      widget.onNavigateToMap?.call();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Consumer<MessagesProvider>(
      builder: (context, messagesProvider, child) {
        _syncChannelAutoReadTimer(messagesProvider);
        final messages = messagesProvider.buildDisplayMessages(
          _getFilteredMessages(messagesProvider),
        );
        final bottomInset = MediaQuery.of(context).viewPadding.bottom;
        final composerBottomPadding = bottomInset > 0 ? 2.0 : 10.0;

        return GestureDetector(
          behavior: HitTestBehavior.translucent,
          onTap: () => FocusScope.of(context).unfocus(),
          child: Stack(
            children: [
              Positioned.fill(
                child: MessagesContent(
                  messages: messages,
                  scrollController: _scrollController,
                  highlightedMessageId: _highlightedMessageId,
                  bottomContentPadding:
                      _composerOverlayHeight + composerBottomPadding,
                  onRefresh: _handleRefresh,
                  onNavigateToMap: widget.onNavigateToMap,
                  onMessageTap: _handleMessageTap,
                  onReplyToMessage: _replyToMessage,
                ),
              ),
              Positioned(
                left: 0,
                right: 0,
                bottom: 0,
                child: MessagesComposer(
                  textController: _textController,
                  focusNode: _focusNode,
                  messageByteLimiter: _messageByteLimiter,
                  messageByteCount: _messageByteCount,
                  maxMessageBytes: _maxMessageBytes,
                  isRecording: _isRecording,
                  isSendingVoice: _isSendingVoice,
                  voiceSupported: _voiceSupported,
                  bottomPadding: composerBottomPadding,
                  destinationLabel: _getDestinationLabel(),
                  destinationAvatar: _buildDestinationAvatar(context),
                  destinationLocked: _isDestinationLocked,
                  mentionSuggestions: _mentionSuggestions,
                  mentionQuery: _mentionQuery,
                  onMentionSelected: _selectMention,
                  onShowComposerActions: _showComposerActions,
                  onShowRecipientSelector: _showRecipientSelector,
                  onStartVoiceRecording: _startVoiceRecording,
                  onStopAndSendVoice: _stopAndSendVoice,
                  onSendMessage: _sendMessage,
                  onLongPressSend: _isContactDestination()
                      ? _showSendModeSheet
                      : null,
                  regionScopeName: _channelRegionScopeName,
                  onRegionScopeTap: _channelRegionScopeName != null
                      ? _showRegionScopeSheet
                      : null,
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

/// Bottom sheet for selecting a region scope for the current channel.
class _RegionScopeSheet extends StatefulWidget {
  final String? currentScopeName;
  final AppLocalizations l10n;
  final ValueChanged<String?> onScopeSelected;

  const _RegionScopeSheet({
    required this.currentScopeName,
    required this.l10n,
    required this.onScopeSelected,
  });

  @override
  State<_RegionScopeSheet> createState() => _RegionScopeSheetState();
}

class _RegionScopeSheetState extends State<_RegionScopeSheet> {
  final TextEditingController _nameController = TextEditingController();

  @override
  void dispose() {
    _nameController.dispose();
    super.dispose();
  }

  void _submitManualName() {
    var name = _nameController.text.trim();
    if (name.isEmpty) return;
    if (!name.startsWith('#')) name = '#$name';
    if (utf8.encode(name).length > 30) return;
    widget.onScopeSelected(name);
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final colorScheme = theme.colorScheme;
    final l10n = widget.l10n;

    return SafeArea(
      child: ConstrainedBox(
        constraints: BoxConstraints(
          maxHeight: MediaQuery.of(context).size.height * 0.72,
        ),
        child: SingleChildScrollView(
          padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                l10n.regionScope,
                style: theme.textTheme.titleLarge?.copyWith(
                  fontWeight: FontWeight.w700,
                ),
              ),
              const SizedBox(height: 4),
              Text(
                l10n.regionScopeWarning,
                style: theme.textTheme.bodySmall?.copyWith(
                  color: colorScheme.onSurfaceVariant,
                ),
              ),
              const SizedBox(height: 16),

              _RegionOptionTile(
                label: l10n.regionScopeNone,
                isSelected: widget.currentScopeName == null,
                onTap: () => widget.onScopeSelected(null),
              ),
              const SizedBox(height: 8),

              Row(
                children: [
                  Expanded(
                    child: TextField(
                      controller: _nameController,
                      decoration: InputDecoration(
                        hintText: l10n.enterRegionName,
                        isDense: true,
                        prefixText: '#',
                        border: OutlineInputBorder(
                          borderRadius: BorderRadius.circular(12),
                        ),
                        contentPadding: const EdgeInsets.symmetric(
                          horizontal: 12,
                          vertical: 10,
                        ),
                      ),
                      onSubmitted: (_) => _submitManualName(),
                    ),
                  ),
                  const SizedBox(width: 8),
                  FilledButton.tonal(
                    onPressed: _submitManualName,
                    child: const Icon(Icons.check_rounded, size: 20),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _RegionOptionTile extends StatelessWidget {
  final String label;
  final bool isSelected;
  final VoidCallback onTap;

  const _RegionOptionTile({
    required this.label,
    required this.isSelected,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;
    return Material(
      color: isSelected
          ? colorScheme.primaryContainer
          : colorScheme.surfaceContainerLow,
      borderRadius: BorderRadius.circular(12),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(12),
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
          child: Row(
            children: [
              Icon(
                isSelected
                    ? Icons.radio_button_checked_rounded
                    : Icons.radio_button_off_rounded,
                size: 20,
                color: isSelected
                    ? colorScheme.primary
                    : colorScheme.onSurfaceVariant,
              ),
              const SizedBox(width: 12),
              Expanded(
                child: Text(
                  label,
                  style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                    fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
                    color: isSelected
                        ? colorScheme.onPrimaryContainer
                        : colorScheme.onSurface,
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
