part of '../chat_view.dart';

class ChatView extends ConsumerStatefulWidget {
  final String roomId;
  const ChatView({super.key, required this.roomId});

  @override
  ConsumerState<ChatView> createState() => _ChatViewState();
}

class _ChatViewState extends ConsumerState<ChatView> {
  final _scrollCtrl = ScrollController();
  Event? _replyTo;
  bool _loadingHistory = false;
  bool _historyExhausted = false;
  bool _draggingOver = false;
  bool _pinnedOpen = false;
  bool _profileExpanded = false;
  bool _showScrollDown = false;
  final Set<String> _selectedIds = {};

  // Unread state captured at session open (before marking as read)
  String? _sessionFullyReadId;
  bool _sessionHasUnread = false;
  bool _sessionCaptured = false;
  bool _didScrollToUnread = false;
  bool _loadingFuture = false;
  // Fade-out of the "Neue Nachrichten" divider once the user reaches the bottom.
  bool _unreadFading = false;
  Timer? _unreadFadeTimer;

  // Jump-to-message (from search): scroll to + briefly highlight a target event.
  final _jumpKey = GlobalKey();
  String? _jumpEventId;
  String? _highlightEventId;
  bool _highlightOn = false;
  bool _jumpInProgress = false;
  Timer? _highlightTimer;

  void _startJump(String eventId) {
    _highlightTimer?.cancel();
    setState(() {
      _jumpEventId = eventId;
      _highlightEventId = eventId;
      _highlightOn = true;
      _jumpInProgress = true;
    });
    // Consume the request so it doesn't re-fire.
    ref.read(pendingJumpEventProvider.notifier).state = null;
    WidgetsBinding.instance.addPostFrameCallback((_) => _attemptJump(0));
  }

  void _attemptJump(int attempt) {
    if (!mounted || _jumpEventId == null) return;
    final ctx = _jumpKey.currentContext;
    if (ctx != null) {
      Scrollable.ensureVisible(
        ctx,
        alignment: 0.4,
        duration: const Duration(milliseconds: 300),
        curve: Curves.easeOut,
      );
      _jumpInProgress = false;
      // Hold the highlight briefly, then fade it out smoothly.
      _highlightTimer?.cancel();
      _highlightTimer = Timer(const Duration(milliseconds: 1300), () {
        if (!mounted) return;
        setState(() => _highlightOn = false); // triggers the AnimatedOpacity fade
        _highlightTimer = Timer(const Duration(milliseconds: 950), () {
          if (mounted) setState(() { _highlightEventId = null; _jumpEventId = null; });
        });
      });
      return;
    }
    if (attempt >= 40) {
      // Couldn't reach the event — give up gracefully (room is at least open).
      setState(() { _jumpInProgress = false; _highlightEventId = null; _jumpEventId = null; _highlightOn = false; });
      return;
    }
    // Not built yet — load older history toward the target every few tries.
    final timeline = ref.read(timelineProvider(widget.roomId)).valueOrNull;
    if (timeline != null && timeline.canRequestHistory && attempt % 3 == 0) {
      timeline.requestHistory(historyCount: 100).catchError((_) {});
    }
    WidgetsBinding.instance.addPostFrameCallback((_) {
      Future.delayed(const Duration(milliseconds: 120), () => _attemptJump(attempt + 1));
    });
  }

  // GlobalKey for the "Neue Nachrichten" divider — used by ensureVisible to
  // scroll precisely to the divider on chat open.
  final _unreadDividerKey = GlobalKey();

  bool get _inSelectionMode => _selectedIds.isNotEmpty;

  void _toggleSelect(String eventId) {
    setState(() {
      if (_selectedIds.contains(eventId)) {
        _selectedIds.remove(eventId);
      } else {
        _selectedIds.add(eventId);
      }
    });
  }

  void _clearSelection() => setState(() => _selectedIds.clear());

  @override
  void initState() {
    super.initState();
    _scrollCtrl.addListener(_onScroll);
  }

  @override
  void didUpdateWidget(ChatView oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.roomId != widget.roomId) {
      _profileExpanded = false;
      _sessionFullyReadId = null;
      _sessionHasUnread = false;
      _sessionCaptured = false;
      _didScrollToUnread = false;
      _loadingFuture = false;
      _historyExhausted = false;
      _unreadFading = false;
      _unreadFadeTimer?.cancel();
      _jumpEventId = null;
      _highlightEventId = null;
      _highlightOn = false;
      _jumpInProgress = false;
      _highlightTimer?.cancel();
      // The GlobalKey stays alive — it'll simply have no context until the
      // new room's divider (if any) is built.
    }
  }

  @override
  void dispose() {
    _unreadFadeTimer?.cancel();
    _highlightTimer?.cancel();
    _scrollCtrl.dispose();
    super.dispose();
  }

  void _captureAndMarkRead(Timeline timeline) {
    if (!_sessionCaptured) {
      final room = timeline.room;
      final fullyRead = room.fullyRead; // "" if never set
      final newFullyReadId = fullyRead.isNotEmpty ? fullyRead : null;

      // Show the divider only if there's a genuinely unread VISIBLE message
      // FROM SOMEONE ELSE after the read marker. Own messages and hidden events
      // (reactions, receipts, edits) don't count — otherwise the divider ends
      // up below all messages (e.g. right after one's own latest message).
      final me = room.client.userID;
      final events = timeline.events; // newest-first
      final frIndex =
          fullyRead.isEmpty ? -1 : events.indexWhere((e) => e.eventId == fullyRead);
      // Indices [0, upTo) are newer than the read marker.
      final upTo = frIndex >= 0 ? frIndex : events.length;
      var newHasUnread = false;
      for (var i = 0; i < upTo; i++) {
        final e = events[i];
        if (_MessageList._isVisible(e) && e.senderId != me) {
          newHasUnread = true;
          break;
        }
      }

      _sessionCaptured = true;

      // setState so _MessageList rebuilds with the captured unread marker.
      if (mounted) {
        setState(() {
          _sessionFullyReadId = newFullyReadId;
          _sessionHasUnread = newHasUnread;
        });
        if (newHasUnread) {
          WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToUnread(timeline));
        }
      }
    }
    _markRead(timeline);
  }

  void _scrollToUnread(Timeline timeline) {
    if (_didScrollToUnread) return;
    _attemptScrollToUnread(0);
  }

  // The "Neue Nachrichten" divider may not be laid out yet on first open (lazy
  // reverse ListView only builds visible + cacheExtent items). Retry until its
  // context exists, nudging the scroll upward periodically so the list builds
  // toward the divider when there are many unread messages.
  void _attemptScrollToUnread(int attempt) {
    if (_didScrollToUnread || !mounted) return;
    final ctx = _unreadDividerKey.currentContext;
    if (ctx != null) {
      _didScrollToUnread = true;
      // alignment 0.85: in a reverse list this lands the divider near the top
      // of the viewport with a little read context above it and the unread
      // messages filling the screen below.
      Scrollable.ensureVisible(
        ctx,
        alignment: 0.85,
        duration: const Duration(milliseconds: 250),
        curve: Curves.easeOut,
      );
      return;
    }
    if (attempt >= 24) {
      _didScrollToUnread = true; // give up — stay at the newest messages
      return;
    }
    if (attempt > 0 && attempt % 4 == 0 && _scrollCtrl.hasClients) {
      final pos = _scrollCtrl.position;
      final target = (pos.pixels + 1500).clamp(0.0, pos.maxScrollExtent);
      if (target > pos.pixels) _scrollCtrl.jumpTo(target);
    }
    WidgetsBinding.instance.addPostFrameCallback((_) {
      Future.delayed(const Duration(milliseconds: 70),
          () => _attemptScrollToUnread(attempt + 1));
    });
  }

  void _markRead([Timeline? tl]) {
    // Cancel any pending notification for this room (user is now reading it).
    cancelNotificationForRoom(widget.roomId);

    final sendReceipts = ref.read(privacyReadReceiptsProvider);
    if (!sendReceipts) return;
    final room = ref.read(roomProvider(widget.roomId));
    if (room == null) return;
    final timeline = tl ?? ref.read(timelineProvider(widget.roomId)).valueOrNull;
    final latest = timeline?.events.firstOrNull;
    if (latest != null && latest.status.isSynced) {
      room.setReadMarker(latest.eventId, mRead: latest.eventId)
          .catchError((_) {});
    } else {
      room.markUnread(false).catchError((_) {});
    }
  }

  Future<void> _onScroll() async {
    if (!_scrollCtrl.hasClients) return;
    final pixels = _scrollCtrl.position.pixels;
    final maxExtent = _scrollCtrl.position.maxScrollExtent;

    if (pixels < 60) {
      _markRead(null);
      // Reached the newest messages → the "Neue Nachrichten" divider is no
      // longer useful. Fade it out, then remove it.
      if (_sessionHasUnread && !_unreadFading) {
        setState(() => _unreadFading = true);
        _unreadFadeTimer?.cancel();
        _unreadFadeTimer = Timer(const Duration(milliseconds: 900), () {
          if (mounted) setState(() { _sessionHasUnread = false; _unreadFading = false; });
        });
      }
    }
    final shouldShow = pixels > 400;
    if (shouldShow != _showScrollDown) {
      setState(() => _showScrollDown = shouldShow);
    }

    final timeline = ref.read(timelineProvider(widget.roomId)).valueOrNull;
    if (timeline == null) return;

    // Backward pagination — older events near the top. We keep requesting until
    // a request genuinely returns nothing new (real start of room), tracked via
    // _historyExhausted, so we neither stop early nor hammer the server.
    if (!_loadingHistory &&
        !_historyExhausted &&
        pixels >= maxExtent - 600) {
      _loadingHistory = true;
      final oldestBefore = timeline.events.isNotEmpty ? timeline.events.last.eventId : null;
      final lenBefore = timeline.events.length;
      try {
        await timeline.requestHistory(historyCount: 60);
      } catch (_) {}
      final oldestAfter = timeline.events.isNotEmpty ? timeline.events.last.eventId : null;
      // Nothing older arrived → we've reached the beginning of the room.
      if (timeline.events.length == lenBefore && oldestBefore == oldestAfter) {
        _historyExhausted = true;
      }
      await Future.delayed(const Duration(milliseconds: 200));
      _loadingHistory = false;
    }

    // Forward pagination — only relevant after jumping to a historical anchor
    // (e.g. from search). At the live edge canRequestFuture is false, so this
    // won't fire during normal reading and can't pull the view back down.
    if (!_loadingFuture &&
        timeline.canRequestFuture &&
        pixels < 200) {
      _loadingFuture = true;
      try {
        await timeline.requestFuture(historyCount: 60);
      } catch (_) {}
      await Future.delayed(const Duration(milliseconds: 250));
      _loadingFuture = false;
    }
  }

  @override
  Widget build(BuildContext context) {
    final pt = PyramidTheme.of(context);
    final room = ref.watch(roomProvider(widget.roomId));
    final timelineAsync = ref.watch(timelineProvider(widget.roomId));
    final membersOpen = ref.watch(membersPanelOpenProvider);

    // A search result requested a jump to a specific message. Using watch (not
    // listen) so it also fires when this ChatView is freshly mounted for a
    // DIFFERENT room — listen would miss a value set before registration.
    final pendingJump = ref.watch(pendingJumpEventProvider);
    if (pendingJump != null && !_jumpInProgress && pendingJump != _jumpEventId) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (mounted &&
            !_jumpInProgress &&
            ref.read(pendingJumpEventProvider) == pendingJump) {
          _startJump(pendingJump);
        }
      });
    }

    final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux;
    final body = Stack(
        fit: StackFit.expand,
        children: [
          Row(
            children: [
              Expanded(
                child: Container(
                  color: pt.bg1,
                  child: Column(
                    children: [
                      if (_inSelectionMode)
                        _SelectionHeader(
                          pt: pt,
                          count: _selectedIds.length,
                          selectedIds: Set.from(_selectedIds),
                          room: room,
                          timeline: ref.read(timelineProvider(widget.roomId)).valueOrNull,
                          currentUserId: room?.client.userID ?? '',
                          onClear: _clearSelection,
                          onReply: () {
                            final tl = ref.read(timelineProvider(widget.roomId)).valueOrNull;
                            if (tl == null) return;
                            try {
                              final event = tl.events.firstWhere((e) => e.eventId == _selectedIds.first);
                              setState(() { _replyTo = event; _selectedIds.clear(); });
                            } catch (_) {}
                          },
                        )
                      else if (room != null && room.isDirectChat)
                        // DM: aufklappbarer Banner-Header — Avatar und Name
                        // skalieren mit, Banner und Bio blenden ein.
                        // ValueKey: pro Raum ein frischer State, damit beim
                        // schnellen Kanalwechsel keinerlei Daten (Avatar,
                        // Banner, laufende Futures) vom Vorgänger übrigbleiben.
                        _DmExpandingHeader(
                          key: ValueKey('dmheader:${room.id}'),
                          room: room,
                          roomId: widget.roomId,
                          pt: pt,
                          expanded: _profileExpanded,
                          onToggle: () => setState(
                              () => _profileExpanded = !_profileExpanded),
                          membersOpen: membersOpen,
                          pinnedOpen: _pinnedOpen,
                          onToggleMembers: () => ref
                              .read(membersPanelOpenProvider.notifier)
                              .update((s) => !s),
                          onOpenSearch: () =>
                              ref.read(activeModalProvider.notifier).state =
                                  ModalKind.search,
                          onTogglePinned: () =>
                              setState(() => _pinnedOpen = !_pinnedOpen),
                        )
                      else
                        _ChatHeader(
                          room: room,
                          roomId: widget.roomId,
                          pt: pt,
                          membersOpen: membersOpen,
                          pinnedOpen: _pinnedOpen,
                          onToggleMembers: () => ref
                              .read(membersPanelOpenProvider.notifier)
                              .update((s) => !s),
                          onOpenSearch: () =>
                              ref.read(activeModalProvider.notifier).state =
                                  ModalKind.search,
                          onTogglePinned: () =>
                              setState(() => _pinnedOpen = !_pinnedOpen),
                        ),
                      const _ConnectionBanner(),
                      Expanded(
                        child: Stack(
                          children: [
                            timelineAsync.when(
                              loading: () => Center(
                                child: CircularProgressIndicator(
                                  strokeWidth: 2,
                                  color: pt.accent,
                                ),
                              ),
                              error: (e, _) => Center(
                                child: Text(
                                  'Could not load messages',
                                  style: TextStyle(color: pt.fgDim, fontSize: 14),
                                ),
                              ),
                              data: (timeline) {
                                WidgetsBinding.instance.addPostFrameCallback(
                                    (_) => _captureAndMarkRead(timeline));
                                return _MessageList(
                                  timeline: timeline,
                                  scrollCtrl: _scrollCtrl,
                                  currentUserId: room?.client.userID ?? '',
                                  pt: pt,
                                  onReply: (event) => setState(() => _replyTo = event),
                                  selectedIds: _selectedIds,
                                  onToggleSelect: _toggleSelect,
                                  sessionFullyReadId: _sessionFullyReadId,
                                  sessionHasUnread: _sessionHasUnread,
                                  unreadFading: _unreadFading,
                                  unreadDividerKey: _sessionHasUnread ? _unreadDividerKey : null,
                                  jumpEventId: _jumpEventId,
                                  jumpKey: _jumpKey,
                                  highlightEventId: _highlightEventId,
                                  highlightOn: _highlightOn,
                                );
                              },
                            ),
                            if (_showScrollDown)
                              Positioned(
                                right: 16,
                                bottom: 12,
                                child: AnimatedOpacity(
                                  opacity: _showScrollDown ? 1 : 0,
                                  duration: const Duration(milliseconds: 150),
                                  child: GestureDetector(
                                    onTap: () => _scrollCtrl.animateTo(
                                      0,
                                      duration: const Duration(milliseconds: 300),
                                      curve: Curves.easeOut,
                                    ),
                                    child: Container(
                                      width: 36,
                                      height: 36,
                                      decoration: BoxDecoration(
                                        color: pt.bg0,
                                        borderRadius: BorderRadius.circular(18),
                                        border: Border.all(color: pt.border),
                                        boxShadow: [
                                          BoxShadow(
                                            color: Colors.black.withAlpha(80),
                                            blurRadius: 8,
                                            offset: const Offset(0, 2),
                                          ),
                                        ],
                                      ),
                                      child: Icon(Icons.keyboard_arrow_down_rounded, size: 20, color: pt.fgMuted),
                                    ),
                                  ),
                                ),
                              ),
                          ],
                        ),
                      ),
                      if (room != null) _TypingIndicator(room: room, pt: pt),
                      if (room != null)
                        ChatComposer(
                          room: room,
                          replyTo: _replyTo,
                          onClearReply: () => setState(() => _replyTo = null),
                        ),
                    ],
                  ),
                ),
              ),
              if (_pinnedOpen)
                PinnedMessagesPanel(
                  roomId: widget.roomId,
                  onClose: () => setState(() => _pinnedOpen = false),
                ),
            ],
          ),
          // Drag-and-drop overlay — bleibt gemountet und blendet animiert
          // ein/aus, damit der Übergang flüssig statt abrupt ist.
          Positioned.fill(
            child: IgnorePointer(
              child: AnimatedOpacity(
                opacity: _draggingOver ? 1 : 0,
                duration: const Duration(milliseconds: 160),
                curve: Curves.easeOutCubic,
                child: _DragOverlay(pt: pt, active: _draggingOver),
              ),
            ),
          ),
        ],
    );

    if (!isDesktop) return body;

    return DropTarget(
      onDragEntered: (_) => setState(() => _draggingOver = true),
      onDragExited: (_) => setState(() => _draggingOver = false),
      onDragDone: (details) async {
        setState(() => _draggingOver = false);
        if (room == null) return;
        final files = details.files.map((xf) => File(xf.path)).toList();
        if (files.isEmpty) return;
        if (!mounted) return;
        await AttachmentDialog.show(context, files, room, replyTo: _replyTo);
        if (mounted) setState(() => _replyTo = null);
      },
      child: body,
    );
  }
}
