import 'dart:convert'; import 'dart:developer'; import 'dart:math' as math; import 'package:injectable/injectable.dart'; import 'package:drift/drift.dart' as drift; import 'package:mnemo_cards_backend/database/database.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; import 'package:mnemo_cards_common/mnemo_cards_common.dart'; import '../packs/free_packs_distributor.dart'; import '../statistics/session_tracker.dart'; import '../statistics/statistics_calculator.dart'; import '../statistics/achievement_manager.dart'; import 'secure.dart'; import 'user_drift_extension.dart'; Map _onlineUsers = {}; @lazySingleton class UserManager { final AppDatabase _db; final FreePacksDistributor _freePacksDistributor; final SessionTracker _sessionTracker; final StatisticsCalculator _statisticsCalculator; final AchievementManager _achievementManager; UserManager( this._db, this._freePacksDistributor, this._sessionTracker, this._statisticsCalculator, this._achievementManager, ); Future fetchUser(String id) async { final user = await _db.userDao.getUserById(id); if (user == null) return null; return await user.toUserModel(); } DateTime? lastOnline(String id) => _onlineUsers[id]; Future createOrGetAuthToken(UserModel user, String externalId) async { if (user.id == null) { throw Exception('Cant create token for empty id'); } final now = DateTime.now(); final token = await _db.userDao.getTokenByUserId(user.id!); if (token != null) { if (token.expires.isAfter(now)) { return token.token; } await _db.userDao.deleteToken(token.id); } final userToken = Secure.token(); await _db.userDao.createToken( TokensCompanion.insert( token: userToken, externalUserId: externalId, userId: user.id!, expires: now.add(const Duration(days: 360)), ), ); return userToken; } Future getUserByToken(String authToken) async { final token = await _db.userDao.getTokenByValue(authToken); if (token == null) { return null; } final now = DateTime.now(); if (token.expires.isBefore(now)) { await _db.userDao.deleteToken(token.id); return null; } final user = await fetchUser(token.userId); if (user != null) { _onlineUsers[token.userId] = now; if (_onlineUsers.length > 300) { updateOnlineUsers(); } } return user; } Future updateOnlineUsers() async { if (_onlineUsers.isEmpty) { return; } final userIds = _onlineUsers.keys.whereNotNull().toList(); for (final userId in userIds) { final lastOnline = _onlineUsers[userId]; if (lastOnline != null) { await _db.userDao.updateUserDataPartial( UserDatasCompanion( userId: drift.Value(userId), lastTimeOnline: drift.Value(lastOnline), updatedAt: drift.Value(DateTime.now()), ), ); } } _onlineUsers.clear(); } Future<(UserModel, String)> createOrGetUser({ required String externalId, required String email, String? name, }) async { // Check if user exists by externalId final existingUser = await _db.userDao.getUserByExternalId(externalId); if (existingUser != null) { print('User found $name $email'); final userModel = await existingUser.toUserModel(); final token = await createOrGetAuthToken(userModel, externalId); return (userModel, token); } print('Creating new user $name $email'); // Create new user final userCompanion = UsersCompanion.insert( externalUserId: externalId, name: drift.Value(name), email: drift.Value(email), ); final userId = await _db.userDao.createUser(userCompanion); // Create user data await _db.userDao.createUserData( UserDatasCompanion.insert( userId: userId, registrationDate: drift.Value(DateTime.now()), ), ); final user = await _db.userDao.getUserById(userId); if (user == null) { throw Exception('Failed to create user'); } final userModel = await user.toUserModel(); final token = await createOrGetAuthToken(userModel, externalId); // Give free packs to new user await _freePacksDistributor.giveFreePacksToUser(userModel); print('User $name $email created successfully'); return (userModel, token); } /// Обновить настройки пользователя Future updateUserSettings(UserModel user, UserSettingsDto settings) async { if (user.id == null) { throw Exception('User ID is required'); } await _db.userDao.updateUserPartial( UsersCompanion( id: drift.Value(user.id!), userSettings: drift.Value(jsonEncode(settings.toJson())), updatedAt: drift.Value(DateTime.now()), ), ); } /// Добавить статистику теста Future addTestStatistics(UserModel user, TestStatisticsDto testStat) async { if (user.id == null) { throw Exception('User ID is required'); } // Получить или создать UserData var userData = await _db.userDao.getUserData(user.id!); if (userData == null) { await _db.userDao.createUserData( UserDatasCompanion.insert(userId: user.id!), ); userData = await _db.userDao.getUserData(user.id!); if (userData == null) { throw Exception('Failed to create user data'); } } // Проверить, не был ли этот sessionToken уже обработан if (userData.lastTestSessionToken == testStat.sessionToken) { log('Old session token, skipping'); return; } // Получить или создать TestStatistic final existingStat = await _db.testDao.getTestStatistics(user.id!, testStat.testId); // Обновить результаты теста // results хранится как Map, где ключ 'attempts' содержит список попыток final currentResults = existingStat?.results ?? {}; final attemptsKey = 'attempts'; final currentAttempts = (currentResults[attemptsKey] as List?) ?? []; final newAttempt = { 'sessionToken': testStat.sessionToken, 'words': testStat.words.words.map((w) => w.toJson()).toList(), }; final updatedAttempts = [...currentAttempts, newAttempt]; final updatedResults = { ...currentResults, attemptsKey: updatedAttempts, }; if (existingStat != null) { // Обновить существующую статистику final updatedStat = existingStat.copyWith( results: drift.Value(updatedResults), updatedAt: DateTime.now(), ); await _db.testDao.updateTestStatistics(updatedStat); } else { // Создать новую статистику await _db.testDao.createTestStatistics( TestStatisticsCompanion.insert( userId: user.id!, testId: testStat.testId, results: drift.Value(updatedResults), completedAt: drift.Value(DateTime.now()), ), ); } // Обновить статистику пользователя final now = DateTime.now(); final todayNormalized = DateTime(now.year, now.month, now.day); final studyDates = List.from(userData.studyDates ?? []); // Добавить сегодняшнюю дату если еще нет if (!studyDates.any((date) => date.year == todayNormalized.year && date.month == todayNormalized.month && date.day == todayNormalized.day)) { studyDates.add(todayNormalized); } // Пересчитать streak final currentStreak = _statisticsCalculator.calculateStreak(studyDates); final longestStreak = math.max(userData.longestStreak, currentStreak); // Обновить UserData await _db.userDao.updateUserDataPartial( UserDatasCompanion( userId: drift.Value(user.id!), lastTestSessionToken: drift.Value(testStat.sessionToken), studyDates: drift.Value(studyDates), currentStreak: drift.Value(currentStreak), longestStreak: drift.Value(longestStreak), lastTimeOnline: drift.Value(now), updatedAt: drift.Value(now), ), ); } }