import 'package:flutter/material.dart';
import 'package:flutter/services.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';
import 'talk_power_warning.dart';

/// Connected-server snapshot with welcome text, channels, and clients.
class SnapshotView extends StatefulWidget {
  /// Construct a snapshot view.
  const SnapshotView({
    super.key,
    required this.snapshot,
    required this.audioStats,
    required this.currentVoiceChannelId,
    required this.pendingVoiceChannelId,
    required this.localInputMuted,
    required this.localOutputMuted,
    required this.hasJoinPending,
    required this.canJoinVoiceChannel,
    required this.onJoinChannel,
    required this.onJoinChannelWithPassword,
    this.enableClientLongPressMenu = false,
    this.onOpenClientInfo,
    this.onOpenClientChat,
    this.onOpenClientPoke,
    this.onTs3ServerLink,
  });

  /// Current bridge snapshot.
  final rust.BridgeSnapshot snapshot;

  /// Latest audio stats, used for local speaking state.
  final rust.BridgeAudioStats? audioStats;

  /// Current voice channel id.
  final BigInt? currentVoiceChannelId;

  /// Pending join target, if any.
  final BigInt? pendingVoiceChannelId;

  /// Local input mute state.
  final bool localInputMuted;

  /// Local output mute state.
  final bool localOutputMuted;

  /// True while a channel join is in flight.
  final bool hasJoinPending;

  /// True when the local client may join voice channels.
  final bool canJoinVoiceChannel;

  /// Join an unlocked channel.
  final ValueChanged<rust.BridgeChannel> onJoinChannel;

  /// Join a password-protected channel.
  final ValueChanged<rust.BridgeChannel> onJoinChannelWithPassword;

  /// True on touch-only mobile hosts where long-press opens client actions.
  final bool enableClientLongPressMenu;

  /// Open richer profile details for a non-self client.
  final ValueChanged<rust.BridgeClient>? onOpenClientInfo;

  /// Open a direct chat with a non-self client.
  final ValueChanged<rust.BridgeClient>? onOpenClientChat;

  /// Open a poke composer for a non-self client.
  final ValueChanged<rust.BridgeClient>? onOpenClientPoke;

  /// Handle TeamSpeak server links embedded in server-provided text.
  final Ts3ServerLinkHandler? onTs3ServerLink;

  @override
  State<SnapshotView> createState() => _SnapshotViewState();
}

class _SnapshotViewState extends State<SnapshotView> {
  static const _indentPerLevel = 12.0;
  static const _expandColumnWidth = 28.0;
  static const _channelIconColumnWidth = 24.0;
  static const _channelTextGap = 8.0;
  static const _userRowStartIndent = 32.0;

  final _scrollController = ScrollController();
  final Map<BigInt, bool> _channelExpandedById = {};
  final _clientVolumePrefs = _ClientVolumePreferences.instance;
  bool _welcomeExpanded = false;
  double _welcomeHeight = 0;
  final _welcomeKey = GlobalKey();

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_onScroll);
    _clientVolumePrefs.addListener(_onClientVolumePrefsChanged);
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final ctx = _welcomeKey.currentContext;
      if (ctx != null) {
        final box = ctx.findRenderObject() as RenderBox?;
        if (box != null && mounted) {
          setState(() => _welcomeHeight = box.size.height);
        }
      }
    });
  }

  void _onScroll() {
    if (_welcomeExpanded && _scrollController.offset > _welcomeHeight) {
      setState(() => _welcomeExpanded = false);
    }
  }

  void _onClientVolumePrefsChanged() {
    if (mounted) setState(() {});
  }

  @override
  void dispose() {
    _clientVolumePrefs.removeListener(_onClientVolumePrefsChanged);
    _scrollController.removeListener(_onScroll);
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppL10n.of(context);
    final theme = Theme.of(context);
    final tree = _buildChannelTree(widget.snapshot.channels);
    final clientsByChannel = <BigInt, List<rust.BridgeClient>>{};
    for (final c in widget.snapshot.clients) {
      if (!c.isServerQuery) {
        clientsByChannel.putIfAbsent(c.channel, () => []).add(c);
      }
    }

    return ListView(
      controller: _scrollController,
      children: [
        Text(
          l10n.countChannelsAndClients(
            widget.snapshot.channels.length,
            widget.snapshot.clients.length,
          ),
          style: theme.textTheme.bodyMedium,
        ),
        if (widget.snapshot.welcomeMessage.isNotEmpty) ...[
          const SizedBox(height: 8),
          _WelcomeMessageTile(
            key: _welcomeKey,
            welcomeMessage: widget.snapshot.welcomeMessage,
            expanded: _welcomeExpanded,
            onToggle: () =>
                setState(() => _welcomeExpanded = !_welcomeExpanded),
            onTs3ServerLink: widget.onTs3ServerLink,
          ),
        ],
        const Divider(height: 24),
        for (final node in tree.roots)
          ..._channelTreeRows(theme, node, clientsByChannel, 0),
      ],
    );
  }

  List<Widget> _channelTreeRows(
    ThemeData theme,
    _ChannelTreeNode node,
    Map<BigInt, List<rust.BridgeClient>> clientsByChannel,
    int depth,
  ) {
    final channel = node.channel;
    final clients = clientsByChannel[channel.id] ?? const <rust.BridgeClient>[];
    final hasVisibleChildren = clients.isNotEmpty || node.children.isNotEmpty;
    final expanded = _isChannelExpanded(channel.id);
    final channelIndent = (depth.clamp(0, 8)) * _indentPerLevel;

    return [
      _channelTile(
        theme,
        channel,
        channelIndent: channelIndent,
        hasVisibleChildren: hasVisibleChildren,
        expanded: expanded,
        onToggleExpanded: hasVisibleChildren
            ? () => _toggleChannelExpanded(channel.id)
            : null,
      ),
      if (expanded) ...[
        for (final client in clients) _clientTile(theme, client, channelIndent),
        for (final child in node.children)
          ..._channelTreeRows(theme, child, clientsByChannel, depth + 1),
      ],
    ];
  }

  Widget _channelTile(
    ThemeData theme,
    rust.BridgeChannel channel, {
    required double channelIndent,
    required bool hasVisibleChildren,
    required bool expanded,
    required VoidCallback? onToggleExpanded,
  }) {
    final spacer = parseSpacerChannelName(channel.name);
    final onTap =
        widget.hasJoinPending ||
            !widget.canJoinVoiceChannel ||
            channel.id == widget.currentVoiceChannelId
        ? null
        : () => channel.hasPassword
              ? widget.onJoinChannelWithPassword(channel)
              : widget.onJoinChannel(channel);

    if (spacer.isSpacer) {
      return InkWell(
        onTap: onTap,
        child: ConstrainedBox(
          constraints: const BoxConstraints(minHeight: 40),
          child: Row(
            children: [
              SizedBox(width: channelIndent),
              _expandButton(
                theme,
                hasVisibleChildren: hasVisibleChildren,
                expanded: expanded,
                onPressed: onToggleExpanded,
              ),
              const SizedBox(width: _channelTextGap),
              Expanded(child: _SpacerChannelContent(spacer: spacer)),
            ],
          ),
        ),
      );
    }

    return InkWell(
      onTap: onTap,
      child: ConstrainedBox(
        constraints: const BoxConstraints(minHeight: 40),
        child: Row(
          children: [
            SizedBox(width: channelIndent),
            _expandButton(
              theme,
              hasVisibleChildren: hasVisibleChildren,
              expanded: expanded,
              onPressed: onToggleExpanded,
            ),
            SizedBox(
              width: _channelIconColumnWidth,
              child: Align(
                alignment: Alignment.centerLeft,
                child: Icon(
                  Icons.tag,
                  color: theme.colorScheme.onSurfaceVariant,
                ),
              ),
            ),
            const SizedBox(width: _channelTextGap),
            Expanded(
              child: Text(
                channel.name,
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
              ),
            ),
            if (channel.hasPassword) ...[
              const SizedBox(width: 8),
              Icon(
                Icons.lock_outline,
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ],
          ],
        ),
      ),
    );
  }

  Widget _clientTile(
    ThemeData theme,
    rust.BridgeClient client,
    double channelIndent,
  ) {
    final status = _clientVoiceStatusIcon(theme, client);
    final nameStyle = client.isServerQuery
        ? TextStyle(color: theme.colorScheme.onSurfaceVariant)
        : status.isSpeaking
        ? TextStyle(
            color: theme.colorScheme.primary,
            fontWeight: FontWeight.w600,
          )
        : null;
    final isSelf = client.id == widget.snapshot.ownClientId;
    final volumePref = isSelf
        ? const _ClientVolumePreference()
        : _clientVolumePrefs.preferenceFor(client.id);
    final canOpenPeerActions =
        !isSelf &&
        (widget.onOpenClientChat != null || widget.onOpenClientPoke != null);
    final canOpenClientMenu =
        widget.onOpenClientInfo != null || canOpenPeerActions || !isSelf;

    final decoration = status.isSpeaking
        ? BoxDecoration(
            color: theme.colorScheme.surfaceContainerHighest.withValues(
              alpha: 0.62,
            ),
            borderRadius: BorderRadius.circular(8),
          )
        : null;

    final tile = Padding(
      padding: EdgeInsets.only(
        left: channelIndent + _userRowStartIndent,
        right: 8,
      ),
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 120),
        curve: Curves.easeOut,
        decoration: decoration,
        child: ListTile(
          dense: true,
          visualDensity: VisualDensity.compact,
          leading: _ClientAvatarVoiceIndicator(
            name: client.name,
            speaking: status.isSpeaking,
            badgeIcon: status.badgeIcon,
            badgeColor: status.badgeColor,
            badgeTooltip: status.badgeTooltip,
          ),
          title: Text(client.name, style: nameStyle),
          trailing: volumePref.isModified
              ? _ClientVolumeIndicator(preference: volumePref)
              : null,
        ),
      ),
    );
    if (!canOpenClientMenu) return tile;

    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onSecondaryTapDown: (details) =>
          _showClientMenu(client, details.globalPosition),
      onLongPressStart: widget.enableClientLongPressMenu
          ? (details) => _showClientMenu(client, details.globalPosition)
          : null,
      child: tile,
    );
  }

  Future<void> _showClientMenu(
    rust.BridgeClient client,
    Offset globalPosition,
  ) async {
    final overlay = Overlay.of(context).context.findRenderObject();
    if (overlay is! RenderBox) return;
    final isSelf = client.id == widget.snapshot.ownClientId;

    final selected = await showMenu<_ClientMenuAction>(
      context: context,
      position: RelativeRect.fromRect(
        Rect.fromLTWH(globalPosition.dx, globalPosition.dy, 0, 0),
        Offset.zero & overlay.size,
      ),
      items: [
        if (widget.onOpenClientInfo != null)
          PopupMenuItem(
            value: _ClientMenuAction.info,
            child: ListTile(
              dense: true,
              leading: const Icon(Icons.info_outline),
              title: Text(AppL10n.of(context).clientInfoAction),
            ),
          ),
        if (!isSelf)
          PopupMenuItem(
            value: _ClientMenuAction.volume,
            child: ListTile(
              dense: true,
              leading: const Icon(Icons.volume_up),
              title: Text(AppL10n.of(context).clientVolumeAction),
            ),
          ),
        if (!isSelf && widget.onOpenClientChat != null)
          PopupMenuItem(
            value: _ClientMenuAction.directMessage,
            child: ListTile(
              dense: true,
              leading: const Icon(Icons.chat_bubble_outline),
              title: Text(AppL10n.of(context).chatDirectMessageAction),
            ),
          ),
        if (!isSelf && widget.onOpenClientPoke != null)
          PopupMenuItem(
            value: _ClientMenuAction.poke,
            child: ListTile(
              dense: true,
              leading: const Icon(Icons.notifications_active_outlined),
              title: Text(AppL10n.of(context).chatPokeAction),
            ),
          ),
      ],
    );
    if (!mounted || selected == null) return;
    switch (selected) {
      case _ClientMenuAction.info:
        widget.onOpenClientInfo?.call(client);
      case _ClientMenuAction.directMessage:
        widget.onOpenClientChat?.call(client);
      case _ClientMenuAction.poke:
        widget.onOpenClientPoke?.call(client);
      case _ClientMenuAction.volume:
        await _showVolumeSheet(client);
    }
  }

  Future<void> _showVolumeSheet(rust.BridgeClient client) async {
    await showModalBottomSheet<void>(
      context: context,
      isScrollControlled: true,
      useSafeArea: true,
      builder: (sheetContext) => _ClientVolumeSheet(client: client),
    );
  }

  Widget _expandButton(
    ThemeData theme, {
    required bool hasVisibleChildren,
    required bool expanded,
    required VoidCallback? onPressed,
  }) {
    if (!hasVisibleChildren) {
      return const SizedBox(
        width: _expandColumnWidth,
        height: _expandColumnWidth,
      );
    }
    return Semantics(
      button: true,
      child: GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: onPressed,
        child: SizedBox(
          width: _expandColumnWidth,
          height: _expandColumnWidth,
          child: Icon(
            expanded ? Icons.expand_more : Icons.chevron_right,
            color: theme.colorScheme.onSurfaceVariant,
          ),
        ),
      ),
    );
  }

  bool _isChannelExpanded(BigInt channelId) {
    return _channelExpandedById[channelId] ?? true;
  }

  void _toggleChannelExpanded(BigInt channelId) {
    setState(() {
      _channelExpandedById[channelId] = !_isChannelExpanded(channelId);
    });
  }

  ({
    IconData? badgeIcon,
    Color? badgeColor,
    String? badgeTooltip,
    bool isSpeaking,
  })
  _clientVoiceStatusIcon(ThemeData theme, rust.BridgeClient client) {
    final isSelf = client.id == widget.snapshot.ownClientId;
    final inCurrentChannel = client.channel == widget.currentVoiceChannelId;
    final outputMuted = isSelf ? widget.localOutputMuted : client.outputMuted;
    final inputMuted = isSelf ? widget.localInputMuted : client.inputMuted;
    final neededTalkPower = snapshotNeededTalkPower(
      widget.snapshot,
      client.channel,
    );
    final talkPowerBlocked =
        isSelf &&
        inCurrentChannel &&
        isTalkPowerBlocked(
          talkPower: client.talkPower,
          neededTalkPower: neededTalkPower,
          talkPowerGranted: client.talkPowerGranted,
        );
    final rawSpeaking = isSelf
        ? (widget.audioStats?.pttActive ?? false)
        : client.isSpeaking;
    final transmitAllowed =
        !outputMuted &&
        !inputMuted &&
        (!isSelf || (inCurrentChannel && !talkPowerBlocked));
    final speaking = rawSpeaking && transmitAllowed;

    final IconData? badgeIcon;
    final Color? badgeColor;
    final String? badgeTooltip;
    if (outputMuted) {
      badgeIcon = Icons.volume_off;
      badgeColor = theme.colorScheme.error;
      badgeTooltip = 'Speaker muted';
    } else if (inputMuted) {
      badgeIcon = Icons.mic_off;
      badgeColor = theme.colorScheme.error;
      badgeTooltip = 'Microphone muted';
    } else if (talkPowerBlocked) {
      badgeIcon = Icons.volume_off;
      badgeColor = theme.colorScheme.error;
      badgeTooltip =
          'Insufficient talk power (${client.talkPower} < $neededTalkPower)';
    } else {
      badgeIcon = null;
      badgeColor = null;
      badgeTooltip = null;
    }

    return (
      badgeIcon: badgeIcon,
      badgeColor: badgeColor,
      badgeTooltip: badgeTooltip,
      isSpeaking: speaking,
    );
  }
}

enum _ClientMenuAction { info, directMessage, poke, volume }

class _ClientVolumePreference {
  const _ClientVolumePreference({this.volume = 1.0, this.muted = false});

  final double volume;
  final bool muted;

  bool get isModified => muted || (volume - 1.0).abs() > 0.001;
  bool get isEffectivelyMuted => muted || volume <= 0.001;
}

class _ClientVolumePreferences extends ChangeNotifier {
  _ClientVolumePreferences._();

  static final instance = _ClientVolumePreferences._();

  final Map<BigInt, _ClientVolumePreference> _byClientId = {};

  _ClientVolumePreference preferenceFor(BigInt clientId) {
    return _byClientId[clientId] ?? const _ClientVolumePreference();
  }

  void setPreference(BigInt clientId, _ClientVolumePreference preference) {
    if (preference.isModified) {
      _byClientId[clientId] = preference;
    } else {
      _byClientId.remove(clientId);
    }
    notifyListeners();
  }
}

class _ClientAvatarVoiceIndicator extends StatelessWidget {
  const _ClientAvatarVoiceIndicator({
    required this.name,
    required this.speaking,
    required this.badgeIcon,
    required this.badgeColor,
    required this.badgeTooltip,
  });

  final String name;
  final bool speaking;
  final IconData? badgeIcon;
  final Color? badgeColor;
  final String? badgeTooltip;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final initial = name.trim().isEmpty ? '?' : name.trim()[0].toUpperCase();

    return SizedBox(
      width: 40,
      height: 40,
      child: Stack(
        clipBehavior: Clip.none,
        children: [
          Align(
            alignment: Alignment.center,
            child: AnimatedContainer(
              duration: const Duration(milliseconds: 120),
              curve: Curves.easeOut,
              width: 34,
              height: 34,
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                color: speaking
                    ? theme.colorScheme.primaryContainer
                    : theme.colorScheme.surfaceContainerHighest,
                border: speaking
                    ? Border.all(color: theme.colorScheme.primary, width: 2)
                    : null,
                boxShadow: speaking
                    ? [
                        BoxShadow(
                          color: theme.colorScheme.primary.withValues(
                            alpha: 0.20,
                          ),
                          blurRadius: 8,
                          spreadRadius: 1,
                        ),
                      ]
                    : null,
              ),
              child: Center(
                child: Text(
                  initial,
                  style: theme.textTheme.labelLarge?.copyWith(
                    color: speaking
                        ? theme.colorScheme.onPrimaryContainer
                        : theme.colorScheme.onSurfaceVariant,
                    fontWeight: FontWeight.w700,
                  ),
                ),
              ),
            ),
          ),
          if (badgeIcon != null && badgeColor != null && badgeTooltip != null)
            Positioned(
              right: 0,
              bottom: 0,
              child: Tooltip(
                message: badgeTooltip!,
                child: Container(
                  width: 18,
                  height: 18,
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    color: theme.colorScheme.surface,
                    border: Border.all(color: theme.colorScheme.outlineVariant),
                  ),
                  child: Icon(badgeIcon, size: 12, color: badgeColor),
                ),
              ),
            ),
        ],
      ),
    );
  }
}

class _ClientVolumeIndicator extends StatelessWidget {
  const _ClientVolumeIndicator({required this.preference});

  final _ClientVolumePreference preference;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final percent = (preference.volume * 100).round();
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        if (preference.isEffectivelyMuted)
          Tooltip(
            message: 'Locally muted',
            child: Icon(
              Icons.volume_off,
              size: 18,
              color: theme.colorScheme.error,
            ),
          ),
        if ((preference.volume - 1.0).abs() > 0.001) ...[
          if (preference.isEffectivelyMuted) const SizedBox(width: 6),
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
            decoration: BoxDecoration(
              color: theme.colorScheme.surfaceContainerHighest,
              borderRadius: BorderRadius.circular(999),
            ),
            child: Text(
              '$percent%',
              style: theme.textTheme.labelSmall?.copyWith(
                color: preference.isEffectivelyMuted
                    ? theme.colorScheme.error
                    : theme.colorScheme.onSurfaceVariant,
                fontWeight: FontWeight.w600,
              ),
            ),
          ),
        ],
      ],
    );
  }
}

class _SpacerChannelContent extends StatelessWidget {
  const _SpacerChannelContent({required this.spacer});

  final SpacerChannelNameParseResult spacer;

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

    if (spacer.isBlankSpacer) {
      return const SizedBox(height: 20);
    }

    if (spacer.specialType != null) {
      return SizedBox(
        height: 22,
        child: CustomPaint(
          painter: _SpacerLinePainter(
            color: color.withValues(alpha: 0.72),
            type: spacer.specialType!,
          ),
        ),
      );
    }

    if (spacer.isRepeating) {
      return LayoutBuilder(
        builder: (context, constraints) {
          final pattern = spacer.text.isEmpty ? ' ' : spacer.text;
          final estimatedColumns = (constraints.maxWidth / 8).ceil().clamp(
            1,
            256,
          );
          return Text(
            channelSpacerLabel(
              formatSpacerChannelName(
                SpacerChannelNameFormatOptions(
                  alignment: spacer.alignment,
                  isRepeating: true,
                  uniqueSuffix: spacer.uniqueSuffix,
                  text: pattern,
                ),
              ),
              repeatColumns: estimatedColumns,
            ),
            maxLines: 1,
            overflow: TextOverflow.clip,
            softWrap: false,
            style: theme.textTheme.bodyMedium?.copyWith(color: color),
          );
        },
      );
    }

    return Text(
      spacer.text,
      textAlign: switch (spacer.alignment) {
        SpacerAlignment.left => TextAlign.left,
        SpacerAlignment.right => TextAlign.right,
        SpacerAlignment.center => TextAlign.center,
        null => TextAlign.center,
      },
      maxLines: 1,
      overflow: TextOverflow.ellipsis,
      style: theme.textTheme.bodyMedium?.copyWith(
        color: color,
        fontWeight: FontWeight.w600,
      ),
    );
  }
}

class _SpacerLinePainter extends CustomPainter {
  const _SpacerLinePainter({required this.color, required this.type});

  final Color color;
  final SpacerSpecialType type;

  @override
  void paint(Canvas canvas, Size size) {
    final y = size.height / 2;
    final paint = Paint()
      ..color = color
      ..strokeCap = StrokeCap.square
      ..strokeWidth = 1.4;

    switch (type) {
      case SpacerSpecialType.solidLine:
        canvas.drawLine(Offset(0, y), Offset(size.width, y), paint);
      case SpacerSpecialType.dashLine:
        _drawPattern(canvas, size.width, y, paint, const [8, 5]);
      case SpacerSpecialType.dotLine:
        final dotPaint = Paint()..color = color;
        for (var x = 1.5; x < size.width; x += 7) {
          canvas.drawCircle(Offset(x, y), 1.5, dotPaint);
        }
      case SpacerSpecialType.dashDotLine:
        _drawPattern(canvas, size.width, y, paint, const [10, 4, 2, 4]);
      case SpacerSpecialType.dashDotDotLine:
        _drawPattern(canvas, size.width, y, paint, const [10, 4, 2, 4, 2, 4]);
    }
  }

  void _drawPattern(
    Canvas canvas,
    double width,
    double y,
    Paint paint,
    List<double> pattern,
  ) {
    var x = 0.0;
    var index = 0;
    while (x < width) {
      final length = pattern[index % pattern.length];
      if (index.isEven) {
        final end = x + length > width ? width : x + length;
        canvas.drawLine(Offset(x, y), Offset(end, y), paint);
      }
      x += length;
      index += 1;
    }
  }

  @override
  bool shouldRepaint(covariant _SpacerLinePainter oldDelegate) {
    return oldDelegate.color != color || oldDelegate.type != type;
  }
}

class _ChannelTree {
  const _ChannelTree({required this.roots});

  final List<_ChannelTreeNode> roots;
}

class _ChannelTreeNode {
  _ChannelTreeNode(this.channel);

  final rust.BridgeChannel channel;
  final List<_ChannelTreeNode> children = [];
}

_ChannelTree _buildChannelTree(List<rust.BridgeChannel> channels) {
  final byParent = <BigInt, List<rust.BridgeChannel>>{};
  final knownIds = {for (final channel in channels) channel.id};

  for (final channel in channels) {
    final parent = knownIds.contains(channel.parent)
        ? channel.parent
        : BigInt.zero;
    byParent.putIfAbsent(parent, () => []).add(channel);
  }

  _ChannelTreeNode buildNode(rust.BridgeChannel channel) {
    final node = _ChannelTreeNode(channel);
    for (final child in byParent[channel.id] ?? const <rust.BridgeChannel>[]) {
      node.children.add(buildNode(child));
    }
    return node;
  }

  return _ChannelTree(
    roots: [
      for (final channel
          in byParent[BigInt.zero] ?? const <rust.BridgeChannel>[])
        buildNode(channel),
    ],
  );
}

class _WelcomeMessageTile extends StatelessWidget {
  const _WelcomeMessageTile({
    super.key,
    required this.welcomeMessage,
    required this.expanded,
    required this.onToggle,
    this.onTs3ServerLink,
  });

  final String welcomeMessage;
  final bool expanded;
  final VoidCallback onToggle;
  final Ts3ServerLinkHandler? onTs3ServerLink;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Container(
      decoration: BoxDecoration(
        color: theme.colorScheme.surfaceContainerHighest,
        borderRadius: BorderRadius.circular(6),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          InkWell(
            onTap: () {
              HapticFeedback.selectionClick();
              onToggle();
            },
            borderRadius: const BorderRadius.vertical(top: Radius.circular(6)),
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
              child: Row(
                children: [
                  Icon(
                    expanded ? Icons.expand_less : Icons.expand_more,
                    size: 18,
                    color: theme.colorScheme.onSurfaceVariant,
                  ),
                  const SizedBox(width: 4),
                  Text(
                    AppL10n.of(context).serverWelcomeHeading,
                    style: theme.textTheme.labelMedium?.copyWith(
                      color: theme.colorScheme.onSurfaceVariant,
                    ),
                  ),
                ],
              ),
            ),
          ),
          AnimatedSwitcher(
            duration: const Duration(milliseconds: 200),
            child: expanded
                ? Padding(
                    key: const ValueKey('welcome-expanded'),
                    padding: const EdgeInsets.fromLTRB(8, 0, 8, 8),
                    child: BbCodeText(
                      welcomeMessage,
                      linkTrust: LinkTrustService.instance,
                      onTs3ServerLink: onTs3ServerLink,
                    ),
                  )
                : const SizedBox(
                    key: ValueKey('welcome-collapsed'),
                    width: double.infinity,
                  ),
          ),
        ],
      ),
    );
  }
}

class _ClientVolumeSheet extends StatefulWidget {
  const _ClientVolumeSheet({required this.client});

  final rust.BridgeClient client;

  @override
  State<_ClientVolumeSheet> createState() => _ClientVolumeSheetState();
}

class _ClientVolumeSheetState extends State<_ClientVolumeSheet> {
  static const _maxVolume = 1.5;
  final _prefs = _ClientVolumePreferences.instance;
  double _volume = 1.0;
  bool _muted = false;
  double _volumeBeforeMute = 1.0;

  @override
  void initState() {
    super.initState();
    final preference = _prefs.preferenceFor(widget.client.id);
    _volume = preference.volume.clamp(0.0, _maxVolume);
    _muted = preference.muted;
    _volumeBeforeMute = _volume <= 0.001 ? 1.0 : _volume;
  }

  void _applyVolume() {
    _prefs.setPreference(
      widget.client.id,
      _ClientVolumePreference(volume: _volume, muted: _muted),
    );
    rust.setClientVolume(
      clientId: widget.client.id,
      volume: _muted ? 0.0 : _volume,
    );
  }

  @override
  Widget build(BuildContext context) {
    final l10n = AppL10n.of(context);
    final theme = Theme.of(context);
    final percent = (_volume * 100).round();
    final isModified = _volume != 1.0 || _muted;

    return Padding(
      padding: EdgeInsets.fromLTRB(
        16,
        16,
        16,
        16 + MediaQuery.of(context).viewInsets.bottom,
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Row(
            children: [
              Expanded(
                child: Text(
                  l10n.clientVolumeTitle(widget.client.name),
                  style: theme.textTheme.titleMedium,
                ),
              ),
              IconButton(
                icon: Icon(
                  _muted ? Icons.volume_off : Icons.volume_up,
                  color: _muted ? theme.colorScheme.error : null,
                ),
                tooltip: _muted
                    ? l10n.clientVolumeUnmuteAction
                    : l10n.clientVolumeMuteAction,
                onPressed: () {
                  setState(() {
                    if (!_muted) {
                      _volumeBeforeMute = _volume;
                      _muted = true;
                    } else {
                      _muted = false;
                      _volume = _volumeBeforeMute;
                    }
                  });
                  _applyVolume();
                },
              ),
            ],
          ),
          const SizedBox(height: 8),
          Row(
            children: [
              Expanded(
                child: Slider(
                  value: _volume,
                  min: 0.0,
                  max: _maxVolume,
                  divisions: 30,
                  label: l10n.clientVolumeLabel(percent),
                  onChanged: _muted
                      ? null
                      : (v) {
                          setState(() => _volume = v);
                          _applyVolume();
                        },
                ),
              ),
              SizedBox(
                width: 52,
                child: Text(
                  l10n.clientVolumeLabel(percent),
                  style: theme.textTheme.bodyMedium?.copyWith(
                    color: _muted ? theme.colorScheme.error : null,
                  ),
                  textAlign: TextAlign.end,
                ),
              ),
            ],
          ),
          if (isModified)
            Align(
              alignment: Alignment.centerLeft,
              child: TextButton.icon(
                icon: const Icon(Icons.restart_alt, size: 18),
                label: Text(l10n.clientVolumeResetAction),
                onPressed: () {
                  setState(() {
                    _volume = 1.0;
                    _muted = false;
                    _volumeBeforeMute = 1.0;
                  });
                  _applyVolume();
                },
              ),
            ),
          const SizedBox(height: 8),
        ],
      ),
    );
  }
}
