import 'dart:async' show unawaited;

import 'package:flutter/material.dart';

import '../l10n/generated/app_localizations.dart';
import '../services/channel_spacer.dart';
import '../services/link_trust_service.dart';
import '../services/snapshot_state_mapper.dart';
import '../services/ts3_server_link.dart';
import '../src/rust/api.dart' as rust;
import 'bbcode_text.dart';

const double _chatSidebarTileExtent = 92;
const double _chatSidebarCompactTileExtent = 76;
const double _chatSidebarCompactHeight = 84;
const double _chatMobileBreakpoint = 600;

/// One chat/activity message shown in the chat hub.
class ChatEntry {
  /// Construct a chat entry.
  const ChatEntry({
    required this.senderId,
    required this.senderName,
    required this.message,
    required this.target,
    this.isSelf = false,
    this.timestamp,
    this.countsTowardUnread = true,
  });

  /// Sender client id.
  final BigInt senderId;

  /// Sender display name.
  final String senderName;

  /// Message body.
  final String message;

  /// Chat target.
  final rust.BridgeMessageTarget target;

  /// True when the local client sent this message.
  final bool isSelf;

  /// Local time when this message was received or sent.
  final DateTime? timestamp;

  /// Whether this entry should increment the message unread badge.
  final bool countsTowardUnread;

  /// True for direct-message targets.
  bool get isPrivate => target is rust.BridgeMessageTarget_Client;

  /// True for poke targets.
  bool get isPoke => target is rust.BridgeMessageTarget_Poke;

  /// True for synthetic or protocol-driven server activity rows.
  bool get isServerActivity =>
      target is rust.BridgeMessageTarget_Server && !countsTowardUnread;
}

String chatTimeLabel(DateTime? timestamp) {
  if (timestamp == null) return '--:--:--';
  String two(int value) => value.toString().padLeft(2, '0');
  return '${two(timestamp.hour)}:${two(timestamp.minute)}:${two(timestamp.second)}';
}

String pokeHistoryLine(AppL10n l10n, ChatEntry entry) {
  final time = chatTimeLabel(entry.timestamp);
  final peerName = entry.senderName.isNotEmpty ? entry.senderName : 'Unknown';
  final message = entry.message.trim();
  if (entry.isSelf) {
    if (message.isEmpty) {
      return l10n.pokeHistorySelfNoMessage(time, peerName);
    }
    return l10n.pokeHistorySelfWithMessage(time, peerName, message);
  }
  if (message.isEmpty) {
    return l10n.pokeHistoryIncomingNoMessage(time, peerName);
  }
  return l10n.pokeHistoryIncomingWithMessage(time, peerName, message);
}

enum _ActivityTone {
  positive,
  transition,
  warning,
  plain,
  username,
  channel,
  adminActor,
  automationActor,
  group,
}

sealed class _ActivitySegment {
  const _ActivitySegment(this.text, this.tone, {this.bold = false});

  final String text;
  final _ActivityTone tone;
  final bool bold;
}

class _ActivityTextSegment extends _ActivitySegment {
  const _ActivityTextSegment(super.text, super.tone, {super.bold = false});
}

List<_ActivitySegment> _activitySegments(String message) {
  final quotedMatches = RegExp(r'"[^"]+"').allMatches(message).toList();
  if (quotedMatches.isEmpty) {
    return [_ActivityTextSegment(message, _activityActionTone(message))];
  }

  final segments = <_ActivitySegment>[];
  var cursor = 0;
  for (var i = 0; i < quotedMatches.length; i++) {
    final match = quotedMatches[i];
    if (match.start > cursor) {
      final plain = message.substring(cursor, match.start);
      if (plain.isNotEmpty) {
        segments.add(
          _ActivityTextSegment(plain, _activityActionTone(plain.trim())),
        );
      }
    }
    final quoted = match.group(0)!;
    segments.add(_activityEntitySegment(quoted, message, i));
    cursor = match.end;
  }
  if (cursor < message.length) {
    final tail = message.substring(cursor);
    if (tail.isNotEmpty) {
      segments.add(
        _ActivityTextSegment(tail, _activityActionTone(tail.trim())),
      );
    }
  }
  return segments;
}

_ActivitySegment _activityEntitySegment(
  String quoted,
  String fullMessage,
  int quotedIndex,
) {
  final lower = fullMessage.toLowerCase();
  final after = fullMessage
      .substring(fullMessage.indexOf(quoted) + quoted.length)
      .toLowerCase();
  final value = quoted.substring(1, quoted.length - 1);

  if (lower.contains('channel group ')) {
    if (quotedIndex == 0) {
      return _ActivityTextSegment(quoted, _ActivityTone.group);
    }
    return _activityActorSegment(value, quoted);
  }
  if (lower.contains('server group ')) {
    if (quotedIndex == 0) {
      return _ActivityTextSegment(quoted, _ActivityTone.group);
    }
    return _activityActorSegment(value, quoted);
  }
  if (after.startsWith(' connected to channel') ||
      after.startsWith(' switched from channel') ||
      after.startsWith(' disconnected') ||
      after.startsWith(' was moved from channel') ||
      after.startsWith(' is now ')) {
    return _activityActorSegment(value, quoted);
  }
  if (lower.contains('channel ') &&
      (after.startsWith('.') ||
          after.startsWith(' to') ||
          after.startsWith(' from') ||
          after.startsWith(' by') ||
          after.isEmpty)) {
    return _ActivityTextSegment(quoted, _ActivityTone.channel, bold: true);
  }
  if (lower.contains('channel ') &&
      (quotedIndex == 1 || quotedIndex == 2 || quotedIndex == 3)) {
    return _ActivityTextSegment(quoted, _ActivityTone.channel, bold: true);
  }
  return _activityActorSegment(value, quoted);
}

_ActivitySegment _activityActorSegment(String value, String quoted) {
  final lower = value.toLowerCase();
  if (lower.contains('auto') ||
      lower.contains('automation') ||
      lower.contains('bot')) {
    return _ActivityTextSegment(
      quoted,
      _ActivityTone.automationActor,
      bold: true,
    );
  }
  if (lower.contains('server') ||
      lower.contains('admin') ||
      lower.contains('vigorous pro')) {
    return _ActivityTextSegment(quoted, _ActivityTone.adminActor, bold: true);
  }
  return _ActivityTextSegment(quoted, _ActivityTone.username, bold: true);
}

_ActivityTone _activityActionTone(String text) {
  final lower = text.toLowerCase();
  if (lower.contains('connected')) return _ActivityTone.positive;
  if (lower.contains('disconnected') ||
      lower.contains('dropped') ||
      lower.contains('lost') ||
      lower.contains('shutdown')) {
    return _ActivityTone.warning;
  }
  if (lower.contains('switched') ||
      lower.contains('moved') ||
      lower.contains('created') ||
      lower.contains('deleted') ||
      lower.contains('renamed') ||
      lower.contains('assigned') ||
      lower.contains('removed')) {
    return _ActivityTone.transition;
  }
  return _ActivityTone.plain;
}

Color _activityColor(BuildContext context, _ActivityTone tone) {
  return switch (tone) {
    _ActivityTone.positive => const Color(0xFF2F7D57),
    _ActivityTone.transition => const Color(0xFF5A7394),
    _ActivityTone.warning => const Color(0xFFC06A2B),
    _ActivityTone.plain => const Color(0xFF6C7C8F),
    _ActivityTone.username => const Color(0xFFC64A4A),
    _ActivityTone.channel => const Color(0xFF3B6EA8),
    _ActivityTone.adminActor => const Color(0xFF426B9A),
    _ActivityTone.automationActor => const Color(0xFFB64C4C),
    _ActivityTone.group => const Color(0xFF8B7A68),
  };
}

/// Resolve the best chat target to show when opening the chat page.
///
/// Returns null to show the chat hub.
rust.BridgeMessageTarget? resolveInitialChatTarget({
  required List<ChatEntry> messages,
  required BigInt? currentVoiceChannelId,
}) {
  bool hasServerActivity = false;
  for (final message in messages.reversed) {
    if (message.isPrivate) return message.target;
    if (message.target is rust.BridgeMessageTarget_Server) {
      hasServerActivity = true;
    }
  }
  if (currentVoiceChannelId != null) {
    return const rust.BridgeMessageTarget.channel();
  }
  if (hasServerActivity) {
    return const rust.BridgeMessageTarget.server();
  }
  return null;
}

/// Build server-activity entries from a fresh snapshot diff.
List<ChatEntry> buildServerActivityEntries({
  required rust.BridgeSnapshot previous,
  required rust.BridgeSnapshot current,
  DateTime? timestamp,
}) {
  final now = timestamp ?? DateTime.now();
  final previousClients = {
    for (final client in previous.clients) client.id: client,
  };
  final currentClients = {
    for (final client in current.clients) client.id: client,
  };
  final previousChannels = {
    for (final channel in previous.channels) channel.id: channel,
  };
  final currentChannels = {
    for (final channel in current.channels) channel.id: channel,
  };
  final entries = <ChatEntry>[];

  String clientLabel(rust.BridgeClient client, rust.BridgeSnapshot snapshot) {
    if (client.id == snapshot.ownClientId) return 'You';
    final name = client.name.isNotEmpty ? client.name : 'Unknown';
    return '"$name"';
  }

  String channelLabel(rust.BridgeSnapshot snapshot, BigInt channelId) {
    final name = snapshotChannelName(snapshot, channelId);
    if (name.isNotEmpty) return 'channel "$name"';
    return 'channel ${channelId.toString()}';
  }

  void addEntry(String message) {
    entries.add(
      ChatEntry(
        senderId: BigInt.zero,
        senderName: 'Server',
        message: message,
        target: const rust.BridgeMessageTarget.server(),
        timestamp: now,
        countsTowardUnread: false,
      ),
    );
  }

  for (final client in current.clients) {
    if (client.isServerQuery) continue;
    final previousClient = previousClients[client.id];
    if (previousClient == null) {
      addEntry(
        '${clientLabel(client, current)} connected to ${channelLabel(current, client.channel)}',
      );
      continue;
    }
    if (previousClient.channel != client.channel) {
      final fromChannel = channelLabel(previous, previousClient.channel);
      final toChannel = channelLabel(current, client.channel);
      addEntry(
        '${clientLabel(client, current)} switched from $fromChannel to $toChannel',
      );
      continue;
    }
    if (previousClient.name != client.name && client.name.isNotEmpty) {
      final previousName = previousClient.id == current.ownClientId
          ? 'You'
          : (previousClient.name.isNotEmpty ? previousClient.name : 'Unknown');
      final currentName = clientLabel(client, current);
      addEntry('$previousName is now $currentName');
    }
  }

  for (final client in previous.clients) {
    if (client.isServerQuery) continue;
    if (currentClients.containsKey(client.id)) continue;
    addEntry(
      '${clientLabel(client, previous)} disconnected from ${channelLabel(previous, client.channel)}',
    );
  }

  for (final channel in current.channels) {
    final previousChannel = previousChannels[channel.id];
    if (previousChannel == null) {
      addEntry('Channel created: ${channelLabel(current, channel.id)}');
      continue;
    }
    if (previousChannel.name != channel.name) {
      addEntry('Channel renamed to ${channelLabel(current, channel.id)}');
      continue;
    }
    if (previousChannel.parent != channel.parent ||
        previousChannel.order != channel.order) {
      addEntry('Channel moved: ${channelLabel(current, channel.id)}');
    }
  }

  for (final channel in previous.channels) {
    if (currentChannels.containsKey(channel.id)) continue;
    addEntry('Channel deleted: ${channelLabel(previous, channel.id)}');
  }

  return entries;
}

class ChatClientGroups {
  const ChatClientGroups({
    required this.clientsByChannel,
    required this.ungroupedClients,
  });

  final Map<BigInt, List<rust.BridgeClient>> clientsByChannel;
  final List<rust.BridgeClient> ungroupedClients;
}

class ChatClientPickerGroup {
  const ChatClientPickerGroup({
    required this.channelName,
    required this.clients,
  });

  final String channelName;
  final List<rust.BridgeClient> clients;
}

ChatClientGroups groupChatClientsForPicker(rust.BridgeSnapshot snapshot) {
  final ownId = snapshot.ownClientId;
  final clients =
      snapshot.clients
          .where((client) => client.id != ownId && !client.isServerQuery)
          .toList()
        ..sort((a, b) => a.name.compareTo(b.name));
  final channelIds = {for (final channel in snapshot.channels) channel.id};
  final byChannel = <BigInt, List<rust.BridgeClient>>{};
  final ungrouped = <rust.BridgeClient>[];

  for (final client in clients) {
    if (channelIds.contains(client.channel)) {
      byChannel.putIfAbsent(client.channel, () => []).add(client);
    } else {
      ungrouped.add(client);
    }
  }

  return ChatClientGroups(
    clientsByChannel: byChannel,
    ungroupedClients: ungrouped,
  );
}

List<ChatClientPickerGroup> filterChatClientPickerGroups({
  required List<rust.BridgeChannel> channels,
  required Map<BigInt, List<rust.BridgeClient>> clientsByChannel,
  required List<rust.BridgeClient> ungroupedClients,
  required String query,
}) {
  final normalizedQuery = query.trim().toLowerCase();
  bool matches(rust.BridgeClient client) {
    return normalizedQuery.isEmpty ||
        client.name.toLowerCase().contains(normalizedQuery);
  }

  final ungrouped = ungroupedClients.where(matches).toList();
  return [
    for (final channel in channels)
      if (clientsByChannel.containsKey(channel.id))
        ChatClientPickerGroup(
          channelName: channelSpacerLabel(channel.name),
          clients: clientsByChannel[channel.id]!.where(matches).toList(),
        ),
    if (ungrouped.isNotEmpty)
      ChatClientPickerGroup(channelName: 'Other', clients: ungrouped),
  ].where((group) => group.clients.isNotEmpty).toList();
}

String chatTargetTitle(
  rust.BridgeMessageTarget target, {
  required String channelName,
  required String clientName,
}) {
  switch (target) {
    case rust.BridgeMessageTarget_Server():
      return 'Server Activity';
    case rust.BridgeMessageTarget_Channel():
      return channelName.isNotEmpty ? '# $channelName' : 'Channel';
    case rust.BridgeMessageTarget_Client():
      return clientName.isNotEmpty ? clientName : 'Direct Message';
    case rust.BridgeMessageTarget_Poke():
      return clientName.isNotEmpty ? 'Poke: $clientName' : 'Poke';
  }
}

String chatEmptyTitle(rust.BridgeMessageTarget target) {
  switch (target) {
    case rust.BridgeMessageTarget_Server():
      return 'No server activity yet';
    case rust.BridgeMessageTarget_Channel():
      return 'No channel messages yet';
    case rust.BridgeMessageTarget_Client():
      return 'No private messages yet';
    case rust.BridgeMessageTarget_Poke():
      return 'No pokes';
  }
}

String chatEmptyBody(
  rust.BridgeMessageTarget target, {
  required String channelName,
}) {
  switch (target) {
    case rust.BridgeMessageTarget_Server():
      return 'Joins, leaves, and disconnects will appear here.';
    case rust.BridgeMessageTarget_Channel():
      if (channelName.isNotEmpty) {
        return 'Start the conversation in #$channelName.';
      }
      return 'Join a channel to start chatting.';
    case rust.BridgeMessageTarget_Client():
      return 'Select a user to start a private chat.';
    case rust.BridgeMessageTarget_Poke():
      return 'Pokes will appear here when someone needs your attention.';
  }
}

String chatInputPlaceholder(
  rust.BridgeMessageTarget target, {
  required String channelName,
  required String clientName,
}) {
  switch (target) {
    case rust.BridgeMessageTarget_Server():
      return 'Message server...';
    case rust.BridgeMessageTarget_Channel():
      return 'Message #$channelName...';
    case rust.BridgeMessageTarget_Client():
      return 'Message $clientName...';
    case rust.BridgeMessageTarget_Poke():
      return 'Poke message...';
  }
}

bool canSendToChatTarget(
  rust.BridgeMessageTarget target,
  BigInt? currentChannelId,
) {
  switch (target) {
    case rust.BridgeMessageTarget_Channel():
      return currentChannelId != null;
    default:
      return true;
  }
}

String? chatSendBlockedReason(
  rust.BridgeMessageTarget target,
  BigInt? currentChannelId,
) {
  switch (target) {
    case rust.BridgeMessageTarget_Channel():
      if (currentChannelId == null) {
        return 'Join a channel to send messages.';
      }
      return null;
    default:
      return null;
  }
}

/// Chat and activity hub page.
class ChatPage extends StatefulWidget {
  /// Construct a chat page.
  const ChatPage({
    super.key,
    required this.messages,
    required this.snapshot,
    this.messagesSource,
    this.snapshotSource,
    this.refreshListenable,
    this.initialTarget,
    this.initialClientName = '',
    this.onTs3ServerLink,
  });

  /// Backing message list. The detail view appends self-sent messages.
  final List<ChatEntry> messages;

  /// Latest TeamSpeak snapshot.
  final rust.BridgeSnapshot snapshot;

  /// Optional live source for messages while this route stays open.
  final List<ChatEntry> Function()? messagesSource;

  /// Optional live source for snapshots while this route stays open.
  final rust.BridgeSnapshot Function()? snapshotSource;

  /// Triggers a rebuild when the backing chat state changes.
  final Listenable? refreshListenable;

  /// Non-null to open a specific target directly; null for hub.
  final rust.BridgeMessageTarget? initialTarget;

  /// Initial selected client name for direct-message or poke targets.
  final String initialClientName;

  /// Handle TeamSpeak server links embedded in chat messages.
  final Ts3ServerLinkHandler? onTs3ServerLink;

  @override
  State<ChatPage> createState() => _ChatPageState();
}

class _ChatPageState extends State<ChatPage> {
  late rust.BridgeMessageTarget _selectedTarget;
  String _selectedClientName = '';
  final Set<BigInt> _closedPrivateChats = {};

  List<ChatEntry> get _messages =>
      widget.messagesSource?.call() ?? widget.messages;

  rust.BridgeSnapshot get _snapshot =>
      widget.snapshotSource?.call() ?? widget.snapshot;

  @override
  void initState() {
    super.initState();
    _selectedTarget =
        widget.initialTarget ??
        resolveInitialChatTarget(
          messages: _messages,
          currentVoiceChannelId: _currentChannelId,
        ) ??
        const rust.BridgeMessageTarget.server();
    _selectedClientName = widget.initialClientName;
  }

  BigInt? get _currentChannelId {
    return ownClientSnapshotState(_snapshot)?.channelId;
  }

  @override
  Widget build(BuildContext context) {
    if (widget.refreshListenable != null) {
      return ListenableBuilder(
        listenable: widget.refreshListenable!,
        builder: (context, _) => _buildScaffold(context),
      );
    }
    return _buildScaffold(context);
  }

  Widget _buildScaffold(BuildContext context) {
    final snapshot = _snapshot;
    final messages = _messages;
    final currentChannelId = _currentChannelId;
    final channelName = snapshotChannelName(snapshot, currentChannelId);
    final l10n = AppL10n.of(context);
    final detail = _ChatDetailView(
      target: _selectedTarget,
      clientName: _selectedClientName,
      snapshot: snapshot,
      messages: messages,
      currentChannelId: currentChannelId,
      channelName: channelName,
      onTs3ServerLink: widget.onTs3ServerLink,
    );
    return Scaffold(
      appBar: AppBar(
        title: Text('${l10n.chatAction} - ${snapshot.serverName}'),
        actions: [
          IconButton(
            tooltip: l10n.chatCloseAction,
            icon: const Icon(Icons.close),
            onPressed: _selectedPrivateClientId == null
                ? null
                : _closeSelectedPrivateChat,
          ),
        ],
      ),
      body: LayoutBuilder(
        builder: (context, constraints) {
          final sidebar = _ChatSidebar(
            compact: constraints.maxWidth < _chatMobileBreakpoint,
            selectedTarget: _selectedTarget,
            privateChats: _privateChats,
            onSelect: _selectTarget,
            onNewPrivateChat: () => _pickClient((id, name) {
              _closedPrivateChats.remove(id);
              _selectTarget(rust.BridgeMessageTarget.client(id), name: name);
            }),
          );
          if (constraints.maxWidth < _chatMobileBreakpoint) {
            return Column(
              children: [
                sidebar,
                const Divider(height: 1),
                Expanded(child: detail),
              ],
            );
          }
          return Row(
            children: [
              sidebar,
              const VerticalDivider(width: 1),
              Expanded(child: detail),
            ],
          );
        },
      ),
    );
  }

  BigInt? get _selectedPrivateClientId {
    final target = _selectedTarget;
    return target is rust.BridgeMessageTarget_Client ? target.field0 : null;
  }

  List<_PrivateChatItem> get _privateChats {
    final chats = <BigInt, _PrivateChatItem>{};
    for (final message in _messages) {
      final target = message.target;
      if (target is! rust.BridgeMessageTarget_Client) continue;
      final id = target.field0;
      if (_closedPrivateChats.contains(id)) continue;
      final existing = chats[id];
      final name = existing?.name.isNotEmpty == true
          ? existing!.name
          : _privateChatName(id, message.senderName);
      chats[id] = _PrivateChatItem(id: id, name: name);
    }
    final selectedId = _selectedPrivateClientId;
    if (selectedId != null && !_closedPrivateChats.contains(selectedId)) {
      chats.putIfAbsent(
        selectedId,
        () => _PrivateChatItem(
          id: selectedId,
          name: _selectedClientName.isNotEmpty ? _selectedClientName : 'Direct',
        ),
      );
    }
    return chats.values.toList()..sort((a, b) => a.name.compareTo(b.name));
  }

  String _privateChatName(BigInt id, String fallback) {
    for (final client in _snapshot.clients) {
      if (client.id == id && client.name.isNotEmpty) return client.name;
    }
    return fallback.isNotEmpty && fallback != 'You' ? fallback : 'Direct';
  }

  void _selectTarget(rust.BridgeMessageTarget target, {String name = ''}) {
    setState(() {
      _selectedTarget = target;
      _selectedClientName = name;
    });
  }

  void _closeSelectedPrivateChat() {
    final id = _selectedPrivateClientId;
    if (id == null) return;
    setState(() {
      _closedPrivateChats.add(id);
      _selectedTarget = _currentChannelId != null
          ? const rust.BridgeMessageTarget.channel()
          : const rust.BridgeMessageTarget.server();
      _selectedClientName = '';
    });
  }

  void _pickClient(void Function(BigInt id, String name) cb) {
    final groups = groupChatClientsForPicker(widget.snapshot);
    showDialog(
      context: context,
      builder: (ctx) => _ClientPickerDialog(
        channels: widget.snapshot.channels,
        byChannel: groups.clientsByChannel,
        ungrouped: groups.ungroupedClients,
        onSelected: (c) {
          cb(c.id, c.name);
          Navigator.pop(ctx);
        },
      ),
    );
  }
}

class _PrivateChatItem {
  const _PrivateChatItem({required this.id, required this.name});

  final BigInt id;
  final String name;
}

class _ChatSidebar extends StatelessWidget {
  const _ChatSidebar({
    required this.compact,
    required this.selectedTarget,
    required this.privateChats,
    required this.onSelect,
    required this.onNewPrivateChat,
  });

  final bool compact;
  final rust.BridgeMessageTarget selectedTarget;
  final List<_PrivateChatItem> privateChats;
  final void Function(rust.BridgeMessageTarget target, {String name}) onSelect;
  final VoidCallback onNewPrivateChat;

  @override
  Widget build(BuildContext context) {
    final l10n = AppL10n.of(context);
    final fixedItems = [
      _ChatSidebarItem(
        compact: compact,
        icon: Icons.dns_outlined,
        label: 'Server',
        selected: selectedTarget is rust.BridgeMessageTarget_Server,
        onTap: () => onSelect(const rust.BridgeMessageTarget.server()),
      ),
      _ChatSidebarItem(
        compact: compact,
        icon: Icons.tag,
        label: 'Channel',
        selected: selectedTarget is rust.BridgeMessageTarget_Channel,
        onTap: () => onSelect(const rust.BridgeMessageTarget.channel()),
      ),
    ];
    final privateItems = [
      for (final chat in privateChats)
        _ChatSidebarItem(
          compact: compact,
          icon: Icons.person_outline,
          label: chat.name.isNotEmpty ? chat.name : 'Direct',
          selected: switch (selectedTarget) {
            rust.BridgeMessageTarget_Client(:final field0) => field0 == chat.id,
            _ => false,
          },
          onTap: () => onSelect(
            rust.BridgeMessageTarget.client(chat.id),
            name: chat.name,
          ),
        ),
    ];
    final addButton = Padding(
      padding: EdgeInsets.all(compact ? 4 : 8),
      child: IconButton.filledTonal(
        tooltip: l10n.chatNewPrivateAction,
        icon: const Icon(Icons.add),
        onPressed: onNewPrivateChat,
      ),
    );
    final compactDivider = ColoredBox(
      color: Theme.of(context).dividerColor,
      child: const SizedBox(width: 1, height: double.infinity),
    );

    if (compact) {
      return SizedBox(
        height: _chatSidebarCompactHeight,
        child: Row(
          children: [
            ...fixedItems,
            compactDivider,
            Expanded(
              child: ListView(
                scrollDirection: Axis.horizontal,
                padding: EdgeInsets.zero,
                children: privateItems,
              ),
            ),
            compactDivider,
            addButton,
          ],
        ),
      );
    }

    return SizedBox(
      width: _chatSidebarTileExtent,
      child: Column(
        children: [
          ...fixedItems,
          const Divider(height: 1),
          Expanded(
            child: ListView(padding: EdgeInsets.zero, children: privateItems),
          ),
          const Divider(height: 1),
          addButton,
        ],
      ),
    );
  }
}

class _ChatSidebarItem extends StatelessWidget {
  const _ChatSidebarItem({
    required this.compact,
    required this.icon,
    required this.label,
    required this.selected,
    required this.onTap,
  });

  final bool compact;
  final IconData icon;
  final String label;
  final bool selected;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final colorScheme = theme.colorScheme;
    final fg = selected
        ? colorScheme.onSecondaryContainer
        : colorScheme.onSurface;
    final tileExtent = compact
        ? _chatSidebarCompactTileExtent
        : _chatSidebarTileExtent;
    return Material(
      color: Colors.transparent,
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(8),
        child: Ink(
          width: tileExtent,
          height: compact ? 72 : tileExtent,
          decoration: BoxDecoration(
            color: selected
                ? colorScheme.secondaryContainer
                : Colors.transparent,
            borderRadius: BorderRadius.circular(8),
          ),
          child: Padding(
            padding: EdgeInsets.symmetric(
              horizontal: compact ? 6 : 8,
              vertical: compact ? 8 : 10,
            ),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                SizedBox(
                  width: 24,
                  height: 24,
                  child: Stack(
                    clipBehavior: Clip.none,
                    alignment: Alignment.center,
                    children: [Icon(icon, size: 18, color: fg)],
                  ),
                ),
                const SizedBox(height: 8),
                Text(
                  label,
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                  textAlign: TextAlign.center,
                  style: theme.textTheme.labelSmall?.copyWith(
                    color: fg,
                    height: 1.1,
                    fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class _ClientPickerDialog extends StatefulWidget {
  const _ClientPickerDialog({
    required this.channels,
    required this.byChannel,
    required this.ungrouped,
    required this.onSelected,
  });

  final List<rust.BridgeChannel> channels;
  final Map<BigInt, List<rust.BridgeClient>> byChannel;
  final List<rust.BridgeClient> ungrouped;
  final void Function(rust.BridgeClient c) onSelected;

  @override
  State<_ClientPickerDialog> createState() => _ClientPickerDialogState();
}

class _ClientPickerDialogState extends State<_ClientPickerDialog> {
  final _searchCtl = TextEditingController();
  String _query = '';

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

  @override
  Widget build(BuildContext context) {
    final l10n = AppL10n.of(context);
    final groups = filterChatClientPickerGroups(
      channels: widget.channels,
      clientsByChannel: widget.byChannel,
      ungroupedClients: widget.ungrouped,
      query: _query,
    );

    return Dialog(
      child: SizedBox(
        width: 360,
        height: 520,
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.fromLTRB(20, 20, 20, 8),
              child: TextField(
                controller: _searchCtl,
                decoration: InputDecoration(
                  hintText: l10n.chatSearchClientsHint,
                  prefixIcon: const Icon(Icons.search),
                  border: const OutlineInputBorder(),
                  isDense: true,
                ),
                onChanged: (v) => setState(() => _query = v),
              ),
            ),
            Expanded(
              child: ListView(
                children: [
                  for (final group in groups)
                    _ChannelGroup(
                      channelName: group.channelName,
                      clients: group.clients,
                      onSelected: widget.onSelected,
                    ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _ChannelGroup extends StatelessWidget {
  const _ChannelGroup({
    required this.channelName,
    required this.clients,
    required this.onSelected,
  });

  final String channelName;
  final List<rust.BridgeClient> clients;
  final void Function(rust.BridgeClient) onSelected;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    if (clients.isEmpty) return const SizedBox.shrink();
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
          child: Text(
            channelName,
            style: theme.textTheme.labelMedium?.copyWith(
              color: theme.colorScheme.primary,
              fontWeight: FontWeight.w600,
            ),
          ),
        ),
        for (final c in clients)
          ListTile(
            leading: CircleAvatar(
              radius: 12,
              child: Text(
                c.name.isNotEmpty ? c.name[0].toUpperCase() : '?',
                style: const TextStyle(fontSize: 10),
              ),
            ),
            title: Text(c.name),
            onTap: () => onSelected(c),
          ),
      ],
    );
  }
}

class _ChatDetailView extends StatefulWidget {
  const _ChatDetailView({
    required this.target,
    required this.clientName,
    required this.snapshot,
    required this.messages,
    required this.currentChannelId,
    required this.channelName,
    this.onTs3ServerLink,
  });

  final rust.BridgeMessageTarget target;
  final String clientName;
  final rust.BridgeSnapshot snapshot;
  final List<ChatEntry> messages;
  final BigInt? currentChannelId;
  final String channelName;
  final Ts3ServerLinkHandler? onTs3ServerLink;

  @override
  State<_ChatDetailView> createState() => _ChatDetailViewState();
}

class _ChatDetailViewState extends State<_ChatDetailView> {
  final _textCtl = TextEditingController();
  final _scrollCtl = ScrollController();
  int _lastRenderedMessageCount = -1;
  rust.BridgeMessageTarget? _lastRenderedTarget;

  Iterable<ChatEntry> get _filtered {
    if (widget.target is rust.BridgeMessageTarget_Channel) {
      return widget.messages.where(
        (m) => m.target == widget.target || m.isPoke,
      );
    }
    return widget.messages.where((m) => m.target == widget.target);
  }

  bool get _canSend =>
      canSendToChatTarget(widget.target, widget.currentChannelId);

  String? get _blockedReason =>
      chatSendBlockedReason(widget.target, widget.currentChannelId);

  String get _title => chatTargetTitle(
    widget.target,
    channelName: widget.channelName,
    clientName: widget.clientName,
  );

  @override
  void dispose() {
    _textCtl.dispose();
    _scrollCtl.dispose();
    super.dispose();
  }

  void _send() {
    final text = _textCtl.text.trim();
    if (text.isEmpty || !_canSend) return;
    _textCtl.clear();
    unawaited(rust.sendChatMessage(message: text, target: widget.target));
    final ownId = widget.snapshot.ownClientId;
    setState(() {
      widget.messages.add(
        ChatEntry(
          senderId: ownId,
          senderName: widget.target is rust.BridgeMessageTarget_Poke
              ? widget.clientName
              : 'You',
          message: text,
          target: widget.target,
          isSelf: true,
          timestamp: DateTime.now(),
        ),
      );
      if (widget.messages.length > 200) {
        widget.messages.removeRange(0, widget.messages.length - 200);
      }
    });
    _scrollToBottom();
  }

  void _scrollToBottom() {
    if (_scrollCtl.hasClients) {
      _scrollCtl.animateTo(
        _scrollCtl.position.maxScrollExtent,
        duration: const Duration(milliseconds: 100),
        curve: Curves.easeOut,
      );
    }
  }

  void _scheduleScrollIfNeeded(int messageCount) {
    final targetChanged = _lastRenderedTarget != widget.target;
    final countChanged = _lastRenderedMessageCount != messageCount;
    _lastRenderedTarget = widget.target;
    _lastRenderedMessageCount = messageCount;
    if (!targetChanged && !countChanged) return;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) _scrollToBottom();
    });
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final msgs = _filtered.toList();
    _scheduleScrollIfNeeded(msgs.length);
    final placeholder = chatInputPlaceholder(
      widget.target,
      channelName: widget.channelName,
      clientName: widget.clientName,
    );

    return Column(
      children: [
        Container(
          height: 48,
          padding: const EdgeInsets.symmetric(horizontal: 16),
          alignment: Alignment.centerLeft,
          decoration: BoxDecoration(
            border: Border(
              bottom: BorderSide(color: theme.colorScheme.outlineVariant),
            ),
          ),
          child: Text(_title, style: theme.textTheme.titleMedium),
        ),
        Expanded(
          child: msgs.isEmpty
              ? Center(
                  child: Padding(
                    padding: const EdgeInsets.all(32),
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Icon(
                          Icons.forum_outlined,
                          size: 48,
                          color: theme.colorScheme.onSurfaceVariant,
                        ),
                        const SizedBox(height: 16),
                        Text(
                          chatEmptyTitle(widget.target),
                          style: theme.textTheme.titleMedium,
                          textAlign: TextAlign.center,
                        ),
                        const SizedBox(height: 8),
                        Text(
                          chatEmptyBody(
                            widget.target,
                            channelName: widget.channelName,
                          ),
                          style: theme.textTheme.bodyMedium?.copyWith(
                            color: theme.colorScheme.onSurfaceVariant,
                          ),
                          textAlign: TextAlign.center,
                        ),
                      ],
                    ),
                  ),
                )
              : ListView.builder(
                  controller: _scrollCtl,
                  padding: const EdgeInsets.symmetric(vertical: 8),
                  itemCount: msgs.length,
                  itemBuilder: (_, i) => _MessageBubble(
                    entry: msgs[i],
                    onTs3ServerLink: widget.onTs3ServerLink,
                  ),
                ),
        ),
        if (_blockedReason != null)
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(12),
            color: theme.colorScheme.surfaceContainerHighest,
            child: Text(
              _blockedReason!,
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
              textAlign: TextAlign.center,
            ),
          ),
        if (_canSend && _blockedReason == null)
          Padding(
            padding: const EdgeInsets.all(8),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _textCtl,
                    decoration: InputDecoration(
                      hintText: placeholder,
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(24),
                      ),
                      contentPadding: const EdgeInsets.symmetric(
                        horizontal: 16,
                        vertical: 10,
                      ),
                    ),
                    textInputAction: TextInputAction.send,
                    onSubmitted: (_) => _send(),
                  ),
                ),
                const SizedBox(width: 8),
                IconButton.filled(
                  icon: const Icon(Icons.send),
                  onPressed: _send,
                  tooltip: 'Send',
                ),
              ],
            ),
          ),
      ],
    );
  }
}

class _MessageBubble extends StatelessWidget {
  const _MessageBubble({required this.entry, this.onTs3ServerLink});

  final ChatEntry entry;
  final Ts3ServerLinkHandler? onTs3ServerLink;

  @override
  Widget build(BuildContext context) {
    if (entry.isPoke) {
      return _PokeHistoryRow(entry: entry);
    }
    if (entry.isServerActivity) {
      return _ServerActivityRow(entry: entry);
    }
    final theme = Theme.of(context);
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          CircleAvatar(
            radius: 14,
            child: Text(
              entry.senderName.isNotEmpty
                  ? entry.senderName[0].toUpperCase()
                  : '?',
              style: const TextStyle(fontSize: 12),
            ),
          ),
          const SizedBox(width: 8),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  entry.senderName,
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    fontSize: 12,
                    color: entry.isSelf
                        ? theme.colorScheme.primary
                        : entry.isPrivate
                        ? theme.colorScheme.tertiary
                        : null,
                  ),
                ),
                const SizedBox(height: 2),
                Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 10,
                    vertical: 6,
                  ),
                  decoration: BoxDecoration(
                    color: entry.isSelf
                        ? theme.colorScheme.primaryContainer
                        : theme.colorScheme.surfaceContainerHighest,
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: DefaultTextStyle.merge(
                    style: theme.textTheme.bodyMedium,
                    child: BbCodeText(
                      entry.message,
                      linkTrust: LinkTrustService.instance,
                      onTs3ServerLink: onTs3ServerLink,
                    ),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class _ServerActivityRow extends StatelessWidget {
  const _ServerActivityRow({required this.entry});

  final ChatEntry entry;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final timestampStyle = theme.textTheme.bodySmall?.copyWith(
      color: const Color(0xFF8A98A8),
      fontStyle: FontStyle.italic,
      fontWeight: FontWeight.w400,
      letterSpacing: 0.15,
      fontFeatures: const [FontFeature.tabularFigures()],
    );
    final baseStyle = theme.textTheme.bodySmall?.copyWith(
      color: const Color(0xFF6C7C8F),
      fontWeight: FontWeight.w400,
      fontSize: 13,
      height: 1.45,
      letterSpacing: 0.05,
    );
    final spans = <InlineSpan>[
      TextSpan(
        text: '<${chatTimeLabel(entry.timestamp)}> ',
        style: timestampStyle,
      ),
    ];
    for (final segment in _activitySegments(entry.message)) {
      spans.add(
        TextSpan(
          text: segment.text,
          style: baseStyle?.copyWith(
            color: _activityColor(context, segment.tone),
            fontWeight: segment.bold ? FontWeight.w700 : FontWeight.w400,
          ),
        ),
      );
    }
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 3),
      child: DecoratedBox(
        decoration: BoxDecoration(
          color: const Color(0xFFF7F9FC),
          borderRadius: BorderRadius.circular(10),
        ),
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
          child: Text.rich(TextSpan(children: spans)),
        ),
      ),
    );
  }
}

class _PokeHistoryRow extends StatelessWidget {
  const _PokeHistoryRow({required this.entry});

  final ChatEntry entry;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final l10n = AppL10n.of(context);
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
      child: DecoratedBox(
        decoration: BoxDecoration(
          color: theme.colorScheme.secondaryContainer.withValues(alpha: 0.45),
          borderRadius: BorderRadius.circular(8),
          border: Border.all(
            color: theme.colorScheme.secondary.withValues(alpha: 0.22),
          ),
        ),
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Icon(
                Icons.notifications_none,
                size: 16,
                color: theme.colorScheme.onSecondaryContainer,
              ),
              const SizedBox(width: 8),
              Expanded(
                child: Text(
                  pokeHistoryLine(l10n, entry),
                  style: theme.textTheme.bodySmall?.copyWith(
                    color: theme.colorScheme.onSecondaryContainer,
                    fontFeatures: const [FontFeature.tabularFigures()],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
