import 'dart:async';

import 'package:flutter/material.dart' hide Visibility;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:matrix/matrix.dart';
// SpaceChild wird vom matrix-Paket nicht öffentlich exportiert.
// ignore: implementation_imports
import 'package:matrix/src/utils/space_child.dart';
import 'package:pyramid/core/app_state.dart';
import 'package:pyramid/core/matrix_client.dart';
import 'package:pyramid/core/theme.dart';
import 'package:pyramid/features/rooms/rooms_provider.dart';
import 'package:pyramid/features/call/mini_call_widget.dart';
import 'package:pyramid/features/rooms/user_panel.dart';
import 'package:pyramid/features/spaces/space_admin_dialog.dart';
import 'package:pyramid/widgets/create_join_dialog.dart';
import 'package:pyramid/widgets/hover_region.dart';
import 'package:pyramid/widgets/mxc_image.dart';
import 'package:pyramid/widgets/new_dm_dialog.dart';

// ─── Helpers ──────────────────────────────────────────────────────────────────

const _kVoiceRoomType = 'io.pyramid.voice_channel';
const _kSpaceBannerType = 'io.pyramid.space.banner';

bool _isVoiceRoom(Room room) =>
    room.getState(EventTypes.RoomCreate)?.content['type'] == _kVoiceRoomType;

int _compareSpaceChildren(SpaceChild a, SpaceChild b) {
  final ao = a.order;
  final bo = b.order;
  if (ao.isEmpty && bo.isEmpty) return 0;
  if (ao.isEmpty) return 1;
  if (bo.isEmpty) return -1;
  return ao.compareTo(bo);
}

// ─── Panel shell ──────────────────────────────────────────────────────────────

class RoomsPanel extends ConsumerStatefulWidget {
  const RoomsPanel({super.key});

  @override
  ConsumerState<RoomsPanel> createState() => _RoomsPanelState();
}

class _RoomsPanelState extends ConsumerState<RoomsPanel> {
  final _filterCtrl = TextEditingController();
  String _filter = '';
  final Map<String, bool> _collapsed = {};

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

  @override
  Widget build(BuildContext context) {
    final pt = PyramidTheme.of(context);
    final roomsAsync = ref.watch(roomListProvider);
    final activeSpaceId = ref.watch(activeSpaceIdProvider);
    final call = ref.watch(callStateProvider);
    final voip = ref.watch(voipStateProvider);

    final isRealSpace = activeSpaceId != null &&
        activeSpaceId != 'dms' &&
        activeSpaceId != 'rooms';

    final client = ref.watch(matrixClientProvider).valueOrNull;
    final forceJoined = ref.watch(forceJoinedSpacesProvider);
    final activeSpaceRoom =
        isRealSpace ? client?.getRoomById(activeSpaceId) : null;
    final isInvitedSpace = activeSpaceRoom?.membership == Membership.invite &&
        !forceJoined.contains(activeSpaceId);

    return Container(
      color: pt.bg1,
      child: Column(
        children: [
          _Header(pt: pt, spaceId: activeSpaceId),
          if (!isInvitedSpace)
            _FilterInput(
              controller: _filterCtrl,
              pt: pt,
              onChanged: (v) => setState(() => _filter = v),
            ),
          Expanded(
            child: isInvitedSpace
                ? _SpaceInviteView(space: activeSpaceRoom!, pt: pt)
                : isRealSpace
                ? _SpaceRoomsList(
                    spaceId: activeSpaceId,
                    filter: _filter,
                    collapsed: _collapsed,
                    pt: pt,
                    onToggleSection: (id) => setState(() {
                      _collapsed[id] = !(_collapsed[id] ?? false);
                    }),
                  )
                : roomsAsync.when(
                    loading: () => Center(
                      child: SizedBox(
                        width: 20,
                        height: 20,
                        child: CircularProgressIndicator(
                            strokeWidth: 2, color: pt.accent),
                      ),
                    ),
                    error: (e, _) => Center(
                      child: Text('Error',
                          style: TextStyle(color: pt.fgDim, fontSize: 12)),
                    ),
                    data: (rooms) => _RoomsList(
                      rooms: rooms,
                      filter: _filter,
                      spaceId: activeSpaceId,
                      collapsed: _collapsed,
                      pt: pt,
                      onToggleSection: (id) => setState(() {
                        _collapsed[id] = !(_collapsed[id] ?? false);
                      }),
                    ),
                  ),
          ),
          // Call-Dock: immer ÜBER dem Konto-Balken, animiert ein-/ausblenden.
          // Im Mobil-Querformat (Höhe knapp) liegen Call-Balken und
          // Konto-Balken nebeneinander statt übereinander.
          Builder(builder: (context) {
            final hasVoipCall = voip.currentCall != null &&
                voip.currentCall!.state != CallState.kEnded;
            final hasCallDock = call.isActive || hasVoipCall;
            final isLandscapeCompact =
                MediaQuery.sizeOf(context).height < 500;

            final callDock = AnimatedSize(
              duration: const Duration(milliseconds: 220),
              curve: Curves.easeOutCubic,
              alignment: Alignment.bottomCenter,
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  if (call.isActive) MiniCallWidget(call: call),
                  if (hasVoipCall) MiniCallWidget(call: voip),
                ],
              ),
            );

            if (isLandscapeCompact && hasCallDock) {
              return Row(
                crossAxisAlignment: CrossAxisAlignment.end,
                children: [
                  Expanded(child: callDock),
                  const Expanded(child: UserPanel()),
                ],
              );
            }
            return Column(
              mainAxisSize: MainAxisSize.min,
              children: [callDock, const UserPanel()],
            );
          }),
        ],
      ),
    );
  }
}

// ─── Space invite view ─────────────────────────────────────────────────────────

class _SpaceInviteView extends ConsumerStatefulWidget {
  final Room space;
  final PyramidTheme pt;
  const _SpaceInviteView({required this.space, required this.pt});

  @override
  ConsumerState<_SpaceInviteView> createState() => _SpaceInviteViewState();
}

class _SpaceInviteViewState extends ConsumerState<_SpaceInviteView> {
  bool _accepting = false;
  bool _declining = false;

  Future<void> _accept() async {
    setState(() => _accepting = true);
    final client = widget.space.client;
    final id = widget.space.id;
    try {
      await client.joinRoomById(id).timeout(const Duration(seconds: 20));
      // Server confirmed the join. Switch to the channel list optimistically —
      // the local sync membership may lag behind, so don't wait for it.
      ref.read(forceJoinedSpacesProvider.notifier).add(id);
      if (mounted) setState(() => _accepting = false);
    } catch (e) {
      if (mounted) {
        setState(() => _accepting = false);
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content: Text(
              'Beitritt fehlgeschlagen: ${e.toString().split('\n').first}'),
          backgroundColor: widget.pt.danger,
        ));
      }
    }
  }

  Future<void> _decline() async {
    setState(() => _declining = true);
    try {
      await widget.space.leave();
      if (mounted) {
        ref.read(activeSpaceIdProvider.notifier).state = 'rooms';
      }
    } catch (_) {
      if (mounted) setState(() => _declining = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    final pt = widget.pt;
    final client = ref.watch(matrixClientProvider).valueOrNull;
    final name = widget.space.getLocalizedDisplayname();
    final busy = _accepting || _declining;

    return Center(
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Container(
              width: 72,
              height: 72,
              decoration: BoxDecoration(
                color: pt.accent.withAlpha(40),
                borderRadius: BorderRadius.circular(20),
              ),
              child: client != null && widget.space.avatar != null
                  ? ClipRRect(
                      borderRadius: BorderRadius.circular(20),
                      child: MxcAvatar(
                        mxcUri: widget.space.avatar,
                        client: client,
                        size: 72,
                        borderRadius: BorderRadius.circular(20),
                        placeholder: (_) => Center(
                          child: Text(
                            name.isNotEmpty ? name[0].toUpperCase() : '?',
                            style: TextStyle(
                                color: pt.accent,
                                fontSize: 28,
                                fontWeight: FontWeight.w700),
                          ),
                        ),
                      ),
                    )
                  : Center(
                      child: Text(
                        name.isNotEmpty ? name[0].toUpperCase() : '?',
                        style: TextStyle(
                            color: pt.accent,
                            fontSize: 28,
                            fontWeight: FontWeight.w700),
                      ),
                    ),
            ),
            const SizedBox(height: 16),
            Text('Einladung zum Space',
                style: TextStyle(color: pt.fgDim, fontSize: 13)),
            const SizedBox(height: 4),
            Text(
              name,
              textAlign: TextAlign.center,
              style: TextStyle(
                  color: pt.fg, fontSize: 18, fontWeight: FontWeight.w700),
            ),
            const SizedBox(height: 24),
            Row(
              children: [
                Expanded(
                  child: OutlinedButton(
                    style: OutlinedButton.styleFrom(
                      foregroundColor: pt.danger,
                      side: BorderSide(color: pt.danger.withAlpha(100)),
                      padding: const EdgeInsets.symmetric(vertical: 12),
                      shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(8)),
                    ),
                    onPressed: busy ? null : _decline,
                    child: _declining
                        ? const SizedBox(
                            width: 16,
                            height: 16,
                            child: CircularProgressIndicator.adaptive(
                                strokeWidth: 2))
                        : const Text('Ablehnen'),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      backgroundColor: pt.accent,
                      foregroundColor: pt.accentFg,
                      elevation: 0,
                      padding: const EdgeInsets.symmetric(vertical: 12),
                      shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(8)),
                    ),
                    onPressed: busy ? null : _accept,
                    child: _accepting
                        ? const SizedBox(
                            width: 16,
                            height: 16,
                            child: CircularProgressIndicator.adaptive(
                                strokeWidth: 2))
                        : const Text('Beitreten'),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ─── Header ───────────────────────────────────────────────────────────────────

enum _SpaceMenuItem {
  newDm, createGroup, createRoom, joinRoom, discoverRooms,
  editSpace, adminSpace, leaveSpace,
}

class _Header extends ConsumerWidget {
  final PyramidTheme pt;
  final String? spaceId;
  const _Header({required this.pt, required this.spaceId});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final isRealSpace = spaceId != null && spaceId != 'dms' && spaceId != 'rooms';
    final size = MediaQuery.sizeOf(context);
    final isMobile = size.width < 900 || size.height < 500;

    final spaces = ref.watch(spacesProvider).valueOrNull ?? [];
    Room? activeSpace;
    if (isRealSpace) {
      try {
        activeSpace = spaces.firstWhere((s) => s.id == spaceId);
      } catch (_) {}
    }
    final client = ref.watch(matrixClientProvider).valueOrNull;
    final isAdmin =
        activeSpace != null && activeSpace.ownPowerLevel >= 50;

    final name = isRealSpace
        ? (activeSpace?.getLocalizedDisplayname() ?? 'Space')
        : (spaceId == 'dms' ? 'Chats' : 'Rooms');

    void handleMenu(_SpaceMenuItem item) {
      switch (item) {
        case _SpaceMenuItem.newDm:
          showDialog(context: context, builder: (_) => const NewDmDialog());
        case _SpaceMenuItem.createGroup:
          showDialog(
              context: context, builder: (_) => const CreateJoinDialog());
        case _SpaceMenuItem.createRoom:
          showDialog(
              context: context, builder: (_) => const CreateJoinDialog());
        case _SpaceMenuItem.joinRoom:
          showDialog(
              context: context,
              builder: (_) =>
                  const CreateJoinDialog(initialTab: CreateJoinTab.join));
        case _SpaceMenuItem.discoverRooms:
          showDialog(
              context: context,
              builder: (_) =>
                  const CreateJoinDialog(initialTab: CreateJoinTab.discover));
        case _SpaceMenuItem.editSpace:
          if (activeSpace != null) {
            showDialog(
                context: context,
                builder: (_) => SpaceEditDialog(space: activeSpace!));
          }
        case _SpaceMenuItem.adminSpace:
          if (activeSpace != null) {
            showDialog(
                context: context,
                builder: (_) => SpaceAdminDialog(space: activeSpace!));
          }
        case _SpaceMenuItem.leaveSpace:
          if (activeSpace != null) {
            final space = activeSpace;
            showDialog<bool>(
              context: context,
              builder: (ctx) => AlertDialog(
                backgroundColor: pt.bg2,
                shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(pt.rXl),
                    side: BorderSide(color: pt.border)),
                title: Text('Space verlassen?',
                    style: TextStyle(
                        color: pt.fg,
                        fontSize: 16,
                        fontWeight: FontWeight.w700)),
                content: Text(
                    'Du verlässt "${space.getLocalizedDisplayname()}".',
                    style: TextStyle(color: pt.fgMuted, fontSize: 13)),
                actions: [
                  TextButton(
                      onPressed: () => Navigator.pop(ctx, false),
                      child: Text('Abbrechen',
                          style: TextStyle(color: pt.fgMuted))),
                  ElevatedButton(
                    style: ElevatedButton.styleFrom(
                        backgroundColor: pt.danger,
                        foregroundColor: Colors.white,
                        elevation: 0,
                        shape: RoundedRectangleBorder(
                            borderRadius: BorderRadius.circular(8))),
                    onPressed: () => Navigator.pop(ctx, true),
                    child: const Text('Verlassen'),
                  ),
                ],
              ),
            ).then((confirmed) {
              if (confirmed == true) {
                space.leave().then((_) {
                  ref.read(activeSpaceIdProvider.notifier).state = 'rooms';
                });
              }
            });
          }
      }
    }

    List<PopupMenuEntry<_SpaceMenuItem>> buildMenuItems() {
      if (spaceId == 'dms' || spaceId == null) {
        return [
          PopupMenuItem(
              value: _SpaceMenuItem.newDm,
              child: _MenuRow(
                  Icons.chat_bubble_outline_rounded, 'Neue Unterhaltung', pt)),
          PopupMenuItem(
              value: _SpaceMenuItem.createGroup,
              child: _MenuRow(Icons.group_add_rounded, 'Gruppe erstellen', pt)),
        ];
      } else if (spaceId == 'rooms') {
        return [
          PopupMenuItem(
              value: _SpaceMenuItem.createRoom,
              child: _MenuRow(Icons.add_rounded, 'Raum erstellen', pt)),
          PopupMenuItem(
              value: _SpaceMenuItem.joinRoom,
              child: _MenuRow(Icons.login_rounded, 'Beitreten', pt)),
          PopupMenuItem(
              value: _SpaceMenuItem.discoverRooms,
              child: _MenuRow(Icons.explore_rounded, 'Entdecken', pt)),
        ];
      } else {
        return [
          if (isAdmin) ...[
            PopupMenuItem(
                value: _SpaceMenuItem.adminSpace,
                child: _MenuRow(
                    Icons.admin_panel_settings_rounded, 'Space verwalten', pt)),
            PopupMenuItem(
                value: _SpaceMenuItem.editSpace,
                child: _MenuRow(Icons.edit_outlined, 'Name/Beschreibung', pt)),
            const PopupMenuDivider(),
          ],
          PopupMenuItem(
              value: _SpaceMenuItem.leaveSpace,
              child: _MenuRow(Icons.exit_to_app_rounded, 'Space verlassen', pt,
                  danger: true)),
        ];
      }
    }

    // Check for banner — space banner for real spaces, server banner otherwise.
    final bannerOverrides = ref.watch(spaceBannerOverrideProvider);
    final serverBanners = ref.watch(serverBannerProvider).valueOrNull ?? {};
    String? bannerUrl;
    if (isRealSpace && activeSpace != null) {
      bannerUrl = bannerOverrides.containsKey(activeSpace.id)
          ? bannerOverrides[activeSpace.id]
          : activeSpace.getState(_kSpaceBannerType)?.content['url'] as String?;
    } else if (client != null) {
      bannerUrl = serverBanners[client.homeserver.toString()];
    }
    final hasBanner = bannerUrl != null && client != null;

    Widget controlsRow = Row(
      children: [
        if (isMobile)
          PyrIconBtn(
            icon: const Icon(Icons.menu_rounded),
            tooltip: 'Spaces',
            onPressed: () => Scaffold.of(context).openDrawer(),
          )
        else ...[
          PyrIconBtn(
            icon: const Icon(Icons.menu_rounded),
            tooltip: 'Toggle sidebar',
            onPressed: () =>
                ref.read(railCollapsedProvider.notifier).update((s) => !s),
          ),
          const SizedBox(width: 4),
        ],
        // Space avatar (only when no banner)
        if (isRealSpace && !hasBanner && client != null && activeSpace != null) ...[
          Container(
            width: 28,
            height: 28,
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(8),
              color: pt.accent.withAlpha(40),
            ),
            child: ClipRRect(
              borderRadius: BorderRadius.circular(8),
              child: activeSpace.avatar != null
                  ? MxcAvatar(
                      mxcUri: activeSpace.avatar,
                      client: client,
                      size: 28,
                      borderRadius: BorderRadius.circular(8),
                      placeholder: (_) => Center(
                        child: Text(
                          name.isNotEmpty ? name[0].toUpperCase() : '?',
                          style: TextStyle(
                              color: pt.accent,
                              fontSize: 13,
                              fontWeight: FontWeight.w700),
                        ),
                      ),
                    )
                  : Center(
                      child: Text(
                        name.isNotEmpty ? name[0].toUpperCase() : '?',
                        style: TextStyle(
                            color: pt.accent,
                            fontSize: 13,
                            fontWeight: FontWeight.w700),
                      ),
                    ),
            ),
          ),
          const SizedBox(width: 8),
        ],
        Expanded(
          child: Text(
            name,
            style: TextStyle(
              color: hasBanner ? Colors.white : pt.fg,
              fontSize: 15,
              fontWeight: isRealSpace ? FontWeight.w700 : FontWeight.w600,
              shadows: hasBanner
                  ? [const Shadow(blurRadius: 4, color: Colors.black45)]
                  : null,
            ),
            overflow: TextOverflow.ellipsis,
          ),
        ),
        if (isRealSpace && isAdmin)
          PyrIconBtn(
            icon: Icon(Icons.admin_panel_settings_rounded,
                color: hasBanner ? Colors.white70 : null),
            tooltip: 'Space verwalten',
            onPressed: () => handleMenu(_SpaceMenuItem.adminSpace),
          ),
        if (!isRealSpace)
          _NotificationsButton(pt: pt, hasBanner: hasBanner),
        PopupMenuButton<_SpaceMenuItem>(
          tooltip: 'Erstellen',
          color: pt.bg2,
          shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(10),
              side: BorderSide(color: pt.border)),
          itemBuilder: (_) => buildMenuItems(),
          onSelected: handleMenu,
          child: Padding(
            padding: const EdgeInsets.all(8),
            child: Icon(Icons.add_rounded,
                color: hasBanner ? Colors.white70 : pt.fgDim, size: 18),
          ),
        ),
      ],
    );

    if (hasBanner) {
      return SizedBox(
        height: 110,
        child: Stack(
          fit: StackFit.expand,
          children: [
            // Banner image
            MxcImage(
              mxcUri: bannerUrl,
              client: client,
              fit: BoxFit.cover,
              placeholder: Container(color: pt.bg0),
            ),
            // Gradient overlay — bottom-heavy so text is readable
            DecoratedBox(
              decoration: const BoxDecoration(
                gradient: LinearGradient(
                  begin: Alignment.topCenter,
                  end: Alignment.bottomCenter,
                  stops: [0.0, 0.4, 1.0],
                  colors: [
                    Color(0x00000000),
                    Color(0x55000000),
                    Color(0xCC000000),
                  ],
                ),
              ),
            ),
            // Controls at the bottom
            Positioned(
              left: 0,
              right: 0,
              bottom: 0,
              child: Padding(
                padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
                child: controlsRow,
              ),
            ),
            // Bottom border
            Positioned(
              left: 0,
              right: 0,
              bottom: 0,
              child: Container(height: 1, color: pt.border.withAlpha(80)),
            ),
          ],
        ),
      );
    }

    return Container(
      height: isRealSpace ? 56 : 52,
      padding: const EdgeInsets.symmetric(horizontal: 14),
      decoration: BoxDecoration(
        color: isRealSpace ? pt.bg0 : pt.bg1,
        border: Border(bottom: BorderSide(color: pt.border)),
      ),
      child: controlsRow,
    );
  }
}

class _MenuRow extends StatelessWidget {
  final IconData icon;
  final String label;
  final PyramidTheme pt;
  final bool danger;
  const _MenuRow(this.icon, this.label, this.pt, {this.danger = false});

  @override
  Widget build(BuildContext context) => Row(children: [
        Icon(icon, size: 14, color: danger ? pt.danger : pt.fgMuted),
        const SizedBox(width: 8),
        Text(label,
            style: TextStyle(
                color: danger ? pt.danger : pt.fg, fontSize: 13)),
      ]);
}

// ─── Notifications button ──────────────────────────────────────────────────────

/// Glocke im Header: Popup mit allen Räumen, die ungelesene Nachrichten oder
/// Mentions haben. Tippen öffnet den jeweiligen Raum.
class _NotificationsButton extends ConsumerWidget {
  final PyramidTheme pt;
  final bool hasBanner;
  const _NotificationsButton({required this.pt, required this.hasBanner});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final rooms = ref.watch(roomListProvider).valueOrNull ?? [];
    final unread = rooms
        .where((r) =>
            r.membership == Membership.join &&
            (r.notificationCount > 0 || r.highlightCount > 0))
        .toList()
      ..sort((a, b) {
        final hl = b.highlightCount.compareTo(a.highlightCount);
        if (hl != 0) return hl;
        return b.notificationCount.compareTo(a.notificationCount);
      });

    return PopupMenuButton<String>(
      tooltip: 'Benachrichtigungen',
      color: pt.bg2,
      shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(10),
          side: BorderSide(color: pt.border)),
      offset: const Offset(0, 36),
      onSelected: (roomId) {
        ref.read(activeRoomIdProvider.notifier).state = roomId;
        ref.read(viewModeProvider.notifier).state = ViewMode.chat;
      },
      itemBuilder: (_) {
        if (unread.isEmpty) {
          return [
            PopupMenuItem<String>(
              enabled: false,
              child: Text('Keine neuen Benachrichtigungen',
                  style: TextStyle(color: pt.fgDim, fontSize: 13)),
            ),
          ];
        }
        return unread
            .map((r) => PopupMenuItem<String>(
                  value: r.id,
                  child: Row(
                    children: [
                      Icon(
                        r.highlightCount > 0
                            ? Icons.alternate_email_rounded
                            : (r.isDirectChat
                                ? Icons.chat_bubble_outline_rounded
                                : Icons.tag_rounded),
                        size: 14,
                        color:
                            r.highlightCount > 0 ? pt.danger : pt.fgMuted,
                      ),
                      const SizedBox(width: 8),
                      Expanded(
                        child: Text(
                          r.getLocalizedDisplayname(),
                          style: TextStyle(color: pt.fg, fontSize: 13),
                          overflow: TextOverflow.ellipsis,
                        ),
                      ),
                      const SizedBox(width: 8),
                      Container(
                        padding: const EdgeInsets.symmetric(
                            horizontal: 6, vertical: 1),
                        decoration: BoxDecoration(
                          color: r.highlightCount > 0 ? pt.danger : pt.accent,
                          borderRadius: BorderRadius.circular(999),
                        ),
                        child: Text(
                          '${r.notificationCount > 0 ? r.notificationCount : r.highlightCount}',
                          style: const TextStyle(
                              color: Colors.white,
                              fontSize: 10,
                              fontWeight: FontWeight.w700),
                        ),
                      ),
                    ],
                  ),
                ))
            .toList();
      },
      child: Padding(
        padding: const EdgeInsets.all(8),
        child: Stack(
          clipBehavior: Clip.none,
          children: [
            Icon(Icons.notifications_none_rounded,
                size: 18, color: hasBanner ? Colors.white70 : pt.fgDim),
            if (unread.isNotEmpty)
              Positioned(
                top: -1,
                right: -1,
                child: Container(
                  width: 8,
                  height: 8,
                  decoration: BoxDecoration(
                    color: pt.danger,
                    shape: BoxShape.circle,
                    border: Border.all(color: pt.bg1, width: 1.5),
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }
}

// ─── Filter input ──────────────────────────────────────────────────────────────

class _FilterInput extends StatelessWidget {
  final TextEditingController controller;
  final PyramidTheme pt;
  final ValueChanged<String> onChanged;
  const _FilterInput(
      {required this.controller, required this.pt, required this.onChanged});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
      child: Stack(
        alignment: Alignment.centerLeft,
        children: [
          Padding(
            padding: const EdgeInsets.only(left: 8),
            child: Icon(Icons.search_rounded, size: 14, color: pt.fgDim),
          ),
          TextField(
            controller: controller,
            onChanged: onChanged,
            style: TextStyle(color: pt.fg, fontSize: 13),
            decoration: InputDecoration(
              hintText: 'Filter rooms',
              hintStyle: TextStyle(color: pt.fgDim, fontSize: 13),
              contentPadding: const EdgeInsets.fromLTRB(28, 7, 10, 7),
              filled: true,
              fillColor: pt.bg2,
              border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(pt.rSm),
                  borderSide: BorderSide.none),
              enabledBorder: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(pt.rSm),
                  borderSide: const BorderSide(color: Colors.transparent)),
              focusedBorder: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(pt.rSm),
                  borderSide: BorderSide(color: pt.accent, width: 1)),
              isDense: true,
            ),
            cursorColor: pt.accent,
          ),
        ],
      ),
    );
  }
}

// ─── Space rooms list ──────────────────────────────────────────────────────────

class _SpaceRoomsList extends ConsumerStatefulWidget {
  final String spaceId;
  final String filter;
  final Map<String, bool> collapsed;
  final PyramidTheme pt;
  final ValueChanged<String> onToggleSection;

  const _SpaceRoomsList({
    required this.spaceId,
    required this.filter,
    required this.collapsed,
    required this.pt,
    required this.onToggleSection,
  });

  @override
  ConsumerState<_SpaceRoomsList> createState() => _SpaceRoomsListState();
}

class _SpaceRoomsListState extends ConsumerState<_SpaceRoomsList> {
  // Local order override after drag-and-drop (room IDs in order per category key)
  final Map<String, List<String>> _localOrder = {};

  // Joinable channels discovered via the space hierarchy (not yet a member).
  List<_JoinableRoom> _joinable = [];
  final Set<String> _joining = {};

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final client = ref.read(matrixClientProvider).valueOrNull;
      if (client != null) _loadHierarchy(client);
    });
  }

  @override
  void didUpdateWidget(covariant _SpaceRoomsList old) {
    super.didUpdateWidget(old);
    if (old.spaceId != widget.spaceId) {
      setState(() => _joinable = []);
      final client = ref.read(matrixClientProvider).valueOrNull;
      if (client != null) _loadHierarchy(client);
    }
  }

  /// Queries the server-side space hierarchy and keeps only rooms the user is
  /// not yet a member of — these are the channels they can still join.
  Future<void> _loadHierarchy(Client client) async {
    try {
      final resp = await client.getSpaceHierarchy(
        widget.spaceId,
        maxDepth: 3,
        suggestedOnly: false,
      );
      final joinable = <_JoinableRoom>[];
      for (final chunk in resp.rooms) {
        if (chunk.roomId == widget.spaceId) continue;
        if (chunk.roomType == 'm.space') continue; // skip sub-spaces
        // Keep every channel the hierarchy reports. The build method filters out
        // the ones already rendered in the joined sections — relying on local
        // membership here is unreliable because Continuwuity's sync lags behind.
        joinable.add(_JoinableRoom(
          roomId: chunk.roomId,
          name: chunk.name ?? chunk.canonicalAlias ?? chunk.roomId,
          isVoice: chunk.roomType == _kVoiceRoomType,
          memberCount: chunk.numJoinedMembers,
        ));
      }
      if (mounted) setState(() => _joinable = joinable);
    } catch (_) {
      // Hierarchy endpoint unavailable / no permission — silently ignore.
    }
  }

  Future<void> _joinAndEnter(Client client, _JoinableRoom jr) async {
    setState(() => _joining.add(jr.roomId));
    try {
      await client.joinRoomById(jr.roomId).timeout(const Duration(seconds: 20));
      // Give the sync a brief chance to deliver the room object, but don't block
      // on it — the local membership may lag behind the server.
      Room? room = client.getRoomById(jr.roomId);
      if (room == null) {
        try {
          await client.onSync.stream
              .firstWhere((_) => client.getRoomById(jr.roomId) != null)
              .timeout(const Duration(seconds: 4));
        } catch (_) {}
        room = client.getRoomById(jr.roomId);
      }
      if (!mounted) return;
      setState(() => _joining.remove(jr.roomId));
      if (jr.isVoice) {
        // LiveKit only needs the room id + a display name, so connect even if the
        // matrix room object hasn't synced locally yet.
        final call = ref.read(callStateProvider);
        if (call.isActive) await call.hangUp();
        ref.read(activeVoiceRoomIdProvider.notifier).state = jr.roomId;
        await call.startCall(
          roomName: jr.roomId,
          roomDisplayName: room?.getLocalizedDisplayname() ?? jr.name,
          identity: client.userID ?? '',
          audioOnly: true,
          voiceChannel: true,
          matrixClient: client,
          matrixRoomId: jr.roomId,
        );
      } else if (room != null) {
        ref.read(activeRoomIdProvider.notifier).state = room.id;
        ref.read(viewModeProvider.notifier).state = ViewMode.chat;
      }
    } catch (e) {
      if (mounted) {
        setState(() => _joining.remove(jr.roomId));
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content:
              Text('Beitritt fehlgeschlagen: ${e.toString().split('\n').first}'),
          backgroundColor: widget.pt.danger,
        ));
      }
    }
  }

  Future<void> _applyReorder(
      Room space, String categoryKey, List<SpaceChild> newOrder) async {
    try {
      for (var i = 0; i < newOrder.length; i++) {
        final child = newOrder[i];
        if (child.roomId == null) continue;
        final newOrd = (i + 1).toString().padLeft(4, '0');
        if (child.order != newOrd) {
          await space.setSpaceChild(child.roomId!,
              via: child.via.isEmpty ? null : child.via, order: newOrd);
        }
      }
    } catch (_) {
    }
  }

  @override
  Widget build(BuildContext context) {
    // Rebuild when rooms list changes (sync)
    ref.watch(roomListProvider);
    final client = ref.watch(matrixClientProvider).valueOrNull;
    final activeRoomId = ref.watch(activeRoomIdProvider);

    if (client == null) {
      return Center(
          child: SizedBox(
              width: 20,
              height: 20,
              child: CircularProgressIndicator(
                  strokeWidth: 2, color: widget.pt.accent)));
    }

    final space = client.getRoomById(widget.spaceId);
    if (space == null) {
      return Center(
          child: Text('Space nicht gefunden',
              style: TextStyle(color: widget.pt.fgDim, fontSize: 12)));
    }

    final isAdmin = space.ownPowerLevel >= 50;
    final allChildren = space.spaceChildren;

    // Separate sub-spaces (categories) from direct rooms
    final subSpaceEntries = <SpaceChild>[];
    final directEntries = <SpaceChild>[];
    for (final c in allChildren) {
      if (c.roomId == null) continue;
      final room = client.getRoomById(c.roomId!);
      if (room == null || room.membership == Membership.leave) continue;
      if (room.isSpace) {
        subSpaceEntries.add(c);
      } else {
        directEntries.add(c);
      }
    }
    subSpaceEntries.sort(_compareSpaceChildren);
    directEntries.sort(_compareSpaceChildren);

    // Collect invited rooms
    final invites = allChildren
        .where((c) => c.roomId != null)
        .map((c) => client.getRoomById(c.roomId!))
        .whereType<Room>()
        .where((r) => r.membership == Membership.invite)
        .toList();

    // IDs already rendered in the joined/invited sections — used to avoid showing
    // them again under "available channels" from the hierarchy.
    final shownIds = <String>{...invites.map((r) => r.id)};

    void selectRoom(Room room) {
      ref.read(activeRoomIdProvider.notifier).state = room.id;
      ref.read(viewModeProvider.notifier).state = ViewMode.chat;
    }

    void joinVoiceChannel(Room room) async {
      final call = ref.read(callStateProvider);
      final activeVoice = ref.read(activeVoiceRoomIdProvider);
      // Toggle: leave if already in this channel
      if (call.isVoiceChannel && activeVoice == room.id) {
        ref.read(activeVoiceRoomIdProvider.notifier).state = null;
        await call.hangUp();
        return;
      }
      if (call.isActive) await call.hangUp();
      ref.read(activeVoiceRoomIdProvider.notifier).state = room.id;
      final userId = client.userID ?? '';
      await call.startCall(
        roomName: room.id,
        roomDisplayName: room.getLocalizedDisplayname(),
        identity: userId,
        audioOnly: true,
        voiceChannel: true,
        matrixClient: client,
        matrixRoomId: room.id,
      );
    }

    // Build category sections for sub-spaces
    List<Widget> categoryWidgets = [];
    for (final catEntry in subSpaceEntries) {
      final subSpace = client.getRoomById(catEntry.roomId!)!;
      final catId = subSpace.id;
      final catName = subSpace.getLocalizedDisplayname();
      final isCollapsed = widget.collapsed[catId] ?? false;

      final catChildren = subSpace.spaceChildren
          .where((c) => c.roomId != null)
          .map((c) => client.getRoomById(c.roomId!))
          .whereType<Room>()
          .where((r) =>
              r.membership != Membership.leave && !r.isSpace)
          .toList();
      shownIds.addAll(catChildren.map((r) => r.id));

      final filtered = catChildren
          .where((r) => widget.filter.isEmpty ||
              r.getLocalizedDisplayname()
                  .toLowerCase()
                  .contains(widget.filter.toLowerCase()))
          .toList();

      categoryWidgets.add(_CategoryHeader(
        name: catName.toUpperCase(),
        isCollapsed: isCollapsed,
        pt: widget.pt,
        onTap: () => widget.onToggleSection(catId),
      ));
      if (!isCollapsed) {
        for (final room in filtered) {
          final isVoice = _isVoiceRoom(room);
          categoryWidgets.add(_RoomItem(
            key: ValueKey('cat_${room.id}'),
            room: room,
            isActive: isVoice ? false : activeRoomId == room.id,
            isInVoice: isVoice && ref.read(activeVoiceRoomIdProvider) == room.id,
            pt: widget.pt,
            onTap: () => isVoice ? joinVoiceChannel(room) : selectRoom(room),
          ));
        }
      }
    }

    // Build direct rooms section (reorderable for admin)
    final directRooms = directEntries
        .map((c) => client.getRoomById(c.roomId!))
        .whereType<Room>()
        .toList();
    shownIds.addAll(directRooms.map((r) => r.id));

    // Apply local order override if present
    final localOrd = _localOrder['__direct__'];
    if (localOrd != null) {
      directRooms.sort((a, b) {
        final ai = localOrd.indexOf(a.id);
        final bi = localOrd.indexOf(b.id);
        if (ai == -1 && bi == -1) return 0;
        if (ai == -1) return 1;
        if (bi == -1) return -1;
        return ai.compareTo(bi);
      });
    }

    final filteredDirect = directRooms
        .where((r) => widget.filter.isEmpty ||
            r.getLocalizedDisplayname()
                .toLowerCase()
                .contains(widget.filter.toLowerCase()))
        .toList();

    // Joinable channels from the hierarchy that aren't already listed above.
    final visibleJoinable = _joinable
        .where((jr) =>
            !shownIds.contains(jr.roomId) &&
            (widget.filter.isEmpty ||
                jr.name.toLowerCase().contains(widget.filter.toLowerCase())))
        .toList();

    // Build the complete list
    return ListView(
      padding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
      children: [
        // Invite cards
        if (invites.isNotEmpty) ...[
          Padding(
            padding: const EdgeInsets.fromLTRB(8, 6, 8, 2),
            child: Text('EINLADUNGEN',
                style: TextStyle(
                    color: widget.pt.accent,
                    fontSize: 10,
                    fontWeight: FontWeight.w700,
                    letterSpacing: 0.6)),
          ),
          ...invites.map((r) => Padding(
              padding: const EdgeInsets.symmetric(vertical: 2),
              child: _InviteItem(room: r, pt: widget.pt))),
          const SizedBox(height: 4),
        ],
        // Sub-space categories
        ...categoryWidgets,
        // Direct rooms
        if (filteredDirect.isNotEmpty) ...[
          if (subSpaceEntries.isNotEmpty)
            _CategoryHeader(
              name: 'KANÄLE',
              isCollapsed: widget.collapsed['__direct__'] ?? false,
              pt: widget.pt,
              onTap: () => widget.onToggleSection('__direct__'),
            ),
          if (!(widget.collapsed['__direct__'] ?? false))
            if (isAdmin)
              // Reorderable list for admins
              ReorderableListView(
                shrinkWrap: true,
                physics: const NeverScrollableScrollPhysics(),
                buildDefaultDragHandles: false,
                onReorderItem: (oldIdx, newIdx) {
                  final ids = filteredDirect.map((r) => r.id).toList();
                  final moved = ids.removeAt(oldIdx);
                  ids.insert(newIdx, moved);
                  setState(() => _localOrder['__direct__'] = ids);
                  // Apply to Matrix
                  final newOrder = ids
                      .map((id) =>
                          directEntries.firstWhere((c) => c.roomId == id,
                              orElse: () => directEntries.first))
                      .toList();
                  _applyReorder(space, '__direct__', newOrder);
                },
                children: [
                  for (var i = 0; i < filteredDirect.length; i++)
                    _DraggableRoomItem(
                      key: ValueKey(filteredDirect[i].id),
                      room: filteredDirect[i],
                      index: i,
                      isActive: _isVoiceRoom(filteredDirect[i]) ? false : activeRoomId == filteredDirect[i].id,
                      isInVoice: _isVoiceRoom(filteredDirect[i]) && ref.read(activeVoiceRoomIdProvider) == filteredDirect[i].id,
                      pt: widget.pt,
                      onTap: () => _isVoiceRoom(filteredDirect[i]) ? joinVoiceChannel(filteredDirect[i]) : selectRoom(filteredDirect[i]),
                    ),
                ],
              )
            else
              ...filteredDirect.map((r) {
                final isVoice = _isVoiceRoom(r);
                return _RoomItem(
                    key: ValueKey(r.id),
                    room: r,
                    isActive: isVoice ? false : activeRoomId == r.id,
                    isInVoice: isVoice && ref.read(activeVoiceRoomIdProvider) == r.id,
                    pt: widget.pt,
                    onTap: () => isVoice ? joinVoiceChannel(r) : selectRoom(r),
                  );
              }),
        ],
        // Joinable channels from the space hierarchy (not yet a member)
        if (visibleJoinable.isNotEmpty) ...[
          Padding(
            padding: const EdgeInsets.fromLTRB(8, 12, 8, 2),
            child: Text('VERFÜGBARE KANÄLE',
                style: TextStyle(
                    color: widget.pt.fgDim,
                    fontSize: 10,
                    fontWeight: FontWeight.w700,
                    letterSpacing: 0.6)),
          ),
          ...visibleJoinable.map((jr) => _JoinableRoomItem(
                jr: jr,
                joining: _joining.contains(jr.roomId),
                pt: widget.pt,
                onJoin: () => _joinAndEnter(client, jr),
              )),
        ],
        if (filteredDirect.isEmpty &&
            categoryWidgets.isEmpty &&
            invites.isEmpty &&
            visibleJoinable.isEmpty)
          Padding(
            padding: const EdgeInsets.all(16),
            child: Center(
                child: Text('Keine Kanäle',
                    style:
                        TextStyle(color: widget.pt.fgDim, fontSize: 13))),
          ),
      ],
    );
  }
}

// ─── Joinable room (from space hierarchy) ─────────────────────────────────────

class _JoinableRoom {
  final String roomId;
  final String name;
  final bool isVoice;
  final int memberCount;
  const _JoinableRoom({
    required this.roomId,
    required this.name,
    required this.isVoice,
    required this.memberCount,
  });
}

class _JoinableRoomItem extends StatefulWidget {
  final _JoinableRoom jr;
  final bool joining;
  final PyramidTheme pt;
  final VoidCallback onJoin;
  const _JoinableRoomItem({
    required this.jr,
    required this.joining,
    required this.pt,
    required this.onJoin,
  });

  @override
  State<_JoinableRoomItem> createState() => _JoinableRoomItemState();
}

class _JoinableRoomItemState extends State<_JoinableRoomItem> {
  bool _hovered = false;

  @override
  Widget build(BuildContext context) {
    final pt = widget.pt;
    final jr = widget.jr;
    return MouseRegion(
      cursor: SystemMouseCursors.click,
      onEnter: (_) => setState(() => _hovered = true),
      onExit: (_) => setState(() => _hovered = false),
      child: GestureDetector(
        onTap: widget.joining ? null : widget.onJoin,
        child: AnimatedContainer(
          duration: const Duration(milliseconds: 150),
          margin: const EdgeInsets.symmetric(vertical: 1),
          padding: EdgeInsets.symmetric(
              horizontal: pt.rowPadX, vertical: pt.rowPadY),
          decoration: BoxDecoration(
            color: _hovered ? pt.bgHover : Colors.transparent,
            borderRadius: BorderRadius.circular(pt.rSm),
          ),
          child: Row(
            children: [
              SizedBox(
                width: 16,
                height: 16,
                child: Icon(
                  jr.isVoice ? Icons.mic_rounded : Icons.tag_rounded,
                  size: 14,
                  color: pt.fgDim,
                ),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: Text(
                  jr.name,
                  style: TextStyle(
                      color: pt.fgMuted,
                      fontSize: 14,
                      fontWeight: FontWeight.w500),
                  overflow: TextOverflow.ellipsis,
                ),
              ),
              if (widget.joining)
                const SizedBox(
                  width: 14,
                  height: 14,
                  child: CircularProgressIndicator.adaptive(strokeWidth: 2),
                )
              else
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
                  decoration: BoxDecoration(
                    color: _hovered ? pt.accent : pt.bg3,
                    borderRadius: BorderRadius.circular(6),
                  ),
                  child: Text(
                    'Beitreten',
                    style: TextStyle(
                        color: _hovered ? pt.accentFg : pt.fgMuted,
                        fontSize: 11,
                        fontWeight: FontWeight.w600),
                  ),
                ),
            ],
          ),
        ),
      ),
    );
  }
}

// ─── Category header ───────────────────────────────────────────────────────────

class _CategoryHeader extends StatefulWidget {
  final String name;
  final bool isCollapsed;
  final PyramidTheme pt;
  final VoidCallback onTap;
  const _CategoryHeader(
      {required this.name,
      required this.isCollapsed,
      required this.pt,
      required this.onTap});

  @override
  State<_CategoryHeader> createState() => _CategoryHeaderState();
}

class _CategoryHeaderState extends State<_CategoryHeader> {
  bool _hovered = false;

  @override
  Widget build(BuildContext context) {
    final pt = widget.pt;
    return MouseRegion(
      cursor: SystemMouseCursors.click,
      onEnter: (_) => setState(() => _hovered = true),
      onExit: (_) => setState(() => _hovered = false),
      child: GestureDetector(
        onTap: widget.onTap,
        child: Padding(
          padding: const EdgeInsets.fromLTRB(8, 10, 8, 2),
          child: Row(children: [
            AnimatedRotation(
              turns: widget.isCollapsed ? -0.25 : 0,
              duration: const Duration(milliseconds: 180),
              child: Icon(Icons.keyboard_arrow_down_rounded,
                  size: 12,
                  color: _hovered ? pt.fgMuted : pt.fgDim),
            ),
            const SizedBox(width: 4),
            Text(
              widget.name,
              style: TextStyle(
                color: _hovered ? pt.fgMuted : pt.fgDim,
                fontSize: 11,
                fontWeight: FontWeight.w600,
                letterSpacing: 0.5,
              ),
            ),
          ]),
        ),
      ),
    );
  }
}

// ─── Draggable room item (admin only) ─────────────────────────────────────────

class _DraggableRoomItem extends StatefulWidget {
  final Room room;
  final int index;
  final bool isActive;
  final bool isInVoice;
  final PyramidTheme pt;
  final VoidCallback onTap;
  const _DraggableRoomItem({
    super.key,
    required this.room,
    required this.index,
    required this.isActive,
    this.isInVoice = false,
    required this.pt,
    required this.onTap,
  });

  @override
  State<_DraggableRoomItem> createState() => _DraggableRoomItemState();
}

class _DraggableRoomItemState extends State<_DraggableRoomItem> {
  bool _hovered = false;

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      onEnter: (_) => setState(() => _hovered = true),
      onExit: (_) => setState(() => _hovered = false),
      child: Row(children: [
        ReorderableDragStartListener(
          index: widget.index,
          child: MouseRegion(
            cursor: SystemMouseCursors.grab,
            child: AnimatedOpacity(
              opacity: _hovered ? 1.0 : 0.0,
              duration: const Duration(milliseconds: 120),
              child: Padding(
                padding: const EdgeInsets.symmetric(horizontal: 3),
                child: Icon(Icons.drag_indicator_rounded,
                    size: 12, color: widget.pt.fgDim),
              ),
            ),
          ),
        ),
        Expanded(
          child: _RoomItem(
            room: widget.room,
            isActive: widget.isActive,
            isInVoice: widget.isInVoice,
            pt: widget.pt,
            onTap: widget.onTap,
          ),
        ),
      ]),
    );
  }
}

// ─── Regular rooms list (DMs + all-rooms) ─────────────────────────────────────

class _RoomsList extends ConsumerWidget {
  final List<Room> rooms;
  final String filter;
  final String? spaceId;
  final Map<String, bool> collapsed;
  final PyramidTheme pt;
  final ValueChanged<String> onToggleSection;

  const _RoomsList({
    required this.rooms,
    required this.filter,
    required this.spaceId,
    required this.collapsed,
    required this.pt,
    required this.onToggleSection,
  });

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final activeRoomId = ref.watch(activeRoomIdProvider);
    final isDms = spaceId == 'dms';

    final invites = rooms
        .where((r) =>
            r.membership == Membership.invite &&
            isDms == r.isDirectChat)
        .toList();

    final filtered = rooms.where((r) {
      if (r.membership == Membership.invite) return false;
      if (isDms != r.isDirectChat) return false;
      if (filter.isEmpty) return true;
      return r.getLocalizedDisplayname()
          .toLowerCase()
          .contains(filter.toLowerCase());
    }).toList();

    if (filtered.isEmpty && invites.isEmpty) {
      return Center(
          child:
              Text('No rooms', style: TextStyle(color: pt.fgDim, fontSize: 13)));
    }

    final sectionTitle = isDms ? 'Direct Messages' : 'Rooms';
    final sectionId = isDms ? 'dms' : 'rooms';
    final isCollapsed = collapsed[sectionId] ?? false;

    return ListView(
      padding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
      children: [
        if (invites.isNotEmpty) ...[
          Padding(
            padding: const EdgeInsets.fromLTRB(8, 6, 8, 2),
            child: Text('EINLADUNGEN',
                style: TextStyle(
                    color: pt.accent,
                    fontSize: 10,
                    fontWeight: FontWeight.w700,
                    letterSpacing: 0.6)),
          ),
          ...invites.map((r) => Padding(
              padding: const EdgeInsets.symmetric(vertical: 2),
              child: _InviteItem(room: r, pt: pt))),
          const SizedBox(height: 4),
        ],
        _SectionHeader(
          title: sectionTitle,
          count: filtered.length,
          isCollapsed: isCollapsed,
          pt: pt,
          onToggle: () => onToggleSection(sectionId),
          onAddTap: isDms
              ? () => showDialog(
                  context: context, builder: (_) => const NewDmDialog())
              : () => showDialog(
                  context: context, builder: (_) => const CreateJoinDialog()),
        ),
        if (!isCollapsed)
          ...filtered.map((room) => _RoomItem(
                room: room,
                isActive: activeRoomId == room.id,
                pt: pt,
                onTap: () {
                  ref.read(activeRoomIdProvider.notifier).state = room.id;
                  ref.read(viewModeProvider.notifier).state = ViewMode.chat;
                },
              )),
      ],
    );
  }
}

// ─── Section header ────────────────────────────────────────────────────────────

class _SectionHeader extends StatefulWidget {
  final String title;
  final int count;
  final bool isCollapsed;
  final PyramidTheme pt;
  final VoidCallback onToggle;
  final VoidCallback? onAddTap;

  const _SectionHeader({
    required this.title,
    required this.count,
    required this.isCollapsed,
    required this.pt,
    required this.onToggle,
    this.onAddTap,
  });

  @override
  State<_SectionHeader> createState() => _SectionHeaderState();
}

class _SectionHeaderState extends State<_SectionHeader> {
  bool _hovered = false;

  @override
  Widget build(BuildContext context) {
    final pt = widget.pt;
    return MouseRegion(
      onEnter: (_) => setState(() => _hovered = true),
      onExit: (_) => setState(() => _hovered = false),
      child: Container(
        padding: const EdgeInsets.fromLTRB(8, 6, 8, 4),
        child: Row(children: [
          Expanded(
            child: GestureDetector(
              onTap: widget.onToggle,
              child: Row(children: [
                AnimatedRotation(
                  turns: widget.isCollapsed ? -0.25 : 0,
                  duration: const Duration(milliseconds: 200),
                  curve: Curves.easeOutBack,
                  child: Icon(Icons.keyboard_arrow_down_rounded,
                      size: 14,
                      color: _hovered ? pt.fgMuted : pt.fgDim),
                ),
                const SizedBox(width: 4),
                Text(widget.title.toUpperCase(),
                    style: TextStyle(
                        color: _hovered ? pt.fgMuted : pt.fgDim,
                        fontSize: 11,
                        fontWeight: FontWeight.w600,
                        letterSpacing: 0.04 * 11)),
                const Spacer(),
                Text('${widget.count}',
                    style: TextStyle(color: pt.fgDim, fontSize: 11)),
              ]),
            ),
          ),
          if (_hovered && widget.onAddTap != null) ...[
            const SizedBox(width: 4),
            GestureDetector(
              behavior: HitTestBehavior.opaque,
              onTap: widget.onAddTap,
              child: SizedBox(
                width: 18,
                height: 18,
                child: Center(
                    child: Icon(Icons.add, size: 12, color: pt.fgMuted)),
              ),
            ),
          ] else
            const SizedBox(width: 22),
        ]),
      ),
    );
  }
}

// ─── Room item ─────────────────────────────────────────────────────────────────

class _RoomItem extends ConsumerStatefulWidget {
  final Room room;
  final bool isActive;
  final bool isInVoice;
  final PyramidTheme pt;
  final VoidCallback onTap;

  const _RoomItem({
    super.key,
    required this.room,
    required this.isActive,
    this.isInVoice = false,
    required this.pt,
    required this.onTap,
  });

  @override
  ConsumerState<_RoomItem> createState() => _RoomItemState();
}

class _RoomItemState extends ConsumerState<_RoomItem> {
  bool _hovered = false;

  Future<void> _leaveRoom() async {
    final pt = widget.pt;
    final confirmed = await showDialog<bool>(
      context: context,
      builder: (ctx) => AlertDialog(
        backgroundColor: pt.bg2,
        shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(pt.rXl),
            side: BorderSide(color: pt.border)),
        title: Text('Raum verlassen?',
            style: TextStyle(color: pt.fg, fontSize: 16, fontWeight: FontWeight.w700)),
        content: Text(
            '"${widget.room.getLocalizedDisplayname()}" verlassen?',
            style: TextStyle(color: pt.fgMuted, fontSize: 13)),
        actions: [
          TextButton(
              onPressed: () => Navigator.pop(ctx, false),
              child: Text('Abbrechen', style: TextStyle(color: pt.fgMuted))),
          ElevatedButton(
            style: ElevatedButton.styleFrom(
                backgroundColor: pt.danger,
                foregroundColor: Colors.white,
                elevation: 0,
                shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(8))),
            onPressed: () => Navigator.pop(ctx, true),
            child: const Text('Verlassen'),
          ),
        ],
      ),
    );
    if (confirmed != true || !mounted) return;
    try {
      final id = widget.room.id;
      await widget.room.leave();
      if (mounted && ref.read(activeRoomIdProvider) == id) {
        ref.read(activeRoomIdProvider.notifier).state = null;
      }
    } catch (_) {}
  }

  Future<void> _leaveDm() async {
    final pt = widget.pt;
    final confirmed = await showDialog<bool>(
      context: context,
      builder: (ctx) => AlertDialog(
        backgroundColor: pt.bg2,
        shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(pt.rXl),
            side: BorderSide(color: pt.border)),
        title: Text('Chat verlassen?',
            style: TextStyle(color: pt.fg, fontSize: 16, fontWeight: FontWeight.w700)),
        content: Text(
            'Unterhaltung mit "${widget.room.getLocalizedDisplayname()}" schließen?',
            style: TextStyle(color: pt.fgMuted, fontSize: 13)),
        actions: [
          TextButton(
              onPressed: () => Navigator.pop(ctx, false),
              child: Text('Abbrechen', style: TextStyle(color: pt.fgMuted))),
          ElevatedButton(
            style: ElevatedButton.styleFrom(
                backgroundColor: pt.danger,
                foregroundColor: Colors.white,
                elevation: 0,
                shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(8))),
            onPressed: () => Navigator.pop(ctx, true),
            child: const Text('Verlassen'),
          ),
        ],
      ),
    );
    if (confirmed != true || !mounted) return;
    try {
      final id = widget.room.id;
      await widget.room.leave();
      if (mounted) {
        if (ref.read(activeRoomIdProvider) == id) {
          ref.read(activeRoomIdProvider.notifier).state = null;
        }
      }
    } catch (_) {}
  }

  Future<void> _showRoomSettings() async {
    await showDialog(
      context: context,
      builder: (_) => RoomSettingsDialog(room: widget.room),
    );
  }

  Future<void> _inviteUser() async {
    final pt = widget.pt;
    final ctrl = TextEditingController();
    final userId = await showDialog<String>(
      context: context,
      builder: (ctx) => AlertDialog(
        backgroundColor: pt.bg2,
        shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(pt.rXl),
            side: BorderSide(color: pt.border)),
        title: Text('Nutzer einladen',
            style: TextStyle(
                color: pt.fg, fontSize: 16, fontWeight: FontWeight.w700)),
        content: TextField(
          controller: ctrl,
          autofocus: true,
          style: TextStyle(color: pt.fg, fontSize: 13),
          decoration: InputDecoration(
            hintText: '@nutzer:server',
            hintStyle: TextStyle(color: pt.fgDim, fontSize: 12),
            filled: true,
            fillColor: pt.bg3,
            isDense: true,
            border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(8),
                borderSide: BorderSide(color: pt.border)),
            enabledBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(8),
                borderSide: BorderSide(color: pt.border)),
            focusedBorder: OutlineInputBorder(
                borderRadius: BorderRadius.circular(8),
                borderSide: BorderSide(color: pt.accent)),
          ),
          cursorColor: pt.accent,
          onSubmitted: (v) => Navigator.pop(ctx, v.trim()),
        ),
        actions: [
          TextButton(
              onPressed: () => Navigator.pop(ctx),
              child: Text('Abbrechen', style: TextStyle(color: pt.fgMuted))),
          TextButton(
              onPressed: () => Navigator.pop(ctx, ctrl.text.trim()),
              child: Text('Einladen', style: TextStyle(color: pt.accent))),
        ],
      ),
    );
    ctrl.dispose();
    if (userId == null || userId.isEmpty || !mounted) return;
    try {
      await widget.room.invite(userId);
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content: Text('Fehler: ${e.toString().split('\n').first}'),
          backgroundColor: widget.pt.danger,
        ));
      }
    }
  }

  /// Touch-friendly action menu — opened via long-press (the hover menu is not
  /// reachable on touch devices).
  void _showContextMenu() {
    final pt = widget.pt;
    final room = widget.room;
    final isDm = room.isDirectChat;

    Widget action(IconData icon, String label, VoidCallback onTap,
        {bool danger = false}) {
      return ListTile(
        leading: Icon(icon, size: 20, color: danger ? pt.danger : pt.fgMuted),
        title: Text(label,
            style: TextStyle(
                color: danger ? pt.danger : pt.fg, fontSize: 14)),
        onTap: () {
          Navigator.pop(context);
          onTap();
        },
      );
    }

    showModalBottomSheet(
      context: context,
      backgroundColor: pt.bg2,
      shape: const RoundedRectangleBorder(
          borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
      builder: (ctx) => SafeArea(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Padding(
              padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
              child: Row(children: [
                Expanded(
                  child: Text(room.getLocalizedDisplayname(),
                      style: TextStyle(
                          color: pt.fg,
                          fontSize: 15,
                          fontWeight: FontWeight.w700),
                      overflow: TextOverflow.ellipsis),
                ),
              ]),
            ),
            Divider(height: 1, color: pt.border),
            if (!isDm) ...[
              action(Icons.person_add_outlined, 'Nutzer einladen', _inviteUser),
              action(Icons.settings_rounded, 'Einstellungen', _showRoomSettings),
            ],
            action(
              Icons.exit_to_app_rounded,
              isDm ? 'Chat verlassen' : 'Raum verlassen',
              isDm ? _leaveDm : _leaveRoom,
              danger: true,
            ),
            const SizedBox(height: 8),
          ],
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final pt = widget.pt;
    final room = widget.room;
    final active = widget.isActive;
    final inVoice = widget.isInVoice;
    final name = room.getLocalizedDisplayname();
    final unread = room.notificationCount;
    final isDm = room.isDirectChat;
    final isVoice = _isVoiceRoom(room);
    final voiceParticipants = isVoice ? ref.watch(voiceParticipantsProvider(room.id)) : <Map<String, dynamic>>[];
    final client = ref.watch(matrixClientProvider).valueOrNull;

    Color textColor = (active || inVoice || unread > 0 || _hovered) ? pt.fg : pt.fgMuted;

    return MouseRegion(
      cursor: SystemMouseCursors.click,
      onEnter: (_) => setState(() => _hovered = true),
      onExit: (_) => setState(() => _hovered = false),
      child: GestureDetector(
        onTap: widget.onTap,
        onLongPress: _showContextMenu,
        child: Stack(
          children: [
            if (active)
              Positioned(
                left: 0,
                top: 6,
                bottom: 6,
                child: Container(
                  width: 3,
                  decoration: BoxDecoration(
                    color: pt.accent,
                    borderRadius: const BorderRadius.only(
                      topRight: Radius.circular(2),
                      bottomRight: Radius.circular(2),
                    ),
                  ),
                ),
              ),
            AnimatedContainer(
              duration: const Duration(milliseconds: 150),
              curve: Curves.easeOutCubic,
              margin: const EdgeInsets.symmetric(vertical: 1),
              padding: EdgeInsets.symmetric(
                  horizontal: pt.rowPadX, vertical: pt.rowPadY),
              decoration: BoxDecoration(
                color: active
                    ? pt.accentSoft
                    : inVoice
                        ? const Color(0xFF57F287).withAlpha(25)
                        : _hovered ? pt.bgHover : Colors.transparent,
                borderRadius: BorderRadius.circular(pt.rSm),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: [
                  Row(
                    children: [
                      if (isDm) ...[
                        _DmAvatar(room: room, pt: pt),
                      ] else ...[
                        SizedBox(
                          width: 16,
                          height: 16,
                          child: Icon(
                            isVoice ? Icons.mic_rounded : _roomIcon(room),
                            size: 14,
                            color: isVoice
                                ? (inVoice ? const Color(0xFF57F287) : _hovered ? const Color(0xFF57F287) : const Color(0xFF57F287).withAlpha(180))
                                : (active ? pt.accent : _hovered ? pt.fg : pt.fgDim),
                          ),
                        ),
                      ],
                      const SizedBox(width: 8),
                      Expanded(
                        child: Text(
                          name,
                          style: TextStyle(
                            color: textColor,
                            fontSize: 14,
                            fontWeight: unread > 0 || active || inVoice
                                ? FontWeight.w600
                                : FontWeight.w500,
                          ),
                          overflow: TextOverflow.ellipsis,
                        ),
                      ),
                      if (isVoice && voiceParticipants.isNotEmpty)
                        Padding(
                          padding: const EdgeInsets.only(right: 4),
                          child: Text(
                            '${voiceParticipants.length}',
                            style: TextStyle(
                                color: const Color(0xFF57F287).withAlpha(200),
                                fontSize: 11,
                                fontWeight: FontWeight.w600),
                          ),
                        ),
                      if (!isVoice && _hovered) ...[
                        if (isDm)
                          _HoverAction(icon: Icons.close, pt: pt, onTap: _leaveDm)
                        else
                          _RoomMenuBtn(
                            pt: pt,
                            onInvite: _inviteUser,
                            onSettings: _showRoomSettings,
                            onLeave: _leaveRoom,
                          ),
                      ] else if (!isVoice && unread > 0) ...[
                        _UnreadBadge(
                            count: unread,
                            isMention: room.highlightCount > 0,
                            pt: pt),
                      ],
                    ],
                  ),
                  // Voice participants (Discord-style named rows)
                  if (isVoice && voiceParticipants.isNotEmpty)
                    Padding(
                      padding: const EdgeInsets.only(top: 2, left: 24),
                      child: _VoiceParticipantList(
                        participants: voiceParticipants,
                        client: client,
                        pt: pt,
                      ),
                    ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  IconData _roomIcon(Room room) {
    if (room.encrypted) return Icons.lock_outline_rounded;
    return Icons.tag_rounded;
  }
}

// ─── Voice participant list (Discord-style named rows) ────────────────────────

class _VoiceParticipantList extends StatelessWidget {
  final List<Map<String, dynamic>> participants;
  final Client? client;
  final PyramidTheme pt;
  const _VoiceParticipantList(
      {required this.participants, required this.client, required this.pt});

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.min,
      children: participants.map((p) {
        final avatarUrl = p['avatarUrl'] as String?;
        final name = (p['displayName'] as String? ?? '?');
        final letter = name.isNotEmpty ? name[0].toUpperCase() : '?';
        return Padding(
          padding: const EdgeInsets.only(top: 3),
          child: Row(
            children: [
              SizedBox(
                width: 16,
                height: 16,
                child: client != null && avatarUrl != null
                    ? MxcAvatar(
                        mxcUri: Uri.tryParse(avatarUrl),
                        client: client!,
                        size: 16,
                        borderRadius: BorderRadius.circular(8),
                        placeholder: (_) => _Initials(letter: letter, pt: pt, size: 16),
                      )
                    : _Initials(letter: letter, pt: pt, size: 16),
              ),
              const SizedBox(width: 6),
              Expanded(
                child: Text(
                  name,
                  style: TextStyle(
                    color: const Color(0xFF57F287).withAlpha(200),
                    fontSize: 12,
                    fontWeight: FontWeight.w500,
                  ),
                  overflow: TextOverflow.ellipsis,
                ),
              ),
              Icon(Icons.mic_rounded,
                  size: 11,
                  color: const Color(0xFF57F287).withAlpha(140)),
            ],
          ),
        );
      }).toList(),
    );
  }
}

class _Initials extends StatelessWidget {
  final String letter;
  final PyramidTheme pt;
  final double size;
  const _Initials({required this.letter, required this.pt, this.size = 18});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: size,
      height: size,
      decoration: BoxDecoration(
        color: pt.bg3,
        borderRadius: BorderRadius.circular(size / 2),
      ),
      child: Center(
        child: Text(letter,
            style: TextStyle(
                color: pt.fg,
                fontSize: size * 0.5,
                fontWeight: FontWeight.w700)),
      ),
    );
  }
}

class _VoiceBarBtn extends StatefulWidget {
  final IconData icon;
  final bool active;
  final Color activeColor;
  final Color inactiveColor;
  final PyramidTheme pt;
  final VoidCallback onTap;

  const _VoiceBarBtn({
    required this.icon,
    required this.active,
    required this.activeColor,
    required this.inactiveColor,
    required this.pt,
    required this.onTap,
  });

  @override
  State<_VoiceBarBtn> createState() => _VoiceBarBtnState();
}

class _VoiceBarBtnState extends State<_VoiceBarBtn> {
  bool _hovered = false;

  @override
  Widget build(BuildContext context) {
    final pt = widget.pt;
    final color = widget.active ? widget.activeColor : widget.inactiveColor;
    return MouseRegion(
      cursor: SystemMouseCursors.click,
      onEnter: (_) => setState(() => _hovered = true),
      onExit: (_) => setState(() => _hovered = false),
      child: GestureDetector(
        onTap: widget.onTap,
        child: AnimatedContainer(
          duration: const Duration(milliseconds: 120),
          width: 28,
          height: 28,
          decoration: BoxDecoration(
            color: _hovered ? pt.bg3 : Colors.transparent,
            borderRadius: BorderRadius.circular(6),
          ),
          child: Center(child: Icon(widget.icon, size: 16, color: color)),
        ),
      ),
    );
  }
}

// ─── Invite item ───────────────────────────────────────────────────────────────

class _InviteItem extends ConsumerStatefulWidget {
  final Room room;
  final PyramidTheme pt;
  const _InviteItem({required this.room, required this.pt});

  @override
  ConsumerState<_InviteItem> createState() => _InviteItemState();
}

class _InviteItemState extends ConsumerState<_InviteItem> {
  bool _accepting = false;
  bool _declining = false;

  Future<void> _accept() async {
    setState(() => _accepting = true);
    try {
      await widget.room.join();
      if (mounted) {
        ref.read(activeRoomIdProvider.notifier).state = widget.room.id;
        ref.read(viewModeProvider.notifier).state = ViewMode.chat;
      }
    } catch (_) {
      if (mounted) setState(() => _accepting = false);
    }
  }

  Future<void> _decline() async {
    setState(() => _declining = true);
    try {
      await widget.room.leave();
    } catch (_) {
      if (mounted) setState(() => _declining = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    final pt = widget.pt;
    final name = widget.room.getLocalizedDisplayname();
    final isDm = widget.room.isDirectChat;
    return Container(
      margin: const EdgeInsets.symmetric(vertical: 1),
      padding: const EdgeInsets.fromLTRB(10, 8, 10, 8),
      decoration: BoxDecoration(
        color: pt.accentSoft,
        borderRadius: BorderRadius.circular(pt.rSm),
        border: Border.all(color: pt.accent.withAlpha(50)),
      ),
      child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
        Row(children: [
          Icon(isDm ? Icons.person_rounded : Icons.tag_rounded,
              size: 12, color: pt.accent),
          const SizedBox(width: 6),
          Expanded(
            child: Text(name,
                style: TextStyle(
                    color: pt.fg,
                    fontSize: 13,
                    fontWeight: FontWeight.w500),
                overflow: TextOverflow.ellipsis),
          ),
        ]),
        const SizedBox(height: 6),
        Row(children: [
          Expanded(
            child: OutlinedButton(
              style: OutlinedButton.styleFrom(
                foregroundColor: pt.danger,
                side: BorderSide(color: pt.danger.withAlpha(80)),
                padding: const EdgeInsets.symmetric(vertical: 4),
                minimumSize: Size.zero,
                tapTargetSize: MaterialTapTargetSize.shrinkWrap,
                shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(6)),
              ),
              onPressed: (_declining || _accepting) ? null : _decline,
              child: _declining
                  ? const SizedBox(
                      width: 12,
                      height: 12,
                      child: CircularProgressIndicator.adaptive(strokeWidth: 1.5))
                  : const Text('Ablehnen', style: TextStyle(fontSize: 12)),
            ),
          ),
          const SizedBox(width: 6),
          Expanded(
            child: ElevatedButton(
              style: ElevatedButton.styleFrom(
                backgroundColor: pt.accent,
                foregroundColor: pt.accentFg,
                elevation: 0,
                padding: const EdgeInsets.symmetric(vertical: 4),
                minimumSize: Size.zero,
                tapTargetSize: MaterialTapTargetSize.shrinkWrap,
                shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(6)),
              ),
              onPressed: (_accepting || _declining) ? null : _accept,
              child: _accepting
                  ? const SizedBox(
                      width: 12,
                      height: 12,
                      child: CircularProgressIndicator.adaptive(strokeWidth: 1.5))
                  : const Text('Annehmen', style: TextStyle(fontSize: 12)),
            ),
          ),
        ]),
      ]),
    );
  }
}

// ─── DM avatar ─────────────────────────────────────────────────────────────────

class _DmAvatar extends ConsumerStatefulWidget {
  final Room room;
  final PyramidTheme pt;
  const _DmAvatar({required this.room, required this.pt});

  @override
  ConsumerState<_DmAvatar> createState() => _DmAvatarState();
}

class _DmAvatarState extends ConsumerState<_DmAvatar> {
  CachedPresence? _presence;
  StreamSubscription<CachedPresence>? _sub;

  @override
  void initState() {
    super.initState();
    _loadPresence();
  }

  @override
  void dispose() {
    _sub?.cancel();
    super.dispose();
  }

  Future<void> _loadPresence() async {
    final client = ref.read(matrixClientProvider).valueOrNull;
    if (client == null) return;
    final userId = widget.room.directChatMatrixID;
    if (userId == null) return;
    try {
      final p = await client.fetchCurrentPresence(userId);
      if (mounted) setState(() => _presence = p);
    } catch (_) {}
    _sub = client.onPresenceChanged.stream
        .where((p) => p.userid == userId)
        .listen((p) {
      if (mounted) setState(() => _presence = p);
    });
  }

  Color? _dotColor() {
    final p = _presence;
    if (p == null) return null;
    // Server-Presence kann veraltet sein (z.B. nie als offline gemeldet).
    // Nur als online werten, wenn die letzte Aktivität wirklich frisch ist.
    final last = p.lastActiveTimestamp;
    final fresh = last != null &&
        DateTime.now().difference(last) < const Duration(minutes: 5);
    if (p.presence == PresenceType.online &&
        (p.currentlyActive == true || fresh)) {
      return PyramidColors.online;
    }
    if (p.presence == PresenceType.unavailable) return PyramidColors.away;
    return null;
  }

  @override
  Widget build(BuildContext context) {
    final name = widget.room.getLocalizedDisplayname();
    final initial = name.isNotEmpty ? name[0].toUpperCase() : '?';
    final color = _colorFromName(name);
    final client = ref.watch(matrixClientProvider).valueOrNull;

    Widget avatar;
    if (client != null && widget.room.avatar != null) {
      avatar = MxcAvatar(
        mxcUri: widget.room.avatar,
        client: client,
        size: 22,
        borderRadius: BorderRadius.circular(11),
        placeholder: (_) =>
            _InitialCircle(initial: initial, color: color, size: 22),
      );
    } else {
      avatar = _InitialCircle(initial: initial, color: color, size: 22);
    }

    final dot = _dotColor();
    if (dot == null) return avatar;
    return Stack(clipBehavior: Clip.none, children: [
      avatar,
      Positioned(
        right: -2,
        bottom: -2,
        child: Container(
          width: 9,
          height: 9,
          decoration: BoxDecoration(
            color: dot,
            shape: BoxShape.circle,
            border: Border.all(color: widget.pt.bg1, width: 1.5),
          ),
        ),
      ),
    ]);
  }

  Color _colorFromName(String name) {
    const colors = [
      Color(0xFF06B6D4), Color(0xFFA855F7), Color(0xFF10B981),
      Color(0xFFF43F5E), Color(0xFF3B82F6), Color(0xFFF59E0B),
    ];
    if (name.isEmpty) return colors[0];
    return colors[name.codeUnitAt(0) % colors.length];
  }
}

class _InitialCircle extends StatelessWidget {
  final String initial;
  final Color color;
  final double size;
  const _InitialCircle(
      {required this.initial, required this.color, required this.size});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: size,
      height: size,
      decoration: BoxDecoration(color: color, shape: BoxShape.circle),
      child: Center(
          child: Text(initial,
              style: const TextStyle(
                  color: Colors.white,
                  fontSize: 10,
                  fontWeight: FontWeight.w600))),
    );
  }
}

// ─── Hover action ──────────────────────────────────────────────────────────────

class _RoomMenuBtn extends StatefulWidget {
  final PyramidTheme pt;
  final VoidCallback onInvite;
  final VoidCallback onSettings;
  final VoidCallback onLeave;
  const _RoomMenuBtn({required this.pt, required this.onInvite, required this.onSettings, required this.onLeave});

  @override
  State<_RoomMenuBtn> createState() => _RoomMenuBtnState();
}

class _RoomMenuBtnState extends State<_RoomMenuBtn> {
  bool _hovered = false;

  @override
  Widget build(BuildContext context) {
    return PopupMenuButton<String>(
      color: widget.pt.bg2,
      shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(10),
          side: BorderSide(color: widget.pt.border)),
      offset: const Offset(-100, 24),
      tooltip: '',
      onSelected: (v) {
        if (v == 'invite') widget.onInvite();
        if (v == 'settings') widget.onSettings();
        if (v == 'leave') widget.onLeave();
      },
      itemBuilder: (_) => [
        PopupMenuItem(
          value: 'invite',
          height: 36,
          child: Row(children: [
            Icon(Icons.person_add_outlined, size: 14, color: widget.pt.fgMuted),
            const SizedBox(width: 8),
            Text('Nutzer einladen', style: TextStyle(color: widget.pt.fg, fontSize: 13)),
          ]),
        ),
        PopupMenuItem(
          value: 'settings',
          height: 36,
          child: Row(children: [
            Icon(Icons.settings_rounded, size: 14, color: widget.pt.fgMuted),
            const SizedBox(width: 8),
            Text('Einstellungen', style: TextStyle(color: widget.pt.fg, fontSize: 13)),
          ]),
        ),
        PopupMenuDivider(height: 1),
        PopupMenuItem(
          value: 'leave',
          height: 36,
          child: Row(children: [
            Icon(Icons.exit_to_app_rounded, size: 14, color: widget.pt.danger),
            const SizedBox(width: 8),
            Text('Raum verlassen', style: TextStyle(color: widget.pt.danger, fontSize: 13)),
          ]),
        ),
      ],
      child: MouseRegion(
        onEnter: (_) => setState(() => _hovered = true),
        onExit: (_) => setState(() => _hovered = false),
        child: AnimatedContainer(
          duration: const Duration(milliseconds: 120),
          width: 20,
          height: 20,
          decoration: BoxDecoration(
            color: _hovered ? widget.pt.bgActive : Colors.transparent,
            borderRadius: BorderRadius.circular(4),
          ),
          child: Center(
              child: Icon(Icons.more_horiz_rounded,
                  size: 12,
                  color: _hovered ? widget.pt.fg : widget.pt.fgDim)),
        ),
      ),
    );
  }
}

class _HoverAction extends StatefulWidget {
  final IconData icon;
  final PyramidTheme pt;
  final VoidCallback? onTap;
  const _HoverAction({required this.icon, required this.pt, this.onTap});

  @override
  State<_HoverAction> createState() => _HoverActionState();
}

class _HoverActionState extends State<_HoverAction> {
  bool _hovered = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: widget.onTap,
      child: MouseRegion(
        cursor: widget.onTap != null
            ? SystemMouseCursors.click
            : MouseCursor.defer,
        onEnter: (_) => setState(() => _hovered = true),
        onExit: (_) => setState(() => _hovered = false),
        child: AnimatedContainer(
          duration: const Duration(milliseconds: 120),
          width: 20,
          height: 20,
          decoration: BoxDecoration(
            color: _hovered ? widget.pt.bgActive : Colors.transparent,
            borderRadius: BorderRadius.circular(4),
          ),
          child: Center(
              child: Icon(widget.icon,
                  size: 12,
                  color: _hovered ? widget.pt.fg : widget.pt.fgDim)),
        ),
      ),
    );
  }
}

// ─── Unread badge ──────────────────────────────────────────────────────────────

class _UnreadBadge extends StatelessWidget {
  final int count;
  final bool isMention;
  final PyramidTheme pt;
  const _UnreadBadge(
      {required this.count, required this.isMention, required this.pt});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 5),
      constraints: const BoxConstraints(minWidth: 18, minHeight: 18),
      decoration: BoxDecoration(
        color: isMention ? pt.danger : pt.accent,
        borderRadius: BorderRadius.circular(999),
      ),
      child: Center(
          child: Text(count > 99 ? '99+' : '$count',
              style: TextStyle(
                  color: isMention ? Colors.white : pt.accentFg,
                  fontSize: 10,
                  fontWeight: FontWeight.w700))),
    );
  }
}
