import 'dart:async';
import 'dart:developer';

import 'package:flutter_app_badge/flutter_app_badge.dart';

import '../../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
import '../../../../../api/marianumcloud/talk/room/get_room_response.dart';
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
import '../repository/chat_list_repository.dart';
import 'chat_list_event.dart';
import 'chat_list_state.dart';

class ChatListBloc
    extends
        LoadableHydratedBloc<ChatListEvent, ChatListState, ChatListRepository> {
  bool _forceRenew = false;
  Timer? _autoRefreshTimer;
  Duration? _autoRefreshInterval;

  @override
  void retry() {
    _forceRenew = true;
    super.retry();
  }

  @override
  Future<void> close() {
    _autoRefreshTimer?.cancel();
    return super.close();
  }

  /// Silent refresh — explicit pull-to-refresh and tab-activation are non-silent.
  void setAutoRefreshInterval(Duration? interval) {
    if (interval == _autoRefreshInterval) return;
    _autoRefreshInterval = interval;
    _autoRefreshTimer?.cancel();
    _autoRefreshTimer = null;
    if (interval == null) return;
    _autoRefreshTimer = Timer.periodic(interval, (_) {
      if (isClosed) return;
      refresh(silent: true);
    });
  }

  @override
  ChatListRepository repository() => ChatListRepository();

  @override
  ChatListState fromNothing() => const ChatListState();

  @override
  ChatListState fromStorage(Map<String, dynamic> json) =>
      ChatListState.fromJson(json);

  @override
  Map<String, dynamic>? toStorage(ChatListState state) => state.toJson();

  @override
  Future<void> gatherData() async {
    final renew = _forceRenew;
    _forceRenew = false;

    Object? capturedError;
    final rooms = await repo.data.getRooms(
      renew: renew,
      onError: (e) => capturedError = e,
    );
    add(DataGathered((s) => s.copyWith(rooms: rooms)));
    _updateAppBadge(rooms);

    if (capturedError != null) throw capturedError!;
  }

  Future<void> refresh({bool renew = true, bool silent = false}) async {
    if (!silent) add(RefetchStarted<ChatListState>());
    Object? capturedError;
    try {
      final rooms = await repo.data.getRooms(
        renew: renew,
        onError: (e) => capturedError = e,
      );
      add(DataGathered((s) => s.copyWith(rooms: rooms)));
      _updateAppBadge(rooms);
    } catch (e) {
      capturedError = e;
    }
    if (capturedError != null) addLoadingError(capturedError!);
  }

  /// Creates (or resolves) a 1:1 chat and returns its room token, or null in
  /// demo mode. Refreshes the list so the room shows up.
  Future<String?> createDirectChat(String invite) async {
    final token = await repo.data.createDirectRoom(invite);
    await refresh();
    return token;
  }

  int? lastReadMessageFor(String token) {
    final rooms = innerState?.rooms;
    if (rooms == null) return null;
    for (final room in rooms.data) {
      if (room.token == token) return room.lastReadMessage;
    }
    return null;
  }

  /// Optimistic — server-side mark-as-read is the caller's job.
  void markRoomAsRead(String token, int lastMessageId) {
    _mutateRoom(token, (r) {
      if (r.unreadMessages == 0 && r.lastReadMessage >= lastMessageId) {
        return false;
      }
      r.unreadMessages = 0;
      r.unreadMention = false;
      r.unreadMentionDirect = false;
      if (lastMessageId > r.lastReadMessage) r.lastReadMessage = lastMessageId;
      return true;
    });
  }

  /// Clears unread too — long-poll only feeds this in for an actively-open chat.
  void applyIncomingMessage(String token, GetChatResponseObject message) {
    _mutateRoom(token, (r) {
      final wasRead =
          r.unreadMessages == 0 && r.lastReadMessage >= message.id;
      final hasNewer = r.lastMessage.id >= message.id;
      if (wasRead && hasNewer) return false;
      r.unreadMessages = 0;
      r.unreadMention = false;
      r.unreadMentionDirect = false;
      if (message.id > r.lastReadMessage) r.lastReadMessage = message.id;
      if (message.id > r.lastMessage.id) r.lastMessage = message;
      if (message.timestamp > r.lastActivity) r.lastActivity = message.timestamp;
      return true;
    });
  }

  /// Re-wraps in a fresh [GetRoomResponse] so identity-based equality picks it up.
  void _mutateRoom(
    String token,
    bool Function(GetRoomResponseObject room) mutator,
  ) {
    final rooms = innerState?.rooms;
    if (rooms == null) return;
    var changed = false;
    final updated = rooms.data.map((r) {
      if (r.token != token) return r;
      if (mutator(r)) changed = true;
      return r;
    }).toSet();
    if (!changed) return;
    final newRooms = GetRoomResponse(updated)..headers = rooms.headers;
    add(Emit((s) => s.copyWith(rooms: newRooms)));
    _updateAppBadge(newRooms);
  }

  void _updateAppBadge(GetRoomResponse rooms) {
    try {
      final unread = rooms.data.fold<int>(
        0,
        (a, room) => a + room.unreadMessages,
      );
      FlutterAppBadge.count(unread);
    } on Object catch (e) {
      log('Failed to update app badge: $e');
    }
  }
}
