import 'dart:async';
import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';

import '../../../api/marianumcloud/talk/chat/get_chat_response.dart';
import '../../../api/marianumcloud/talk/room/get_room_response.dart';
import '../../../extensions/date_time.dart';
import '../../../notification/notification_tasks.dart';
import '../../../routing/app_routes.dart';
import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
import '../../../state/app/modules/chat/bloc/chat_bloc.dart';
import '../../../state/app/modules/chat/bloc/chat_state.dart';
import '../../../state/app/modules/chat_list/bloc/chat_list_bloc.dart';
import '../../../widget/chat_background.dart';
import '../../../widget/clickable_app_bar.dart';
import '../../../widget/user_avatar.dart';
import 'data/chat_search_controller.dart';
import 'details/chat_info.dart';
import 'talk_navigator.dart';
import 'widgets/chat_bubble.dart';
import 'widgets/chat_search_app_bar.dart';
import 'widgets/chat_textfield.dart';

class ChatView extends StatefulWidget {
  final GetRoomResponseObject room;
  final String selfId;
  final UserAvatar avatar;

  const ChatView({
    super.key,
    required this.room,
    required this.selfId,
    required this.avatar,
  });

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

class _ChatViewState extends State<ChatView> with RouteAware {
  final ItemScrollController _itemScrollController = ItemScrollController();
  final ItemPositionsListener _positionsListener =
      ItemPositionsListener.create();
  final TextEditingController _searchTextController = TextEditingController();
  final Map<int, int> _matchIndices = {};

  // Number of rows currently rendered; kept in sync in build so the scroll
  // listener can tell when the oldest row (highest index, reverse list) nears.
  int _itemCount = 0;

  bool _searchActive = false;
  String _searchQuery = '';
  List<ChatSearchMatch> _matches = const [];
  int _activeMatchIndex = 0;
  GetChatResponse? _matchesComputedFor;
  String? _matchesComputedQuery;

  // Captured in initState because the framework has unmounted us by the
  // time dispose runs.
  ChatBloc? _chatBlocRef;
  ChatListBloc? _chatListBlocRef;
  PageRoute<dynamic>? _subscribedRoute;

  @override
  void initState() {
    super.initState();
    _chatBlocRef = context.read<ChatBloc>();
    _chatListBlocRef = context.read<ChatListBloc>();
    _positionsListener.itemPositions.addListener(_onScrollPositions);
    NotificationTasks.clearNotificationsForChat(widget.room.token);
  }

  /// Loads the next older block once the top of the list comes into view.
  void _onScrollPositions() {
    final positions = _positionsListener.itemPositions.value;
    if (positions.isEmpty) return;
    // reverse:true → the highest index is the oldest message (top of screen).
    // Prefetch ~a screenful ahead so the next block is usually already merged
    // before the user reaches the top — the load stays invisible.
    final maxIndex = positions.map((p) => p.index).reduce(math.max);
    if (maxIndex < _itemCount - _kLoadOlderPrefetchRows) return;
    final data = _chatBlocRef?.state.data;
    if (data == null || !data.hasMoreOld || data.isLoadingOlder) return;
    _chatBlocRef?.loadOlder();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final route = ModalRoute.of(context);
    if (route is PageRoute && route != _subscribedRoute) {
      if (_subscribedRoute != null) {
        AppRoutes.chatRouteObserver.unsubscribe(this);
      }
      AppRoutes.chatRouteObserver.subscribe(this, route);
      _subscribedRoute = route;
    }
  }

  @override
  void didPopNext() {
    super.didPopNext();
    // A stacked chat above us was just popped (typical: notification tap
    // opened another chat). The global ChatBloc currently points at that
    // other chat's token, so our isReady predicate fails until we re-claim.
    _chatBlocRef?.setToken(widget.room.token);
  }

  @override
  void dispose() {
    if (_subscribedRoute != null) {
      AppRoutes.chatRouteObserver.unsubscribe(this);
    }
    _positionsListener.itemPositions.removeListener(_onScrollPositions);
    _markAsReadFinal();
    _chatBlocRef?.leaveChat(widget.room.token);
    _searchTextController.dispose();
    super.dispose();
  }

  /// Defensive final mark-as-read so a back-out before the long-poll
  /// could fire doesn't leave the room as unread. Skipped when the bloc
  /// has already moved on to another chat — the response data there
  /// belongs to a different room, and writing its max-id as our marker
  /// would regress our server cursor.
  void _markAsReadFinal() {
    final state = _chatBlocRef?.state.data;
    if (state == null) return;
    if (state.currentToken != widget.room.token) return;
    final response = state.chatResponse;
    if (response == null) return;
    final maxId = response.data.map((m) => m.id).fold<int>(0, math.max);
    if (maxId == 0) return;
    _chatListBlocRef?.markRoomAsRead(widget.room.token, maxId);
    unawaited(_chatBlocRef!.sendServerReadMarker(widget.room.token, maxId));
  }

  @override
  void didUpdateWidget(covariant ChatView oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.room.token != oldWidget.room.token && _searchActive) {
      _exitSearchMode();
    }
  }

  void _refresh() {
    context.read<ChatBloc>().setToken(widget.room.token);
  }

  void _enterSearchMode() {
    setState(() {
      _searchActive = true;
      _searchQuery = '';
      _matches = const [];
      _activeMatchIndex = 0;
      _matchesComputedFor = null;
      _matchesComputedQuery = null;
      _searchTextController.clear();
    });
  }

  void _exitSearchMode() {
    setState(() {
      _searchActive = false;
      _searchQuery = '';
      _matches = const [];
      _activeMatchIndex = 0;
      _matchIndices.clear();
      _matchesComputedFor = null;
      _matchesComputedQuery = null;
      _searchTextController.clear();
    });
  }

  void _onSearchChanged(String q) {
    final chatResponse = context.read<ChatBloc>().state.data?.chatResponse;
    setState(() {
      _searchQuery = q;
      _activeMatchIndex = 0;
      if (chatResponse != null) {
        _recomputeMatches(chatResponse);
      } else {
        _matches = const [];
        _matchesComputedFor = null;
        _matchesComputedQuery = null;
      }
    });
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted && _matches.isNotEmpty) _scrollToActiveMatch();
    });
  }

  void _recomputeMatches(GetChatResponse response) {
    _matches = ChatSearchController.findMatches(response, _searchQuery);
    _activeMatchIndex = _activeMatchIndex.clamp(
      0,
      math.max(0, _matches.length - 1),
    );
    _matchesComputedFor = response;
    _matchesComputedQuery = _searchQuery;
  }

  void _goToPreviousMatch() {
    if (_matches.isEmpty) return;
    setState(() {
      _activeMatchIndex = (_activeMatchIndex + 1) % _matches.length;
    });
    WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToActiveMatch());
  }

  void _goToNextMatch() {
    if (_matches.isEmpty) return;
    setState(() {
      _activeMatchIndex =
          (_activeMatchIndex - 1 + _matches.length) % _matches.length;
    });
    WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToActiveMatch());
  }

  void _scrollToActiveMatch() {
    if (_matches.isEmpty) return;
    if (!_itemScrollController.isAttached) return;
    final id = _matches[_activeMatchIndex].messageId;
    final idx = _matchIndices[id];
    if (idx == null) return;
    _itemScrollController.scrollTo(
      index: idx,
      alignment: 0.4,
      duration: const Duration(milliseconds: 350),
      curve: Curves.easeInOut,
    );
  }

  List<Widget> _buildMessages(GetChatResponse response) {
    if (_searchActive &&
        (response != _matchesComputedFor ||
            _searchQuery != _matchesComputedQuery)) {
      _recomputeMatches(response);
    }

    final matchIds = _matches.map((m) => m.messageId).toSet();
    final activeId = _matches.isNotEmpty
        ? _matches[_activeMatchIndex].messageId
        : null;
    final highlightQuery = _searchActive && _searchQuery.trim().isNotEmpty
        ? _searchQuery
        : null;

    final commonRead = int.parse(
      response.headers?['x-chat-last-common-read'] ?? '0',
    );

    final messages = <Widget>[];
    final chronologicalMatchIndex = <int, int>{};
    var lastDate = DateTime.now();
    for (final element in response.sortByTimestamp()) {
      if (ChatSearchController.isHiddenSystemMessage(element)) continue;

      final elementDate = DateTime.fromMillisecondsSinceEpoch(
        element.timestamp * 1000,
      );

      if (!elementDate.isSameDay(lastDate)) {
        lastDate = elementDate;
        messages.add(
          ChatBubble(
            isSender: false,
            bubbleData: GetChatResponseObject.getDateDummy(element.timestamp),
            chatData: widget.room,
            refetch: ({bool renew = false}) => _refresh(),
          ),
        );
      }

      final isMatch = matchIds.contains(element.id);
      final highlight = isMatch
          ? (element.id == activeId
                ? SearchHighlight.active
                : SearchHighlight.secondary)
          : SearchHighlight.none;

      if (isMatch) chronologicalMatchIndex[element.id] = messages.length;

      messages.add(
        ChatBubble(
          isSender:
              element.actorId == widget.selfId &&
              (element.messageType ==
                      GetRoomResponseObjectMessageType.comment ||
                  element.messageType ==
                      GetRoomResponseObjectMessageType.deletedComment),
          bubbleData: element,
          chatData: widget.room,
          refetch: ({bool renew = false}) => _refresh(),
          isRead: element.id <= commonRead,
          selfId: widget.selfId,
          highlightQuery: highlightQuery,
          matchHighlight: highlight,
        ),
      );
    }

    final total = messages.length;
    _matchIndices
      ..clear()
      ..addEntries(
        chronologicalMatchIndex.entries.map(
          (e) => MapEntry(e.key, (total - 1) - e.value),
        ),
      );

    return messages;
  }

  @override
  Widget build(BuildContext context) {
    // Swallow the first back gesture while the keyboard is visible so it
    // dismisses the IME instead of popping the chat — matches platform UX
    // expectations (e.g. WhatsApp/Telegram) and prevents accidental exits
    // mid-typing.
    final keyboardOpen = MediaQuery.viewInsetsOf(context).bottom > 0;
    return PopScope(
      canPop: !keyboardOpen,
      onPopInvokedWithResult: (didPop, _) {
        if (didPop) return;
        FocusManager.instance.primaryFocus?.unfocus();
      },
      child: Scaffold(
        backgroundColor: const Color(0xffefeae2),
        appBar: _searchActive
            ? ChatSearchAppBar(
                controller: _searchTextController,
                matchCount: _matches.length,
                activeIndex: _matches.isEmpty ? -1 : _activeMatchIndex,
                onChanged: _onSearchChanged,
                onClose: _exitSearchMode,
                onPrevious: _matches.isEmpty ? null : _goToPreviousMatch,
                onNext: _matches.isEmpty ? null : _goToNextMatch,
              )
            : ClickableAppBar(
                onTap: () =>
                    TalkNavigator.pushSplitView(context, ChatInfo(widget.room)),
                appBar: AppBar(
                  title: Row(
                    children: [
                      widget.avatar,
                      const SizedBox(width: 10),
                      Expanded(
                        child: Text(
                          widget.room.displayName,
                          overflow: TextOverflow.ellipsis,
                          maxLines: 1,
                        ),
                      ),
                    ],
                  ),
                  actions: [
                    IconButton(
                      icon: const Icon(Icons.search),
                      tooltip: 'In Chat suchen',
                      onPressed: _enterSearchMode,
                    ),
                  ],
                ),
              ),
        body: ChatBackground(
          child: Column(
            children: [
              Expanded(
                child: LoadableStateConsumer<ChatBloc, ChatState>(
                  isReady: (state) =>
                      state.chatResponse != null &&
                      state.currentToken == widget.room.token,
                  enablePullToRefresh: false,
                  child: (state, _) {
                    final items = _buildMessages(
                      state.chatResponse!,
                    ).reversed.toList();
                    // reverse:true renders index 0 at the bottom, so the top
                    // marker (spinner / start-of-chat) goes at the end.
                    if (state.isLoadingOlder) {
                      items.add(const _LoadingOlderIndicator());
                    } else if (!state.hasMoreOld) {
                      items.add(
                        ChatBubble(
                          isSender: false,
                          bubbleData: GetChatResponseObject.getTextDummy(
                            'Anfang des Chats',
                          ),
                          chatData: widget.room,
                          refetch: ({bool renew = false}) => _refresh(),
                        ),
                      );
                    }
                    _itemCount = items.length;
                    return ScrollablePositionedList.builder(
                      reverse: true,
                      itemScrollController: _itemScrollController,
                      itemPositionsListener: _positionsListener,
                      itemCount: items.length,
                      itemBuilder: (ctx, idx) => items[idx],
                    );
                  },
                ),
              ),
              ColoredBox(
                color: Theme.of(context).colorScheme.surface,
                child: TalkNavigator.isSecondaryVisible(context)
                    ? ChatTextfield(
                        widget.room.token,
                        selfId: widget.selfId,
                        roomType: widget.room.type,
                      )
                    : SafeArea(
                        child: ChatTextfield(
                          widget.room.token,
                          selfId: widget.selfId,
                          roomType: widget.room.type,
                        ),
                      ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

/// How many rows before the oldest one the older-history prefetch kicks in.
const _kLoadOlderPrefetchRows = 15;

class _LoadingOlderIndicator extends StatelessWidget {
  const _LoadingOlderIndicator();

  @override
  Widget build(BuildContext context) => const Padding(
    padding: EdgeInsets.symmetric(vertical: 12),
    child: Center(
      child: SizedBox(
        width: 22,
        height: 22,
        child: CircularProgressIndicator(strokeWidth: 2),
      ),
    ),
  );
}
