import 'package:clock/clock.dart';
import 'package:drift/drift.dart';
import 'package:twonly/core/bridge/wrapper/user_discovery.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/user_discovery.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart';

part 'key_verification.dao.g.dart';

enum VerificationStatus { trusted, partialTrusted, notTrusted }

@DriftAccessor(
  tables: [
    Contacts,
    VerificationTokens,
    KeyVerifications,
    GroupMembers,
    UserDiscoveryUserRelations,
  ],
)
class KeyVerificationDao extends DatabaseAccessor<TwonlyDB>
    with _$KeyVerificationDaoMixin {
  // ignore: matching_super_parameters
  KeyVerificationDao(super.db);

  Future<List<VerificationToken>> getRecentVerificationTokens() {
    // Tokens are only valid for one hour, so if the users are currently offline, the verification notification will still work later.
    final cutoff = DateTime.now().subtract(const Duration(hours: 1));
    return (select(
      verificationTokens,
    )..where((t) => t.createdAt.isBiggerOrEqualValue(cutoff))).get();
  }

  Future<int> insertVerificationToken(Uint8List token) {
    return into(verificationTokens).insert(
      VerificationTokensCompanion.insert(token: token),
    );
  }

  /// Returns a map of contactId → the verification type of the earliest
  /// [KeyVerification] row for that contact.
  Future<Map<int, VerificationType>>
  getFirstVerificationTypeByContacts() async {
    final rows = await (select(
      keyVerifications,
    )..orderBy([(kv) => OrderingTerm.asc(kv.createdAt)])).get();

    final result = <int, VerificationType>{};
    for (final row in rows) {
      result.putIfAbsent(row.contactId, () => row.type);
    }
    return result;
  }

  Future<bool> isContactVerified(int contactId) async {
    final verifierKv = alias(keyVerifications, 'verifierKv');
    final query = select(keyVerifications).join([
      leftOuterJoin(
        verifierKv,
        verifierKv.contactId.equalsExp(keyVerifications.verifiedBy),
      ),
    ])..where(keyVerifications.contactId.equals(contactId));

    final rows = await query.get();
    for (final row in rows) {
      final kv = row.readTable(keyVerifications);
      final hasVerifierKv = row.readTableOrNull(verifierKv) != null;
      if (kv.type == VerificationType.contactSharedByVerified) {
        if (hasVerifierKv) return true;
      } else {
        return true;
      }
    }
    return false;
  }

  Stream<List<(KeyVerification, Contact?)>> watchContactVerification(
    int contactId,
  ) {
    final verifier = alias(contacts, 'verifier');
    final verifierKv = alias(keyVerifications, 'verifierKv');
    final query = select(keyVerifications).join([
      leftOuterJoin(
        verifier,
        verifier.userId.equalsExp(keyVerifications.verifiedBy),
      ),
      leftOuterJoin(
        verifierKv,
        verifierKv.contactId.equalsExp(keyVerifications.verifiedBy),
      ),
    ])..where(keyVerifications.contactId.equals(contactId));

    return query.watch().map((rows) {
      final uniqueKvs =
          <int, (KeyVerification, Contact?, bool isVerifierVerified)>{};

      for (final row in rows) {
        final kv = row.readTable(keyVerifications);
        final contact = row.readTableOrNull(verifier);
        final hasVerifierKv = row.readTableOrNull(verifierKv) != null;

        final existing = uniqueKvs[kv.verificationId];
        if (existing == null || hasVerifierKv) {
          uniqueKvs[kv.verificationId] = (kv, contact, hasVerifierKv);
        }
      }

      return uniqueKvs.values
          .where((item) {
            final kv = item.$1;
            final isVerifierVerified = item.$3;
            if (kv.type == VerificationType.contactSharedByVerified) {
              return isVerifierVerified;
            }
            return true;
          })
          .map((item) => (item.$1, item.$2))
          .toList();
    });
  }

  Future<List<KeyVerification>> getContactVerification(int contactId) async {
    return (select(
      keyVerifications,
    )..where((kv) => kv.contactId.equals(contactId))).get();
  }

  Stream<List<(Contact, DateTime)>> watchTransferredTrustVerifications(
    int contactId,
  ) {
    final kv = keyVerifications;
    final ur = userDiscoveryUserRelations;

    final query =
        (select(contacts)..where((u) => u.userId.equals(contactId).not())).join(
            [
              innerJoin(
                ur,
                ur.fromContactId.equalsExp(contacts.userId),
              ),
              innerJoin(kv, kv.contactId.equalsExp(ur.fromContactId)),
            ],
          )
          ..where(
            ur.announcedUserId.equals(contactId) &
                ur.publicKeyVerifiedTimestamp.isNotNull(),
          )
          ..groupBy([contacts.userId]);

    return query.watch().map((rows) {
      return rows.map((row) {
        final contact = row.readTable(contacts);
        final timestamp = row.readTable(ur).publicKeyVerifiedTimestamp!;
        return (contact, timestamp);
      }).toList();
    });
  }

  Future<int> getTransferredTrustVerificationsCount() async {
    final kv = keyVerifications;
    final ur = userDiscoveryUserRelations;

    final query = selectOnly(ur, distinct: true)
      ..addColumns([ur.announcedUserId])
      ..join([
        innerJoin(contacts, contacts.userId.equalsExp(ur.fromContactId)),
        innerJoin(kv, kv.contactId.equalsExp(ur.fromContactId)),
      ])
      ..where(
        ur.publicKeyVerifiedTimestamp.isNotNull() &
            ur.announcedUserId.equalsExp(ur.fromContactId).not(),
      )
      ..groupBy([ur.announcedUserId]);

    final rows = await query.get();
    return rows.length;
  }

  Future<int> getCountOfContactsWithVerificationBadge() async {
    final kv = keyVerifications;
    final ur = userDiscoveryUserRelations;

    final query = selectOnly(ur, distinct: true)
      ..addColumns([ur.announcedUserId])
      ..join([
        innerJoin(contacts, contacts.userId.equalsExp(ur.fromContactId)),
        innerJoin(kv, kv.contactId.equalsExp(ur.fromContactId)),
      ])
      ..where(
        ur.publicKeyVerifiedTimestamp.isNotNull() &
            ur.announcedUserId.equalsExp(ur.fromContactId).not(),
      )
      ..groupBy([ur.announcedUserId]);

    final rows = await query.get();
    final transferredIds = rows.map((r) => r.read(ur.announcedUserId)!).toSet();

    final directVerifications = await select(kv).get();
    final directIds = directVerifications.map((v) => v.contactId).toSet();

    // Reduce transferred contacts where announcedUserId is already in KeyVerifications
    transferredIds.removeWhere(directIds.contains);

    // Add count of all users who are in the KeyVerification table
    return transferredIds.length + directIds.length;
  }

  Stream<VerificationStatus> watchAllGroupMembersVerified(String groupId) {
    final gm = groupMembers;
    final directKv = alias(keyVerifications, 'directKv');
    final ur = userDiscoveryUserRelations;
    final verifierKv = alias(keyVerifications, 'verifierKv');

    final query = select(gm).join([
      leftOuterJoin(directKv, directKv.contactId.equalsExp(gm.contactId)),
      leftOuterJoin(
        ur,
        ur.announcedUserId.equalsExp(gm.contactId) &
            ur.publicKeyVerifiedTimestamp.isNotNull() &
            ur.fromContactId.equalsExp(gm.contactId).not(),
      ),
      leftOuterJoin(
        verifierKv,
        verifierKv.contactId.equalsExp(ur.fromContactId),
      ),
    ])..where(gm.groupId.equals(groupId));

    return query.watch().map((rows) {
      if (rows.isEmpty) return VerificationStatus.notTrusted;

      final memberTrustMap = <int, ({bool direct, bool partial})>{};

      for (final row in rows) {
        final contactId = row.readTable(gm).contactId;
        final isDirect = row.readTableOrNull(directKv) != null;
        final isPartial = row.readTableOrNull(verifierKv) != null;

        final current =
            memberTrustMap[contactId] ?? (direct: false, partial: false);
        memberTrustMap[contactId] = (
          direct: current.direct || isDirect,
          partial: current.partial || isPartial,
        );
      }

      final allDirect = memberTrustMap.values.every((m) => m.direct);
      if (allDirect) return VerificationStatus.trusted;

      final allAtLeastPartial = memberTrustMap.values.every(
        (m) => m.direct || m.partial,
      );
      if (allAtLeastPartial) return VerificationStatus.partialTrusted;

      return VerificationStatus.notTrusted;
    });
  }

  Stream<int> watchUnverifiedGroupMembersCount(String groupId) {
    final gm = groupMembers;
    final directKv = alias(keyVerifications, 'directKv');
    final ur = userDiscoveryUserRelations;
    final verifierKv = alias(keyVerifications, 'verifierKv');

    final query = select(gm).join([
      leftOuterJoin(directKv, directKv.contactId.equalsExp(gm.contactId)),
      leftOuterJoin(
        ur,
        ur.announcedUserId.equalsExp(gm.contactId) &
            ur.publicKeyVerifiedTimestamp.isNotNull() &
            ur.fromContactId.equalsExp(gm.contactId).not(),
      ),
      leftOuterJoin(
        verifierKv,
        verifierKv.contactId.equalsExp(ur.fromContactId),
      ),
    ])..where(gm.groupId.equals(groupId));

    return query.watch().map((rows) {
      if (rows.isEmpty) return 0;

      final memberTrustMap = <int, ({bool direct, bool partial})>{};

      for (final row in rows) {
        final contactId = row.readTable(gm).contactId;
        final isDirect = row.readTableOrNull(directKv) != null;
        final isPartial = row.readTableOrNull(verifierKv) != null;

        final current =
            memberTrustMap[contactId] ?? (direct: false, partial: false);
        memberTrustMap[contactId] = (
          direct: current.direct || isDirect,
          partial: current.partial || isPartial,
        );
      }

      var count = 0;
      for (final trust in memberTrustMap.values) {
        if (!trust.direct && !trust.partial) {
          count++;
        }
      }
      return count;
    });
  }

  Future<void> addKeyVerification(
    int contactId,
    VerificationType type, {
    int? verifiedBy,
  }) async {
    try {
      await into(keyVerifications).insertOnConflictUpdate(
        KeyVerificationsCompanion(
          contactId: Value(contactId),
          type: Value(type),
          verifiedBy: Value(verifiedBy),
        ),
      );
      if (userService.currentUser.isUserDiscoveryEnabled) {
        await FlutterUserDiscovery.updateVerificationStateForUser(
          callbackId: isolateCallbackId,
          contactId: contactId,
          publicKeyVerifiedTimestamp: clock.now().millisecondsSinceEpoch,
        );
      }
    } catch (e) {
      Log.error(e);
    }
  }

  Future<void> deleteKeyVerification(int contactId) async {
    try {
      await (delete(
        keyVerifications,
      )..where((kv) => kv.contactId.equals(contactId))).go();
      if (userService.currentUser.isUserDiscoveryEnabled) {
        await FlutterUserDiscovery.updateVerificationStateForUser(
          callbackId: isolateCallbackId,
          contactId: contactId,
        );
      }
    } catch (e) {
      Log.error(e);
    }
  }

  Future<void> deleteKeyVerificationById(
    int verificationId,
    int contactId,
  ) async {
    try {
      await (delete(
        keyVerifications,
      )..where((kv) => kv.verificationId.equals(verificationId))).go();
      final remaining = await getContactVerification(contactId);
      if (remaining.isEmpty && userService.currentUser.isUserDiscoveryEnabled) {
        await FlutterUserDiscovery.updateVerificationStateForUser(
          callbackId: isolateCallbackId,
          contactId: contactId,
        );
      }
    } catch (e) {
      Log.error(e);
    }
  }
}
