import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:result_monad/result_monad.dart';

import '../friendica_client/friendica_client.dart';
import '../friendica_client/paged_response.dart';
import '../friendica_client/pages_manager.dart';
import '../friendica_client/paging_data.dart';
import '../globals.dart';
import '../models/auth/profile.dart';
import '../models/exec_error.dart';
import '../models/user_notification.dart';
import '../serializers/mastodon/follow_request_mastodon_extensions.dart';
import '../utils/active_profile_selector.dart';
import 'auth_service.dart';
import 'direct_message_service.dart';
import 'feature_version_checker.dart';
import 'follow_requests_manager.dart';
import 'network_status_service.dart';

class NotificationsManager extends ChangeNotifier {
  static const itemsPerQuery = 50;
  static const minimumDmsAndCrsUpdateDuration = Duration(seconds: 30);
  static final _logger = Logger('NotificationManager');
  final Profile profile;
  final dms = <UserNotification>[];
  final connectionRequests = <UserNotification>[];
  final unread = <UserNotification>[];
  final read = <UserNotification>[];
  var lastDmsUpdate = DateTime(1900);
  var lastCrUpdate = DateTime(1900);

  NotificationsManager(this.profile);

  var _firstLoad = true;

  List<UserNotification> get notifications {
    if (_firstLoad) {
      loadUnreadNotifications(true);
      _firstLoad = false;
    }

    return [...connectionRequests, ...dms, ...unread, ...read];
  }

  void clear({bool withListenerNotification = true}) {
    dms.clear();
    connectionRequests.clear();
    unread.clear();
    read.clear();
    _firstLoad = true;
    notifyListeners();
  }

  void refreshNotifications() async {
    clear(withListenerNotification: false);
    await loadUnreadNotifications(false);
    if (unread.isEmpty && read.isEmpty) {
      await loadOlderNotifications(withListenerNotification: false);
    }
    notifyListeners();
  }

  FutureResult<List<UserNotification>, ExecError> loadUnreadNotifications(
      bool withListenerNotification) async {
    final notificationsFromRefresh = <UserNotification>[];

    final pm = _buildPageManager(profile, false);
    final useActualRequests = getIt<FriendicaVersionChecker>()
        .canUseFeature(RelaticaFeatures.usingActualFollowRequests);
    var hasMore = true;
    var first = true;
    const maxCalls = 3;
    var count = 0;
    while (hasMore && count < maxCalls) {
      final result =
          first ? await pm.initialize(itemsPerQuery) : await pm.nextFromEnd();

      first = false;
      result.match(
          onSuccess: (nd) => print('Got ${nd.data.length} notifications'),
          onError: (e) => debugPrint('Error getting notification: $e'));
      final response = result.getValueOrElse(() => PagedResponse([]));
      response.data
          .where((n) =>
              !useActualRequests || n.type != NotificationType.follow_request)
          .forEach(notificationsFromRefresh.add);
      hasMore = response.next != null;
      count++;
    }

    // filter out connection requests if going to use the real service for that when doing the query
    // get earliest and latest notification ID from unread notifications

    // query all notifications over that in page increments of 25
    // query unread notifications in increments of 25 after the latest ID
    return await _postFetchOperations(
      notificationsFromRefresh,
      withListenerNotification,
    );
  }

  FutureResult<List<UserNotification>, ExecError> loadNewerNotifications({
    bool withListenerNotification = true,
  }) async {
    final (_, highestId) =
        unread.isNotEmpty ? calcLowHigh(unread) : calcLowHigh(read);
    final pm = _buildPageManager(
      profile,
      true,
      initialPages: read.isEmpty && unread.isEmpty
          ? []
          : [
              PagedResponse(
                <String>[],
                previous: PagingData(
                  minId: highestId,
                  limit: itemsPerQuery,
                ),
              )
            ],
    );

    final result = await (unread.isEmpty && read.isEmpty
            ? pm.initialize(itemsPerQuery)
            : pm.previousFromBeginning())
        .andThenAsync(
          (page) async =>
              await _postFetchOperations(page.data, withListenerNotification),
        )
        .withError(
            (error) => _logger.info('Error getting more updates: $error'));
    return result.execErrorCast();
  }

  FutureResult<List<UserNotification>, ExecError> loadOlderNotifications(
      {bool withListenerNotification = true}) async {
    if (unread.isNotEmpty) {
      final result =
          await _loadOlderUnreadNotifications(withListenerNotification);
      if (result.getValueOrElse(() => []).isNotEmpty) {
        return result;
      }
    }

    return _loadOlderReadAndUnreadNotifications(withListenerNotification);
  }

  FutureResult<bool, ExecError> markSeen(UserNotification notification) async {
    final result = await NotificationsClient(profile)
        .clearNotification(notification)
        .withResult((_) {
      unread.remove(notification);
      read.add(notification.copy(dismissed: true));
      read.sort();
      notifyListeners();
    });

    return result.execErrorCast();
  }

  FutureResult<bool, ExecError> markAllAsRead() async {
    final result =
        await NotificationsClient(getIt<AccountsService>().currentProfile)
            .clearNotifications()
            .withResult((_) {
      unread.map((n) => n.copy(dismissed: true)).forEach(read.add);
      unread.clear();
      read.sort();
      notifyListeners();
    });

    return result.execErrorCast();
  }

  List<UserNotification> buildUnreadMessageNotifications(
      bool useActualRequests) {
    final myId = profile.userId;
    final dmsResult = getIt<ActiveProfileSelector<DirectMessageService>>()
        .getForProfile(profile)
        .transform((d) => d.getThreads(unreadyOnly: true).map((t) {
              final fromAccount =
                  t.participants.firstWhere((p) => p.id != myId);
              final latestMessage = t.messages
                  .reduce((s, m) => s.createdAt > m.createdAt ? s : m);
              return UserNotification(
                  id: (fromAccount.hashCode ^
                          t.parentUri.hashCode ^
                          t.title.hashCode)
                      .toString(),
                  type: NotificationType.direct_message,
                  fromId: fromAccount.id,
                  fromName: fromAccount.name,
                  fromUrl: fromAccount.profileUrl,
                  timestamp: latestMessage.createdAt,
                  iid: t.parentUri,
                  dismissed: false,
                  content: '${fromAccount.name} sent you a direct message',
                  link: '');
            }).toList())
        .getValueOrElse(() => []);

    final followRequestResult = !useActualRequests
        ? []
        : getIt<ActiveProfileSelector<FollowRequestsManager>>()
            .getForProfile(profile)
            .transform(
                (fm) => fm.requests.map((r) => r.toUserNotification()).toList())
            .getValueOrElse(() => []);

    return [...dmsResult, ...followRequestResult];
  }

  void updateNotification(UserNotification notification) {}

  FutureResult<List<UserNotification>, ExecError> _postFetchOperations(
    List<UserNotification> notificationsFromRefresh,
    bool withListenerNotification,
  ) async {
    getIt<NetworkStatusService>().startNotificationUpdate();
    if (DateTime.now().difference(lastDmsUpdate) >
        minimumDmsAndCrsUpdateDuration) {
      await getIt<ActiveProfileSelector<DirectMessageService>>()
          .getForProfile(profile)
          .transformAsync((dms) async => await dms.updateThreads());
      lastDmsUpdate = DateTime.now();
    }

    final useActualRequests = getIt<FriendicaVersionChecker>()
        .canUseFeature(RelaticaFeatures.usingActualFollowRequests);

    if (useActualRequests) {
      if (DateTime.now().difference(lastCrUpdate) >
          minimumDmsAndCrsUpdateDuration) {
        await getIt<ActiveProfileSelector<FollowRequestsManager>>()
            .getForProfile(profile)
            .transformAsync((fm) async => fm.update());
        lastCrUpdate = DateTime.now();
      }
    }

    final notifications = <String, UserNotification>{};

    notificationsFromRefresh.removeWhere((n) =>
        n.type == NotificationType.direct_message ||
        (useActualRequests && n.type == NotificationType.follow_request));
    for (final n in notificationsFromRefresh) {
      notifications[n.id] = n;
    }

    getIt<NetworkStatusService>().finishNotificationUpdate();
    for (final n in buildUnreadMessageNotifications(useActualRequests)) {
      notifications[n.id] = n;
    }

    _processNewNotifications(notifications.values);

    if (withListenerNotification) {
      notifyListeners();
    }
    return Result.ok(notifications.values.toList());
  }

  Future<void> _processNewNotifications(
      Iterable<UserNotification> notifications) async {
    final dmsMap = <String, UserNotification>{};
    final crMap = <String, UserNotification>{};
    final unreadMap = <String, UserNotification>{};
    final readMap = <String, UserNotification>{};

    final st = Stopwatch()..start();

    for (int i = 0; i < dms.length; i++) {
      dmsMap[dms[i].id] = dms[i];
    }

    if (st.elapsedMilliseconds > maxProcessingMillis) {
      await Future.delayed(processingSleep, () => st.reset());
    }

    for (int i = 0; i < connectionRequests.length; i++) {
      crMap[connectionRequests[i].id] = connectionRequests[i];
    }

    if (st.elapsedMilliseconds > maxProcessingMillis) {
      await Future.delayed(processingSleep, () => st.reset());
    }

    for (int i = 0; i < unread.length; i++) {
      unreadMap[unread[i].id] = unread[i];
    }

    if (st.elapsedMilliseconds > maxProcessingMillis) {
      await Future.delayed(processingSleep, () => st.reset());
    }

    for (int i = 0; i < read.length; i++) {
      readMap[read[i].id] = read[i];
    }

    dms.clear();
    connectionRequests.clear();
    unread.clear();
    read.clear();

    for (final n in notifications) {
      if (st.elapsedMilliseconds > maxProcessingMillis) {
        await Future.delayed(processingSleep, () => st.reset());
      }
      dmsMap.remove(n.id);
      crMap.remove(n.id);
      unreadMap.remove(n.id);
      readMap.remove(n.id);
      if (n.dismissed) {
        readMap[n.id] = n;
        continue;
      }

      switch (n.type) {
        case NotificationType.direct_message:
          dmsMap[n.id] = n;
          break;
        case NotificationType.follow_request:
          crMap[n.id] = n;
          break;
        default:
          unreadMap[n.id] = n;
      }
    }

    dms
      ..addAll(dmsMap.values)
      ..sort();
    connectionRequests
      ..addAll(crMap.values)
      ..sort();
    unread
      ..addAll(unreadMap.values)
      ..sort();
    read
      ..addAll(readMap.values)
      ..sort();
  }

  FutureResult<List<UserNotification>, ExecError> _loadOlderUnreadNotifications(
      bool withListenerNotification) async {
    final (lowestId, _) = calcLowHigh(unread);
    final pm = _buildPageManager(
      profile,
      false,
      initialPages: [
        PagedResponse(
          <String>[],
          next: PagingData(
            maxId: lowestId,
            limit: itemsPerQuery,
          ),
        )
      ],
    );

    final result = await pm
        .nextFromEnd()
        .andThenAsync(
          (page) async =>
              await _postFetchOperations(page.data, withListenerNotification),
        )
        .withError(
            (error) => _logger.info('Error getting more updates: $error'));

    return result.execErrorCast();
  }

  FutureResult<List<UserNotification>, ExecError>
      _loadOlderReadAndUnreadNotifications(
          bool withListenerNotification) async {
    final (lowestId, _) =
        read.isNotEmpty ? calcLowHigh(read) : calcLowHigh(unread);
    final pm = _buildPageManager(
      profile,
      true,
      initialPages: read.isEmpty && unread.isEmpty
          ? []
          : [
              PagedResponse(
                <String>[],
                next: PagingData(
                  maxId: lowestId,
                  limit: itemsPerQuery,
                ),
              )
            ],
    );
    final result = await (read.isEmpty && unread.isEmpty
            ? pm.initialize(itemsPerQuery)
            : pm.nextFromEnd())
        .andThenAsync(
          (page) async =>
              await _postFetchOperations(page.data, withListenerNotification),
        )
        .withError(
            (error) => _logger.info('Error getting more updates: $error'));
    return result.execErrorCast();
  }
}

(int lowest, int highest) calcLowHigh(List<UserNotification> notifications) {
  int highestNotificationId = -1;
  int lowestNotificationId = 0x7FFFFFFFFFFFFFFF;
  final ids = notifications
      .where((n) =>
          n.type != NotificationType.direct_message &&
          n.type != NotificationType.follow_request)
      .map((n) => int.parse(n.id));

  for (var id in ids) {
    if (id > highestNotificationId) {
      highestNotificationId = id;
    }

    if (id < lowestNotificationId) {
      lowestNotificationId = id;
    }
  }

  return (lowestNotificationId, highestNotificationId);
}

PagesManager<List<UserNotification>, String> _buildPageManager(
        Profile profile, bool includeAll,
        {List<PagedResponse> initialPages = const []}) =>
    PagesManager<List<UserNotification>, String>(
      initialPages: initialPages,
      idMapper: (nn) => nn.map((n) => n.id).toList(),
      onRequest: (pd) async =>
          await NotificationsClient(profile).getNotifications(pd, includeAll),
    );
