import 'dart:async';
import 'dart:math' as math;
import 'dart:ui' show lerpDouble;

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart' show listEquals;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:komet/main.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../contacts/edit_contact_sheet.dart';
import '../../../backend/modules/complaints.dart';
import '../../../backend/modules/contacts.dart';
import '../../../backend/modules/messages.dart' show ContactCache;
import '../../../core/cache/info_cache.dart';
import '../../../core/calls/call_controller.dart';
import '../../../core/config/app_show_extra_info.dart';
import '../../../core/config/app_stories.dart';
import '../../../core/storage/app_database.dart';
import '../../../core/utils/format.dart';
import '../../../core/utils/logger.dart';
import '../../../core/utils/haptics.dart';
import '../../../l10n/app_localizations.dart';
import '../../../models/chat_info.dart';
import '../../../models/contact_info.dart';
import '../../../models/story.dart';
import '../../widgets/animated_text_swap.dart';
import '../../widgets/avatar_history_screen.dart';
import '../../widgets/chat_info/shared_content_tabs.dart';
import '../../widgets/connection_status.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/formatted_message_text.dart';
import '../../widgets/reload_on_reconnect.dart';
import '../../widgets/glossy_pill.dart';
import '../../widgets/komet_avatar.dart';
import '../../widgets/profile_header_scroll.dart';
import '../../widgets/profile_hero.dart';
import '../../widgets/swipe_route.dart';
import '../../../backend/modules/chats.dart';
import '../calls/call_screen.dart';
import '../contacts/open_contact_profile.dart';
import '../stories/story_owner_info.dart';
import '../stories/story_peanut.dart';
import '../stories/story_ring.dart';
import '../stories/story_viewer_screen.dart';
import 'chat_screen.dart';
import 'group_invite_sheets.dart';
import 'profile_action_sheets.dart';

class _MemberInfo {
  final int id;
  final String? name;
  final String? avatarUrl;
  final bool isAdmin;
  final bool isOwner;
  final bool isMe;
  final String? alias;
  final int? seenTime;
  final int presenceStatus;
  final bool blocked;
  final bool isContact;

  const _MemberInfo({
    required this.id,
    this.name,
    this.avatarUrl,
    required this.isAdmin,
    required this.isOwner,
    required this.isMe,
    this.alias,
    this.seenTime,
    required this.presenceStatus,
    this.blocked = false,
    this.isContact = false,
  });

  bool get isOnline => presenceStatus == 1;
}

enum ChatInfoTab { media }

class ChatInfoScreen extends StatefulWidget {
  final int chatId;
  final String name;
  final String imageUrl;
  final String chatType;

  final int? dialogPeerId;
  final ChatInfoTab? initialTab;
  final Object? heroTag;
  final bool openedFromChat;

  final void Function(String messageId, int time)? onJumpToMessage;

  const ChatInfoScreen({
    super.key,
    required this.chatId,
    required this.name,
    required this.imageUrl,
    required this.chatType,
    this.dialogPeerId,
    this.initialTab,
    this.heroTag,
    this.openedFromChat = false,
    this.onJumpToMessage,
  });

  @override
  State<ChatInfoScreen> createState() => _ChatInfoScreenState();
}

class _ChatInfoScreenState extends State<ChatInfoScreen>
    with ReloadOnReconnect {
  final _tabScrollController = ScrollController();
  ScrollController? _bodyScrollController;

  int _myId = 0;
  bool _isLoading = true;
  bool _extraContactExpanded = false;
  ChatInfo? _chatInfo;
  String _selectedTab = '';
  bool _descExpanded = false;
  bool _showRealName = false;

  int? _otherId;
  ContactInfo? _contactData;
  CachedContact? _localContact;
  int? _seenTime;
  bool _isOnline = false;
  int _presenceStatus = 0;
  bool _isBot = false;

  final List<_MemberInfo> _members = [];
  final List<_MemberInfo> _owners = [];
  final List<_MemberInfo> _admins = [];
  final List<_MemberInfo> _contactMembers = [];
  final List<_MemberInfo> _otherMembers = [];
  final Set<int> _seenMemberIds = {};
  Set<int> _contactIds = {};
  int _memberMarker = 0;
  bool _membersLoading = false;
  bool _membersEnd = false;

  int _mediaChatId = 0;
  String? _anchorMsgId;

  int _dontDisturbUntil = 0;
  int _lastEventTime = 0;
  bool _blocked = false;
  bool _muteBusy = false;
  bool _addContactBusy = false;

  StoryPreview? _storyPreview;
  List<Story> _unreadStories = const [];
  final GlobalKey _avatarKey = GlobalKey();

  final PageController _avatarPageController = PageController();
  List<String> _avatarPages = const [];
  int _avatarIndex = 0;
  int _avatarTotal = 0;
  bool _avatarHover = false;
  bool _avatarHistoryBusy = false;
  bool _avatarHistoryLoaded = false;

  double _headerDelta = 0;
  bool _expandArmed = false;
  bool _headerEverExpanded = false;

  @override
  void initState() {
    super.initState();
    storiesModule.storiesChanged.addListener(_onStoriesChanged);
    _load();
  }

  void _onStoriesChanged() {
    if (!mounted) return;
    setState(_refreshUnreadStories);
  }

  @override
  void dispose() {
    storiesModule.storiesChanged.removeListener(_onStoriesChanged);
    _tabScrollController.dispose();
    _bodyScrollController?.dispose();
    _avatarPageController.dispose();
    super.dispose();
  }

  AppLocalizations get l10n => AppLocalizations.of(context)!;

  List<String> get _tabs {
    final showInfo = AppShowExtraInfo.current.value;
    switch (widget.chatType) {
      case 'DIALOG':
        if (_isBot) {
          return [
            if (showInfo) 'Info',
            l10n.chatInfoTabMedia,
            l10n.chatInfoTabFiles,
            l10n.chatInfoTabVoice,
            l10n.chatInfoTabLinks,
          ];
        }
        return [
          l10n.chatInfoTabGeneralChats,
          l10n.chatInfoTabMedia,
          if (showInfo) 'Info',
          l10n.chatInfoTabFiles,
          l10n.chatInfoTabVoice,
          l10n.chatInfoTabLinks,
        ];
      case 'CHAT':
        return [
          l10n.chatInfoTabMembers,
          if (showInfo) 'Info',
          l10n.chatInfoTabMedia,
          l10n.chatInfoTabFiles,
          l10n.chatInfoTabVoice,
          l10n.chatInfoTabLinks,
        ];
      case 'CHANNEL':
        return [
          if (showInfo) 'Info',
          l10n.chatInfoTabMedia,
          l10n.chatInfoTabFiles,
          l10n.chatInfoTabVoice,
          l10n.chatInfoTabLinks,
        ];
      default:
        return [if (showInfo) 'Info'];
    }
  }

  @override
  void reloadAfterReconnect() => _load();

  Future<void> _load() async {
    final profile = await AppDatabase.loadActiveProfile();
    _myId = profile?.id ?? 0;

    final info = await ChatInfoFetch.get(widget.chatId);
    if (!mounted) return;
    _chatInfo = info;

    _mediaChatId = (info?.raw['id'] as int?) ?? widget.chatId;

    final cached = await chats.getChat(_myId, _mediaChatId);
    if (!mounted) return;
    if (cached.isNotEmpty) {
      _dontDisturbUntil = cached.first.dontDisturbUntil;
      _lastEventTime = cached.first.lastEventTime;
    }
    final serverEventTime = (info?.raw['lastEventTime'] as int?) ?? 0;
    if (serverEventTime > _lastEventTime) _lastEventTime = serverEventTime;

    final lastMessage = info?.raw['lastMessage'];
    if (lastMessage is Map) {
      _anchorMsgId = lastMessage['id']?.toString();
    }
    if (_anchorMsgId == null && info != null) {
      try {
        final recent = await messagesModule.fetchHistory(
          _myId,
          _mediaChatId,
          count: 1,
        );
        if (recent.isNotEmpty) _anchorMsgId = recent.first.id;
      } catch (_) {}
      if (!mounted) return;
    }

    if (widget.chatType == 'DIALOG') {
      _otherId = widget.dialogPeerId;
      if (_otherId == null && info != null) {
        for (final id in info.participantIds) {
          if (id != _myId) {
            _otherId = id;
            break;
          }
        }
      }

      if (_otherId != null) {
        _localContact = await ContactsModule.getContact(_myId, _otherId!);
        final contact = await ContactInfoFetch.get(_otherId!);
        if (contact != null) {
          _contactData = contact;
          _isBot = _contactData!.options.contains('BOT');
        }

        final presence = await PresenceFetch.get(_otherId!);
        if (presence != null) {
          _seenTime = presence['seen'] as int?;
          final st = (presence['status'] as int?) ?? 0;
          _presenceStatus = st;
          _isOnline = st == 1;
        }

        if (!_isBot && _otherId != _myId) _loadBlockedState(_otherId!);
        if (!_isBot) unawaited(_loadStories(_otherId!));
        unawaited(_loadAvatarHistory(_otherId!));
      }
    } else if (info == null) {
      setState(() => _isLoading = false);
      return;
    } else if (widget.chatType == 'CHAT') {
      _contactIds = (await AppDatabase.loadContactIds(_myId)).toSet();
      await _loadLeaders();
      await _fetchMembersPage(initial: true);
    }

    if (mounted) {
      setState(() {
        _isLoading = false;
        if (_selectedTab.isEmpty && _tabs.isNotEmpty) {
          _selectedTab = _initialTabLabel() ?? _tabs.first;
        }
      });
    }
  }

  Future<void> _loadBlockedState(int peerId) async {
    final blocked = await ContactsModule.isBlocked(api, peerId);
    if (!mounted || blocked == _blocked) return;
    setState(() => _blocked = blocked);
  }

  String? _initialTabLabel() {
    if (widget.initialTab != ChatInfoTab.media) return null;
    final media = AppLocalizations.of(context)!.chatInfoTabMedia;
    return _tabs.contains(media) ? media : null;
  }

  _MemberInfo _memberFrom(ChatMemberEntry e) => _MemberInfo(
    id: e.id,
    name: e.name,
    avatarUrl: e.avatarUrl,
    isAdmin: _chatInfo?.isAdmin(e.id) ?? false,
    isOwner: _chatInfo?.isOwner(e.id) ?? false,
    isMe: e.id == _myId,
    alias: _chatInfo?.adminAlias(e.id),
    seenTime: e.seenTime,
    presenceStatus: e.presenceStatus,
    blocked: e.blocked,
    isContact: _contactIds.contains(e.id),
  );

  Future<void> _loadLeaders() async {
    final info = _chatInfo;
    if (info == null) return;

    final owner = info.owner;
    final leaderIds = <int>[
      if (owner != null && owner != 0) owner,
      for (final a in info.adminIds)
        if (a != owner) a,
    ];
    if (leaderIds.isEmpty) return;

    final contacts = await ContactInfoFetch.getMany(leaderIds);
    final presence = await PresenceFetch.getMany(leaderIds);
    if (!mounted) return;

    for (final id in leaderIds) {
      if (!_seenMemberIds.add(id)) continue;
      final c = contacts[id];
      final pres = presence[id];
      _addMember(
        _MemberInfo(
          id: id,
          name: c?.displayName ?? ContactCache.get(id),
          avatarUrl: c?.avatarUrl ?? ContactCache.getAvatar(id),
          isAdmin: info.isAdmin(id),
          isOwner: info.isOwner(id),
          isMe: id == _myId,
          alias: info.adminAlias(id),
          seenTime: pres?['seen'] as int?,
          presenceStatus: (pres?['status'] as int?) ?? 0,
          blocked: c?.isDeleted ?? false,
          isContact: _contactIds.contains(id),
        ),
      );
    }
    _rebuildMembers();
  }

  int _memberRank(_MemberInfo m) {
    if (m.isOwner) return 0;
    if (m.isAdmin) return 1;
    if (m.isContact) return 2;
    return 3;
  }

  void _addMember(_MemberInfo m) {
    switch (_memberRank(m)) {
      case 0:
        _owners.add(m);
      case 1:
        _admins.add(m);
      case 2:
        _contactMembers.add(m);
      default:
        _otherMembers.add(m);
    }
  }

  void _rebuildMembers() {
    _members
      ..clear()
      ..addAll(_owners)
      ..addAll(_admins)
      ..addAll(_contactMembers)
      ..addAll(_otherMembers);
  }

  Future<void> _fetchMembersPage({bool initial = false}) async {
    if (_membersLoading || _membersEnd) return;
    _membersLoading = true;
    if (!initial && mounted) setState(() {});

    final page = await chats.getChatMembers(
      api,
      widget.chatId,
      marker: _memberMarker,
    );
    _membersLoading = false;
    if (!mounted) return;

    if (page == null) {
      if (!initial) setState(() {});
      return;
    }

    var added = 0;
    final fresh = <int>[];
    for (final e in page.members) {
      if (_seenMemberIds.add(e.id)) {
        _addMember(_memberFrom(e));
        fresh.add(e.id);
        added++;
      }
    }
    if (added > 0) _rebuildMembers();
    if (fresh.isNotEmpty && AppStories.current.value) {
      unawaited(storiesModule.loadOwnersPreviews(fresh));
    }

    final total = _chatInfo?.participantsCount;
    if (page.members.isEmpty ||
        added == 0 ||
        page.marker == _memberMarker ||
        (total != null && _members.length >= total)) {
      _membersEnd = true;
    }
    _memberMarker = page.marker;

    if (!initial) setState(() {});
  }

  void _onBodyScroll() {
    if (!mounted || widget.chatType != 'CHAT') return;
    if (_membersLoading || _membersEnd) return;
    if (_selectedTab != AppLocalizations.of(context)!.chatInfoTabMembers)
      return;
    final controller = _bodyScrollController;
    if (controller == null || !controller.hasClients) return;
    final pos = controller.position;
    if (pos.pixels >= pos.maxScrollExtent - 400) {
      _fetchMembersPage();
    }
  }

  String? get _inviteLink {
    final link = _chatInfo?.link;
    return (link != null && link.isNotEmpty) ? link : null;
  }

  Future<void> _openAddMembers() async {
    final exclude = {_myId, ..._members.map((m) => m.id)};
    final added = await showAddMembersSheet(
      context,
      chatId: widget.chatId,
      excludeIds: exclude,
    );
    if (added == true && mounted) await _refreshMembers();
  }

  void _openInviteLink(String link) {
    showInviteLinkSheet(
      context,
      link: link,
      title: widget.name,
      avatarUrl: widget.imageUrl,
    );
  }

  Future<void> _refreshMembers() async {
    _contactMembers.clear();
    _otherMembers.clear();
    _seenMemberIds
      ..clear()
      ..addAll(_owners.map((m) => m.id))
      ..addAll(_admins.map((m) => m.id));
    _memberMarker = 0;
    _membersEnd = false;
    _membersLoading = false;
    _rebuildMembers();
    if (mounted) setState(() {});
    await _fetchMembersPage(initial: true);
    if (mounted) setState(() {});
  }

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

    return Scaffold(
      backgroundColor: cs.surface,
      floatingActionButtonLocation: FloatingActionButtonLocation.startFloat,
      floatingActionButton: const ConnectionSpinner(),
      body: _buildScrollBody(cs),
    );
  }

  static const double _headerAvatarSize = 96;
  static const double _headerCollapsedBody = 232;
  static const double _headerVignette = 64;

  bool get _headerHasPhoto => widget.imageUrl.isNotEmpty && !_peerDeleted;

  Widget _buildScrollBody(ColorScheme cs) {
    return LayoutBuilder(
      builder: (context, viewport) {
        final media = MediaQuery.of(context);
        final topPad = media.padding.top;
        final collapsedH = topPad + _headerCollapsedBody;
        final expandedH = _headerHasPhoto
            ? math.max(
                collapsedH,
                math.min(media.size.width, viewport.maxHeight * 0.62),
              )
            : collapsedH;
        final delta = expandedH - collapsedH;
        _syncHeaderDelta(delta);
        final controller = _bodyScrollController ??=
            (ScrollController(initialScrollOffset: delta)
              ..addListener(_onBodyScroll));

        return NotificationListener<ScrollNotification>(
          onNotification: (n) => _onHeaderScrollNotification(n, delta),
          child: CustomScrollView(
            key: ValueKey(delta),
            controller: controller,
            physics: HeaderPullScrollPhysics(
              delta: delta,
              isArmed: () => _expandArmed,
              parent: const BouncingScrollPhysics(),
            ),
            slivers: [
              SliverPersistentHeader(
                delegate: MorphHeaderDelegate(
                  collapsedExtent: collapsedH,
                  expandedExtent: expandedH,
                  headerBuilder: (ctx, t) =>
                      _buildMorphHeader(ctx, cs, _headerHasPhoto ? t : 0.0),
                ),
              ),
              SliverToBoxAdapter(
                child: ConstrainedBox(
                  constraints: BoxConstraints(
                    minHeight: math.max(0, viewport.maxHeight - collapsedH),
                  ),
                  child: _buildBody(cs),
                ),
              ),
            ],
          ),
        );
      },
    );
  }

  void _syncHeaderDelta(double delta) {
    if (_headerDelta == delta) return;
    final prev = _headerDelta;
    _headerDelta = delta;
    if (_bodyScrollController == null) return;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final c = _bodyScrollController;
      if (!mounted || c == null || !c.hasClients) return;
      final target = (c.offset + (delta - prev)).clamp(
        0.0,
        c.position.maxScrollExtent,
      );
      c.jumpTo(target);
    });
  }

  bool _onHeaderScrollNotification(ScrollNotification n, double delta) {
    if (n.depth != 0) return false;
    if (n is ScrollStartNotification) {
      if (n.dragDetails != null) {
        _expandArmed = delta > 0 && n.metrics.pixels <= delta + 8;
      }
    } else if (n is ScrollEndNotification) {
      _snapHeader(delta);
    }
    return false;
  }

  void _snapHeader(double delta) {
    final c = _bodyScrollController;
    if (c == null || !c.hasClients || delta <= 0) return;
    final offset = c.offset;
    if (offset <= 0 || offset >= delta) return;
    final target = (offset < delta / 2 ? 0.0 : delta).clamp(
      0.0,
      c.position.maxScrollExtent,
    );
    if ((target - offset).abs() < 1) return;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted || !c.hasClients) return;
      c.animateTo(
        target,
        duration: const Duration(milliseconds: 260),
        curve: Curves.easeOutCubic,
      );
    });
  }

  Widget _buildMorphHeader(BuildContext context, ColorScheme cs, double t) {
    final topPad = MediaQuery.paddingOf(context).top;
    if (t > 0) {
      _headerEverExpanded = true;
      final peerId = _otherId;
      if (!_avatarHistoryLoaded && peerId != null) {
        unawaited(_loadAvatarHistory(peerId));
      }
    }
    final iconColor = Color.lerp(cs.onSurface, Colors.white, t)!;
    final nameColor = Color.lerp(cs.onSurface, Colors.white, t)!;
    final subColor = Color.lerp(
      cs.onSurfaceVariant,
      Colors.white.withValues(alpha: 0.85),
      t,
    )!;
    final ringOpacity = (1 - t * 3).clamp(0.0, 1.0);
    final chipOpacity = ((t - 0.3) / 0.5).clamp(0.0, 1.0);
    final unread = _unreadStories;
    final totalPhotos = math.max(_avatarTotal, _avatarPages.length);

    return ClipRect(
      child: LayoutBuilder(
        builder: (context, constraints) {
          final w = constraints.maxWidth;
          final h = constraints.maxHeight;
          const size = _headerAvatarSize;
          final avatarRect = Rect.lerp(
            Rect.fromLTWH((w - size) / 2, topPad + 52, size, size),
            Rect.fromLTWH(0, 0, w, h),
            t,
          )!;
          final radius = lerpDouble(size / 2, 0, t)!;

          return Stack(
            clipBehavior: Clip.hardEdge,
            children: [
              Positioned.fromRect(
                rect: avatarRect,
                child: _headerAvatar(cs, radius, t),
              ),
              if (_headerHasPhoto) ...[
                Positioned(
                  left: 0,
                  right: 0,
                  top: 0,
                  height: topPad + 72,
                  child: IgnorePointer(
                    child: Opacity(
                      opacity: t,
                      child: const DecoratedBox(
                        decoration: BoxDecoration(
                          gradient: LinearGradient(
                            begin: Alignment.topCenter,
                            end: Alignment.bottomCenter,
                            colors: [Colors.black45, Colors.transparent],
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
                Positioned(
                  left: 0,
                  right: 0,
                  bottom: 0,
                  height: 170,
                  child: IgnorePointer(
                    child: Opacity(
                      opacity: t,
                      child: const DecoratedBox(
                        decoration: BoxDecoration(
                          gradient: LinearGradient(
                            begin: Alignment.topCenter,
                            end: Alignment.bottomCenter,
                            colors: [Colors.transparent, Colors.black54],
                            stops: [0.0, 0.62],
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
                Positioned(
                  left: 0,
                  right: 0,
                  bottom: 0,
                  height: _headerVignette,
                  child: IgnorePointer(
                    child: Opacity(
                      opacity: t,
                      child: DecoratedBox(
                        decoration: BoxDecoration(
                          gradient: LinearGradient(
                            begin: Alignment.topCenter,
                            end: Alignment.bottomCenter,
                            colors: [
                              cs.surface.withValues(alpha: 0),
                              cs.surface.withValues(alpha: 0.55),
                              cs.surface,
                            ],
                            stops: const [0.0, 0.55, 1.0],
                          ),
                        ),
                      ),
                    ),
                  ),
                ),
              ],
              Positioned.fromRect(
                rect: avatarRect.inflate(7 * (1 - t)),
                child: IgnorePointer(
                  child: Opacity(
                    opacity: ringOpacity,
                    child: CustomPaint(
                      painter: _storyPreview == null
                          ? null
                          : SegmentedRingPainter(
                              total: _storyPreview!.totalCount,
                              read: _storyPreview!.readCount,
                              unreadColors: [cs.primary, cs.tertiary, cs.primary],
                              readColor: cs.outlineVariant,
                              strokeWidth: 3.4,
                            ),
                    ),
                  ),
                ),
              ),
              Positioned(
                left: 4,
                right: 4,
                top: topPad + 4,
                child: Row(
                  children: [
                    IconButton(
                      icon: Icon(Icons.arrow_back, color: iconColor),
                      onPressed: () => Navigator.pop(context),
                    ),
                    Expanded(
                      child: chipOpacity > 0 && unread.isNotEmpty
                          ? Align(
                              alignment: Alignment.centerLeft,
                              child: Opacity(
                                opacity: chipOpacity,
                                child: _storyChip(unread),
                              ),
                            )
                          : const SizedBox.shrink(),
                    ),
                    if (chipOpacity > 0 && totalPhotos > 1)
                      Opacity(
                        opacity: chipOpacity,
                        child: Padding(
                          padding: const EdgeInsets.only(right: 4),
                          child: Text(
                            '${_avatarIndex + 1}/$totalPhotos',
                            style: const TextStyle(
                              color: Colors.white,
                              fontSize: 14,
                              fontWeight: FontWeight.w500,
                            ),
                          ),
                        ),
                      ),
                    _buildMoreButton(cs, iconColor),
                  ],
                ),
              ),
              Positioned(
                left: 0,
                right: 0,
                bottom: lerpDouble(16, 18, t)!,
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    _headerAligned(t, _buildNameRow(cs, nameColor, t)),
                    const SizedBox(height: 2),
                    _headerAligned(
                      t,
                      SelectionArea(
                        child: Text(
                          _subtitle(),
                          style: TextStyle(color: subColor, fontSize: 14),
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ],
          );
        },
      ),
    );
  }

  Widget _headerAligned(double t, Widget child) {
    return Align(
      alignment: Alignment.lerp(Alignment.center, Alignment.centerLeft, t)!,
      child: Padding(
        padding: EdgeInsets.symmetric(horizontal: lerpDouble(12, 18, t)!),
        child: child,
      ),
    );
  }

  Widget _headerAvatar(ColorScheme cs, double radius, double t) {
    final expanded = t > 0.5;
    final openHistory = _headerHasPhoto
        ? () => AvatarHistoryScreen.open(
            context,
            contactId: _otherId ?? widget.dialogPeerId ?? 0,
            name: widget.name,
            currentAvatarUrl: _avatarPages.isEmpty
                ? widget.imageUrl
                : _avatarPages[_avatarIndex.clamp(0, _avatarPages.length - 1)],
          )
        : null;
    final openStories = _storyPreview == null ? null : _openStories;

    return KeyedSubtree(
      key: _avatarKey,
      child: GestureDetector(
        onTap: expanded ? openHistory : (openStories ?? openHistory),
        onLongPress: expanded
            ? null
            : (openStories == null ? null : openHistory),
        child: Stack(
          fit: StackFit.expand,
          children: [
            ProfileHeroAvatar(
              tag: widget.heroTag,
              size: _headerAvatarSize,
              child: ClipRRect(
                borderRadius: BorderRadius.circular(radius),
                child: _headerAvatarContent(cs),
              ),
            ),
            Offstage(
              offstage: t < 0.5 || _avatarPages.length < 2,
              child: ClipRRect(
                borderRadius: BorderRadius.circular(radius),
                child: _avatarPager(cs, t),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _avatarPager(ColorScheme cs, double t) {
    final pages = _avatarPages;
    if (pages.length < 2) return const SizedBox.shrink();
    final interactive = t > 0.5;
    return MouseRegion(
      onEnter: (_) {
        if (!_avatarHover) setState(() => _avatarHover = true);
      },
      onExit: (_) {
        if (_avatarHover) setState(() => _avatarHover = false);
      },
      child: Stack(
        fit: StackFit.expand,
        children: [
          ScrollConfiguration(
            behavior: ScrollConfiguration.of(context).copyWith(
              dragDevices: PointerDeviceKind.values.toSet(),
              scrollbars: false,
              overscroll: false,
            ),
            child: PageView.builder(
              controller: _avatarPageController,
              itemCount: pages.length,
              physics: interactive
                  ? const PageScrollPhysics()
                  : const NeverScrollableScrollPhysics(),
              onPageChanged: (i) => setState(() => _avatarIndex = i),
              itemBuilder: (_, i) => _avatarPhoto(cs, pages[i]),
            ),
          ),
          if (interactive && _avatarHover) ...[
            _avatarArrow(
              alignment: Alignment.centerLeft,
              icon: Icons.chevron_left,
              enabled: _avatarIndex > 0,
              onTap: () => _stepAvatar(-1),
            ),
            _avatarArrow(
              alignment: Alignment.centerRight,
              icon: Icons.chevron_right,
              enabled: _avatarIndex < pages.length - 1,
              onTap: () => _stepAvatar(1),
            ),
          ],
        ],
      ),
    );
  }

  Widget _avatarArrow({
    required Alignment alignment,
    required IconData icon,
    required bool enabled,
    required VoidCallback onTap,
  }) {
    return Align(
      alignment: alignment,
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 8),
        child: AnimatedOpacity(
          duration: const Duration(milliseconds: 150),
          opacity: enabled ? 1 : 0,
          child: IgnorePointer(
            ignoring: !enabled,
            child: GestureDetector(
              behavior: HitTestBehavior.opaque,
              onTap: onTap,
              child: Container(
                width: 36,
                height: 36,
                decoration: const BoxDecoration(
                  color: Colors.black38,
                  shape: BoxShape.circle,
                ),
                child: Icon(icon, color: Colors.white, size: 24),
              ),
            ),
          ),
        ),
      ),
    );
  }

  void _stepAvatar(int delta) {
    final target = (_avatarIndex + delta).clamp(0, _avatarPages.length - 1);
    if (target == _avatarIndex) return;
    _avatarPageController.animateToPage(
      target,
      duration: const Duration(milliseconds: 260),
      curve: Curves.easeOutCubic,
    );
  }

  Widget _avatarPhoto(ColorScheme cs, String url) {
    return CachedNetworkImage(
      imageUrl: url,
      fit: BoxFit.cover,
      memCacheWidth: _headerEverExpanded ? 720 : 288,
      fadeInDuration: const Duration(milliseconds: 150),
      errorWidget: (_, _, _) => ColoredBox(
        color: cs.surfaceContainerHigh,
        child: Center(
          child: Text(
            widget.name.isNotEmpty ? widget.name[0].toUpperCase() : '?',
            style: TextStyle(color: cs.onSurfaceVariant, fontSize: 32),
          ),
        ),
      ),
    );
  }

  Widget _headerAvatarContent(ColorScheme cs) {
    if (_peerDeleted) {
      return _ghostAvatar(radius: _headerAvatarSize / 2, fontSize: 52);
    }
    final url = _avatarPages.isNotEmpty ? _avatarPages.first : widget.imageUrl;
    if (url.isEmpty) {
      return KometAvatar(
        name: widget.name,
        size: _headerAvatarSize,
        fontSize: 36,
        fadeIn: false,
      );
    }
    return _avatarPhoto(cs, url);
  }

  void _refreshUnreadStories() {
    final preview = _storyPreview;
    if (preview == null || preview.unreadCount <= 0) {
      _unreadStories = const [];
      return;
    }
    final stories = storiesModule.cachedStories(preview.owner.ownerId);
    if (stories == null || stories.isEmpty) {
      _unreadStories = const [];
      return;
    }
    final from = (stories.length - preview.unreadCount).clamp(
      0,
      stories.length,
    );
    _unreadStories = stories.sublist(from);
  }

  Widget _storyChip(List<Story> unread) {
    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: _openStories,
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          StoryPeanut(stories: unread),
          const SizedBox(width: 8),
          Flexible(
            child: Text(
              '${unread.length} '
              '${pluralRu(unread.length, 'история', 'истории', 'историй')}',
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
              style: const TextStyle(
                color: Colors.white,
                fontSize: 17,
                fontWeight: FontWeight.w600,
                fontFamily: 'Outfit',
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildBody(ColorScheme cs) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          if (_isLoading)
            ..._loadingBlocks(cs)
          else ...[
            _buildActions(cs),
            const SizedBox(height: 16),
            _buildPersistentInfo(cs),
            _buildTabBar(cs),
            const SizedBox(height: 12),
            _buildTabContent(cs),
            SizedBox(height: 40 + MediaQuery.paddingOf(context).bottom),
          ],
        ],
      ),
    );
  }

  ContactName? _nameEntry(String type) {
    final data = _contactData;
    if (data == null) return null;
    for (final n in data.names) {
      if (n.type == type) return n;
    }
    return null;
  }

  bool get _isContact => _localContact != null;

  bool get _peerDeleted => _contactData?.isDeleted ?? false;

  String _joinName(String first, String last) =>
      last.trim().isEmpty ? first.trim() : '${first.trim()} ${last.trim()}';

  String get _customName {
    final c = _localContact;
    if (c != null) return _joinName(c.firstName, c.lastName ?? '');
    return widget.name;
  }

  Widget _buildMoreButton(ColorScheme cs, [Color? iconColor]) {
    final entries = _moreMenuEntries();
    final color = iconColor ?? cs.onSurface;
    if (entries.isEmpty) {
      return IconButton(
        icon: Icon(Icons.more_vert, color: color),
        onPressed: null,
      );
    }
    return PopupMenuButton<VoidCallback>(
      icon: Icon(Icons.more_vert, color: color),
      onSelected: (action) => action(),
      itemBuilder: (_) => [
        for (final entry in entries)
          PopupMenuItem<VoidCallback>(
            value: entry.onTap,
            child: Row(
              children: [
                Icon(
                  entry.icon,
                  size: 20,
                  color: entry.destructive ? cs.error : cs.onSurface,
                ),
                const SizedBox(width: 12),
                Text(
                  entry.label,
                  style: entry.destructive ? TextStyle(color: cs.error) : null,
                ),
              ],
            ),
          ),
      ],
    );
  }

  List<({IconData icon, String label, bool destructive, VoidCallback onTap})>
  _moreMenuEntries() {
    if (_isLoading) return const [];
    final entries =
        <({IconData icon, String label, bool destructive, VoidCallback onTap})>[];

    if (widget.chatType == 'DIALOG') {
      if (_isContact) {
        entries.add((
          icon: Symbols.edit,
          label: l10n.editContactMenu,
          destructive: false,
          onTap: _openEdit,
        ));
      }
      if (!_isBot && _otherId != null && _otherId != _myId) {
        entries.add((
          icon: _blocked ? Symbols.lock_open : Symbols.block,
          label: _blocked ? l10n.chatInfoMenuUnblock : l10n.chatInfoMenuBlock,
          destructive: !_blocked,
          onTap: _toggleBlock,
        ));
      }
      entries.add((
        icon: Symbols.delete,
        label: l10n.chatInfoMenuDeleteChat,
        destructive: true,
        onTap: _deleteChat,
      ));
    }

    entries.add((
      icon: Symbols.mop,
      label: l10n.chatInfoMenuClearHistory,
      destructive: true,
      onTap: _clearHistory,
    ));

    return entries;
  }

  Future<void> _openEdit() async {
    final oneme = _nameEntry('ONEME');
    final local = _localContact;
    final peerId = _otherId ?? widget.dialogPeerId ?? 0;
    if (peerId == 0) return;

    final result = await showEditContactSheet(
      context,
      contactId: peerId,
      avatarUrl: _contactData?.avatarUrl ?? local?.baseUrl ?? widget.imageUrl,
      customFirst: local?.firstName ?? '',
      customLast: local?.lastName ?? '',
      onemeFirst: oneme?.firstName ?? '',
      onemeLast: oneme?.lastName ?? '',
    );
    if (!mounted || result == null) return;

    switch (result.action) {
      case EditContactAction.updated:
        final fresh = await ContactsModule.getContact(_myId, peerId);
        if (!mounted) return;
        setState(() {
          _localContact = fresh;
          _showRealName = false;
        });
      case EditContactAction.removed:
        Navigator.of(context).pop();
    }
  }

  String? get _realName {
    final data = _contactData;
    if (data == null) return null;
    for (final n in data.names) {
      if (n.type == 'ONEME') {
        final combined = [n.firstName, n.lastName]
            .where((s) => s != null && s.trim().isNotEmpty)
            .map((s) => s!.trim())
            .join(' ');
        if (combined.isNotEmpty) return combined;
        final label = n.label;
        if (label != null && label.isNotEmpty) return label;
      }
    }
    return null;
  }

  Widget _buildNameRow(ColorScheme cs, Color textColor, double t) {
    final nameStyle = TextStyle(
      color: textColor,
      fontSize: lerpDouble(22, 25, t)!,
      fontWeight: FontWeight.w700,
      fontFamily: 'Outfit',
    );
    final custom = _customName;
    final real = _realName;
    final hasToggle = _isContact && real != null && real != custom;

    return Row(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        SizedBox(width: (hasToggle ? 30.0 : 0.0) * (1 - t)),
        Flexible(
          child: SelectionArea(
            child: ProfileHeroName(
              tag: widget.heroTag,
              text: custom,
              style: nameStyle,
              child: AnimatedTextSwap(
                showAlternate: _showRealName,
                alignment: Alignment.center,
                alternate: Text(
                  real ?? custom,
                  style: nameStyle,
                  textAlign: TextAlign.center,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
                child: Text(
                  custom,
                  style: nameStyle,
                  textAlign: TextAlign.center,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
              ),
            ),
          ),
        ),
        SizedBox(
          width: hasToggle ? 30 : 0,
          height: 28,
          child: hasToggle
              ? IconButton(
                  padding: EdgeInsets.zero,
                  constraints: const BoxConstraints(
                    minWidth: 28,
                    minHeight: 28,
                  ),
                  iconSize: 20,
                  color: _showRealName
                      ? Color.lerp(cs.primary, Colors.white, t)
                      : textColor.withValues(alpha: 0.7),
                  icon: Icon(
                    _showRealName ? Symbols.visibility : Symbols.visibility_off,
                  ),
                  tooltip: real,
                  onPressed: () =>
                      setState(() => _showRealName = !_showRealName),
                )
              : null,
        ),
      ],
    );
  }

  String _subtitle() {
    switch (widget.chatType) {
      case 'DIALOG':
        if (_peerDeleted) return l10n.chatInfoMemberDeleted;
        if (_isBot) return l10n.contactProfileBot;
        if (_isOnline) return l10n.contactProfileOnline;
        if (_presenceStatus == 2 || _presenceStatus == 3)
          return l10n.contactProfileRecentlyActive;
        if (_seenTime != null && _seenTime! > 0) {
          return formatLastSeen(_seenTime!);
        }
        return '';
      case 'CHAT':
        final total = _chatInfo?.participantsCount ?? _members.length;
        return '$total ${pluralRu(total, 'участник', 'участника', 'участников')}';
      case 'CHANNEL':
        final count = _chatInfo?.participantsCount ?? 0;
        return '$count ${pluralRu(count, 'подписчик', 'подписчика', 'подписчиков')}';
      default:
        return '';
    }
  }

  bool get _isMuted {
    if (_dontDisturbUntil == ChatsModule.muteOff) return false;
    if (_dontDisturbUntil < 0) return true;
    return _dontDisturbUntil > DateTime.now().millisecondsSinceEpoch;
  }

  bool get _iAmAdmin {
    final info = _chatInfo;
    if (info == null || _myId == 0) return false;
    return info.isOwner(_myId) || info.isAdmin(_myId);
  }

  bool get _isGroupOrChannel =>
      widget.chatType == 'CHAT' || widget.chatType == 'CHANNEL';

  Widget _buildActions(ColorScheme cs) {
    final muteBtn = (
      icon: _isMuted ? Icons.notifications_off : Icons.notifications,
      label: _isMuted
          ? l10n.chatInfoActionMuted
          : l10n.contactProfileActionSound,
      onTap: _muteBusy ? null : _toggleMute,
    );
    final chatBtn = (
      icon: Icons.chat_bubble,
      label: l10n.contactProfileActionChat,
      onTap: _openChat,
    );
    final leaveBtn = (
      icon: Icons.exit_to_app,
      label: l10n.chatInfoActionLeave,
      onTap: _leaveChat,
    );

    final List<({IconData icon, String label, VoidCallback? onTap})> btns;
    if (widget.chatType == 'DIALOG') {
      btns = [
        chatBtn,
        muteBtn,
        if (!_isBot)
          (
            icon: Icons.call,
            label: l10n.contactProfileActionCall,
            onTap: _confirmAndStartCall,
          ),
      ];
    } else if (widget.chatType == 'CHANNEL') {
      btns = [muteBtn, leaveBtn];
    } else {
      btns = [chatBtn, muteBtn, leaveBtn];
    }

    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Row(
          children: [
            for (int i = 0; i < btns.length; i++) ...[
              _actionBtn(cs, btns[i].icon, btns[i].label, btns[i].onTap),
              if (i < btns.length - 1) const SizedBox(width: 8),
            ],
          ],
        ),
        if (_canAddContact) ...[
          const SizedBox(height: 8),
          _wideActionBtn(
            cs,
            Symbols.person_add,
            l10n.contactProfileActionAddContact,
            _addContactBusy ? null : _addToContacts,
          ),
        ],
      ],
    );
  }

  bool get _canAddContact =>
      widget.chatType == 'DIALOG' &&
      !_isContact &&
      !_isBot &&
      !_peerDeleted &&
      _otherId != null &&
      _otherId != _myId;

  Future<void> _addToContacts() async {
    final peerId = _otherId;
    if (peerId == null || _addContactBusy) return;
    setState(() => _addContactBusy = true);

    CachedContact? contact;
    try {
      contact = await ContactsModule.addContact(api, peerId, '');
    } catch (_) {}

    if (!mounted) return;
    setState(() {
      _addContactBusy = false;
      if (contact != null) {
        _localContact = contact;
        _showRealName = false;
      }
    });
    showCustomNotification(
      context,
      contact != null ? l10n.nfcContactAdded : l10n.addContactError,
    );
  }

  void _openChat() {
    if (widget.openedFromChat) {
      Navigator.of(context).pop();
      return;
    }
    pushSwipeable(
      context,
      (_) => ChatScreen(
        chatId: _mediaChatId,
        name: widget.name,
        imageUrl: widget.imageUrl,
        chatType: widget.chatType,
      ),
    );
  }

  Future<void> _toggleMute() async {
    if (_muteBusy) return;
    setState(() => _muteBusy = true);
    final muted = _isMuted;
    final target = muted ? ChatsModule.muteOff : ChatsModule.muteForever;
    final error = await chats.setChatMute(
      api,
      chatId: _mediaChatId,
      dontDisturbUntil: target,
    );
    if (!mounted) return;
    setState(() {
      _muteBusy = false;
      if (error == null) _dontDisturbUntil = target;
    });
    showCustomNotification(
      context,
      error ??
          (muted ? l10n.chatInfoNotificationsOn : l10n.chatInfoNotificationsOff),
    );
  }

  Future<void> _confirmAndStartCall() async {
    final peerId = _otherId;
    if (peerId == null || peerId == _myId) return;

    final choice = await showBlurredConfirm(
      context,
      title: l10n.chatInfoCallConfirmTitle,
      message: l10n.chatInfoCallConfirmMessage(_customName),
      confirmLabel: l10n.chatInfoConfirmYes,
      cancelLabel: l10n.chatInfoConfirmNo,
    );
    if (!mounted || !choice.confirmed) return;

    final navigator = Navigator.of(context);
    final avatarUrl = widget.imageUrl.isNotEmpty ? widget.imageUrl : null;
    final active = CallController.instance.activeSession;
    if (active != null) {
      await navigator.push(
        MaterialPageRoute(
          builder: (_) =>
              CallScreen(name: _customName, avatarUrl: avatarUrl, session: active),
        ),
      );
      return;
    }

    try {
      final session = await CallController.instance.startOutgoing(peerId);
      if (!mounted) return;
      await navigator.push(
        MaterialPageRoute(
          builder: (_) => CallScreen(
            name: _customName,
            avatarUrl: avatarUrl,
            session: session,
          ),
        ),
      );
    } catch (_) {
      if (!mounted) return;
      showCustomNotification(context, l10n.chatInfoCallFailed);
    }
  }

  Future<void> _leaveChat() async {
    final isChannel = widget.chatType == 'CHANNEL';
    final choice = await showBlurredConfirm(
      context,
      title: isChannel
          ? l10n.chatInfoLeaveChannelTitle
          : l10n.chatInfoLeaveGroupTitle,
      message: isChannel
          ? l10n.chatInfoLeaveChannelMessage
          : l10n.chatInfoLeaveGroupMessage,
      confirmLabel: l10n.chatInfoLeaveConfirm,
      cancelLabel: l10n.chatInfoActionCancel,
      destructive: true,
    );
    if (!mounted || !choice.confirmed) return;

    final ok = await chats.leaveChat(api, chatId: _mediaChatId);
    if (!mounted) return;
    if (!ok) {
      showCustomNotification(context, l10n.chatInfoLeaveFailed);
      return;
    }
    Navigator.of(context).popUntil((route) => route.isFirst);
  }

  Future<void> _clearHistory() async {
    final canClearForAll = _isGroupOrChannel && _iAmAdmin;
    final choice = await showBlurredConfirm(
      context,
      title: l10n.chatInfoClearHistoryTitle,
      message: l10n.chatInfoClearHistoryMessage,
      confirmLabel: l10n.chatInfoClearHistoryConfirm,
      cancelLabel: l10n.chatInfoActionCancel,
      destructive: true,
      checkboxLabel: canClearForAll ? l10n.chatInfoClearHistoryForAll : null,
    );
    if (!mounted || !choice.confirmed) return;

    final error = await chats.clearHistory(
      api,
      chatId: _mediaChatId,
      lastEventTime: _lastEventTime,
      forAll: canClearForAll && choice.checked,
    );
    if (!mounted) return;
    showCustomNotification(context, error ?? l10n.chatInfoClearHistoryDone);
  }

  Future<void> _deleteChat() async {
    final choice = await showBlurredConfirm(
      context,
      title: l10n.chatInfoDeleteChatTitle,
      message: l10n.chatInfoDeleteChatMessage,
      confirmLabel: l10n.chatInfoDeleteChatConfirm,
      cancelLabel: l10n.chatInfoActionCancel,
      destructive: true,
    );
    if (!mounted || !choice.confirmed) return;

    final error = await chats.deleteChat(
      api,
      chatId: _mediaChatId,
      lastEventTime: _lastEventTime,
      forAll: false,
    );
    if (!mounted) return;
    if (error != null) {
      showCustomNotification(context, error);
      return;
    }
    Navigator.of(context).popUntil((route) => route.isFirst);
  }

  Future<void> _toggleBlock() async {
    final peerId = _otherId;
    if (peerId == null) return;

    final block = !_blocked;
    if (block) {
      final choice = await showBlurredConfirm(
        context,
        title: l10n.chatInfoBlockConfirmTitle,
        message: l10n.chatInfoBlockConfirmMessage(_customName),
        confirmLabel: l10n.chatInfoConfirmYes,
        cancelLabel: l10n.chatInfoConfirmNo,
        destructive: true,
      );
      if (!mounted || !choice.confirmed) return;
    }

    final ok = await ContactsModule.setBlocked(api, peerId, block);
    if (!mounted) return;
    if (!ok) {
      showCustomNotification(context, l10n.chatInfoBlockFailed);
      return;
    }
    setState(() => _blocked = block);
    showCustomNotification(
      context,
      block ? l10n.chatInfoBlockDone : l10n.chatInfoUnblockDone,
    );
    if (block) await _openComplaintCard(peerId);
  }

  Future<void> _openComplaintCard(int peerId) async {
    if (!mounted) return;
    await showComplaintCard(
      context,
      title: l10n.chatInfoComplaintTitle,
      subtitle: l10n.chatInfoComplaintSubtitle,
      sendLabel: l10n.chatInfoComplaintSend,
      closeLabel: l10n.chatInfoComplaintClose,
      emptyLabel: l10n.chatInfoComplaintEmpty,
      loadReasons: () async {
        final reasons = await ComplaintsModule.reasonsFor(
          api,
          ComplaintsModule.userTypeId,
        );
        return reasons
            .map((r) => (id: r.reasonId, title: r.reasonTitle))
            .toList();
      },
      onSend: (reasonId) async {
        final ok = await ComplaintsModule.sendComplaint(
          api,
          reasonId: reasonId,
          typeId: ComplaintsModule.userTypeId,
          ids: [peerId],
        );
        if (!mounted) return ok;
        showCustomNotification(
          context,
          ok ? l10n.chatInfoComplaintSent : l10n.chatInfoComplaintFailed,
        );
        return ok;
      },
    );
  }

  Widget _actionBtn(
    ColorScheme cs,
    IconData icon,
    String label, [
    VoidCallback? onTap,
  ]) {
    return Expanded(
      child: GlossyPill(
        onTap: onTap,
        color: cs.surfaceContainerHigh,
        borderRadius: BorderRadius.circular(14),
        padding: const EdgeInsets.symmetric(vertical: 10),
        depth: 6,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(icon, color: cs.primary, size: 22),
            const SizedBox(height: 4),
            Text(
              label,
              style: TextStyle(color: cs.onSurface, fontSize: 11),
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }

  Widget _wideActionBtn(
    ColorScheme cs,
    IconData icon,
    String label, [
    VoidCallback? onTap,
  ]) {
    return GlossyPill(
      onTap: onTap,
      color: cs.surfaceContainerHigh,
      borderRadius: BorderRadius.circular(14),
      padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
      depth: 6,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(icon, color: cs.primary, size: 22),
          const SizedBox(width: 8),
          Flexible(
            child: Text(
              label,
              style: TextStyle(color: cs.onSurface, fontSize: 13),
              textAlign: TextAlign.center,
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildPersistentInfo(ColorScheme cs) {
    final items = <Widget>[];

    if (widget.chatType == 'DIALOG') {
      if (_isBot) {
        final link = _contactData?.raw['link'] as String?;
        if (link != null && link.isNotEmpty) {
          items.add(
            _simpleInfoCard(
              cs,
              l10n.contactProfileInfoLink,
              link,
              isLink: true,
            ),
          );
        }
      } else {
        final phone = _contactData?.raw['phone'];
        final phoneInt = phone is int
            ? phone
            : int.tryParse(phone?.toString() ?? '');
        if (phoneInt != null && phoneInt > 0) {
          items.add(
            _simpleInfoCard(cs, l10n.loginPhoneNumber, formatPhone(phoneInt)!),
          );
        }
        final bio =
            (_contactData?.raw['description'] as String?) ??
            (_contactData?.raw['about'] as String?);
        if (bio != null && bio.isNotEmpty) {
          if (items.isNotEmpty) items.add(const SizedBox(height: 8));
          items.add(_simpleInfoCard(cs, l10n.chatInfoBio, bio));
        }
      }
    } else {
      final link = _chatInfo?.link;
      if (link != null && link.isNotEmpty) {
        items.add(_linkCard(cs, link));
      }
      final desc = _chatInfo?.description;
      if (desc != null && desc.isNotEmpty) {
        if (items.isNotEmpty) items.add(const SizedBox(height: 8));
        items.add(_collapsibleDescCard(cs, desc));
      }
    }

    if (items.isEmpty) return const SizedBox.shrink();
    return SelectionArea(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [...items, const SizedBox(height: 16)],
      ),
    );
  }

  Widget _simpleInfoCard(
    ColorScheme cs,
    String label,
    String value, {
    bool isLink = false,
  }) {
    return GlossyPill(
      color: cs.surfaceContainerHigh,
      borderRadius: BorderRadius.circular(14),
      padding: const EdgeInsets.fromLTRB(16, 12, 16, 14),
      depth: 6,
      child: SizedBox(
        width: double.infinity,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              label,
              style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
            ),
            const SizedBox(height: 4),
            FormattedMessageText(
              text: value,
              ranges: const [],
              entityMode: TextEntityMode.copy,
              style: TextStyle(
                color: isLink ? cs.primary : cs.onSurface,
                fontSize: 16,
                fontWeight: FontWeight.w500,
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _linkCard(ColorScheme cs, String link) {
    return GlossyPill(
      color: cs.surfaceContainerHigh,
      borderRadius: BorderRadius.circular(14),
      padding: const EdgeInsets.fromLTRB(16, 12, 8, 14),
      depth: 6,
      child: Row(
        children: [
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  l10n.chatInfoInviteLink,
                  style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
                ),
                const SizedBox(height: 4),
                FormattedMessageText(
                  text: link,
                  ranges: const [],
                  entityMode: TextEntityMode.copy,
                  style: TextStyle(color: cs.primary, fontSize: 15),
                ),
              ],
            ),
          ),
          IconButton(
            icon: Icon(Icons.qr_code_2, color: cs.primary, size: 22),
            onPressed: () {},
          ),
        ],
      ),
    );
  }

  Widget _collapsibleDescCard(ColorScheme cs, String desc) {
    const int collapsedLines = 3;
    final isLong = desc.length > 120;

    return GlossyPill(
      color: cs.surfaceContainerHigh,
      borderRadius: BorderRadius.circular(14),
      padding: const EdgeInsets.all(16),
      depth: 6,
      child: SizedBox(
        width: double.infinity,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              l10n.contactProfileInfoDescription,
              style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
            ),
            const SizedBox(height: 4),
            FormattedMessageText(
              text: desc,
              ranges: const [],
              entityMode: TextEntityMode.copy,
              style: TextStyle(color: cs.onSurface, fontSize: 15, height: 1.4),
              maxLines: (_descExpanded || !isLong) ? null : collapsedLines,
              overflow: (_descExpanded || !isLong)
                  ? null
                  : TextOverflow.ellipsis,
            ),
            if (isLong) ...[
              const SizedBox(height: 6),
              GestureDetector(
                onTap: () => setState(() => _descExpanded = !_descExpanded),
                child: Text(
                  _descExpanded ? l10n.chatInfoCollapse : l10n.chatInfoShowMore,
                  style: TextStyle(color: cs.primary, fontSize: 13),
                ),
              ),
            ],
          ],
        ),
      ),
    );
  }

  Widget _buildTabBar(ColorScheme cs) {
    return LayoutBuilder(
      builder: (context, constraints) => ScrollConfiguration(
        behavior: ScrollConfiguration.of(context).copyWith(
          dragDevices: {
            PointerDeviceKind.touch,
            PointerDeviceKind.mouse,
            PointerDeviceKind.trackpad,
          },
        ),
        child: Listener(
          onPointerSignal: (event) {
            if (event is PointerScrollEvent) {
              final delta = event.scrollDelta.dy != 0
                  ? event.scrollDelta.dy
                  : event.scrollDelta.dx;
              _tabScrollController.animateTo(
                (_tabScrollController.offset + delta).clamp(
                  _tabScrollController.position.minScrollExtent,
                  _tabScrollController.position.maxScrollExtent,
                ),
                duration: const Duration(milliseconds: 80),
                curve: Curves.easeOut,
              );
            }
          },
          child: SingleChildScrollView(
            controller: _tabScrollController,
            scrollDirection: Axis.horizontal,
            child: ConstrainedBox(
              constraints: BoxConstraints(minWidth: constraints.maxWidth),
              child: Row(
                mainAxisSize: MainAxisSize.min,
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  for (int i = 0; i < _tabs.length; i++) ...[
                    _tabChip(cs, _tabs[i]),
                    if (i < _tabs.length - 1) const SizedBox(width: 8),
                  ],
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget _tabChip(ColorScheme cs, String tab) {
    final selected = tab == _selectedTab;
    return GestureDetector(
      onTap: () => setState(() => _selectedTab = tab),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 180),
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        decoration: BoxDecoration(
          color: selected ? cs.primaryContainer : cs.surfaceContainerHigh,
          borderRadius: BorderRadius.circular(20),
        ),
        child: Text(
          tab,
          style: TextStyle(
            color: selected ? cs.onPrimaryContainer : cs.onSurfaceVariant,
            fontWeight: selected ? FontWeight.w600 : FontWeight.normal,
            fontSize: 14,
          ),
        ),
      ),
    );
  }

  Widget _buildTabContent(ColorScheme cs) {
    if (_selectedTab.isEmpty) return const SizedBox.shrink();
    return AnimatedSwitcher(
      duration: const Duration(milliseconds: 180),
      child: KeyedSubtree(key: ValueKey(_selectedTab), child: _tabBody(cs)),
    );
  }

  Widget _tabBody(ColorScheme cs) {
    if (_selectedTab == 'Info') return _buildInfoTabContent(cs);
    if (_selectedTab == l10n.chatInfoTabMembers) {
      return _buildMembersTabContent(cs);
    }
    if (_selectedTab == l10n.chatInfoTabGeneralChats) {
      final peerId = _otherId;
      if (peerId == null) {
        return _buildPlaceholder(
          cs,
          l10n.chatInfoEmptyGeneralChats,
          Icons.group,
        );
      }
      return CommonChatsTab(
        key: const ValueKey('tab-common-chats'),
        userId: peerId,
        emptyLabel: l10n.chatInfoEmptyGeneralChats,
      );
    }
    if (_selectedTab == l10n.chatInfoTabMedia) {
      return _sharedTab(
        cs,
        SharedContentKind.media,
        l10n.chatInfoEmptyMedia,
        Icons.photo_library,
      );
    }
    if (_selectedTab == l10n.chatInfoTabFiles) {
      return _sharedTab(
        cs,
        SharedContentKind.files,
        l10n.chatInfoEmptyFiles,
        Icons.description,
      );
    }
    if (_selectedTab == l10n.chatInfoTabVoice) {
      return _sharedTab(
        cs,
        SharedContentKind.voice,
        l10n.chatInfoEmptyVoice,
        Icons.mic,
      );
    }
    if (_selectedTab == l10n.chatInfoTabLinks) {
      return _sharedTab(
        cs,
        SharedContentKind.links,
        l10n.chatInfoEmptyLinks,
        Icons.link,
      );
    }
    return const SizedBox.shrink();
  }

  Widget _sharedTab(
    ColorScheme cs,
    SharedContentKind kind,
    String emptyLabel,
    IconData emptyIcon,
  ) {
    final anchor = _anchorMsgId;
    if (anchor == null) return _buildPlaceholder(cs, emptyLabel, emptyIcon);
    return SharedMediaTab(
      key: ValueKey('tab-shared-$kind'),
      chatId: _mediaChatId,
      anchorMessageId: anchor,
      myId: _myId,
      sourceName: widget.name,
      kind: kind,
      emptyLabel: emptyLabel,
      emptyIcon: emptyIcon,
      onGoToMessage: _goToMessage,
      scrollController: _bodyScrollController,
    );
  }

  void _goToMessage(String messageId, int time) {
    final jumpInParent = widget.onJumpToMessage;
    if (jumpInParent != null && _mediaChatId == widget.chatId) {
      jumpInParent(messageId, time);
      return;
    }
    pushSwipeable(
      context,
      (_) => ChatScreen(
        chatId: _mediaChatId,
        name: widget.name,
        imageUrl: widget.imageUrl,
        chatType: widget.chatType,
        initialMessageId: messageId,
        initialMessageTime: time,
      ),
    );
  }

  Widget _buildPlaceholder(ColorScheme cs, String label, IconData icon) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 48),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(
            icon,
            color: cs.onSurfaceVariant.withValues(alpha: 0.35),
            size: 48,
          ),
          const SizedBox(height: 12),
          Text(
            label,
            style: TextStyle(color: cs.onSurfaceVariant, fontSize: 15),
          ),
        ],
      ),
    );
  }

  Widget _buildInfoTabContent(ColorScheme cs) {
    final items = <Widget>[];

    items.add(_buildInfoRowsCard(cs));

    return SelectionArea(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: items,
      ),
    );
  }

  Widget _buildInfoRowsCard(ColorScheme cs) {
    return GlossyPill(
      color: cs.surfaceContainerHigh,
      borderRadius: BorderRadius.circular(14),
      padding: const EdgeInsets.fromLTRB(16, 12, 16, 14),
      depth: 6,
      child: _buildAllInfoRows(cs),
    );
  }

  Widget _buildMembersTabContent(ColorScheme cs) {
    return Container(
      decoration: BoxDecoration(
        color: cs.surfaceContainerHigh,
        borderRadius: BorderRadius.circular(14),
      ),
      child: Column(
        children: [
          _memberAction(
            cs,
            Icons.person_add,
            l10n.chatInfoAddMember,
            _openAddMembers,
          ),
          if (_inviteLink != null) ...[
            _listDivider(cs),
            _memberAction(
              cs,
              Icons.link,
              l10n.chatInfoInviteByLink,
              () => _openInviteLink(_inviteLink!),
            ),
          ],
          ..._members.expand((m) => [_listDivider(cs), _memberTile(cs, m)]),
          if (_membersLoading || !_membersEnd) ...[
            _listDivider(cs),
            _membersFooter(cs),
          ],
        ],
      ),
    );
  }

  Widget _membersFooter(ColorScheme cs) {
    if (_membersLoading) {
      return const Padding(
        padding: EdgeInsets.symmetric(vertical: 16),
        child: Center(
          child: SizedBox(
            width: 22,
            height: 22,
            child: CircularProgressIndicator(strokeWidth: 2),
          ),
        ),
      );
    }
    return InkWell(
      onTap: () => _fetchMembersPage(),
      borderRadius: BorderRadius.circular(14),
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
        child: Row(
          children: [
            Icon(Icons.expand_more, color: cs.primary, size: 26),
            const SizedBox(width: 14),
            Text(
              l10n.chatInfoShowMore,
              style: TextStyle(color: cs.primary, fontSize: 16),
            ),
          ],
        ),
      ),
    );
  }

  Widget _memberAction(
    ColorScheme cs,
    IconData icon,
    String label,
    VoidCallback onTap,
  ) {
    return InkWell(
      onTap: onTap,
      borderRadius: BorderRadius.circular(14),
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
        child: Row(
          children: [
            Icon(icon, color: cs.primary, size: 26),
            const SizedBox(width: 14),
            Text(label, style: TextStyle(color: cs.onSurface, fontSize: 16)),
          ],
        ),
      ),
    );
  }

  Widget _listDivider(ColorScheme cs) => Divider(
    height: 1,
    indent: 56,
    endIndent: 0,
    color: cs.outlineVariant.withValues(alpha: 0.3),
  );

  Widget _memberTile(ColorScheme cs, _MemberInfo member) {
    final name =
        member.name ??
        ContactCache.get(member.id) ??
        (member.isMe ? l10n.callParticipantYou : '${member.id}');
    final avatar = member.avatarUrl ?? ContactCache.getAvatar(member.id);

    final String sublabel;
    if (member.blocked) {
      sublabel = l10n.chatInfoMemberDeleted;
    } else if (member.isMe) {
      sublabel = l10n.callParticipantYou;
    } else if (member.presenceStatus == 1) {
      sublabel = l10n.contactProfileOnline;
    } else if (member.presenceStatus == 2 || member.presenceStatus == 3) {
      sublabel = l10n.contactProfileRecentlyActive;
    } else if (member.seenTime != null && member.seenTime! > 0) {
      sublabel = formatLastSeen(member.seenTime!);
    } else {
      sublabel = l10n.contactProfileRecentlyActive;
    }

    final String? roleLabel = member.isOwner
        ? l10n.chatInfoRoleOwner
        : (member.isAdmin ? l10n.chatInfoRoleAdmin : null);

    final story = member.blocked || !AppStories.current.value
        ? null
        : storiesModule.previewOf(member.id);
    final avatarRadius = story == null ? 22.0 : 19.0;

    return InkWell(
      onTap: member.isMe
          ? null
          : () => openContactDialogProfile(
              context,
              contactId: member.id,
              name: name,
              avatarUrl: avatar,
            ),
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
        child: Row(
          children: [
            if (member.blocked)
              _ghostAvatar()
            else
              _memberAvatar(
                cs,
                story: story,
                radius: avatarRadius,
                name: name,
                avatarUrl: avatar,
              ),
            const SizedBox(width: 14),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    name,
                    style: TextStyle(
                      color: cs.onSurface,
                      fontSize: 15,
                      fontWeight: FontWeight.w500,
                    ),
                  ),
                  Text(
                    sublabel,
                    style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
                  ),
                ],
              ),
            ),
            if (member.alias != null)
              _memberTag(cs, member.alias!)
            else if (roleLabel != null)
              Text(
                roleLabel,
                style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
              ),
          ],
        ),
      ),
    );
  }

  Widget _memberAvatar(
    ColorScheme cs, {
    required StoryPreview? story,
    required double radius,
    required String name,
    String? avatarUrl,
  }) {
    final circle = (avatarUrl != null && avatarUrl.isNotEmpty)
        ? CircleAvatar(
            radius: radius,
            backgroundImage: CachedNetworkImageProvider(
              avatarUrl,
              maxWidth: 144,
              maxHeight: 144,
            ),
            backgroundColor: cs.primaryContainer,
          )
        : CircleAvatar(
            radius: radius,
            backgroundColor: cs.primaryContainer,
            child: Text(
              name.isNotEmpty ? name[0].toUpperCase() : '?',
              style: TextStyle(
                color: cs.onPrimaryContainer,
                fontSize: radius * 0.72,
              ),
            ),
          );
    if (story == null) return circle;
    return Builder(
      builder: (avatarContext) => GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: () => _openMemberStories(avatarContext, story, name, avatarUrl),
        child: StoryAvatarRing(
          diameter: radius * 2,
          total: story.totalCount,
          read: story.readCount,
          strokeWidth: 2.2,
          ringGap: 3,
          haloWidth: 1.5,
          child: circle,
        ),
      ),
    );
  }

  void _openMemberStories(
    BuildContext avatarContext,
    StoryPreview story,
    String name,
    String? avatarUrl,
  ) {
    Haptics.tap();
    unawaited(
      openStoryViewer(
        context,
        previews: [story],
        origin: storyOriginOf(avatarContext),
        ownerOverrides: {
          story.owner.ownerId: StoryOwnerInfo(name: name, avatarUrl: avatarUrl),
        },
      ),
    );
  }

  Widget _ghostAvatar({double radius = 22, double fontSize = 24}) {
    return CircleAvatar(
      radius: radius,
      backgroundColor: const Color(0xFFD4D4D4),
      child: Text('👻', style: TextStyle(fontSize: fontSize)),
    );
  }

  Widget _memberTag(ColorScheme cs, String label) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
      decoration: BoxDecoration(
        color: cs.primaryContainer,
        borderRadius: BorderRadius.circular(8),
      ),
      child: Text(
        label,
        style: TextStyle(
          color: cs.onPrimaryContainer,
          fontSize: 12,
          fontWeight: FontWeight.w600,
        ),
      ),
    );
  }

  Widget _buildAllInfoRows(ColorScheme cs) {
    final rows = <({String label, String value})>[];
    final chat = _chatInfo?.raw;
    if (chat == null) {
      return Text(
        l10n.chatInfoNoData,
        style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
      );
    }

    void add(String label, dynamic val, {bool tsFormat = false}) {
      if (val == null) return;
      if (val is bool && !val) return;
      String str;
      if (tsFormat && val is int && val > 1) {
        str = formatDateTimeNumeric(DateTime.fromMillisecondsSinceEpoch(val));
      } else if (val is bool) {
        str = l10n.callValueYes;
      } else {
        str = val.toString();
      }
      if (str.isEmpty) return;
      rows.add((label: label, value: str));
    }

    final type = widget.chatType;
    add(l10n.chatInfoRowId, chat['id']);

    if (type == 'DIALOG') {
      add(l10n.chatInfoRowCreated, chat['created'], tsFormat: true);
      add(l10n.chatInfoRowModified, chat['modified'], tsFormat: true);
      add(l10n.callInfoStatus, chat['status']);
    }

    if (type == 'CHAT') {
      add(l10n.chatInfoRowMembersCount, chat['participantsCount']);
      final owner = chat['owner'] as int?;
      if (owner != null && owner != 0) {
        add(l10n.chatInfoRowOwner, ContactCache.get(owner) ?? '$owner');
      }
      add(l10n.chatInfoRowCreatedGroup, chat['created'], tsFormat: true);
      add(
        l10n.chatInfoRowJoined,
        (chat['joinTime'] as int?) != null && (chat['joinTime'] as int) > 1
            ? chat['joinTime']
            : null,
        tsFormat: true,
      );
      add(l10n.chatInfoRowModifiedGroup, chat['modified'], tsFormat: true);
      add(l10n.chatInfoRowHasBots, chat['hasBots'] as bool?);
      final blocked = chat['blockedParticipantsCount'] as int?;
      if (blocked != null && blocked > 0) {
        add(l10n.chatInfoRowBlockedCount, blocked);
      }
      final opts = chat['options'] as Map?;
      add(l10n.chatInfoRowOfficialGroup, opts?['OFFICIAL'] as bool?);
      add(l10n.chatInfoRowSignAdmin, opts?['SIGN_ADMIN'] as bool?);
      add(l10n.callInfoStatus, chat['status']);
    }

    if (type == 'CHANNEL') {
      add(l10n.chatInfoRowSubscribersCount, chat['participantsCount']);
      add(l10n.chatInfoRowCreated, chat['created'], tsFormat: true);
      add(l10n.chatInfoRowModified, chat['modified'], tsFormat: true);
      final opts = chat['options'] as Map?;
      add(l10n.chatInfoRowOfficialChannel, opts?['OFFICIAL'] as bool?);
      add(l10n.chatInfoRowComments, opts?['COMMENTS'] as bool?);
      add(l10n.chatInfoRowRkn, opts?['A_PLUS_CHANNEL'] as bool?);
      add(l10n.chatInfoRowSignAdmin, opts?['SIGN_ADMIN'] as bool?);
      add(
        l10n.chatInfoRowOnlyAdmin,
        opts?['ONLY_ADMIN_CAN_ADD_MEMBER'] as bool?,
      );
      add(l10n.callInfoStatus, chat['status']);
    }

    if (rows.isEmpty) {
      return Text(
        l10n.chatInfoNoData,
        style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
      );
    }

    final extraRows = _buildExtraContactRows();

    return AnimatedSize(
      duration: const Duration(milliseconds: 220),
      curve: Curves.easeOutCubic,
      alignment: Alignment.topCenter,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisSize: MainAxisSize.min,
        children: [
          for (int i = 0; i < rows.length; i++) ...[
            _infoRow(
              cs,
              rows[i].label,
              rows[i].value,
              trailing: _trailingFor(rows[i].label, cs),
            ),
            if (i < rows.length - 1 ||
                (_extraContactExpanded && extraRows.isNotEmpty))
              Divider(
                height: 10,
                color: cs.outlineVariant.withValues(alpha: 0.25),
              ),
          ],
          if (_extraContactExpanded)
            for (int i = 0; i < extraRows.length; i++) ...[
              _infoRow(cs, extraRows[i].label, extraRows[i].value),
              if (i < extraRows.length - 1)
                Divider(
                  height: 10,
                  color: cs.outlineVariant.withValues(alpha: 0.25),
                ),
            ],
        ],
      ),
    );
  }

  List<({String label, String value})> _buildExtraContactRows() {
    final c = _contactData;
    if (c == null) return const [];
    final rows = <({String label, String value})>[];
    final reg = c.raw['registrationTime'];
    if (reg is int && reg > 0) {
      rows.add((
        label: l10n.contactProfileInfoRegistration,
        value: formatDateTimeNumeric(DateTime.fromMillisecondsSinceEpoch(reg)),
      ));
    }
    final upd = c.raw['updateTime'];
    if (upd is int && upd > 0) {
      rows.add((
        label: l10n.contactProfileInfoUpdated,
        value: formatDateTimeNumeric(DateTime.fromMillisecondsSinceEpoch(upd)),
      ));
    }
    final country = c.raw['country'];
    if (country is String && country.isNotEmpty) {
      rows.add((label: l10n.contactProfileInfoCountry, value: country));
    }
    final gender = c.raw['gender'];
    if (gender is int) {
      final g = formatGender(gender);
      if (g != null) rows.add((label: l10n.contactProfileInfoGender, value: g));
    }
    final phone = c.raw['phone'];
    if (phone is int && phone > 0) {
      rows.add((label: l10n.contactProfileInfoPhone, value: '+$phone'));
    } else if (phone is String && phone.isNotEmpty && phone != '***') {
      rows.add((label: l10n.contactProfileInfoPhone, value: phone));
    }
    final accStatus = c.raw['accountStatus'];
    if (accStatus is int && accStatus != 0) {
      rows.add((
        label: l10n.contactProfileInfoAccountStatus,
        value: accStatus.toString(),
      ));
    }
    final opts = c.raw['options'];
    if (opts is List && opts.isNotEmpty) {
      rows.add((
        label: l10n.contactProfileInfoFlags,
        value: opts.whereType<String>().join(', '),
      ));
    }
    final link = c.raw['link'];
    if (link is String && link.isNotEmpty) {
      rows.add((label: l10n.contactProfileInfoLink, value: link));
    }
    return rows;
  }

  Widget? _trailingFor(String label, ColorScheme cs) {
    if (label != l10n.chatInfoRowId) return null;
    if (widget.chatType != 'DIALOG') return null;
    if (_contactData == null) return null;
    return IconButton(
      tooltip: _extraContactExpanded
          ? l10n.chatInfoHideExtra
          : l10n.chatInfoShowMoreExtra,
      icon: AnimatedRotation(
        turns: _extraContactExpanded ? 0.125 : 0,
        duration: const Duration(milliseconds: 220),
        child: Icon(Symbols.add_circle, color: cs.primary, size: 22),
      ),
      padding: EdgeInsets.zero,
      constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
      onPressed: () =>
          setState(() => _extraContactExpanded = !_extraContactExpanded),
    );
  }

  Widget _infoRow(
    ColorScheme cs,
    String label,
    String value, {
    Widget? trailing,
  }) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  label,
                  style: TextStyle(color: cs.onSurfaceVariant, fontSize: 10),
                ),
                Text(
                  value,
                  style: TextStyle(
                    color: cs.onSurface,
                    fontSize: 12,
                    fontWeight: FontWeight.w500,
                  ),
                ),
              ],
            ),
          ),
          ?trailing,
        ],
      ),
    );
  }

  Future<void> _loadAvatarHistory(int peerId) async {
    if (!_headerHasPhoto || _avatarHistoryBusy) return;
    _avatarHistoryBusy = true;
    final cached = ContactsModule.cachedPhotos(peerId);
    if (cached != null) _applyAvatarPhotos(cached);
    try {
      final photos = await ContactsModule.fetchPhotos(api, peerId, count: 30);
      if (!mounted) return;
      _avatarHistoryLoaded = true;
      _applyAvatarPhotos(photos);
    } catch (e) {
      logger.w('Не удалось получить историю аватарок $peerId: $e');
    } finally {
      _avatarHistoryBusy = false;
    }
  }

  void _applyAvatarPhotos(ContactPhotos photos) {
    final urls = <String>[];
    if (widget.imageUrl.isNotEmpty) urls.add(widget.imageUrl);
    for (final url in photos.urls) {
      if (url.isNotEmpty && !urls.contains(url)) urls.add(url);
    }
    if (urls.isEmpty || listEquals(urls, _avatarPages)) return;
    setState(() {
      _avatarPages = urls;
      _avatarTotal = math.max(photos.total, urls.length);
      _avatarIndex = _avatarIndex.clamp(0, urls.length - 1);
    });
  }

  Future<void> _loadStories(int peerId) async {
    if (!AppStories.current.value || _peerDeleted) return;
    final cached = storiesModule.previewOf(peerId);
    if (cached != null && !cached.isEmpty && mounted) {
      setState(() {
        _storyPreview = cached;
        _refreshUnreadStories();
      });
    }
    final fresh = await storiesModule.loadOwnerPreview(
      StoryOwner(ownerId: peerId),
    );
    if (!mounted) return;
    setState(() {
      _storyPreview = (fresh == null || fresh.isEmpty) ? null : fresh;
      _refreshUnreadStories();
    });
  }

  Future<void> _openStories() async {
    final preview = _storyPreview;
    if (preview == null) return;
    Haptics.tap();
    final avatarContext = _avatarKey.currentContext;
    await openStoryViewer(
      context,
      previews: [preview],
      origin: avatarContext == null ? null : storyOriginOf(avatarContext),
      ownerOverrides: {
        preview.owner.ownerId: StoryOwnerInfo(
          name: _customName,
          avatarUrl: _contactData?.avatarUrl ?? widget.imageUrl,
        ),
      },
    );
    if (!mounted) return;
    await _loadStories(preview.owner.ownerId);
  }

  List<Widget> _loadingBlocks(ColorScheme cs) {
    Widget block(double w, double h, {double r = 8}) => Container(
      width: w,
      height: h,
      decoration: BoxDecoration(
        color: cs.surfaceContainerHigh,
        borderRadius: BorderRadius.circular(r),
      ),
    );

    return [
      block(double.infinity, 60, r: 14),
      const SizedBox(height: 16),
      block(double.infinity, 36, r: 20),
      const SizedBox(height: 12),
      block(double.infinity, 120, r: 14),
    ];
  }
}
