part of '../chat_view.dart';

class _MessageList extends StatelessWidget {
  final Timeline timeline;
  final ScrollController scrollCtrl;
  final String currentUserId;
  final PyramidTheme pt;
  final ValueChanged<Event> onReply;
  final Set<String> selectedIds;
  final ValueChanged<String> onToggleSelect;
  final String? sessionFullyReadId;
  final bool sessionHasUnread;
  final bool unreadFading;
  /// Attached to the group-level _UnreadDivider so ensureVisible can scroll to it.
  final GlobalKey? unreadDividerKey;
  /// Jump-to-message target (from search): scroll to + highlight this event.
  final String? jumpEventId;
  final GlobalKey? jumpKey;
  final String? highlightEventId;
  final bool highlightOn;

  const _MessageList({
    required this.timeline,
    required this.scrollCtrl,
    required this.currentUserId,
    required this.pt,
    required this.onReply,
    required this.selectedIds,
    required this.onToggleSelect,
    this.sessionFullyReadId,
    this.sessionHasUnread = false,
    this.unreadFading = false,
    this.unreadDividerKey,
    this.jumpEventId,
    this.jumpKey,
    this.highlightEventId,
    this.highlightOn = false,
  });

  @override
  Widget build(BuildContext context) {
    // Dedupe by eventId — during concurrent forward/backward pagination the
    // timeline can transiently contain the same event twice, which would
    // produce duplicate ListView keys and crash the sliver (indexOf assertion).
    final seenIds = <String>{};
    final events = timeline.events
        .where(_isVisible)
        .where((e) => seenIds.add(e.eventId))
        .toList();

    if (events.isEmpty) {
      return Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Icon(Icons.forum_outlined, size: 48, color: pt.fgDim),
            const SizedBox(height: 16),
            Text(
              'No messages yet',
              style: TextStyle(color: pt.fgMuted, fontSize: 14),
            ),
          ],
        ),
      );
    }

    final groups = _buildGroups(events);
    final hasUnread = sessionHasUnread;

    // Resolve the effective fullyReadId: if the captured ID points to a
    // non-visible event (reaction, edit, state) walk the timeline forward
    // (toward older events) until we find the nearest visible one.
    String? fullyReadId = sessionFullyReadId;
    if (fullyReadId != null) {
      final inGroups = groups.any((g) => g.any((e) => e.eventId == fullyReadId));
      if (!inGroups) {
        final tlEvents = timeline.events; // newest-first
        final idx = tlEvents.indexWhere((e) => e.eventId == fullyReadId);
        if (idx >= 0) {
          fullyReadId = null;
          for (int i = idx; i < tlEvents.length; i++) {
            if (_isVisible(tlEvents[i])) {
              fullyReadId = tlEvents[i].eventId;
              break;
            }
          }
        } else {
          fullyReadId = null; // event not even in loaded timeline
        }
      }
    }

    // Read-receipt display — matching Element behaviour:
    //   • The newest unread own group shows ✓ (sent but not read)
    //   • The newest own group AT or BEFORE the partner's receipt shows ✓✓
    //   • If those are the same group → just ✓✓
    final room = timeline.room;

    // Read receipts (Element-style):
    //  • ✓✓ at the single newest event another member has read (any sender) —
    //    moves next to the partner's own reply when they reply/read everything.
    //  • grey ✓ only on my newest message while it's still unread by them.
    // timeline.events is newest-first → lower index = newer.
    //
    // Receipts können je nach Client in `global` ODER `mainThread` stehen
    // (threaded read receipts) — beide zusammenführen, neuere ts gewinnt.
    final mergedReceipts = <String, LatestReceiptStateData>{};
    for (final source in [
      room.receiptState.global.otherUsers,
      room.receiptState.mainThread?.otherUsers ?? const {},
    ]) {
      source.forEach((userId, data) {
        final existing = mergedReceipts[userId];
        if (existing == null || data.ts > existing.ts) {
          mergedReceipts[userId] = data;
        }
      });
    }

    int readIndex = -1;
    int latestReceiptTs = 0;
    mergedReceipts.forEach((userId, data) {
      if (userId == currentUserId) return;
      if (data.ts > latestReceiptTs) latestReceiptTs = data.ts;
      var ri = timeline.events.indexWhere((e) => e.eventId == data.eventId);
      // Receipt kann auf ein unsichtbares Event zeigen (Reaktion, Edit) —
      // zum nächstälteren sichtbaren Event vorrücken, sonst geht der Marker
      // verloren.
      while (ri >= 0 &&
          ri < timeline.events.length &&
          !_isVisible(timeline.events[ri])) {
        ri++;
      }
      if (ri >= timeline.events.length) ri = -1;
      if (ri >= 0 && (readIndex < 0 || ri < readIndex)) readIndex = ri;
    });
    final readEventIds =
        readIndex >= 0 ? {timeline.events[readIndex].eventId} : <String>{};

    // Newest own visible message, and whether it's still unread (newer than the
    // partner's read position, or no read position yet).
    final newestOwnIndex =
        timeline.events.indexWhere((e) => _isVisible(e) && e.senderId == currentUserId);
    var ownUnread = newestOwnIndex >= 0 && (readIndex < 0 || newestOwnIndex < readIndex);
    // Fallback: Das Receipt-Event liegt außerhalb des geladenen Fensters
    // (z.B. sehr alte Nachricht) — wenn der Receipt-Zeitstempel jünger ist als
    // meine neueste Nachricht, gilt sie trotzdem als gelesen.
    if (ownUnread &&
        readIndex < 0 &&
        latestReceiptTs >=
            timeline.events[newestOwnIndex].originServerTs.millisecondsSinceEpoch) {
      readEventIds.add(timeline.events[newestOwnIndex].eventId);
      ownUnread = false;
    }
    final lastOwnGroupKey = (ownUnread)
        ? () {
            for (final g in groups) {
              if (g.first.senderId == currentUserId) return 'g:${g.first.eventId}';
            }
            return null;
          }()
        : null;

    // Flatten groups + date dividers + unread divider into independent list items.
    // itemFullyReadIds[i] is non-null only for a group item that contains the
    // fullyRead event at a non-last position — MessageGroup renders an inline
    // divider after that event.
    final itemKeys = <String>[];
    final itemGroups = <List<Event>?>[];
    final itemDates = <DateTime?>[];
    final itemUnread = <bool>[];
    final itemFullyReadIds = <String?>[];

    bool unreadInserted = false;

    for (var i = 0; i < groups.length; i++) {
      final group = groups[i];
      // Use the newest event (group.last after reversal) for date comparison.
      final groupNewest = group.last.originServerTs;
      final nextGroupNewest = i < groups.length - 1 ? groups[i + 1].last.originServerTs : null;
      final isLastOfDay = nextGroupNewest == null || !_sameDay(groupNewest, nextGroupNewest);

      // Check if this group contains the effective fullyRead event.
      final groupContainsFullyRead = fullyReadId != null &&
          group.any((e) => e.eventId == fullyReadId);

      if (!unreadInserted && hasUnread && groupContainsFullyRead) {
        if (group.last.eventId == fullyReadId) {
          // fullyRead is the NEWEST event in the group → the whole group is
          // read.  Insert a group-level divider between this group and the
          // newer (lower-index) groups.
          itemKeys.add('unread');
          itemGroups.add(null);
          itemDates.add(null);
          itemUnread.add(true);
          itemFullyReadIds.add(null);
        }
        // Whether group-level or inline, we've now handled the marker.
        unreadInserted = true;
      }

      // Key based on oldest event (group.first) — stable identifier.
      itemKeys.add('g:${group.first.eventId}');
      itemGroups.add(group);
      itemDates.add(null);
      itemUnread.add(false);
      // Pass fullyReadId to the group only when the event is mid-group
      // (not the last event) so MessageGroup can render an inline divider.
      // groupContainsFullyRead already guarantees fullyReadId != null.
      final passedFullyReadId =
          (groupContainsFullyRead && group.last.eventId != fullyReadId)
              ? fullyReadId
              : null;
      itemFullyReadIds.add(passedFullyReadId);

      if (isLastOfDay) {
        final d = groupNewest;
        itemKeys.add('d:${d.year}-${d.month}-${d.day}');
        itemGroups.add(null);
        itemDates.add(d);
        itemUnread.add(false);
        itemFullyReadIds.add(null);
      }
    }

    // Fallback: fullyRead event is older than all loaded events — all visible
    // messages are unread. Insert divider above the oldest loaded group (top of list).
    if (!unreadInserted && hasUnread) {
      itemKeys.add('unread');
      itemGroups.add(null);
      itemDates.add(null);
      itemUnread.add(true);
      itemFullyReadIds.add(null);
    }

    final keyIndex = <String, int>{
      for (var i = 0; i < itemKeys.length; i++) itemKeys[i]: i,
    };

    return ListView.builder(
      controller: scrollCtrl,
      reverse: true,
      physics: const ClampingScrollPhysics(),
      padding: const EdgeInsets.only(bottom: 8),
      scrollCacheExtent: const ScrollCacheExtent.pixels(2500),
      itemCount: itemKeys.length,
      findChildIndexCallback: (Key key) {
        // Only return an index when the key still maps to that exact slot —
        // returning a stale/duplicate index trips the sliver indexOf assertion.
        if (key is ValueKey<String>) {
          final idx = keyIndex[key.value];
          if (idx != null && idx < itemKeys.length && itemKeys[idx] == key.value) {
            return idx;
          }
        }
        return null;
      },
      itemBuilder: (context, i) {
        final key = ValueKey(itemKeys[i]);
        if (itemUnread[i]) {
          // Wrap in SizedBox so the list can use the ValueKey for optimisation
          // while _UnreadDivider carries the GlobalKey for ensureVisible.
          return SizedBox(
            key: key,
            child: _UnreadDivider(key: unreadDividerKey, pt: pt, fading: unreadFading),
          );
        }
        final date = itemDates[i];
        if (date != null) return DateDivider(key: key, date: date);
        final group = itemGroups[i]!;
        final groupKey = itemKeys[i];
        // Show the sending/sent indicator on the newest own group.
        final showStatus = groupKey == lastOwnGroupKey;
        return MessageGroup(
          key: key,
          events: group,
          currentUserId: currentUserId,
          room: room,
          timeline: timeline,
          onReply: () => onReply(group.last),
          selectedIds: selectedIds,
          onToggleSelect: onToggleSelect,
          showStatusIndicator: showStatus,
          readEventIds: readEventIds,
          fullyReadId: itemFullyReadIds[i],
          jumpEventId: jumpEventId,
          jumpKey: jumpKey,
          highlightEventId: highlightEventId,
          highlightOn: highlightOn,
        );
      },
    );
  }

  List<List<Event>> _buildGroups(List<Event> events) {
    final groups = <List<Event>>[];
    List<Event>? current;

    for (final event in events) {
      if (current == null ||
          current.first.senderId != event.senderId ||
          event.originServerTs.difference(current.last.originServerTs).abs() >
              const Duration(minutes: 5)) {
        current = [event];
        groups.add(current);
      } else {
        current.add(event);
      }
    }
    // The timeline is newest-first, so each group is [newest, ..., oldest].
    // Reverse each group so messages render oldest-to-newest (top to bottom).
    return groups.map((g) => g.reversed.toList()).toList();
  }

  bool _sameDay(DateTime a, DateTime b) =>
      a.year == b.year && a.month == b.month && a.day == b.day;

  static bool _isVisible(Event e) {
    // NOTE: error-status events are kept visible so a failed send shows a
    // "not sent" state with retry/delete instead of silently disappearing.
    if (!{
      EventTypes.Message,
      EventTypes.Sticker,
      EventTypes.Encrypted,
    }.contains(e.type)) { return false; }
    if ({
      RelationshipTypes.edit,
      RelationshipTypes.reaction,
    }.contains(e.relationshipType)) { return false; }
    if (e.type == EventTypes.Redaction) { return false; }
    return true;
  }
}

class _SelectionHeader extends StatelessWidget {
  final PyramidTheme pt;
  final int count;
  final Set<String> selectedIds;
  final Room? room;
  final Timeline? timeline;
  final String currentUserId;
  final VoidCallback onClear;
  final VoidCallback onReply;

  const _SelectionHeader({
    required this.pt,
    required this.count,
    required this.selectedIds,
    required this.room,
    required this.timeline,
    required this.currentUserId,
    required this.onClear,
    required this.onReply,
  });

  List<Event> _resolveEvents() {
    if (timeline == null) return [];
    return selectedIds
        .map((id) {
          try { return timeline!.events.firstWhere((e) => e.eventId == id); } catch (_) { return null; }
        })
        .whereType<Event>()
        .toList();
  }

  void _copyAll() {
    final texts = _resolveEvents().map((e) => e.body).join('\n');
    Clipboard.setData(ClipboardData(text: texts));
  }

  void _deleteAll(BuildContext context) {
    for (final e in _resolveEvents()) {
      if (e.senderId == currentUserId) {
        e.redactEvent(reason: 'Vom Nutzer gelöscht').catchError((_) => null);
      }
    }
    onClear();
  }

  void _pinAll() {
    if (room == null) return;
    final pinned = List<String>.from(
      room!.getState(EventTypes.RoomPinnedEvents)?.content['pinned'] as List? ?? [],
    );
    for (final e in _resolveEvents()) {
      if (!pinned.contains(e.eventId)) pinned.add(e.eventId);
    }
    room!.setPinnedEvents(pinned).catchError((_) => '');
    onClear();
  }


  @override
  Widget build(BuildContext context) {
    final events = _resolveEvents();
    final canDelete = events.isNotEmpty && events.every((e) => e.senderId == currentUserId);
    final isTextOnly = events.every((e) => e.messageType == 'm.text');

    return Container(
      height: 52,
      padding: const EdgeInsets.symmetric(horizontal: 8),
      decoration: BoxDecoration(
        color: pt.accentSoft,
        border: Border(bottom: BorderSide(color: pt.accent.withAlpha(80))),
      ),
      child: Row(
        children: [
          IconButton(
            onPressed: onClear,
            icon: Icon(Icons.close_rounded, size: 20, color: pt.accent),
            tooltip: 'Auswahl aufheben',
            constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
            padding: EdgeInsets.zero,
          ),
          const SizedBox(width: 4),
          Text(
            '$count ausgewählt',
            style: TextStyle(color: pt.fg, fontSize: 15, fontWeight: FontWeight.w600),
          ),
          const Spacer(),
          if (count == 1)
            _SelBtn(icon: Icons.reply_rounded, tooltip: 'Antworten', pt: pt, onTap: onReply),
          if (isTextOnly)
            _SelBtn(icon: Icons.copy_rounded, tooltip: 'Kopieren', pt: pt, onTap: _copyAll),
          _SelBtn(icon: Icons.push_pin_outlined, tooltip: 'Anpinnen', pt: pt, onTap: _pinAll),
          if (canDelete)
            _SelBtn(
              icon: Icons.delete_outline_rounded,
              tooltip: 'Löschen',
              pt: pt,
              color: pt.danger,
              onTap: () => _deleteAll(context),
            ),
        ],
      ),
    );
  }
}

class _SelBtn extends StatelessWidget {
  final IconData icon;
  final String tooltip;
  final PyramidTheme pt;
  final Color? color;
  final VoidCallback onTap;

  const _SelBtn({required this.icon, required this.tooltip, required this.pt, required this.onTap, this.color});

  @override
  Widget build(BuildContext context) {
    return IconButton(
      onPressed: onTap,
      icon: Icon(icon, size: 20, color: color ?? pt.fg),
      tooltip: tooltip,
      constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
      padding: EdgeInsets.zero,
    );
  }
}

class _UnreadDivider extends StatelessWidget {
  final PyramidTheme pt;
  final bool fading;
  const _UnreadDivider({super.key, required this.pt, this.fading = false});

  @override
  Widget build(BuildContext context) {
    return AnimatedOpacity(
      opacity: fading ? 0.0 : 1.0,
      duration: const Duration(milliseconds: 700),
      curve: Curves.easeOut,
      child: Padding(
        padding: const EdgeInsets.fromLTRB(20, 8, 20, 4),
        child: Row(
          children: [
            Expanded(child: Divider(color: pt.accent.withAlpha(160), height: 1)),
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 10),
              child: Text(
                'Neue Nachrichten',
                style: TextStyle(color: pt.accent, fontSize: 11, fontWeight: FontWeight.w600),
              ),
            ),
            Expanded(child: Divider(color: pt.accent.withAlpha(160), height: 1)),
          ],
        ),
      ),
    );
  }
}
