import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:cryptography/cryptography.dart';
import '/data/models/message_model.dart';
import '/data/models/contact_model.dart';
import 'package:chepuhagram/presentation/widgets/message_bubble.dart';
import 'package:gal/gal.dart';
import 'package:chepuhagram/data/repositories/contact_repository.dart';
import 'package:chepuhagram/domain/services/crypto_service.dart';
import 'package:chepuhagram/data/datasources/ws_client.dart';
import 'package:provider/provider.dart';
import 'package:flutter/rendering.dart';
import '/logic/contact_provider.dart';
import '../../domain/services/api_service.dart';
import 'dart:math';
import 'package:chepuhagram/data/datasources/local_db_service.dart';
import 'package:chepuhagram/main.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'contacts_screen.dart';
import 'package:flutter/services.dart';
import 'user_profile_screen.dart';
import '/core/theme_manager.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:file_picker/file_picker.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'camera_screen.dart';
import 'media_viewer_screen.dart';
import 'package:visibility_detector/visibility_detector.dart';
import 'package:path/path.dart' as p;
import 'package:record/record.dart';
import 'package:camera/camera.dart';
import 'package:ffmpeg_kit_flutter_new_min_gpl/ffmpeg_kit.dart';
import 'package:ffmpeg_kit_flutter_new_min_gpl/return_code.dart';
import '../screens/forward_contact_picker_screen.dart';

class ChatScreen extends StatefulWidget {
  final Contact contact;

  const ChatScreen({super.key, required this.contact});

  @override
  State<ChatScreen> createState() => _ChatScreenState();
}

class _ChatScreenState extends State<ChatScreen> with RouteAware {
  static const String _notificationLaunchKey = 'notification_launch_data';
  static const int _autoMediaLoadLimitBytes = 10 * 1024 * 1024; // 10MB
  int myId = 0;
  late Contact _currentContact;
  bool _isKeyLoading = true;
  final TextEditingController _controller = TextEditingController();
  final FocusNode _inputFocusNode = FocusNode();
  final ContactRepository _contactRepository = ContactRepository();
  final apiService = ApiService();
  final CryptoService _cryptoService = CryptoService();
  List<MessageModel> messages = [];
  StreamSubscription<dynamic>? _socketSubscription;
  final Set<int> _sentReadReceipts = <int>{};
  final LocalDbService _localDbService = LocalDbService();
  final ScrollController _scrollController = ScrollController();
  final Map<int, GlobalKey> _messageKeys = {};
  Map<int, MessageModel> _messageMap = {};
  bool _showScrollToEnd = false;
  MessageModel? _replyTo;
  bool _isOnline = false;
  DateTime? _lastOnline;
  Timer? _onlineTimer;
  DateTime? _lastTypingSent;
  bool _isTyping = false;
  Timer? _typingTimer;
  late SocketService _socketService;
  MessageType _pendingMessageType = MessageType.text;
  String? _pendingFileName;
  File? _pendingFile;
  Uint8List? _previewBytes;
  double _inputBarHeight = 0;

  SecretKey? _chatSharedSecret;
  final Map<String, Future<Uint8List?>> _mediaLoadFutures = {};
  final Map<String, ValueNotifier<double?>> _messageProgressNotifiers = {};

  // Состояния для аудио/видео записи
  bool _isRecording = false;
  bool _isRecordLocked = false; // Режим "замок" (свайп вверх)
  bool _isVoiceMode = true; // true - голосовое, false - кружок
  double _recordDragX = 0.0; // Для отслеживания свайпа влево (отмена)
  double _recordDragY = 0.0; // Для отслеживания свайпа вверх (замок)

  // Дополнительно для UI анимаций (опционально, сколько протащили для отмены)
  static const double _swipeCancelThreshold =
      -80.0; // Порог свайпа влево для отмены
  static const double _swipeLockThreshold =
      -80.0; // Порог свайпа вверх для лока
  final AudioRecorder _audioRecorder = AudioRecorder();

  Stopwatch _stopwatch = Stopwatch();
  Timer? _stopwatchTimer;
  String _stopwatchDisplay = "0:00";

  CameraController? _cameraController;
  List<CameraDescription>? _cameras;
  bool _isCameraInitialized = false;

  @override
  void initState() {
    super.initState();
    _currentContact = widget.contact;
    _socketService = Provider.of<SocketService>(context, listen: false);
    currentActiveChatContactId =
        _currentContact.id; // Устанавливаем активный чат
    flutterLocalNotificationsPlugin.cancel(currentActiveChatContactId!);
    final contactProvider = context.read<ContactProvider>();
    myId = contactProvider.getCurrentUserId() ?? 0;
    // Если ключа нет, загружаем его при входе
    _loadLocalName();
    if (_currentContact.publicKey == null) {
      _loadContactKey();
    }
    _loadHistory();
    _loadOnlineStatus();
    startOnlineUpdates();
    _controller.addListener(_sendTypingStatus);

    _scrollController.addListener(_updateScrollButtonVisibility);

    final socketService = Provider.of<SocketService>(context, listen: false);
    _socketSubscription = socketService.messages.listen(_handleIncomingMessage);

    _initCameras();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    routeObserver.subscribe(this, ModalRoute.of(context) as PageRoute);
  }

  @override
  void didPopNext() {
    print("Пользователь вернулся на этот экран!");
    _loadLocalName();
    flutterLocalNotificationsPlugin.cancel(currentActiveChatContactId!);
  }

  Future<void> _loadLocalName() async {
    final prefs = await SharedPreferences.getInstance();

    final String? savedName = prefs.getString(
      'firstname_${_currentContact.id}',
    );
    final String? savedSurname = prefs.getString(
      'lastname_${_currentContact.id}',
    );
    print('Загружены имя $savedName, $savedSurname');
    if (mounted) {
      setState(() {
        if (savedName != null) {
          _currentContact.name = savedName;
        }
        if (savedSurname != null) {
          _currentContact.surname = savedSurname;
        }
      });
    }
  }

  // Инициализация камер устройства для кружочков
  Future<void> _initCameras() async {
    try {
      _cameras = await availableCameras();
      if (!mounted) return;
      if (_cameras != null && _cameras!.isNotEmpty) {
        // Пытаемся найти фронтальную камеру по умолчанию для кружков
        final frontCamera = _cameras!.firstWhere(
          (camera) => camera.lensDirection == CameraLensDirection.front,
          orElse: () => _cameras!.first,
        );
        _cameraController = CameraController(
          frontCamera,
          ResolutionPreset.medium,
          enableAudio: true, // Звук пишется в видео-файл
        );
      }
    } catch (e) {
      debugPrint("Ошибка инициализации камер: $e");
    }
  }

  // Секундомер
  void _startStopwatch() {
    _stopwatch.reset();
    _stopwatch.start();
    _stopwatchTimer = Timer.periodic(const Duration(milliseconds: 500), (
      timer,
    ) {
      if (_stopwatch.isRunning) {
        setState(() {
          final elapsed = _stopwatch.elapsed;
          String minutes = (elapsed.inMinutes % 60).toString();
          String seconds = (elapsed.inSeconds % 60).toString().padLeft(2, '0');
          _stopwatchDisplay = "$minutes:$seconds";
        });
      }
    });
  }

  void _stopStopwatch() {
    _stopwatch.stop();
    _stopwatchTimer?.cancel();
    setState(() {
      _stopwatchDisplay = "0:00";
    });
  }

  // СТАРТ ЗАПИСИ
  Future<void> _startRecording() async {
    HapticFeedback.lightImpact();
    _startStopwatch();

    setState(() {
      _isRecording = true;
      _isRecordLocked = false;
      _recordDragX = 0.0;
      _recordDragY = 0.0;
    });

    try {
      if (_isVoiceMode) {
        // Проверка разрешений микрофона встроенная в record
        if (await _audioRecorder.hasPermission()) {
          final directory = await getTemporaryDirectory();
          final path =
              '${directory.path}/voice_${DateTime.now().millisecondsSinceEpoch}.m4a';
          await _audioRecorder.start(
            const RecordConfig(encoder: AudioEncoder.aacLc),
            path: path,
          );
        }
      } else {
        // Режим кружочка (видео)
        if (_cameraController != null) {
          await _cameraController!.initialize();
          if (mounted) {
            setState(() {
              _isCameraInitialized = true;
            });
            await _cameraController!.startVideoRecording();
          }
        }
      }
    } catch (e) {
      debugPrint("Ошибка старта записи: $e");
    }
  }

  // ФИКСАЦИЯ НА ЗАМОК
  void _lockRecording() {
    HapticFeedback.mediumImpact();
    setState(() {
      _isRecordLocked = true;
    });
  }

  // ОТМЕНА ЗАПИСИ
  Future<void> _cancelRecording() async {
    HapticFeedback.heavyImpact();
    _stopStopwatch();

    setState(() {
      _isRecording = false;
      _isRecordLocked = false;
      _isCameraInitialized = false;
    });

    try {
      if (_isVoiceMode) {
        await _audioRecorder.stop(); // Просто останавливаем без сохранения
      } else {
        if (_cameraController != null &&
            _cameraController!.value.isRecordingVideo) {
          await _cameraController!.stopVideoRecording();
        }
      }
    } catch (e) {
      debugPrint("Ошибка при отмене записи: $e");
    }
  }

  // УСПЕШНОЕ ЗАВЕРШЕНИЕ И ОТПРАВКА
  Future<void> _stopAndSendRecording() async {
    if (!_isRecording) return;
    _stopStopwatch();

    String? filePath;

    try {
      if (_isVoiceMode) {
        filePath = await _audioRecorder.stop();
      } else {
        if (_cameraController != null &&
            _cameraController!.value.isRecordingVideo) {
          XFile videoFile = await _cameraController!.stopVideoRecording();
          filePath = videoFile.path;
        }
      }
    } catch (e) {
      debugPrint("Ошибка при остановке записи: $e");
    }

    setState(() {
      _isRecording = false;
      _isRecordLocked = false;
      _isCameraInitialized = false;
    });

    if (filePath != null) {
      File fileToSend = File(filePath);
      setState(() {
        _pendingFile = fileToSend;
        _pendingFileName = _isVoiceMode
            ? "Голосовое сообщение.m4a"
            : "Видеосообщение.mp4";
        _pendingMessageType = _isVoiceMode
            ? MessageType.voiceNote
            : MessageType.videoNote;
      });

      // Вызываем твой существующий метод отправки, который упакует файл в чат
      _sendMessage();
    }
  }

  void _toggleRecordMode() {
    if (_isRecording) return;
    setState(() {
      _isVoiceMode = !_isVoiceMode;
    });
  }

  void _updateMessageInList(
    int messageId,
    MessageModel Function(MessageModel) updater,
  ) {
    if (!_messageMap.containsKey(messageId)) return;

    final oldMessage = _messageMap[messageId]!;
    final newMessage = updater(oldMessage);

    setState(() {
      _messageMap[messageId] = newMessage;
      final idx = messages.indexWhere((m) => m.id == messageId);
      if (idx != -1) messages[idx] = newMessage;
    });
  }

  void _sendTypingStatus() {
    final now = DateTime.now();
    if (_lastTypingSent == null ||
        now.difference(_lastTypingSent!) > const Duration(seconds: 3)) {
      _lastTypingSent = now;
      final socketService = Provider.of<SocketService>(context, listen: false);
      socketService.sendMessage({
        'type': 'typing',
        'receiver_id': _currentContact.id,
      });
    }
  }

  void _sendStopTypingStatus() {
    _socketService.sendMessage({
      'type': 'stop_typing',
      'receiver_id': _currentContact.id,
    });
  }

  Future<void> _loadOnlineStatus() async {
    if (currentActiveChatContactId == null) return;
    flutterLocalNotificationsPlugin.cancel(currentActiveChatContactId!);
    try {
      print(
        "🔍 Загружаем онлайн статус для контакта ${_currentContact.name} (ID: ${_currentContact.id})",
      );
      final data = await apiService.getUserById(_currentContact.id);
      if (!mounted) return;
      DateTime now = DateTime.now();

      Duration offset = now.timeZoneOffset;
      print(
        "✅ Получен онлайн статус: ${data['online']}, last_online: ${data['last_online'] != null ? DateTime.tryParse(data['last_online']!)?.add(offset) : null}",
      );

      setState(() {
        _isOnline = data['online'] ?? false;
        if (data['last_online'] != null)
          _lastOnline = DateTime.parse(data['last_online']).add(offset);
        else
          _lastOnline = null;
      });
    } catch (e) {
      print(e);
    }
  }

  void startOnlineUpdates() {
    _onlineTimer = Timer.periodic(const Duration(minutes: 1), (_) {
      _loadOnlineStatus();
    });
  }

  Future<void> _loadContactKey() async {
    if (!mounted) return;
    setState(() => _isKeyLoading = true);
    try {
      final updatedContact = await _contactRepository.fetchContactById(
        _currentContact.id,
      );
      if (!mounted) return;
      setState(() {
        _currentContact = updatedContact;
        _isKeyLoading = false;
      });
      print(updatedContact.publicKey);
    } catch (e) {
      if (!mounted) return;
      setState(() => _isKeyLoading = false);
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text("Не удалось получить ключ шифрования собеседника"),
          behavior: SnackBarBehavior.floating, // Обязательно для margin
          margin: EdgeInsets.only(bottom: 80.0 + 10.0, left: 10.0, right: 10.0),
          duration: Duration(seconds: 3),
        ),
      );
    }
  }

  String _getMediaPreview(MessageType type) {
    switch (type) {
      case MessageType.videoNote:
        return '[Кружок]';
      case MessageType.voiceNote:
        return '[Голосовое]';
      case MessageType.image:
        return '[Фото]';
      case MessageType.video:
        return '[Видео]';
      case MessageType.file:
        return '[Файл]';
      case MessageType.text:
      default:
        return '';
    }
  }

  MessageType _parseMessageTypeString(String? typeStr) {
    switch (typeStr?.toLowerCase()) {
      case 'voicenote':
        return MessageType.voiceNote;
      case 'videonote':
        return MessageType.videoNote;
      case 'image':
        return MessageType.image;
      case 'video':
        return MessageType.video;
      case 'file':
        return MessageType.file;
      case 'text':
      default:
        return MessageType.text;
    }
  }

  @override
  void dispose() {
    currentActiveChatContactId = null;
    _socketSubscription?.cancel();
    _scrollController.removeListener(_updateScrollButtonVisibility);
    _scrollController.dispose();
    _controller.dispose();
    for (final n in _messageProgressNotifiers.values) {
      n.dispose();
    }
    routeObserver.unsubscribe(this);
    _inputFocusNode.dispose();
    _onlineTimer?.cancel();
    _typingTimer?.cancel();
    _controller.removeListener(_sendTypingStatus);
    _sendStopTypingStatus();
    _audioRecorder.dispose();
    _cameraController?.dispose();
    _stopwatchTimer?.cancel();
    _stopwatch.stop();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final themeProv = context.watch<ThemeProvider>();
    return Scaffold(
      resizeToAvoidBottomInset: true,
      appBar: AppBar(
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () {
            if (Navigator.canPop(context)) {
              Navigator.pop(context);
            } else {
              Navigator.of(context).pushReplacement(
                MaterialPageRoute(builder: (_) => const ContactsScreen()),
              );
            }
          },
        ),
        title: GestureDetector(
          onTap: () {
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (_) => UserProfileScreen(
                  userId: _currentContact.id,
                  username: _currentContact.username,
                  name: _currentContact.name,
                ),
              ),
            );
          },
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                '${_currentContact.name} ${_currentContact.surname != 'Unknown' ? _currentContact.surname : ''}',
              ),
              if (_isKeyLoading == true)
                const Text(
                  'загрузка...',
                  style: TextStyle(
                    fontSize: 12,
                    color: Color.fromARGB(255, 219, 219, 219),
                  ),
                )
              else if (_isTyping)
                Row(
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: [
                    const Text(
                      'печатает',
                      style: TextStyle(fontSize: 12, color: Colors.greenAccent),
                    ),
                    const SizedBox(width: 4),
                    TypingIndicator(),
                  ],
                )
              else if (_isOnline)
                const Text(
                  'онлайн',
                  style: TextStyle(fontSize: 12, color: Colors.greenAccent),
                )
              else if (_lastOnline != null)
                Text(
                  'был(а) в сети ${_formatLastOnline(_lastOnline!)}',
                  style: const TextStyle(
                    fontSize: 12,
                    color: Color.fromARGB(255, 219, 219, 219),
                  ),
                )
              else
                const Text(
                  'был(а) недавно',
                  style: TextStyle(
                    fontSize: 12,
                    color: Color.fromARGB(255, 219, 219, 219),
                  ),
                ),
            ],
          ),
        ),
      ),
      body: Container(
        decoration: themeProv.wallpaperPath != null
            ? BoxDecoration(
                image: DecorationImage(
                  image: FileImage(File(themeProv.wallpaperPath!)),
                  fit: BoxFit.cover,
                ),
              )
            : null,
        child: Stack(
          children: [
            Positioned.fill(
              child: CustomScrollView(
                controller: _scrollController,
                reverse: true,
                cacheExtent:
                    0, // Сохраняем: строго запрещает предзагрузку элементов вне экрана
                physics: const AlwaysScrollableScrollPhysics(),
                slivers: [
                  SliverPadding(
                    padding: EdgeInsets.only(
                      bottom:
                          _inputBarHeight *
                              (MediaQuery.of(context).viewInsets.bottom > 0
                                  ? 1.0
                                  : 1.0) +
                          28,
                      left: 8,
                      right: 8,
                      top: 8,
                    ),
                    sliver: SliverList(
                      delegate: SliverChildBuilderDelegate((context, index) {
                        final msg = messages[messages.length - 1 - index];
                        final keyId = msg.id ?? msg.tempId ?? index;
                        final itemKey = _messageKeys.putIfAbsent(
                          keyId,
                          () => GlobalKey(),
                        );
                        final isMedia =
                            msg.messageType == MessageType.image ||
                            msg.messageType == MessageType.video ||
                            msg.messageType == MessageType.file;

                        final showDateDivider = _isNewDay(index);

                        // Формируем основное содержимое элемента сообщения
                        Widget itemChild = Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          key: ValueKey<int>(keyId.hashCode),
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            if (showDateDivider)
                              Container(
                                padding: const EdgeInsets.symmetric(
                                  vertical: 10,
                                ),
                                alignment: Alignment.center,
                                child: Container(
                                  padding: const EdgeInsets.symmetric(
                                    horizontal: 12,
                                    vertical: 4,
                                  ),
                                  decoration: BoxDecoration(
                                    color: Theme.of(context)
                                        .colorScheme
                                        .surfaceContainerHighest
                                        .withOpacity(0.75),
                                    borderRadius: BorderRadius.circular(12),
                                  ),
                                  child: Text(
                                    _formatDividerDate(msg.createdAt),
                                    style: TextStyle(
                                      fontSize: 12,
                                      fontWeight: FontWeight.w600,
                                      color: Theme.of(
                                        context,
                                      ).colorScheme.onSurfaceVariant,
                                    ),
                                  ),
                                ),
                              ),
                            Dismissible(
                              direction: DismissDirection.endToStart,
                              key: ValueKey<String>('dismiss_$keyId'),
                              confirmDismiss:
                                  (DismissDirection direction) async {
                                    String text = msg.text;
                                    if (msg.text.isEmpty &&
                                        msg.messageType == MessageType.image) {
                                      text = "[Фото]";
                                    }
                                    setState(
                                      () => _replyTo = msg.copyWith(text: text),
                                    );
                                    return false;
                                  },
                              child: RepaintBoundary(
                                child: MessageBubble(
                                  key: ValueKey(
                                    '${msg.id ?? msg.tempId}_${msg.localFile?.path ?? 'none'}',
                                  ),
                                  message: msg,
                                  onTap: () => _showMessageActions(msg),
                                  onReplyTap: msg.replyToId != null
                                      ? () => _scrollToMessage(msg.replyToId)
                                      : null,
                                  onImageTap: () => _openFullScreenMedia(msg),
                                  onDownloadRequested: (m) async {
                                    await _ensureFileDecrypted(
                                      m,
                                      dontLoad: false,
                                    );
                                  },
                                  onDownloadRequestedWithoutLoad: (m) async {
                                    await _ensureFileDecrypted(
                                      m,
                                      dontLoad: true,
                                    );
                                  },
                                  autoLoadMedia:
                                      msg.messageType != MessageType.image
                                      ? true
                                      : (msg.fileSize == null ||
                                            msg.fileSize! <=
                                                _autoMediaLoadLimitBytes),
                                  downloadProgress:
                                      _messageProgressNotifiers['${msg.fileId}'],
                                  onDownloadStoped: (m) async {
                                    await _stopFileLoading(msg);
                                  },
                                ),
                              ),
                            ),
                          ],
                        );

                        // Если это медиафайл или документ, оборачиваем в VisibilityDetector
                        if (isMedia) {
                          return VisibilityDetector(
                            key: ValueKey('visible_${keyId}'),
                            onVisibilityChanged: (visibilityInfo) {
                              // Как только элемент показался в зоне видимости хотя бы на 10%
                              if (visibilityInfo.visibleFraction > 0.1) {
                                if (msg.fileSize == null || msg.fileSize == 0) {
                                  print(
                                    "Элемент стал видим. Фоновый запрос размера для: ${msg.fileId}",
                                  );
                                  _fetchFileSizeIfNeeded(msg);
                                }
                              }
                            },
                            child: itemChild,
                          );
                        }

                        // Обычный текст возвращаем без детектора видимости
                        return itemChild;
                      }, childCount: messages.length),
                    ),
                  ),
                ],
              ),
            ),

            // 3. Кнопка скролла
            AnimatedPositioned(
              duration: const Duration(milliseconds: 200),
              curve: Curves.easeOutCubic,
              right: 16.0,
              bottom: _inputBarHeight + 8.0 + 16,
              child: AnimatedOpacity(
                opacity: _showScrollToEnd ? 1.0 : 0.0,
                duration: const Duration(milliseconds: 200),
                curve: Curves.easeInOut,
                child: IgnorePointer(
                  ignoring: !_showScrollToEnd,
                  child: ClipOval(
                    child: BackdropFilter(
                      filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
                      child: DecoratedBox(
                        decoration: BoxDecoration(
                          shape: BoxShape.circle,
                          color: Theme.of(
                            context,
                          ).colorScheme.surfaceVariant.withOpacity(0.75),
                          border: Border.all(
                            color: Theme.of(
                              context,
                            ).dividerColor.withOpacity(0.25),
                            width: 1,
                          ),
                        ),
                        child: SizedBox(
                          width: 40,
                          height: 40,
                          child: IconButton(
                            onPressed: _scrollToBottom,
                            icon: Icon(
                              Icons.keyboard_arrow_down,
                              color: Theme.of(context).colorScheme.onSurface,
                            ),
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ),

            if (_isRecording &&
                !_isVoiceMode &&
                _isCameraInitialized &&
                _cameraController != null)
              IgnorePointer(
                child: Center(
                  child: Container(
                    width:
                        MediaQuery.of(context).size.width *
                        0.9, // Увеличили размер, так как по центру экрана круг должен быть крупным и четким
                    height: MediaQuery.of(context).size.width * 0.9,
                    decoration: BoxDecoration(
                      shape: BoxShape.circle,
                      color: Colors.black,
                      boxShadow: [
                        BoxShadow(
                          color: Colors.black.withOpacity(0.5),
                          blurRadius: 25,
                          spreadRadius: 4,
                        ),
                      ],
                    ),
                    child: ClipOval(
                      child: LayoutBuilder(
                        builder: (context, constraints) {
                          // Берем пропорции самой камеры (важно: для вертикального отображения инвертируем)
                          final cameraAspectRatio =
                              _cameraController!.value.aspectRatio;
                          // Оборачиваем в OverflowBox, чтобы видео заполняло круг по меньшей стороне, а лишнее обрезалось
                          return OverflowBox(
                            alignment: Alignment.center,
                            child: FittedBox(
                              fit: BoxFit.cover,
                              child: SizedBox(
                                width: constraints.maxWidth,
                                height:
                                    constraints.maxWidth * cameraAspectRatio,
                                child: CameraPreview(_cameraController!),
                              ),
                            ),
                          );
                        },
                      ),
                    ),
                  ),
                ),
              ),

            // 4. Плавающее поле ввода
            Positioned(
              left: 0,
              right: 0,
              bottom: 16,
              child: _MeasureSize(
                onChange: (size) {
                  if (_inputBarHeight != size.height) {
                    setState(() {
                      _inputBarHeight = size.height;
                    });

                    if (!_showScrollToEnd) {
                      WidgetsBinding.instance.addPostFrameCallback((_) {
                        if (_scrollController.hasClients) {
                          _scrollController.animateTo(
                            0.0,
                            duration: const Duration(milliseconds: 200),
                            curve: Curves.easeOut,
                          );
                        }
                      });
                    }
                  }
                },
                child: SafeArea(
                  top: false,
                  minimum: const EdgeInsets.fromLTRB(16, 0, 16, 2),
                  child: ClipRRect(
                    borderRadius: BorderRadius.circular(18),
                    clipBehavior: Clip.antiAlias,
                    child: AnimatedSize(
                      duration: const Duration(milliseconds: 250),
                      curve: Curves.easeOutCubic,
                      alignment: Alignment.bottomCenter,
                      child: ClipRRect(
                        borderRadius: BorderRadius.circular(18),
                        clipBehavior: Clip.hardEdge,
                        child: BackdropFilter(
                          filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
                          child: Container(
                            color: Theme.of(
                              context,
                            ).colorScheme.surfaceVariant.withOpacity(0.5),
                            child: _buildMessageInput(),
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  bool _isNewDay(int currentIndex) {
    final int realIndex = messages.length - 1 - currentIndex;

    if (realIndex == 0) return true;

    final currentMsgTime = messages[realIndex].createdAt;
    final previousMsgTime = messages[realIndex - 1].createdAt;

    return currentMsgTime.year != previousMsgTime.year ||
        currentMsgTime.month != previousMsgTime.month ||
        currentMsgTime.day != previousMsgTime.day;
  }

  // Форматирование даты для плашки
  String _formatDividerDate(DateTime date) {
    final now = DateTime.now();
    if (date.year == now.year &&
        date.month == now.month &&
        date.day == now.day) {
      return "Сегодня";
    }
    final yesterday = now.subtract(const Duration(days: 1));
    if (date.year == yesterday.year &&
        date.month == yesterday.month &&
        date.day == yesterday.day) {
      return "Вчера";
    }

    const months = [
      "января",
      "февраля",
      "марта",
      "апреля",
      "мая",
      "июня",
      "июля",
      "августа",
      "сентября",
      "октября",
      "ноября",
      "декабря",
    ];

    return "${date.day} ${months[date.month - 1]} ${date.year != now.year ? date.year : ''}"
        .trim();
  }

  String _formatLastOnline(DateTime lastOnline) {
    final now = DateTime.now();
    final difference = now.difference(lastOnline);

    if (difference.inSeconds < 60) {
      return 'только что';
    } else if (difference.inMinutes < 60) {
      return '${difference.inMinutes} минут${_pluralize(difference.inMinutes, "у", "ы", "")} назад';
    } else if (difference.inHours < 24) {
      return '${difference.inHours} час${_pluralize(difference.inHours, "", "а", "ов")} назад';
    } else if (difference.inDays < 7) {
      return '${difference.inDays} ${_pluralize(difference.inDays, "день", "дня", "дней")} назад';
    } else if (difference.inDays < 30) {
      final weeks = (difference.inDays / 7).floor();
      return '$weeks ${_pluralize(weeks, "неделю", "недели", "недель")} назад';
    } else {
      return 'давно';
    }
  }

  String _pluralize(int count, String form1, String form2, String form5) {
    final mod10 = count % 10;
    final mod100 = count % 100;
    if (mod10 == 1 && mod100 != 11) {
      return form1;
    } else if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) {
      return form2;
    } else {
      return form5;
    }
  }

  Future<void> _showMessageActions(MessageModel msg) async {
    if (!mounted) return;

    await showModalBottomSheet<void>(
      context: context,
      showDragHandle: true,
      builder: (ctx) {
        return SafeArea(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              ListTile(
                leading: const Icon(Icons.reply),
                title: const Text('Ответить'),
                onTap: () {
                  WidgetsBinding.instance.addPostFrameCallback((_) {
                    Navigator.of(ctx).pop();
                    String text = msg.text;
                    if (msg.text.isEmpty &&
                        msg.messageType == MessageType.image) {
                      text = "[Фото]";
                    }
                    setState(() => _replyTo = msg.copyWith(text: text));
                  });
                },
              ),
              if (msg.isMe)
                ListTile(
                  leading: const Icon(Icons.edit),
                  title: const Text('Изменить'),
                  onTap: () {
                    WidgetsBinding.instance.addPostFrameCallback((_) {
                      Navigator.of(ctx).pop();
                      _editMessage(msg);
                    });
                  },
                ),
              ListTile(
                leading: const Icon(Icons.copy),
                title: const Text('Скопировать'),
                onTap: () async {
                  WidgetsBinding.instance.addPostFrameCallback((_) async {
                    Navigator.of(ctx).pop();
                    await Clipboard.setData(ClipboardData(text: msg.text));
                    if (!mounted) return;
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(
                        content: Text('Скопировано'),
                        behavior:
                            SnackBarBehavior.floating, // Обязательно для margin
                        margin: EdgeInsets.only(
                          bottom:
                              80.0 +
                              10.0, // 20px + стандартный отступ (по желанию)
                          left: 10.0,
                          right: 10.0,
                        ),
                        duration: Duration(seconds: 2),
                      ),
                    );
                  });
                },
              ),
              if (msg.messageType == MessageType.image ||
                  msg.messageType == MessageType.video ||
                  msg.messageType == MessageType.videoNote)
                ListTile(
                  leading: const Icon(Icons.save_alt),
                  title: const Text('Сохранить в галерею'),
                  onTap: () {
                    WidgetsBinding.instance.addPostFrameCallback((_) {
                      Navigator.of(ctx).pop();
                      _saveMediaToGallery(msg);
                    });
                  },
                ),
              ListTile(
                leading: const Icon(Icons.forward),
                title: const Text('Переслать'),
                onTap: () {
                  WidgetsBinding.instance.addPostFrameCallback((_) {
                    Navigator.of(ctx).pop();
                    _showForwardContactPicker(msg);
                  });
                },
              ),
              /*if (msg.messageType == MessageType.image ||
                  msg.messageType == MessageType.video ||
                  msg.messageType == MessageType.file ||
                  msg.messageType == MessageType.videoNote ||
                  msg.messageType == MessageType.voiceNote)
                ListTile(
                  leading: const Icon(Icons.delete_outline),
                  title: const Text('Удалить локальный файл'),
                  textColor: Colors.red,
                  iconColor: Colors.red,
                  onTap: () {
                    Navigator.of(ctx).pop();
                    _deleteLocalFile(msg);
                  },
                ),*/
              if (msg.isMe)
                ListTile(
                  leading: const Icon(Icons.delete_outline),
                  title: const Text('Удалить'),
                  textColor: Colors.red,
                  iconColor: Colors.red,
                  onTap: () async {
                    WidgetsBinding.instance.addPostFrameCallback((_) async {
                      Navigator.of(ctx).pop();
                      await _deleteMessage(msg);
                    });
                  },
                ),
              const SizedBox(height: 8),
            ],
          ),
        );
      },
    );
  }

  Future<void> _deleteLocalFile(MessageModel msg) async {
    if (msg.localFile != null && msg.localFile!.existsSync()) {
      try {
        await msg.localFile!.delete();
        debugPrint("Локальный файл успешно удален с диска: ${msg.fileId}");
      } catch (e) {
        debugPrint("Ошибка при физическом удалении файла с диска: $e");
        // Даже если файл не удалился физически, мы всё равно очистим стейт,
        // чтобы приложение не пыталось его прочитать и не падало.
      }

      final sharedPrefs = await SharedPreferences.getInstance();
      final String sizeKey = 'valid_dec_size_${msg.fileId}';
      await sharedPrefs.remove(sizeKey);

      if (mounted) {
        setState(() {
          final idx = messages.indexWhere((m) => m.id == msg.id);
          print(
            "Очистка локального файла для сообщения ${msg.id}. Индекс в списке: $idx",
          );
          if (idx != -1) {
            messages[idx] = messages[idx].copyWith(localFile: null);
          }
        });
      }
    }
  }

  Future<void> _editMessage(MessageModel msg) async {
    final controller = TextEditingController(text: msg.text);
    final result = await showDialog<bool>(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('Изменить сообщение'),
        content: TextField(
          controller: controller,
          minLines: 1,
          maxLines: 5,
          autofocus: true,
          decoration: const InputDecoration(hintText: 'Новый текст сообщения'),
        ),
        actions: [
          TextButton(
            onPressed: () {
              WidgetsBinding.instance.addPostFrameCallback((_) {
                Navigator.of(ctx).pop(false);
              });
            },
            child: const Text('Отмена'),
          ),
          ElevatedButton(
            onPressed: () {
              WidgetsBinding.instance.addPostFrameCallback((_) {
                Navigator.of(ctx).pop(true);
              });
            },
            child: const Text('Сохранить'),
          ),
        ],
      ),
    );

    if (result != true || controller.text.trim().isEmpty) return;

    final newText = controller.text.trim();
    final myPrivKey = await _cryptoService.getPrivateKey();
    if (myPrivKey == null) return;
    final sharedSecret = await _cryptoService.deriveSharedSecret(
      myPrivKey,
      _currentContact.publicKey!,
    );
    final encryptedContent = await _cryptoService.encryptMessage(
      newText,
      sharedSecret,
    );

    final content50 = newText.length > 50 ? newText.substring(0, 50) : newText;
    final encryptedContent50 = await _cryptoService.encryptMessage(
      content50,
      sharedSecret,
    );

    setState(() {
      messages = messages.map((m) {
        if (m.id != null && m.id == msg.id) {
          return m.copyWith(text: newText, editedAt: DateTime.now());
        }
        return m;
      }).toList();
    });

    if (msg.id != null) {
      try {
        await _localDbService.updateMessageContent(
          msg.id!,
          encryptedContent,
          DateTime.now(),
        );
      } catch (_) {}
      Provider.of<SocketService>(context, listen: false).sendMessage({
        'type': 'edit_message',
        'message_id': msg.id,
        'content': encryptedContent,
        'content50': encryptedContent50,
      });
    }
  }

  Future<void> _deleteMessage(MessageModel msg) async {
    setState(() {
      messages.removeWhere(
        (m) =>
            (m.id != null && m.id == msg.id) ||
            (m.tempId != null && m.tempId == msg.tempId),
      );
    });

    final id = msg.id;
    if (id != null) {
      try {
        await _localDbService.deleteMessage(id);
      } catch (_) {}
      Provider.of<SocketService>(
        context,
        listen: false,
      ).sendMessage({'type': 'delete_message', 'message_id': id});
    }
  }

  Future<void> _showForwardContactPicker(MessageModel msg) async {
    // Открываем новый красивый экран выбора вместо bottomSheet
    final selectedContact = await Navigator.of(context).push<Contact?>(
      MaterialPageRoute(
        builder: (context) => ForwardContactPickerScreen(message: msg),
      ),
    );

    // Если контакт был выбран и нажата кнопка «Продолжить»
    if (selectedContact != null && mounted) {
      // Запускаем твою готовую и исправленную функцию пересылки медиа/текста
      await _forwardMessage(msg, selectedContact);
    }
  }

  Future<void> _forwardMessage(
    MessageModel originalMsg,
    Contact targetContact,
  ) async {
    try {
      final isSameChat = _currentContact.id == targetContact.id;
      String? newFileId;
      String? newEncryptedKey;
      File? newLocalFile;

      final tempId = DateTime.now().millisecondsSinceEpoch;

      // 1. E2EE Защита: Если публичного ключа нет в объекте, пробуем запросить его у сервера
      String? targetPublicKey = targetContact.publicKey;
      if (originalMsg.fileId != null &&
          (targetPublicKey == null || targetPublicKey.isEmpty)) {
        debugPrint(
          "==> [Forward] У контакта нет публичного ключа в кэше. Запрашиваем с сервера...",
        );
        try {
          // Вызываем метод твоего API для получения свежих данных пользователя
          final freshContact = await apiService.getUserByUsername(
            targetContact.username,
          );
          if (freshContact != null && freshContact.publicKey != null) {
            targetPublicKey = freshContact.publicKey;
            targetContact.publicKey =
                freshContact.publicKey; // Обновляем инстанс в памяти
          }
        } catch (e) {
          debugPrint(
            "==> [Forward] Не удалось запросить ключ получателя с сервера: $e",
          );
        }
      }

      // 2. Если есть медиа — обрабатываем на сервере и перешифровываем ключи
      if (originalMsg.fileId != null) {
        debugPrint(
          "==> [Forward] Старт копирования медиа. fileId: ${originalMsg.fileId}",
        );

        final copiedFileId = await apiService.copyMediaOnServer(
          originalMsg.fileId!,
          targetContact.id,
        );

        if (copiedFileId == null) {
          throw Exception("Сервер отказал в копировании файла");
        }
        newFileId = copiedFileId;

        // Копируем локальный файл асинхронно, дожидаясь (await) завершения
        if (originalMsg.localFile != null) {
          final directory = await getApplicationDocumentsDirectory();
          // Сохраняем строго под префиксом file_, который ожидает MessageBubble
          final decFile = '${directory.path}/dec_$copiedFileId';
          newLocalFile = await originalMsg.localFile!.copy(decFile);
          print(
            "Локальный файл для пересылки создан по пути: ${newLocalFile.path}",
          );
        } else if (originalMsg.fileId != null) {
          final directory = await getApplicationDocumentsDirectory();
          // Сохраняем строго под префиксом file_, который ожидает MessageBubble
          final decFile = '${directory.path}/dec_$copiedFileId';
          final File oldFile = File(
            '${directory.path}/dec_${originalMsg.fileId!}',
          );
          newLocalFile = await oldFile.copy(decFile);
          print(
            "Локальный файл для пересылки создан через id по пути: ${newLocalFile.path}",
          );
        } else {
          print(
            "Невозможно создать локальную копию файла для пересылки: отсутствует и локальный файл, и fileId.",
          );
        }
        final sharedPrefs = await SharedPreferences.getInstance();
        final String sizeKey = 'valid_dec_size_$copiedFileId';
        final finalFileSize = await newLocalFile?.length();
        if (finalFileSize != null && finalFileSize > 0) {
          // Запоминаем, сколько байт весит ЧИСТЫЙ расшифрованный файл
          await sharedPrefs.setInt(sizeKey, finalFileSize);
          debugPrint(
            "Файл успешно загружен. Размер сохранен: $finalFileSize байт.",
          );
        }

        // Проверяем условия для криптографии
        final myPrivKey = await _cryptoService.getPrivateKey();

        if (originalMsg.encryptedFileKey == null) {
          throw Exception(
            "У оригинального сообщения отсутствует ключ шифрования файла.",
          );
        }
        if (targetPublicKey == null || targetPublicKey.isEmpty) {
          throw Exception(
            "Невозможно переслать медиа: у получателя отсутствует публичный ключ шифрования E2EE.",
          );
        }

        final oldSecret = await _cryptoService.deriveSharedSecret(
          myPrivKey!,
          _currentContact.publicKey!,
        );
        final newSecret = await _cryptoService.deriveSharedSecret(
          myPrivKey,
          targetPublicKey,
        );

        final decryptedKey = await _cryptoService.decryptAesKey(
          originalMsg.encryptedFileKey!,
          oldSecret,
        );
        if (decryptedKey == null) {
          throw Exception("Не удалось расшифровать ключ файла для пересылки");
        }
        newEncryptedKey = await _cryptoService.encryptAesKey(
          decryptedKey,
          newSecret,
        );
      }

      // 3. Отрисовываем сообщение в текущем чате (если пересылаем сами себе)
      // Теперь localFile передается сразу, предотвращая ложные индикаторы загрузки
      print(
        "Перед добавлением пересланного сообщения в UI: newFileId=$newFileId, newEncryptedKey=${newEncryptedKey != null ? 'есть' : 'нет'}, newLocalFile=${newLocalFile != null ? 'есть' : 'нет'}",
      );
      if (isSameChat) {
        final localMsg = MessageModel(
          tempId: tempId,
          text: originalMsg.text,
          isMe: true,
          senderId: myId,
          receiverId: targetContact.id,
          createdAt: DateTime.now(),
          status: MessageStatus.sending,
          messageType: originalMsg.messageType,
          fileId: newFileId ?? originalMsg.fileId,
          fileName: originalMsg.fileName,
          localFile: newLocalFile,
          fileSize: originalMsg.fileSize,
          encryptedFileKey: newEncryptedKey ?? originalMsg.encryptedFileKey,
        );

        setState(() {
          messages.add(localMsg);
        });
        _scrollToBottom();
      }

      // 4. Шифруем текстовую часть контента
      if (targetPublicKey == null || targetPublicKey.isEmpty) {
        throw Exception(
          "У получателя отсутствует публичный ключ для шифрования текста.",
        );
      }

      final textSecret = await _cryptoService.deriveSharedSecret(
        (await _cryptoService.getPrivateKey())!,
        targetPublicKey,
      );
      final encryptedContent = await _cryptoService.encryptMessage(
        originalMsg.text,
        textSecret,
      );

      // 5. Формируем Payload
      final payload = {
        "type": "private_message",
        "receiver_id": targetContact.id,
        "message_type": originalMsg.messageType.name,
        "content": encryptedContent,
        "temp_id": tempId,
        if (newFileId != null) ...{
          "file_id": newFileId,
          "encrypted_key": newEncryptedKey,
        },
      };

      // 6. Отправка и навигация
      final socket = Provider.of<SocketService>(context, listen: false);
      final isSent = socket.sendMessage(payload);

      if (!isSent) throw Exception("Ошибка отправки через сокет");

      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (!mounted) return;
        if (!isSameChat) {
          Navigator.pushReplacement(
            context,
            MaterialPageRoute(
              builder: (context) => ChatScreen(contact: targetContact),
            ),
          );
        } else {
          // Если переслали в текущий чат — обновляем статус
          setState(() {
            final idx = messages.indexWhere((m) => m.tempId == tempId);
            if (idx != -1) {
              messages[idx] = messages[idx].copyWith(
                status: MessageStatus.sent,
                fileId: newFileId ?? originalMsg.fileId,
                localFile: newLocalFile,
              );
            }
          });
        }
      });
    } catch (e) {
      debugPrint("[Error] Ошибка пересылки: $e");
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(e.toString().replaceAll("Exception: ", "")),
          backgroundColor: Colors.redAccent,
        ),
      );
    }
  }

  Widget _buildMessageInput() {
    final bool hasTextOrFile =
        _controller.text.trim().isNotEmpty || _pendingFile != null;
    final bool showSendButton = hasTextOrFile || _isRecordLocked;

    return Stack(
      clipBehavior: Clip.none,
      children: [
        Container(
          constraints: const BoxConstraints(maxHeight: 250),
          decoration: BoxDecoration(
            color: Theme.of(
              context,
            ).colorScheme.surfaceVariant.withOpacity(0.75),
            borderRadius: BorderRadius.circular(18),
            border: Border.all(
              color: Theme.of(context).dividerColor.withOpacity(0.25),
              width: 1,
            ),
          ),
          padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 4.0),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              if (_replyTo != null)
                Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 12,
                    vertical: 8,
                  ),
                  margin: const EdgeInsets.only(bottom: 8),
                  decoration: BoxDecoration(
                    color: Theme.of(context).colorScheme.surface,
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Row(
                    children: [
                      const Icon(Icons.reply, size: 18),
                      const SizedBox(width: 8),
                      Expanded(
                        child: Text(
                          _replyTo!.text.isNotEmpty
                              ? _replyTo!.text
                              : _getMediaPreview(_replyTo!.messageType),
                          maxLines: 1,
                          overflow: TextOverflow.ellipsis,
                        ),
                      ),
                      IconButton(
                        icon: const Icon(Icons.close, size: 18),
                        onPressed: () => setState(() => _replyTo = null),
                      ),
                    ],
                  ),
                ),
              if (_pendingFile != null)
                Container(
                  margin: const EdgeInsets.only(bottom: 6),
                  padding: const EdgeInsets.all(6),
                  decoration: BoxDecoration(
                    color: Theme.of(context).colorScheme.surface,
                    borderRadius: BorderRadius.circular(12),
                    border: Border.all(color: Colors.grey.withOpacity(0.3)),
                  ),
                  child: Row(
                    children: [
                      SizedBox(
                        width: 44,
                        height: 44,
                        child: ClipRRect(
                          borderRadius: BorderRadius.circular(8),
                          child: _buildPreviewIcon(),
                        ),
                      ),
                      const SizedBox(width: 12),
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            Text(
                              _pendingFileName ?? "Файл",
                              maxLines: 1,
                              overflow: TextOverflow.ellipsis,
                              style: const TextStyle(
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                            Text(
                              _pendingMessageType.name.toUpperCase(),
                              style: const TextStyle(fontSize: 12),
                            ),
                          ],
                        ),
                      ),
                      IconButton(
                        icon: const Icon(Icons.close, size: 22),
                        onPressed: () => setState(() {
                          _pendingFile = null;
                          _pendingFileName = null;
                          _previewBytes = null;
                          _pendingMessageType = MessageType.text;
                        }),
                      ),
                    ],
                  ),
                ),
              Row(
                crossAxisAlignment: CrossAxisAlignment.end,
                children: [
                  if (!_isRecording)
                    GestureDetector(
                      onTapDown: (details) {
                        _showPopup(context, details.globalPosition);
                      },
                      child: Container(
                        width: 32,
                        height: 32,
                        alignment: Alignment.center,
                        child: const Icon(Icons.photo, size: 22),
                      ),
                    )
                  else
                    const Padding(
                      padding: EdgeInsets.symmetric(horizontal: 6, vertical: 6),
                      child: Icon(
                        Icons.fiber_manual_record,
                        color: Colors.red,
                        size: 20,
                      ),
                    ),
                  Expanded(
                    child: _isRecording
                        ? Container(
                            padding: const EdgeInsets.symmetric(
                              vertical: 6,
                              horizontal: 4,
                            ),
                            child: Row(
                              children: [
                                Text(
                                  _stopwatchDisplay,
                                  style: const TextStyle(
                                    color: Colors.red,
                                    fontSize: 15,
                                    fontWeight: FontWeight.bold,
                                  ),
                                ),
                                const SizedBox(width: 8),
                                _isRecordLocked
                                    ? Text(
                                        "Удержание записи",
                                        style: TextStyle(
                                          color: Colors.grey.shade500,
                                          fontSize: 11,
                                        ),
                                      )
                                    : Text(
                                        _recordDragX < _swipeCancelThreshold / 2
                                            ? "Отпусти для отмены"
                                            : (_recordDragY <
                                                      _swipeLockThreshold / 2
                                                  ? "Отпусти для удержания"
                                                  : "Проведите вверх для удержания"),
                                        style: TextStyle(
                                          color: Colors.grey.shade500,
                                          fontSize: 11,
                                        ),
                                      ),
                                const SizedBox(width: 4),
                              ],
                            ),
                          )
                        : TextField(
                            controller: _controller,
                            minLines: 1,
                            maxLines: 5,
                            readOnly: _isRecordLocked,
                            textInputAction: TextInputAction.newline,
                            textCapitalization: TextCapitalization.sentences,
                            textAlignVertical: TextAlignVertical.center,
                            style: const TextStyle(fontSize: 15),
                            decoration: InputDecoration(
                              hintText: _isRecordLocked
                                  ? "Запись зафиксирована..."
                                  : "Напиши сообщение...",
                              isDense: true,
                              isCollapsed: true,
                              contentPadding: const EdgeInsets.symmetric(
                                horizontal: 4,
                                vertical: 6,
                              ),
                            ),
                            onChanged: (text) => setState(() {}),
                          ),
                  ),
                  _buildContextButton(showSendButton),
                ],
              ),
            ],
          ),
        ),
      ],
    );
  }

  Widget _buildContextButton(bool showSendButton) {
    if (showSendButton) {
      return GestureDetector(
        onTap: () {
          if (_isRecordLocked) {
            _stopAndSendRecording();
          } else {
            _sendMessage();
          }
        },
        child: Container(
          width: 36,
          height: 36,
          alignment: Alignment.center,
          child: const Icon(Icons.send, size: 22),
        ),
      );
    }

    return GestureDetector(
      onTap: _toggleRecordMode,
      onLongPressStart: (_) => _startRecording(),
      onLongPressMoveUpdate: (details) {
        if (!_isRecording || _isRecordLocked) return;

        setState(() {
          _recordDragX = details.localOffsetFromOrigin.dx;
          _recordDragY = details.localOffsetFromOrigin.dy;
        });

        if (_recordDragX < _swipeCancelThreshold) {
          _cancelRecording();
        } else if (_recordDragY < _swipeLockThreshold) {
          _lockRecording();
        }
      },
      onLongPressEnd: (_) {
        if (_isRecording && !_isRecordLocked) {
          _stopAndSendRecording();
        }
      },
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 100),
        width: 36,
        height: 36,
        alignment: Alignment.center,
        decoration: BoxDecoration(
          color: _isRecording
              ? Colors.red.withOpacity(0.15)
              : Colors.transparent,
          shape: BoxShape.circle,
        ),
        child: Icon(
          _isVoiceMode ? Icons.mic : Icons.videocam,
          size: _isRecording ? 24 : 22,
          color: _isRecording ? Colors.red : Theme.of(context).iconTheme.color,
        ),
      ),
    );
  }

  void _showPopup(BuildContext context, Offset position) async {
    final selected = await showMenu<String>(
      context: context,
      position: RelativeRect.fromLTRB(
        position.dx,
        position.dy,
        position.dx,
        position.dy,
      ),
      items: [
        PopupMenuItem(
          value: 'camera',
          child: Row(
            children: const [
              Icon(Icons.camera_alt),
              SizedBox(width: 8),
              Text("Камера"),
            ],
          ),
        ),
        PopupMenuItem(
          value: 'gallery',
          child: Row(
            children: const [
              Icon(Icons.photo_library),
              SizedBox(width: 8),
              Text("Галерея"),
            ],
          ),
        ),
        PopupMenuItem(
          value: 'file',
          child: Row(
            children: const [
              Icon(Icons.insert_drive_file),
              SizedBox(width: 8),
              Text("Файлы"),
            ],
          ),
        ),
      ],
    );

    // обработка выбора
    switch (selected) {
      case 'camera':
        _pickCamera();
        break;
      case 'gallery':
        _pickGallery();
        break;
      case 'file':
        _pickFile();
        break;
    }
  }

  Future<void> _pickCamera() async {
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      final result = await Navigator.push<(XFile, String)>(
        context,
        MaterialPageRoute(builder: (_) => const CameraScreen()),
      );
      if (result == null) return;
      final file = result.$1;
      final type = result.$2;
      final bytes = type == 'image' ? await file.readAsBytes() : null;
      setState(() {
        if (type == 'image') {
          _previewBytes = bytes;
        }
        _pendingFile = File(file.path);
        _pendingFileName = 'media_${DateTime.now().millisecondsSinceEpoch}';
        _pendingMessageType = type == 'video'
            ? MessageType.video
            : MessageType.image;
      });
    });
  }

  Future<void> _pickGallery() async {
    final photosGranted = await Permission.photos.request();
    final videosGranted = await Permission.videos.request();
    if (!photosGranted.isGranted || !videosGranted.isGranted) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text(
            "Разрешение на доступ к медиа необходимо для выбора фото или видео.",
          ),
          behavior: SnackBarBehavior.floating,
          margin: EdgeInsets.only(bottom: 80.0 + 10.0, left: 10.0, right: 10.0),
          duration: Duration(seconds: 3),
        ),
      );
      return;
    }
    final List<AssetEntity>? result = await AssetPicker.pickAssets(
      context,
      pickerConfig: AssetPickerConfig(
        maxAssets: 1,
        pageSize: 33,
        gridCount: 3,
        pickerTheme: ThemeData(
          brightness: Theme.of(context).brightness,
          primaryColor: Theme.of(context).primaryColor,
          colorScheme: Theme.of(context).colorScheme,
          scaffoldBackgroundColor: Theme.of(context).scaffoldBackgroundColor,
          appBarTheme: AppBarTheme(
            backgroundColor: Theme.of(context).scaffoldBackgroundColor,
            foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,
          ),
        ),
        specialItemBuilder: null,
      ),
    );
    if (result != null && result.isNotEmpty) {
      final asset = result.first;
      try {
        Uint8List? bytes;
        if (asset.type == AssetType.image) {
          bytes = await asset.originBytes;
        }
        final File? file = await asset.file;
        if (file == null) {
          if (!mounted) return;
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('Не удалось получить доступ к файлу медиа.'),
              duration: Duration(seconds: 2),
            ),
          );
          return;
        }
        if (!mounted) return;
        setState(() {
          if (asset.type == AssetType.image && bytes != null) {
            _previewBytes = bytes;
          }
          _pendingFile = file;
          _pendingFileName =
              asset.title ?? 'media_${DateTime.now().millisecondsSinceEpoch}';
          _pendingMessageType = asset.type == AssetType.video
              ? MessageType.video
              : MessageType.image;
        });
      } catch (e) {
        if (!mounted) return;
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Ошибка при выборе медиа: $e'),
            duration: const Duration(seconds: 3),
          ),
        );
      }
    }
  }

  Future<void> _pickFile() async {
    FilePickerResult? result = await FilePicker.pickFiles(type: FileType.any);

    if (result != null && result.files.isNotEmpty) {
      try {
        final file = File(result.files.single.path!);

        if (!mounted) return;
        setState(() {
          _pendingFile = file;
          _pendingFileName = result.files.single.name;
          _pendingMessageType = MessageType.file;
        });
      } catch (e) {
        if (!mounted) return;
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Ошибка при выборе файла: $e'),
            duration: const Duration(seconds: 3),
          ),
        );
      }
    }
  }

  Future<File?> _compressAndCropVideoNoteSafe(File originalVideoFile) async {
    try {
      if (!await originalVideoFile.exists()) {
        debugPrint('==> FFmpeg: Исходный файл не найден на диске.');
        return null;
      }

      final String targetOriginalPath = originalVideoFile.path;
      debugPrint(
        '==> Исходный файл: $targetOriginalPath, размер: ${await originalVideoFile.length()} байт',
      );

      // 1. Мгновенно переименовываем оригинальный файл во временный входной файл
      final String tempInputPath = '${targetOriginalPath}_temp_input.mp4';
      final File movedOriginalFile = await originalVideoFile.rename(
        tempInputPath,
      );

      // 2. Строим команду в виде списка аргументов (List<String>).
      // Больше никаких ручных кавычек вокруг путей типа "$tempInputPath" — плагин сам всё экранирует!
      final List<String> ffmpegArgs = [
        '-i',
        tempInputPath,
        '-vf',
        'crop=min(iw\\,ih):min(iw\\,ih),scale=512:512',
        '-vcodec',
        'libx264',
        '-crf',
        '28',
        '-preset',
        'fast',
        '-y',
        targetOriginalPath,
      ];

      debugPrint(
        '==> FFmpeg: Запуск потоковой обработки через массив аргументов...',
      );

      // 3. Вызываем executeWithArguments вместо обычной строки
      final session = await FFmpegKit.executeWithArguments(ffmpegArgs);
      final returnCode = await session.getReturnCode();

      // Логируем внутренний вывод FFmpeg на случай непредвиденных ошибок кодека устройства
      final output = await session.getOutput();
      if (output != null && output.isNotEmpty) {
        debugPrint('==> FFmpeg Консоль:\n$output');
      }

      if (ReturnCode.isSuccess(returnCode)) {
        final outputFile = File(targetOriginalPath);
        if (await outputFile.exists()) {
          debugPrint(
            '==> FFmpeg: Успех! Новый размер файла: ${await outputFile.length()} байт',
          );
        }

        // Безопасно удаляем временный файл исходника
        if (await movedOriginalFile.exists()) {
          await movedOriginalFile.delete();
        }

        return outputFile;
      } else {
        final failStackTrace = await session.getFailStackTrace();
        debugPrint(
          '==> FFmpeg: Ошибка кодирования. Код возврата: $returnCode. Стек: $failStackTrace',
        );

        // ВОССТАНОВЛЕНИЕ: Возвращаем оригинальный файл на место, если перекодирование не удалось
        if (await movedOriginalFile.exists()) {
          await movedOriginalFile.rename(targetOriginalPath);
        }
        return null;
      }
    } catch (e) {
      debugPrint('==> Критическая ошибка при изменении файла: $e');
      return null;
    }
  }

  Future<void> _sendMessage() async {
    _sendStopTypingStatus();
    String rawText = _controller.text.trim();
    File? file = _pendingFile;
    final MessageType messageType = _pendingMessageType;
    final hasMedia = _pendingFile != null;
    final replyTo = _replyTo;

    if (messageType == MessageType.videoNote ||
        messageType == MessageType.voiceNote) {
      rawText =
          ""; // Для видеозаметок и голосовых сообщений текст не обязателен, игнорируем его
    }

    // Если и текст пустой, и медиа нет — выходим
    if (rawText.isEmpty && !hasMedia) return;
    _scrollToBottom();
    // Блокируем UI на время загрузки
    _controller.clear();
    _pendingFile = null;
    _pendingMessageType = MessageType.text; // Сбрасываем тип медиа
    _previewBytes = null; // Очищаем превью
    _pendingFileName = null;
    _replyTo = null;

    final tempId = DateTime.now().millisecondsSinceEpoch;
    try {
      print(
        "Исходный файл: ${file?.path}, размер: ${await file?.length()} байт",
      );
      if (messageType == MessageType.videoNote && file != null) {
        file = await _compressAndCropVideoNoteSafe(file);
        print(
          "После обработки видеозаметки: ${file?.path}, размер: ${await file?.length()} байт",
        );
      }

      int? fileSize = await file?.length();
      // создаем первичную модель отобрадения
      MessageModel tempMsg = MessageModel(
        senderId: myId,
        receiverId: _currentContact.id,
        createdAt: DateTime.now(),
        isMe: true,
        text: rawText,
        tempId: tempId,
        messageType: messageType,
        localFile: file,
        status: MessageStatus.encrypting,
        fileSize: fileSize,
        replyToId: replyTo?.id,
        replyToText: replyTo?.text,
        fileId: tempId.toString(),
        fileName: file != null ? p.basename(file.path) : "file",
      );

      setState(() => messages.add(tempMsg));
      // 1. Подготовка ключей
      final myPrivKey = await _cryptoService.getPrivateKey();
      final sharedSecret = await _cryptoService.deriveSharedSecret(
        myPrivKey!,
        _currentContact.publicKey!,
      );

      String? fileId;
      String? encryptedFileKey;
      String encryptedContent;
      String encryptedContent50;
      String? encryptedReplyToText;
      _messageProgressNotifiers['${tempMsg.fileId}'] ??= ValueNotifier<double?>(
        0.0,
      );
      _messageProgressNotifiers['${tempMsg.fileId}']!.value = 0.0;
      // 2. Если есть медиа — сначала загружаем его
      if (hasMedia && file != null && fileSize != null) {
        final fileStream = file.openRead();
        final encryptedStream = await _cryptoService.encryptFileStream(
          fileStream,
          sharedSecret,
          totalSize: fileSize,
          onProgress: (received, total) {
            print(received);
            if (total != -1) {
              double progress = received / total;
              if (progress > 1.0) progress = 1.0;
              _messageProgressNotifiers['${tempMsg.fileId}']?.value = progress;
            }
          },
        );
        final fileKeyForServer = encryptedStream.$2;
        final tempDir = await getTemporaryDirectory();
        final encFile = File('${tempDir.path}/enc_${tempId}.tmp');
        final ios = encFile.openWrite();
        await ios.addStream(encryptedStream.$1);
        await ios.close();

        final int exactEncryptedSize = await encFile.length();

        setState(() {
          tempMsg = tempMsg.copyWith(status: MessageStatus.sending);
          final idx = messages.indexWhere((m) => m.tempId == tempId);
          if (idx != -1) {
            messages[idx] = tempMsg;
          }
          fileSize = exactEncryptedSize;
        });

        _messageProgressNotifiers['${tempMsg.fileId}'] ??=
            ValueNotifier<double?>(0.0);
        _messageProgressNotifiers['${tempMsg.fileId}']!.value = 0.0;
        fileId = await apiService.uploadFileStream(
          encFile.openRead(),
          exactEncryptedSize,
          purpose: messageType.name,
          fileName: p.basename(file.path),

          onProgress: (received, total) {
            print(received);
            if (total != -1) {
              double progress = received / total;
              if (progress > 1.0) progress = 1.0;
              _messageProgressNotifiers['${tempMsg.fileId}']?.value = progress;
            }
          },
        );
        if (await encFile.exists()) {
          await encFile.delete();
        }

        if (fileId == null) {
          throw Exception("Ошибка загрузки файла на сервер");
        }

        encryptedFileKey = fileKeyForServer;
      }

      // 3. Шифруем текст сообщения (даже если там пусто, или есть подпись к медиа)
      // Если текста нет, но есть медиа, отправим пустую строку
      final String textToEncrypt = rawText.isNotEmpty
          ? rawText
          : (hasMedia ? "" : "");

      encryptedContent = await _cryptoService.encryptMessage(
        textToEncrypt,
        sharedSecret,
      );

      // Генерируем превью текст в зависимости от типа медиа
      String previewText;
      if (rawText.isNotEmpty) {
        previewText = rawText;
      } else if (hasMedia) {
        previewText = switch (messageType) {
          MessageType.voiceNote => "[Кружок}",
          MessageType.videoNote => "[Голосовое]",
          MessageType.image => "[Фото]",
          MessageType.video => "[Видео]",
          MessageType.file => "[Файл]",
          MessageType.text => "",
        };
      } else {
        previewText = "";
      }
      if (previewText.length > 50) previewText = previewText.substring(0, 50);
      encryptedContent50 = await _cryptoService.encryptMessage(
        previewText,
        sharedSecret,
      );

      if (replyTo?.id != null && replyTo!.text.trim().isNotEmpty) {
        encryptedReplyToText = await _cryptoService.encryptMessage(
          replyTo.text,
          sharedSecret,
        );
      }

      // 4. Создаем локальную модель для мгновенного отображения
      final localMessage = MessageModel(
        tempId: tempId,
        text: rawText,
        isMe: true,
        senderId: myId,
        receiverId: _currentContact.id,
        createdAt: DateTime.now(),
        status: MessageStatus.sending,
        localFile: file,
        messageType: messageType,
        fileId: fileId,
        encryptedFileKey: encryptedFileKey,
        replyToId: replyTo?.id,
        replyToText: replyTo?.text,
        fileSize: await file?.length(),
        fileName: file != null ? p.basename(file.path) : "file",
      );
      final directory = await getApplicationDocumentsDirectory();
      if (file != null) {
        await file.copy('${directory.path}/dec_$fileId');
        print(
          "DEBUG: Сохраняю файл: ${file.path}, существует: ${await file.exists()}, размер: ${await file.length()}",
        );

        final sharedPrefs = await SharedPreferences.getInstance();
        final String sizeKey = 'valid_dec_size_$fileId';
        final finalFileSize = await file.length();
        if (finalFileSize > 0) {
          // Запоминаем, сколько байт весит ЧИСТЫЙ расшифрованный файл
          await sharedPrefs.setInt(sizeKey, finalFileSize);
          debugPrint(
            "Файл успешно загружен. Размер сохранен: $finalFileSize байт.",
          );
        }
      } else {
        print(
          "==> [Warning] Локальный файл отсутствует для сообщения с tempId: $tempId",
        );
      }

      setState(() {
        final idx = messages.indexWhere((m) => m.tempId == tempId);
        if (idx != -1) {
          messages[idx] = localMessage;
        }
        file = null; // Очищаем черновик
      });

      if (hasMedia && (fileId == null || encryptedFileKey == null)) {
        throw Exception(
          'Не удалось загрузить медиа перед отправкой сообщения.',
        );
      }

      // 5. Формируем финальный payload для сокета
      final payload = {
        "type": "private_message",
        "receiver_id": _currentContact.id,
        "message_type": messageType.name,
        "content": encryptedContent,
        "content50": encryptedContent50,
        "temp_id": tempId,
        if (hasMedia) ...{
          "file_id": fileId,
          "encrypted_key": encryptedFileKey,
          if (messageType == MessageType.file)
            "file_name": file != null ? p.basename(file!.path) : "file",
        },
        if (replyTo?.id != null) ...{
          "reply_to_id": replyTo!.id,
          if (encryptedReplyToText != null)
            "reply_to_text": encryptedReplyToText,
        },
      };

      // Логирование для отладки
      print('[DEBUG] _sendMessage payload:');
      print('[DEBUG] - type: ${payload['type']}');
      print('[DEBUG] - receiver_id: ${payload['receiver_id']}');
      print('[DEBUG] - message_type: ${payload['message_type']}');
      print(
        '[DEBUG] - content length: ${(payload['content'] as String?)?.length ?? 0}',
      );
      print('[DEBUG] - temp_id: ${payload['temp_id']}');
      if (hasMedia) {
        print('[DEBUG] - file_id: ${payload['file_id']}');
        print(
          '[DEBUG] - encrypted_key: ${(payload['encrypted_key'] as String?)?.length ?? 0}',
        );
        if (payload.containsKey('file_name')) {
          print('[DEBUG] - file_name: ${payload['file_name']}');
        }
      }

      // 6. Отправка через сокет
      final ok = Provider.of<SocketService>(
        context,
        listen: false,
      ).sendMessage(payload);

      // Обновляем статус
      setState(() {
        final idx = messages.indexWhere((m) => m.tempId == tempId);
        if (idx != -1) {
          messages[idx] = messages[idx].copyWith(
            status: ok ? MessageStatus.sent : MessageStatus.failed,
          );
        }
        _replyTo = null;
      });
    } catch (e) {
      try {
        setState(() {
          final idx = messages.indexWhere((m) => m.tempId == tempId);
          if (idx != -1) {
            messages.removeAt(idx);
          }
          _replyTo = null;
        });
      } catch (e) {
        print(e);
      }
      print(e);
      // В случае ошибки возвращаем текст и медиа в контроллер
      _controller.text = rawText;
      _pendingFile = file;
      _pendingMessageType = messageType;
      _replyTo = replyTo;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text("Ошибка отправки: $e"),
          behavior: SnackBarBehavior.floating,
          margin: EdgeInsets.only(bottom: 80.0 + 10.0, left: 10.0, right: 10.0),
          duration: Duration(seconds: 5),
        ),
      );
    }
  }

  void _handleIncomingMessage(Map<String, dynamic> data) async {
    print('Meesage from websocket: $data');
    DateTime now = DateTime.now();

    Duration offset = now.timeZoneOffset;
    if (data['type'] == 'message_sent') {
      final tempId = int.tryParse(data['temp_id']?.toString() ?? '');
      final serverId = int.tryParse(data['server_id']?.toString() ?? '');
      var ts = DateTime.tryParse(
        data['timestamp']?.toString() ?? '',
      )?.add(offset);

      if (tempId == null) return;
      if (!mounted) return;

      setState(() {
        final idx = messages.indexWhere((m) => m.tempId == tempId);
        if (idx == -1) return;

        // 1. Создаем обновленный объект сообщения с серверным ID
        final updatedMsg = messages[idx].copyWith(
          id: serverId ?? messages[idx].id,
          createdAt: ts ?? messages[idx].createdAt,
          status: MessageStatus.sent,
        );

        // 2. Обновляем его в основном списке
        messages[idx] = updatedMsg;

        // 3. ИСПРАВЛЕНИЕ: Обязательно регистрируем сообщение в мапе по его серверному ID!
        if (serverId != null) {
          _messageMap[serverId] = updatedMsg;
        }
      });
      return;
    }

    // Backward compatibility: старый ack мог приходить как message_delivered с temp_id/server_id
    if (data['type'] == 'message_delivered' && data.containsKey('temp_id')) {
      final tempId = int.tryParse(data['temp_id']?.toString() ?? '');
      final serverId = int.tryParse(data['server_id']?.toString() ?? '');
      var ts = DateTime.tryParse(
        data['timestamp']?.toString() ?? '',
      )?.add(offset);

      if (tempId == null) return;

      if (!mounted) return;
      setState(() {
        final idx = messages.indexWhere((m) => m.tempId == tempId);
        if (idx == -1) return;
        messages[idx] = messages[idx].copyWith(
          id: serverId ?? messages[idx].id,
          createdAt: ts ?? messages[idx].createdAt,
          status: MessageStatus.sent,
        );
      });
      return;
    }

    // Доставка онлайн (получатель был в сети)
    if (data['type'] == 'message_delivered') {
      final messageId = int.tryParse(data['message_id']?.toString() ?? '');
      var ts = DateTime.tryParse(
        data['timestamp']?.toString() ?? '',
      )?.add(offset);
      if (messageId == null) return;

      if (!mounted) return;
      setState(() {
        _updateMessageInList(
          messageId,
          (m) => m.copyWith(status: MessageStatus.delivered),
        );
      });

      if (ts != null) {
        try {
          await _localDbService.updateDeliveredAt(messageId, ts);
        } catch (_) {}
      }
      return;
    }

    if (data['type'] == 'message_edited') {
      final messageId = int.tryParse(data['message_id']?.toString() ?? '');
      var ts = DateTime.tryParse(
        data['edited_at']?.toString() ?? '',
      )?.add(offset);
      if (messageId == null) return;

      final myPrivKey = await _cryptoService.getPrivateKey();
      if (myPrivKey == null) return;
      final sharedSecret = await _cryptoService.deriveSharedSecret(
        myPrivKey,
        _currentContact.publicKey!,
      );
      final decryptedText = await _cryptoService.decryptMessage(
        data['content'],
        sharedSecret,
      );

      if (!mounted) return;
      setState(() {
        messages = messages.map((m) {
          if (m.id != null && m.id == messageId) {
            return m.copyWith(text: decryptedText, editedAt: ts);
          }
          return m;
        }).toList();
      });

      try {
        await _localDbService.updateMessageContent(
          messageId,
          data['content'].toString(),
          ts,
        );
      } catch (_) {}

      // Обновить последнее сообщение в списке контактов
      final contactProvider = context.read<ContactProvider>();
      if (messages.isNotEmpty && messages.last.id == messageId) {
        await contactProvider.updateContactLastMessage(
          widget.contact.id,
          lastMessage: decryptedText,
          lastMessageTime: ts,
          isLastMsgDecrypted: true,
          lastMessageId: messageId,
          isEdited: true,
        );
      }
      return;
    }

    if (data['type'] == 'message_deleted') {
      final messageId = int.tryParse(data['message_id']?.toString() ?? '');
      if (messageId == null) return;
      if (!mounted) return;
      setState(() {
        messages.removeWhere((m) => m.id != null && m.id == messageId);
      });
      try {
        await _localDbService.deleteMessage(messageId);
      } catch (_) {}

      // Обновить последнее сообщение в списке контактов
      final contactProvider = context.read<ContactProvider>();
      if (messages.isEmpty) {
        // Если не осталось сообщений, очистить последнее сообщение
        await contactProvider.updateContactLastMessage(
          widget.contact.id,
          lastMessage: null,
          lastMessageTime: null,
          lastMessageId: null,
        );
      } else {
        // Обновить на предпоследнее сообщение
        await contactProvider.refreshContactLastMessage(widget.contact.id);
      }
      return;
    }

    if (data['type'] == 'message_read') {
      final messageId = int.tryParse(data['message_id'].toString());
      if (messageId == null) return;
      var ts = DateTime.tryParse(
        data['timestamp']?.toString() ?? '',
      )?.add(offset);

      if (!mounted) return;
      setState(() {
        _updateMessageInList(
          messageId,
          (m) => m.copyWith(status: MessageStatus.read),
        );
      });

      if (ts != null) {
        try {
          await _localDbService.updateReadAt(messageId, ts);
        } catch (_) {}
      }
      return;
    }

    if (data['type'] == 'private_message') {
      print('DEBUG incoming private_message raw: $data');
      setState(() {
        _typingTimer?.cancel();
        _isTyping = false;
      });
      final senderId = int.tryParse(data['sender_id']?.toString() ?? '');
      final receiverId = int.tryParse(
        (data['receiver_id'] ?? data['recipient_id'])?.toString() ?? '',
      );
      if (senderId == null || receiverId == null) {
        print(
          'Invalid private_message ids: sender_id=${data['sender_id']} receiver_id=${data['receiver_id'] ?? data['recipient_id']}',
        );
        return;
      }

      // 1. Проверяем, что сообщение именно от того, с кем мы сейчас общаемся
      final isFromPartnerToMe =
          senderId == widget.contact.id && receiverId == myId;
      if (isFromPartnerToMe) {
        try {
          final myPrivKey = await _cryptoService.getPrivateKey();

          // 2. Вычисляем общий секрет для расшифровки
          final sharedSecret = await _cryptoService.deriveSharedSecret(
            myPrivKey!,
            widget.contact.publicKey!,
          );

          // 3. Расшифровываем контент
          final decryptedText = await _cryptoService.decryptMessage(
            data['content'],
            sharedSecret,
          );

          // 4. Добавляем в список и обновляем экран
          String? encryptedFileKey = data['encrypted_key']?.toString();
          Uint8List? decryptedImageBytes;
          // Lazy load images later

          if (!mounted) return;

          final serverMessageId = int.tryParse(data['id']?.toString() ?? '');
          if (serverMessageId != null &&
              !_sentReadReceipts.contains(serverMessageId)) {
            Provider.of<SocketService>(
              context,
              listen: false,
            ).sendReadReceipt(serverMessageId);
            _sentReadReceipts.add(serverMessageId);
          }

          final replyToText = await _decryptReplyText(
            data['reply_to_text']?.toString(),
            sharedSecret,
          );

          setState(() {
            messages.add(
              MessageModel(
                id: int.tryParse(data['id']?.toString() ?? ''),
                text: decryptedText,
                isMe: false,
                senderId: senderId,
                receiverId: myId,
                createdAt: DateTime.parse(data['timestamp']).add(offset),
                status: MessageStatus.delivered,
                replyToId: data['reply_to_id'] == null
                    ? null
                    : int.tryParse(data['reply_to_id'].toString()),
                replyToText: replyToText,
                messageType: _parseMessageTypeString(data['message_type']),
                fileId: data['file_id']?.toString(),
                encryptedFileKey: encryptedFileKey,
                //localFile: decryptedImageBytes,!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
              ),
            );
          });
          // Save to local DB with cached image bytes
          try {
            await _localDbService.saveMessages([messages.last]);
          } catch (e) {
            print('Error saving incoming message to DB: $e');
          }
        } catch (e) {
          print("Ошибка расшифровки входящего сообщения: $e");
        }
      } else {
        print(
          "Сообщение от другого пользователя (ID: $senderId), игнорируем в этом чате",
        );
      }
    }
    if (data['type'] == 'user_online') {
      final userId = int.tryParse(data['user_id']?.toString() ?? '');
      if (userId == widget.contact.id) {
        setState(() => _isOnline = true);
      }
    }
    if (data['type'] == 'user_offline') {
      final userId = int.tryParse(data['user_id']?.toString() ?? '');
      if (userId == widget.contact.id) {
        setState(() {
          _isOnline = false;
          _lastOnline = DateTime.now();
        });

        _loadOnlineStatus();
      }
    }

    if (data['type'] == 'typing' && data['sender_id'] == _currentContact.id) {
      if (mounted) {
        setState(() => _isTyping = true);

        _typingTimer?.cancel();
        _typingTimer = Timer(const Duration(seconds: 4), () {
          if (mounted) setState(() => _isTyping = false);
        });
      }
    }
    if (data['type'] == 'stop_typing' &&
        data['sender_id'] == _currentContact.id) {
      if (mounted) {
        setState(() => _isTyping = false);

        _typingTimer?.cancel();
      }
    }
  }

  Future<void> _loadHistory() async {
    DateTime now = DateTime.now();

    Duration offset = now.timeZoneOffset;
    initialMessage = null; // Сбрасываем данные уведомления при загрузке ключа
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(_notificationLaunchKey);
    try {
      print('[DEBUG] Начало загрузки истории');
      final myPrivKey = await _cryptoService.getPrivateKey();
      final sharedSecret = await _cryptoService.deriveSharedSecret(
        myPrivKey!,
        widget.contact.publicKey!,
      );
      print('[DEBUG] Ключи получены');
      final cached = await _localDbService.getChatHistory(
        widget.contact.id,
        myId,
      );
      print('[DEBUG] Локальная история загружена: ${cached.length} сообщений');

      _chatSharedSecret = sharedSecret;

      // Сюда будем складывать успешно расшифрованные локальные сообщения
      // Используем Map<int, MessageModel>, где ключ — id сообщения, для мгновенного поиска
      Map<int, MessageModel> localMessagesMap = {};

      try {
        for (var msg in cached) {
          final msgId = int.tryParse(msg['id']?.toString() ?? '');
          if (msgId == null) continue;

          try {
            final decrypted = await _cryptoService.decryptMessage(
              msg['content'],
              sharedSecret,
            );

            final deliveredAt = msg['delivered_at'] == null
                ? null
                : DateTime.tryParse(
                    msg['delivered_at'].toString(),
                  )?.add(offset);
            final readAt = msg['read_at'] == null
                ? null
                : DateTime.tryParse(msg['read_at'].toString())?.add(offset);

            MessageStatus status = (msg['sender_id'] == myId)
                ? MessageStatus.sent
                : MessageStatus.delivered;
            if (msg['sender_id'] == myId) {
              if (readAt != null) {
                status = MessageStatus.read;
              } else if (deliveredAt != null) {
                status = MessageStatus.delivered;
              }
            }

            localMessagesMap[msgId] = MessageModel(
              id: msgId,
              text: decrypted,
              isMe: msg['sender_id'] == myId,
              senderId: msg['sender_id'],
              receiverId: msg['receiver_id'],
              createdAt: DateTime.parse(msg['timestamp']).add(offset),
              status: status,
              replyToId: msg['reply_to_id'] == null
                  ? null
                  : int.tryParse(msg['reply_to_id'].toString()),
              replyToText: await _decryptReplyText(
                msg['reply_to_text']?.toString(),
                sharedSecret,
              ),
              editedAt: msg['edited_at'] != null
                  ? DateTime.tryParse(msg['edited_at'].toString())?.add(offset)
                  : null,
              messageType: _parseMessageTypeString(msg['message_type']),
              fileId: msg['file_id']?.toString(),
              encryptedFileKey: msg['encrypted_key']?.toString(),
              fileName: msg['file_name']?.toString(),
              fileSize: msg['file_size'] == null
                  ? null
                  : int.tryParse(msg['file_size'].toString()),
              // ВАЖНО: Если в твоем MessageModel при чтении из БД как-то парсится localFile,
              // обязательно пропиши его инициализацию здесь!
            );
          } catch (e) {
            // Обработка ошибки дешифровки локального сообщения...
            final deliveredAt = msg['delivered_at'] == null
                ? null
                : DateTime.tryParse(
                    msg['delivered_at'].toString(),
                  )?.add(offset);
            final readAt = msg['read_at'] == null
                ? null
                : DateTime.tryParse(msg['read_at'].toString())?.add(offset);

            MessageStatus status = (msg['sender_id'] == myId)
                ? MessageStatus.sent
                : MessageStatus.delivered;
            if (msg['sender_id'] == myId) {
              if (readAt != null) {
                status = MessageStatus.read;
              } else if (deliveredAt != null) {
                status = MessageStatus.delivered;
              }
            }
            localMessagesMap[msgId] = MessageModel(
              id: msgId,
              text: msg['content'],
              isMe: msg['sender_id'] == myId,
              senderId: msg['sender_id'],
              receiverId: msg['receiver_id'],
              createdAt: DateTime.parse(msg['timestamp']).add(offset),
              status: status,
              replyToId: msg['reply_to_id'] == null
                  ? null
                  : int.tryParse(msg['reply_to_id'].toString()),
              replyToText: await _decryptReplyText(
                msg['reply_to_text']?.toString(),
                sharedSecret,
              ),
              editedAt: msg['edited_at'] != null
                  ? DateTime.tryParse(msg['edited_at'].toString())?.add(offset)
                  : null,
              messageType: _parseMessageTypeString(msg['message_type']),
              fileId: msg['file_id']?.toString(),
              encryptedFileKey: msg['encrypted_key']?.toString(),
              fileName: msg['file_name']?.toString(),
              fileSize: msg['file_size'] == null
                  ? null
                  : int.tryParse(msg['file_size'].toString()),
            );
            //print('Ошибка дешифровки сообщения: $e');
          }
        }

        if (localMessagesMap.isNotEmpty) {
          if (!mounted) return;
          setState(() {
            messages = localMessagesMap.values.toList();
            _isKeyLoading = false;
          });
        }
      } catch (e) {
        print(e);
      }

      final history = await apiService.getChatHistory(widget.contact.id);
      print('[DEBUG] Загружена история из API: ${history.length}');

      final alreadyReadIncomingMessageIds = <int>{};
      List<MessageModel> loadedMessages = [];

      for (var msg in history) {
        print(msg);
        final msgId = int.tryParse(msg['id']?.toString() ?? '');
        if (msgId != null &&
            msg['sender_id'] != myId &&
            msg['read_at'] != null) {
          alreadyReadIncomingMessageIds.add(msgId);
        }

        final decrypted = await _cryptoService.decryptMessage(
          msg['content'],
          sharedSecret,
        );

        final deliveredAt = msg['delivered_at'] == null
            ? null
            : DateTime.tryParse(msg['delivered_at'].toString())?.add(offset);
        final readAt = msg['read_at'] == null
            ? null
            : DateTime.tryParse(msg['read_at'].toString())?.add(offset);

        MessageStatus status = (msg['sender_id'] == myId)
            ? MessageStatus.sent
            : MessageStatus.delivered;
        if (msg['sender_id'] == myId) {
          if (readAt != null) {
            status = MessageStatus.read;
          } else if (deliveredAt != null) {
            status = MessageStatus.delivered;
          }
        }

        // КРИТИЧЕСКИЙ ФИКС: Ищем, есть ли это сообщение уже в локальном кэше
        File? existingLocalFile;
        if (msgId != null && localMessagesMap.containsKey(msgId)) {
          existingLocalFile = localMessagesMap[msgId]?.localFile;
        }

        loadedMessages.insert(
          0,
          MessageModel(
            id: msgId,
            text: decrypted,
            isMe: msg['sender_id'] == myId,
            senderId: msg['sender_id'],
            receiverId: msg['receiver_id'],
            createdAt: DateTime.parse(msg['timestamp']).add(offset),
            status: status,
            replyToId: msg['reply_to_id'] == null
                ? null
                : int.tryParse(msg['reply_to_id'].toString()),
            replyToText: await _decryptReplyText(
              msg['reply_to_text']?.toString(),
              sharedSecret,
            ),
            editedAt: msg['edited_at'] != null
                ? DateTime.tryParse(msg['edited_at'].toString())?.add(offset)
                : null,
            messageType: _parseMessageTypeString(msg['message_type']),
            fileId: msg['file_id']?.toString(),
            encryptedFileKey: msg['encrypted_key']?.toString(),
            fileName: msg['file_name']?.toString(),
            fileSize: msg['file_size'] == null
                ? null
                : int.tryParse(msg['file_size'].toString()),
            // СОХРАНЯЕМ ФАЙЛ: Если он уже был скачан, мы не даем ему стать null!
            localFile: existingLocalFile,
          ),
        );
      }

      try {
        print('[DEBUG] Начинаем очищение и сохранение истории в локальную БД');
        await _localDbService.saveMessages(loadedMessages);
        print('[DEBUG] Сообщения сохранени в локальную бд');
      } catch (e) {
        print("[ERROR] Ошибка сохранения истории в локальную базу: $e");
      }

      if (!mounted) return;
      setState(() {
        messages = loadedMessages;
        _isKeyLoading = false;
      });

      // Отправка read_receipt...
      for (final m in loadedMessages) {
        if (m.isMe) continue;
        final id = m.id;
        if (id == null) continue;
        if (alreadyReadIncomingMessageIds.contains(id)) continue;
        if (_sentReadReceipts.contains(id)) continue;
        Provider.of<SocketService>(context, listen: false).sendReadReceipt(id);
        _sentReadReceipts.add(id);
      }
    } catch (e) {
      print("Ошибка загрузки истории: $e");
      if (!mounted) return;
      setState(() => _isKeyLoading = false);
    }
  }

  final Map<String, StreamSubscription<List<int>>> _activeDownloads = {};

  Future<void> _stopFileLoading(MessageModel message) async {
    if (message.fileId == null) return;

    final subscription = _activeDownloads.remove(message.fileId!);
    if (subscription != null) {
      await subscription.cancel();
      debugPrint("Загрузка файла ${message.fileId} отменена пользователем.");

      if (message.localFile != null && message.localFile!.existsSync()) {
        try {
          await message.localFile!.delete();
          debugPrint(
            "Локальный файл успешно удален с диска: ${message.fileId}",
          );
        } catch (e) {
          debugPrint("Ошибка при физическом удалении файла с диска: $e");
        }
      }
    }

    if (mounted) {
      setState(() {
        _messageProgressNotifiers[message.fileId!]?.value = null;
      });
    }
  }

  int _findMessageIndex(MessageModel message) {
    return messages.indexWhere((m) {
      if (message.id != null && m.id == message.id) return true;
      if (message.tempId != null && m.tempId == message.tempId) return true;
      if (message.fileId != null && m.fileId == message.fileId) return true;
      return false;
    });
  }

  Future<void> _fetchFileSizeIfNeeded(MessageModel message) async {
    if (message.fileId == null) return;

    try {
      debugPrint("Фоновый запрос размера для файла: ${message.fileId}");
      final (remoteSize, filename) = await apiService.getRemoteFileSizeAndName(
        message.fileId!,
      );

      if (remoteSize != null && remoteSize > 0 && mounted) {
        if (message.fileSize != null && message.fileSize! > 0) {
          if (message.fileSize != remoteSize) {
            debugPrint(
              "Размер файла на сервере (${remoteSize} байт) отличается от локального (${message.fileSize} байт). Локальный файл признан недействительным.",
            );
            if (message.localFile != null) {
              message.localFile!.delete().catchError((e) {
                debugPrint(
                  "Ошибка удаления недействительного локального файла: $e",
                );
              });
            }
          }
        }
        print(
          "Получен размер файла из сети: $remoteSize байт. Обновляем модель сообщения.",
        );
        setState(() {
          final index = _findMessageIndex(message);
          if (index != -1) {
            messages[index] = messages[index].copyWith(
              fileSize: remoteSize,
              fileName: filename,
            );
          }
        });
        // Опционально: сохраняем в локальную БД, чтобы не дергать сеть в следующий раз
        // await LocalDbService.updateMessageFileSize(message.id, remoteSize);
      }
    } catch (e) {
      debugPrint("Ошибка фонового получения размера файла: $e");
    }
  }

  Future<void> _ensureFileDecrypted(
    MessageModel message, {
    bool dontLoad = false,
  }) async {
    MessageModel msg = message;
    if (_activeDownloads.containsKey(msg.fileId)) return;
    if (msg.fileId == null || _chatSharedSecret == null) return;

    final directory = await getApplicationDocumentsDirectory();
    final decFile = File('${directory.path}/dec_${msg.fileId}');
    final sharedPrefs = await SharedPreferences.getInstance();
    final String sizeKey = 'valid_dec_size_${msg.fileId}';

    // --- НАЧАЛО ИСПРАВЛЕННОГО БЛОКА ВАЛИДАЦИИ РАЗМЕРА ---
    if (await decFile.exists()) {
      final localLength = await decFile.length();
      // Проверяем, есть ли у нас сохраненный эталонный размер именно ДЛЯ РАСШИФРОВАННОГО файла
      final int? expectedDecryptedSize = sharedPrefs.getInt(sizeKey);

      if (expectedDecryptedSize != null) {
        if (localLength != expectedDecryptedSize) {
          debugPrint(
            "Размер локального файла ($localLength байт) не совпадает с сохраненным эталоном ($expectedDecryptedSize байт). Файл поврежден. Удаляем.",
          );
          await decFile.delete().catchError(
            (e) => debugPrint("Ошибка удаления: $e"),
          );
          await sharedPrefs.remove(sizeKey);
        } else {
          debugPrint(
            "Локальный файл успешно прошел валидацию по сохраненному размеру.",
          );
          if (mounted) {
            setState(() {
              final index = _findMessageIndex(msg);
              if (index != -1) {
                messages[index] = messages[index].copyWith(localFile: decFile);
              }
            });
          }
          return;
        }
      } else {
        await decFile.delete().catchError(
          (e) => debugPrint("Удаление пустого файла: $e"),
        );
      }
    }
    // --- КОНЕЦ БЛОКА ВАЛИДАЦИИ ---

    if (dontLoad) return;

    debugPrint("=== ОТЛАДКА СКАЧИВАНИЯ ===");
    debugPrint("ID файла: ${msg.fileId}");
    debugPrint("Ключ файла (Base64): ${msg.encryptedFileKey}");
    debugPrint("Общий ключ чата готов?: ${_chatSharedSecret != null}");

    // Получаем или инициализируем Notifier прогресса для этого файла
    final bool createdNewNotifier =
        _messageProgressNotifiers[msg.fileId!] == null;
    _messageProgressNotifiers[msg.fileId!] ??= ValueNotifier<double?>(0.0);
    if (createdNewNotifier && mounted) {
      setState(() {});
    }
    _messageProgressNotifiers[msg.fileId!]!.value = 0.0;

    // Если размера файла в модели всё ещё нет, запрашиваем его у сервера через HEAD-запрос
    if (msg.fileSize == null || msg.fileSize == 0) {
      debugPrint("Размер файла в модели пуст. Делаем HEAD запрос...");
      final (remoteSize, filename) = await apiService.getRemoteFileSizeAndName(
        msg.fileId!,
      );
      if (remoteSize != null && remoteSize > 0) {
        debugPrint("Успешно получен размер файла из сети: $remoteSize байт");

        if (mounted) {
          setState(() {
            final index = _findMessageIndex(msg);
            if (index != -1) {
              // Берем элемент ИЗ МАССИВА сообщений (там данные актуальнее)
              messages[index] = messages[index].copyWith(
                fileSize: remoteSize,
                fileName: filename,
              );
              // Обновляем локальную переменную msg для дальнейшей корректной работы
              msg = messages[index];
            } else {
              msg = msg.copyWith(fileSize: remoteSize, fileName: filename);
            }
          });
        }
      }
    }

    final response = await apiService.downloadFileAsStream(msg.fileId!);
    final networkStream = response.$1;
    final fileName = response.$2;

    Stream<List<int>> decryptedStream = _cryptoService.decryptFileStream(
      networkStream,
      _chatSharedSecret!,
      msg.encryptedFileKey!,
      totalBytes: msg.fileSize,
      onProgress: (received, total) {
        if (total != -1) {
          double progress = received / total;
          if (progress > 1.0) progress = 1.0;
          _messageProgressNotifiers[msg.fileId!]?.value = progress;
        }
      },
    );

    final iosink = decFile.openWrite();
    final Completer<void> downloadCompleter = Completer<void>();

    final subscription = decryptedStream.listen(
      (chunk) {
        iosink.add(chunk);
      },
      onError: (error) async {
        debugPrint("Ошибка внутри криптострима: $error");
        await iosink.close();
        if (!downloadCompleter.isCompleted) downloadCompleter.complete();

        await decFile.delete();
      },
      onDone: () async {
        await iosink.close();

        // --- СОХРАНЕНИЕ ТЕКУЩЕГО РАЗМЕРА ПОСЛЕ УСПЕШНОЙ ЗАГРУЗКИ ---
        final finalLocalLength = await decFile.length();
        if (finalLocalLength > 0) {
          // Запоминаем, сколько байт весит ЧИСТЫЙ расшифрованный файл
          await sharedPrefs.setInt(sizeKey, finalLocalLength);
          debugPrint(
            "Файл успешно скачан и расшифрован. Сохранен валидный размер: $finalLocalLength байт.",
          );
        }

        if (mounted) {
          setState(() {
            final updatedMsg = msg.copyWith(
              localFile: decFile,
              fileName: fileName,
            );
            final index = _findMessageIndex(msg);
            if (index != -1) {
              messages[index] = updatedMsg;
            }
          });
        }

        try {
          _activeDownloads.remove(msg.fileId)?.cancel();
          if (mounted) {
            setState(() {
              _messageProgressNotifiers[msg.fileId!]?.value = null;
            });
          }
        } catch (e) {
          debugPrint("Ошибка при очистке ресурсов загрузки: $e");
        }
        if (!downloadCompleter.isCompleted) downloadCompleter.complete();
      },
      cancelOnError: true,
    );

    _activeDownloads[msg.fileId!] = subscription;

    try {
      await downloadCompleter.future;
    } catch (e) {
      debugPrint("Загрузка прервана или завершилась с ошибкой: $e");
      if (await decFile.exists()) {
        await decFile.delete().catchError(
          (e) => debugPrint("Ошибка очистки файла: $e"),
        );
      }
      await sharedPrefs.remove(sizeKey); // Чистим ключ при ошибке
    }
  }

  Future<void> _updateScrollButtonVisibility() async {
    if (!mounted) return;
    final shouldShow =
        _scrollController.hasClients && _scrollController.offset > 100;
    if (shouldShow != _showScrollToEnd) {
      setState(() {
        _showScrollToEnd = shouldShow;
      });
    }
  }

  Future<void> _scrollToBottom() async {
    if (!_scrollController.hasClients) return;
    await _scrollController.animateTo(
      0,
      duration: const Duration(milliseconds: 300),
      curve: Curves.easeInOut,
    );
  }

  Future<void> _scrollToMessage(int? messageId) async {
    if (messageId == null) return;
    final itemKey = _messageKeys[messageId];
    if (itemKey?.currentContext == null) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Сообщение не найдено для перехода.'),
          behavior: SnackBarBehavior.floating,
          margin: EdgeInsets.only(bottom: 80.0 + 10.0, left: 10.0, right: 10.0),
        ),
      );
      return;
    }
    await Scrollable.ensureVisible(
      itemKey!.currentContext!,
      duration: const Duration(milliseconds: 300),
      alignment: 0.1,
      curve: Curves.easeInOut,
    );
  }

  void _openFullScreenMedia(MessageModel msg) async {
    print('Открытие медиа');
    if (msg.fileId == null) return;

    // Получаем доступ к папке документов приложения
    final directory = await getApplicationDocumentsDirectory();

    final decPath = '${directory.path}/dec_${msg.fileId}';

    final decFile = File(decPath);

    // 1. ПРОВЕРКА КЭША: если расшифрованный файл уже есть, открываем мгновенно
    if (await decFile.exists() && await decFile.length() > 0) {
      _navigateToViewer(decPath, msg);
      return;
    }

    // Проверяем наличие ключей перед началом долгого процесса
    if (_chatSharedSecret == null || msg.encryptedFileKey == null) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text("Ошибка: Ключи шифрования недоступны")),
      );
      return;
    }

    // Показываем индикатор загрузки (диалог или крутилку)
    _showLoadingDialog();

    try {
      _ensureFileDecrypted(msg);
      final decFile = File(decPath);

      if (await decFile.exists() && await decFile.length() > 0) {
        _navigateToViewer(decPath, msg);
        return;
      }
    } catch (e) {
      debugPrint('Ошибка при обработке и дешифрации файла: $e');
    }

    // Если что-то пошло не так (ошибка сети, ошибка дешифрации)
    if (mounted) {
      // Закрываем _showLoadingDialog(), если он все еще открыт
      WidgetsBinding.instance.addPostFrameCallback((_) {
        Navigator.of(context, rootNavigator: true).pop();
      });

      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text("Не удалось загрузить или расшифровать файл"),
        ),
      );
    }
  }

  void _showLoadingDialog() {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => const Center(
        child: Card(
          child: Padding(
            padding: EdgeInsets.all(20.0),
            child: CircularProgressIndicator(),
          ),
        ),
      ),
    );
  }

  void _navigateToViewer(String path, MessageModel msg) {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      Navigator.push(
        context,
        MaterialPageRoute(
          builder: (_) => MediaViewer(
            items: [
              MediaItem(
                path: path,
                isVideo:
                    msg.messageType == MessageType.video ||
                    msg.messageType == 'video',
              ),
            ],
          ),
        ),
      );
    });
  }

  Future<void> _saveMediaToGallery(MessageModel msg) async {
    final originalFile = msg.localFile;
    if (originalFile == null || !originalFile.existsSync()) return;

    File? tempFile;
    try {
      // 1. Проверка доступности файла
      // Пытаемся открыть файл на чтение. Если файл занят, это выбросит ошибку,
      // и мы подождем перед тем, как копировать.
      bool isFileReady = false;
      int attempts = 0;
      while (!isFileReady && attempts < 5) {
        try {
          final raf = await originalFile.open(mode: FileMode.read);
          await raf.close();
          isFileReady = true;
        } catch (e) {
          await Future.delayed(const Duration(milliseconds: 500));
          attempts++;
        }
      }

      // 2. Копирование с правильным именем
      String ext = p.extension(msg.fileName ?? '');
      if (ext.isEmpty)
        ext = (msg.messageType == MessageType.video) ? '.mp4' : '.jpg';
      final String tempName =
          'save_${DateTime.now().millisecondsSinceEpoch}$ext';
      final Directory tempDir = await getTemporaryDirectory();
      tempFile = await originalFile.copy(p.join(tempDir.path, tempName));

      // 3. Сохранение
      if (msg.messageType == MessageType.video ||
          msg.messageType == MessageType.videoNote) {
        await Gal.putVideo(tempFile.path);
      } else {
        await Gal.putImage(tempFile.path);
      }

      if (!mounted) return;
      ScaffoldMessenger.of(
        context,
      ).showSnackBar(const SnackBar(content: Text('Сохранено в галерею')));
    } catch (e) {
      debugPrint("Ошибка: $e");
      // Если ошибка "битый файл", возможно, Gal еще держит временный файл
    } finally {
      // ВАЖНО: Удаляем только если файл существует
      if (tempFile != null && await tempFile.exists()) {
        try {
          await tempFile.delete();
        } catch (_) {}
      }
    }
  }

  Future<Uint8List?> _downloadAndDecryptImage(
    String fileId,
    String encryptedFileKey,
    SecretKey sharedSecret, {
    void Function(int received, int total)? onProgress,
  }) async {
    try {
      print('DEBUG downloadMedia(fileId=$fileId)');
      final bytes = await apiService.downloadMedia(
        fileId,
        onProgress: onProgress,
      );
      if (bytes == null) {
        print('DEBUG downloadMedia returned null for fileId=$fileId');
        return null;
      }
      print(
        'DEBUG downloadMedia bytes length=${bytes.length} for fileId=$fileId',
      );
      final result = await _cryptoService.decryptMedia(
        bytes,
        encryptedFileKey,
        sharedSecret,
      );
      print(
        'DEBUG decryptImage result length=${result?.length ?? 'null'} for fileId=$fileId',
      );
      return result;
    } catch (e) {
      print('Ошибка загрузки и дешифровки медиа: $e');
      return null;
    }
  }

  Future<String?> _decryptReplyText(
    String? encryptedReplyText,
    SecretKey sharedSecret,
  ) async {
    if (encryptedReplyText == null) return null;
    try {
      return await _cryptoService.decryptMessage(
        encryptedReplyText,
        sharedSecret,
      );
    } catch (_) {
      return encryptedReplyText;
    }
  }

  Widget _buildPreviewIcon() {
    switch (_pendingMessageType) {
      case MessageType.image:
        return _previewBytes != null
            ? Image.memory(_previewBytes!, fit: BoxFit.cover)
            : Container(
                color: Colors.grey.withOpacity(0.2),
                child: const Icon(Icons.image, color: Colors.grey),
              );

      case MessageType.video:
        return Container(
          color: Colors.black,
          child: const Icon(Icons.videocam, color: Colors.white),
        );

      case MessageType.file:
      default:
        return Container(
          color: Colors.blue.withOpacity(0.1),
          child: const Icon(Icons.insert_drive_file, color: Colors.blue),
        );
    }
  }
}

class TypingIndicator extends StatefulWidget {
  const TypingIndicator({super.key});

  @override
  State<TypingIndicator> createState() => _TypingIndicatorState();
}

class _TypingIndicatorState extends State<TypingIndicator>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 600),
    )..repeat(reverse: true); // Анимация идет туда-сюда
  }

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

  Widget _buildDot(int index) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        // Рассчитываем смещение: только отрицательные значения (вверх)
        double delay = index * 0.5; // Увеличили задержку для плавности
        double shift = sin((_controller.value * 2 * pi) + delay);

        // Используем clamp или abs, чтобы точка не уходила ниже базовой линии
        double yOffset = (shift < 0 ? shift : 0) * 4;

        return SizedBox(
          width: 4, // Фиксированная зона для одной точки
          height: 5, // Фиксированная высота зоны анимации
          child: Align(
            alignment: Alignment.bottomCenter, // Точка всегда прижата к низу
            child: Container(
              width: 2,
              height: 2,
              decoration: const BoxDecoration(
                color: Colors.greenAccent,
                shape: BoxShape.circle,
              ),
              transform: Matrix4.translationValues(0, yOffset, 0),
            ),
          ),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 12,
      child: Row(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: List.generate(3, (index) => _buildDot(index)),
      ),
    );
  }
}

typedef _OnWidgetSizeChange = void Function(Size size);

class _MeasureSize extends SingleChildRenderObjectWidget {
  final _OnWidgetSizeChange onChange;

  const _MeasureSize({super.key, required super.child, required this.onChange});

  @override
  RenderObject createRenderObject(BuildContext context) {
    return _MeasureSizeRenderObject(onChange: onChange);
  }

  @override
  void updateRenderObject(
    BuildContext context,
    _MeasureSizeRenderObject renderObject,
  ) {
    renderObject.onChange = onChange;
  }
}

class _MeasureSizeRenderObject extends RenderProxyBox {
  _OnWidgetSizeChange onChange;
  Size? _oldSize;

  _MeasureSizeRenderObject({required this.onChange, RenderBox? child})
    : super(child);

  @override
  void performLayout() {
    super.performLayout();

    final newSize = size;
    if (_oldSize != newSize) {
      _oldSize = newSize;
      WidgetsBinding.instance.addPostFrameCallback((_) {
        onChange(newSize);
      });
    }
  }
}
