import 'dart:async'; import 'dart:collection'; import 'dart:math'; import 'package:drift_postgres/drift_postgres.dart'; import 'package:injectable/injectable.dart'; import 'package:mnemo_cards_backend/database/database.dart'; import 'package:drift/drift.dart' as drift; @lazySingleton class SessionTracker { final AppDatabase _db; /// Active sessions cache: userId -> sessionId final Map _activeSessions = {}; /// Session timers for automatic timeout: sessionId -> timer final Map _sessionTimers = {}; /// Session timeout duration (30 minutes by default) static const Duration _sessionTimeout = Duration(minutes: 30); SessionTracker(this._db); /// Get or create an active session for a user /// /// Returns the session ID of the active session. /// If no active session exists, creates a new one. Future getOrCreateSession( String userId, { String? packId, String? testId, }) async { // Check if user already has an active session if (_activeSessions.containsKey(userId)) { final sessionId = _activeSessions[userId]!; // Reset timeout timer _resetSessionTimer(sessionId); // Update session activity await _updateSessionActivity(sessionId); return sessionId; } // Create new session final sessionId = _generateSessionId(); final now = PgDateTime(DateTime.now()); await _db.statisticsDao.createSession( StudySessionsCompanion.insert( userId: userId, sessionId: drift.Value(sessionId), startTime: now, packId: drift.Value(packId), testId: drift.Value(testId), ), ); // Cache active session _activeSessions[userId] = sessionId; // Start timeout timer _startSessionTimer(sessionId); return sessionId; } /// End a session manually Future endSession( String sessionId, { int? wordsLearned, int? testsCompleted, double? accuracy, }) async { // Cancel timer _sessionTimers[sessionId]?.cancel(); _sessionTimers.remove(sessionId); // Find user for this session final matchingEntries = _activeSessions.entries .where((entry) => entry.value == sessionId) .toList(); final userId = matchingEntries.isNotEmpty ? matchingEntries.first.key : null; if (userId != null) { _activeSessions.remove(userId); } // Get session by sessionId to find its database ID final session = await _db.statisticsDao.getSessionBySessionId(sessionId); if (session == null) return; // Update session in database using database ID await _db.statisticsDao.endSession( session.id, wordsLearned: wordsLearned, testsCompleted: testsCompleted, accuracy: accuracy, ); } /// Update session statistics Future updateSessionStats( String sessionId, { int? wordsLearned, int? testsCompleted, double? accuracy, }) async { // Find session by sessionId final session = await _db.statisticsDao.getSessionBySessionId(sessionId); if (session == null) return; // Update session final updatedSession = session.copyWith( wordsLearned: wordsLearned ?? session.wordsLearned, testsCompleted: testsCompleted ?? session.testsCompleted, accuracy: accuracy ?? session.accuracy, updatedAt: PgDateTime(DateTime.now()), ); await _db.statisticsDao.updateSession(updatedSession); } /// Get active session for user Future getActiveSession(String userId) async { final activeSessions = await _db.statisticsDao.getActiveSessions(userId); return activeSessions.isNotEmpty ? activeSessions.first : null; } /// Get session history for user Future> getUserSessions( String userId, { int? limit, DateTime? fromDate, DateTime? toDate, }) async { return await _db.statisticsDao.getSessionsByUserId( userId, limit: limit, fromDate: fromDate, toDate: toDate, ); } /// Clean up expired sessions (called by cron job) Future cleanupExpiredSessions() async { // End all active sessions that have timed out final now = DateTime.now(); final expiredSessions = []; for (final entry in _activeSessions.entries) { final sessionId = entry.value; final session = await _db.statisticsDao.getSessionBySessionId(sessionId); if (session != null && session.endTime == null && now.difference(session.startTime.dateTime).inMinutes > _sessionTimeout.inMinutes) { expiredSessions.add(sessionId); } } for (final sessionId in expiredSessions) { await endSession(sessionId); } } /// Generate a unique session ID String _generateSessionId() { final random = Random(); final bytes = List.generate(16, (i) => random.nextInt(256)); return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); } /// Start timeout timer for session void _startSessionTimer(String sessionId) { _sessionTimers[sessionId] = Timer(_sessionTimeout, () async { await endSession(sessionId); }); } /// Reset timeout timer for session void _resetSessionTimer(String sessionId) { _sessionTimers[sessionId]?.cancel(); _startSessionTimer(sessionId); } /// Update session activity timestamp Future _updateSessionActivity(String sessionId) async { final session = await _db.statisticsDao.getSessionBySessionId(sessionId); if (session != null) { await _db.statisticsDao.updateSession( session.copyWith(updatedAt: PgDateTime(DateTime.now())), ); } } }