import 'package:drift/drift.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/log.dart';

part 'contacts.dao.g.dart';

@DriftAccessor(tables: [Contacts, KeyVerifications])
class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
  // this constructor is required so that the main database can create an instance
  // of this object.
  // ignore: matching_super_parameters
  ContactsDao(super.db);

  Future<int?> insertContact(ContactsCompanion contact) async {
    try {
      return await into(contacts).insert(contact);
    } catch (e) {
      Log.error(e);
      return null;
    }
  }

  Future<int> insertOnConflictUpdate(ContactsCompanion contact) async {
    try {
      return await into(contacts).insertOnConflictUpdate(contact);
    } catch (e) {
      Log.error(e);
      return 0;
    }
  }

  SingleOrNullSelectable<Contact> getContactByUserId(int userId) {
    return select(contacts)..where((t) => t.userId.equals(userId));
  }

  Future<Contact?> getContactById(int userId) async {
    return (select(
      contacts,
    )..where((t) => t.userId.equals(userId))).getSingleOrNull();
  }

  Future<List<Contact>> getContactsByUsername(
    String username, {
    String username2 = '_______',
  }) async {
    return (select(contacts)..where(
          (t) => t.username.equals(username) | t.username.equals(username2),
        ))
        .get();
  }

  Future<void> deleteContactByUserId(int userId) {
    return (delete(contacts)..where((t) => t.userId.equals(userId))).go();
  }

  Future<void> resetRecoveryDataForAllContacts() async {
    await update(
      contacts,
    ).write(
      const ContactsCompanion(
        recoveryIsTrustedFriend: Value(false),
        recoveryLastHeartbeat: Value(null),
        recoverySecretShare: Value(null),
      ),
    );
  }

  Future<void> updateContact(
    int userId,
    ContactsCompanion updatedValues,
  ) async {
    await (update(
      contacts,
    )..where((c) => c.userId.equals(userId))).write(updatedValues);
    if (updatedValues.blocked.present ||
        updatedValues.displayName.present ||
        updatedValues.nickName.present ||
        updatedValues.username.present) {
      final contact = await getContactByUserId(userId).getSingleOrNull();
      if (contact != null) {
        await updatePushUser(contact);
        final group = await twonlyDB.groupsDao.getDirectChat(userId);
        if (group != null) {
          await twonlyDB.groupsDao.updateGroup(
            group.groupId,
            GroupsCompanion(
              groupName: Value(getContactDisplayName(contact)),
            ),
          );
        }
      }
    }
  }

  Stream<List<Contact>> watchNotAcceptedContacts() {
    return (select(contacts)..where(
          (t) =>
              t.accepted.equals(false) &
              t.blocked.equals(false) &
              t.deletedByUser.equals(false),
        ))
        .watch();
  }

  Stream<Contact?> watchContact(int userid) {
    return (select(
      contacts,
    )..where((t) => t.userId.equals(userid))).watchSingleOrNull();
  }

  Future<List<Contact>> getAllContacts() {
    return select(contacts).get();
  }

  Future<int> getContactsCount() async {
    final count = contacts.userId.count();
    final query = selectOnly(contacts)..addColumns([count]);
    final result = await query.map((row) => row.read(count)).getSingle();
    return result ?? 0;
  }

  Stream<int?> watchContactsBlocked() {
    final count = contacts.userId.count();
    final query = selectOnly(contacts)
      ..where(contacts.blocked.equals(true))
      ..addColumns([count]);
    return query.map((row) => row.read(count)).watchSingle();
  }

  Stream<int?> watchContactsRequestedCount() {
    final count = contacts.requested.count(distinct: true);
    final query = selectOnly(contacts)
      ..where(
        contacts.requested.equals(true) &
            contacts.accepted.equals(false) &
            contacts.deletedByUser.equals(false) &
            contacts.blocked.equals(false),
      )
      ..addColumns([count]);
    return query.map((row) => row.read(count)).watchSingle();
  }

  Stream<List<Contact>> watchAllAcceptedContacts() {
    return (select(contacts)..where(
          (t) =>
              t.blocked.equals(false) &
              t.accepted.equals(true) &
              t.accountDeleted.equals(false),
        ))
        .watch();
  }

  Stream<List<Contact>> watchContactsAnnouncedViaUserDiscovery() {
    return (select(contacts)..where((t) {
          var expr =
              t.userDiscoveryVersion.isNotNull() &
              t.userDiscoveryExcluded.equals(false) &
              t.accountDeleted.equals(false) &
              t.mediaSendCounter.isBiggerOrEqualValue(
                userService.currentUser.requiredSendImages,
              );

          if (userService.currentUser.userDiscoveryRequiresManualApproval) {
            expr = expr & t.userDiscoveryManualApproved.equals(true);
          }

          return expr;
        }))
        .watch();
  }

  Future<List<Contact>> getContactsAnnouncedViaUserDiscovery() async {
    return (select(contacts)..where((t) {
          var expr =
              t.userDiscoveryVersion.isNotNull() &
              t.userDiscoveryExcluded.equals(false) &
              t.accountDeleted.equals(false) &
              t.mediaSendCounter.isBiggerOrEqualValue(
                userService.currentUser.requiredSendImages,
              );

          if (userService.currentUser.userDiscoveryRequiresManualApproval) {
            expr = expr & t.userDiscoveryManualApproved.equals(true);
          }

          return expr;
        }))
        .get();
  }

  Stream<List<Contact>> watchAllContacts() {
    return select(contacts).watch();
  }
}

String getContactDisplayName(Contact user, {int? maxLength}) {
  var name = user.username;
  if (user.nickName != null && user.nickName != '') {
    name = user.nickName!;
  } else if (user.displayName != null) {
    name = user.displayName!;
  }
  if (user.accountDeleted) {
    name = applyStrikethrough(name);
  }
  if (maxLength != null) {
    name = substringBy(name, maxLength);
  }
  return name;
}

String substringBy(String string, int maxLength) {
  if (string.length > maxLength) {
    return '${string.substring(0, maxLength - 3)}...';
  }
  return string;
}

String applyStrikethrough(String text) {
  return text.split('').map((char) => '$char\u0336').join();
}
