import 'dart:collection';
import 'dart:math';

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

import '../data/interfaces/circles_repo_intf.dart';
import '../data/interfaces/connections_repo_intf.dart';
import '../friendica_client/friendica_client.dart';
import '../friendica_client/paging_data.dart';
import '../globals.dart';
import '../models/auth/profile.dart';
import '../models/circle_data.dart';
import '../models/connection.dart';
import '../models/exec_error.dart';
import '../utils/active_profile_selector.dart';
import 'persistent_info_service.dart';

class ConnectionsManager extends ChangeNotifier {
  static final _logger = Logger('$ConnectionsManager');
  late final IConnectionsRepo conRepo;
  late final ICirclesRepo circlesRepo;
  late final Profile profile;
  var circlesNotInitialized = true;
  var _lastUpdateStatus = '';

  String get lastUpdateStatus => _lastUpdateStatus.isNotEmpty
      ? _lastUpdateStatus
      : getIt<ActiveProfileSelector<PersistentInfoService>>()
          .getForProfile(profile)
          .transform(
              (info) => 'Last updated at: ${info.lastMyConnectionsUpdate}')
          .withResult((text) => _lastUpdateStatus = text)
          .getValueOrElse(() => 'Unknown');

  ConnectionsManager(this.profile, this.conRepo, this.circlesRepo);

  void clear() {
    conRepo.clear();
    circlesRepo.clear();
    circlesNotInitialized = true;
    _lastUpdateStatus = '';
    notifyListeners();
  }

  List<Connection> getKnownUsersByName(String name) {
    return conRepo.getKnownUsersByName(name);
  }

  bool upsertConnection(Connection connection) {
    if (connection.status != ConnectionStatus.unknown) {
      return conRepo.upsertConnection(connection);
    }

    return conRepo.getById(connection.id).fold(
          onSuccess: (original) => conRepo.upsertConnection(
            connection.copy(status: original.status),
          ),
          onError: (_) => conRepo.upsertConnection(connection),
        );
  }

  Future<bool> upsertAllConnections(Iterable<Connection> newConnections) async {
    var result = true;
    for (var c in newConnections) {
      result &= await Future.delayed(Duration.zero, () => upsertConnection(c));
    }

    return result;
  }

  Future<void> acceptFollowRequest(Connection connection) async {
    _logger.finest(
        'Attempting to accept follow request ${connection.name}: ${connection.status}');
    await RelationshipsClient(profile).acceptFollow(connection).match(
      onSuccess: (update) {
        _logger
            .finest('Successfully followed ${update.name}: ${update.status}');
        upsertConnection(update);
        notifyListeners();
      },
      onError: (error) {
        _logger.severe('Error following ${connection.name}: $error');
      },
    );
  }

  Future<void> rejectFollowRequest(Connection connection) async {
    _logger.finest(
        'Attempting to accept follow request ${connection.name}: ${connection.status}');
    await RelationshipsClient(profile).rejectFollow(connection).match(
      onSuccess: (update) {
        _logger
            .finest('Successfully followed ${update.name}: ${update.status}');
        upsertConnection(update);
        notifyListeners();
      },
      onError: (error) {
        _logger.severe('Error following ${connection.name}: $error');
      },
    );
  }

  Future<void> ignoreFollowRequest(Connection connection) async {
    _logger.finest(
        'Attempting to accept follow request ${connection.name}: ${connection.status}');
    await RelationshipsClient(profile).ignoreFollow(connection).match(
      onSuccess: (update) {
        _logger
            .finest('Successfully followed ${update.name}: ${update.status}');
        upsertConnection(update);
        notifyListeners();
      },
      onError: (error) {
        _logger.severe('Error following ${connection.name}');
      },
    );
  }

  Future<void> follow(Connection connection) async {
    _logger.finest(
        'Attempting to follow ${connection.name}: ${connection.status}');
    await RelationshipsClient(profile).followConnection(connection).match(
      onSuccess: (update) {
        _logger
            .finest('Successfully followed ${update.name}: ${update.status}');
        upsertConnection(update);
        notifyListeners();
      },
      onError: (error) {
        _logger.severe('Error following ${connection.name}');
      },
    );
  }

  Future<void> unfollow(Connection connection) async {
    _logger.finest(
        'Attempting to unfollow ${connection.name}: ${connection.status}');
    await RelationshipsClient(profile).unFollowConnection(connection).match(
      onSuccess: (update) {
        _logger
            .finest('Successfully unfollowed ${update.name}: ${update.status}');
        upsertConnection(update);
        notifyListeners();
      },
      onError: (error) {
        _logger.severe('Error following ${connection.name}');
      },
    );
  }

  List<Connection> getMyContacts() {
    return conRepo.getMyContacts();
  }

  Future<void> updateAllContacts(bool backgroundUpdate) async {
    _logger.fine('Updating all contacts');
    _lastUpdateStatus = 'Updating';
    final persistentInfo = getIt<ActiveProfileSelector<PersistentInfoService>>()
        .getForProfile(profile)
        .value;
    final originalTime = persistentInfo.lastMyConnectionsUpdate;
    await persistentInfo.updateLastMyConnectionUpdate(DateTime.now());
    notifyListeners();
    final delay = backgroundUpdate
        ? const Duration(minutes: 5)
        : const Duration(seconds: 10);
    try {
      final client = RelationshipsClient(profile);
      final results = <String, Connection>{};
      var moreResults = true;
      var maxId = -1;
      const limit = 50;
      var currentPage = PagingData(limit: limit);
      final originalContacts = conRepo.getMyContacts().toSet();
      while (moreResults) {
        await client.getMyFollowers(currentPage).match(onSuccess: (followers) {
          for (final f in followers.data) {
            originalContacts.remove(f);
            results[f.id] = f.copy(status: ConnectionStatus.theyFollowYou);
            int id = int.parse(f.id);
            maxId = max(maxId, id);
          }
          if (followers.next != null) {
            currentPage = followers.next!;
          }
          moreResults = followers.next != null;
        }, onError: (error) {
          _logger.severe('Error getting followers data: $error');
        });
        upsertAllConnections(results.values);
        notifyListeners();
        await Future.delayed(delay);
      }

      moreResults = true;
      currentPage = PagingData(limit: limit);
      while (moreResults) {
        await client.getMyFollowing(currentPage).match(onSuccess: (following) {
          for (final f in following.data) {
            originalContacts.remove(f);
            if (results.containsKey(f.id)) {
              results[f.id] = f.copy(status: ConnectionStatus.mutual);
            } else {
              results[f.id] = f.copy(status: ConnectionStatus.youFollowThem);
            }
            int id = int.parse(f.id);
            maxId = max(maxId, id);
          }
          if (following.next != null) {
            currentPage = following.next!;
          }
          moreResults = following.next != null;
          upsertAllConnections(results.values);
          notifyListeners();
        }, onError: (error) {
          _logger.severe('Error getting followers data: $error');
        });
        await Future.delayed(delay);
      }

      for (final noLongerFollowed in originalContacts) {
        results[noLongerFollowed.id] =
            noLongerFollowed.copy(status: ConnectionStatus.none);
      }
      upsertAllConnections(results.values);
      final myContacts = conRepo.getMyContacts().toList();
      myContacts.sort((c1, c2) => c1.name.compareTo(c2.name));
      await persistentInfo.updateLastMyConnectionUpdate(DateTime.now());
      _logger.fine('# Contacts:${myContacts.length}');
    } catch (e) {
      await persistentInfo.updateLastMyConnectionUpdate(originalTime);
    }
    _lastUpdateStatus =
        'Last updated at: ${persistentInfo.lastMyConnectionsUpdate}';

    notifyListeners();
  }

  List<CircleData> getMyCircles() {
    if (circlesNotInitialized) {
      circlesNotInitialized = false;
      _updateMyCircles(true);
    }

    return circlesRepo.getMyCircles();
  }

  Result<List<Connection>, ExecError> getCircleMembers(CircleData circle) {
    return circlesRepo
        .getCircleMembers(circle)
        .transform(
          (members) => members
            ..sort((c1, c2) =>
                c1.name.toLowerCase().compareTo(c2.name.toLowerCase())),
        )
        .execErrorCast();
  }

  FutureResult<CircleData, ExecError> createCircle(String newName) async {
    final result = await CirclesClient(profile)
        .createCircle(newName)
        .withResultAsync((newCircle) async {
      circlesRepo.upsertCircle(newCircle);
      notifyListeners();
    });
    return result.execErrorCast();
  }

  FutureResult<CircleData, ExecError> renameCircle(
      String id, String newName) async {
    final result = await CirclesClient(profile)
        .renameCircle(id, newName)
        .withResultAsync((renamedCircle) async {
      circlesRepo.upsertCircle(renamedCircle);
      notifyListeners();
    });
    return result.execErrorCast();
  }

  FutureResult<bool, ExecError> deleteCircle(CircleData circleData) async {
    final result = await CirclesClient(profile)
        .deleteCircle(circleData)
        .withResultAsync((_) async {
      circlesRepo.deleteCircle(circleData);
      notifyListeners();
    });
    return result.execErrorCast();
  }

  void refreshCircles() {
    _updateMyCircles(true);
  }

  Future<void> refreshCircleMemberships(CircleData circle) async {
    var page = PagingData(limit: 50);
    final client = CirclesClient(profile);
    final allResults = <Connection>{};
    var moreResults = true;
    while (moreResults) {
      await client.getCircleMembers(circle, page).match(onSuccess: (results) {
        moreResults = results.data.isNotEmpty && results.next != null;
        page = results.next ?? page;
        allResults.addAll(results.data);
      }, onError: (error) {
        _logger.severe('Error getting circle listing data: $error');
        moreResults = false;
      });
    }

    circlesRepo.deleteCircle(circle);
    circlesRepo.upsertCircle(circle);
    for (final c in allResults) {
      upsertConnection(c);
      circlesRepo.addConnectionToCircle(circle, c);
    }

    notifyListeners();
  }

  Result<List<CircleData>, ExecError> getCirclesForUser(String id) {
    final result = circlesRepo.getCirclesForUser(id);
    if (result.isSuccess) {
      print("Circles for user $id: $result");
      return result;
    }

    if (result.isFailure && result.error.type != ErrorType.notFound) {
      return result;
    }

    _refreshCircleListData(id, true);
    return Result.ok(UnmodifiableListView([]));
  }

  FutureResult<bool, ExecError> addUserToCircle(
      CircleData circle, Connection connection) async {
    _logger.finest('Adding ${connection.name} to circle: ${circle.name}');
    return await CirclesClient(profile)
        .addConnectionToCircle(circle, connection)
        .withResultAsync((_) async => await refreshCircleMemberships(circle))
        .withResult((_) => notifyListeners())
        .mapError((error) {
      _logger.severe(
          'Error adding ${connection.name} from circle: ${circle.name}');
      return error;
    });
  }

  FutureResult<bool, ExecError> removeUserFromCircle(
      CircleData circle, Connection connection) async {
    _logger.finest('Removing ${connection.name} from circle: ${circle.name}');
    return CirclesClient(profile)
        .removeConnectionFromCircle(circle, connection)
        .withResultAsync((_) async => await refreshCircleMemberships(circle))
        .withResult((_) => notifyListeners())
        .mapError(
      (error) {
        _logger.severe(
            'Error removing ${connection.name} from circle: ${circle.name}');
        return error;
      },
    );
  }

  Result<Connection, ExecError> getById(String id, {bool forceUpdate = false}) {
    return conRepo.getById(id).transform((c) {
      if (c.status == ConnectionStatus.unknown && forceUpdate) {
        _refreshConnection(c, true);
      }
      return c;
    }).execErrorCast();
  }

  Result<Connection, ExecError> getByName(String name) {
    return conRepo.getByName(name).andThenSuccess((c) {
      if (c.status == ConnectionStatus.unknown) {
        _refreshConnection(c, true);
      }
      return c;
    }).execErrorCast();
  }

  Result<Connection, ExecError> getByHandle(String handle) {
    return conRepo.getByHandle(handle).andThenSuccess((c) {
      if (c.status == ConnectionStatus.unknown) {
        _refreshConnection(c, true);
      }
      return c;
    }).execErrorCast();
  }

  Future<void> fullRefresh(
    Connection connection, {
    bool withNotifications = true,
  }) async {
    await _updateMyCircles(false);
    await _refreshCircleListData(connection.id, false);
    await _refreshConnection(connection, false);
    if (withNotifications) {
      notifyListeners();
    }
  }

  Future<void> _refreshCircleListData(String id, bool withNotification) async {
    _logger.finest('Refreshing member list data for Connection $id');
    await CirclesClient(profile).getMemberCirclesForConnection(id).match(
      onSuccess: (circles) {
        circlesRepo.updateConnectionCircleData(id, circles);
        if (withNotification) {
          notifyListeners();
        }
      },
      onError: (error) {
        _logger.severe('Error getting list data for $id: $error');
      },
    );
  }

  Future<void> _refreshConnection(
      Connection connection, bool withNotification) async {
    _logger.finest('Refreshing connection data for ${connection.name}');
    await RelationshipsClient(profile)
        .getConnectionWithStatus(connection)
        .match(
      onSuccess: (update) {
        upsertConnection(update);
        if (withNotification) {
          notifyListeners();
        }
      },
      onError: (error) {
        _logger.severe('Error getting updates for ${connection.name}: $error');
      },
    );
  }

  Future<void> _updateMyCircles(bool withNotification) async {
    _logger.finest('Refreshing my circles list');
    await CirclesClient(profile).getCircles().match(
      onSuccess: (circles) {
        _logger.finest('Got updated circles:${circles.map((e) => e.name)}');
        circlesRepo.clearMyCircles();
        circlesRepo.addAllCircles(circles);
        if (withNotification) {
          notifyListeners();
        }
      },
      onError: (error) {
        _logger.severe('Error getting my circles: $error');
      },
    );
  }
}
