import 'dart:async'; import 'dart:collection'; import 'package:injectable/injectable.dart'; import 'package:isar/isar.dart'; import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart'; /// Service for tracking and managing user study sessions /// /// Automatically creates, updates, and manages study sessions based on user activity. /// Sessions are created when users start studying and are ended based on inactivity timeouts. @lazySingleton class SessionTracker { final Isar _isar; /// 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._isar); /// 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(int 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 session = StudySessionModel( sessionId: sessionId, userId: userId, startTime: DateTime.now(), packId: packId, testId: testId, ); // Save to database await _isar.writeTxn(() async { await _isar.studySessionModels.put(session); }); // Track as active _activeSessions[userId] = sessionId; _resetSessionTimer(sessionId); return sessionId; } /// Update session with progress data /// /// Called when user completes a test or makes progress. Future updateSessionProgress( String sessionId, { int wordsLearned = 0, int testsCompleted = 0, double? accuracy, }) async { final session = await _isar.studySessionModels .filter() .sessionIdEqualTo(sessionId) .findFirst(); if (session == null || !session.isActive) return; // Update session with new progress final updatedSession = session.addProgress( wordsLearned: wordsLearned, testsCompleted: testsCompleted, accuracy: accuracy, ); await _isar.writeTxn(() async { await _isar.studySessionModels.put(updatedSession); }); // Reset timeout timer _resetSessionTimer(sessionId); } /// End a session manually with final statistics /// /// Called when user explicitly ends a session or when session times out. Future endSession( String sessionId, { int wordsLearned = 0, int testsCompleted = 0, double accuracy = 0.0, }) async { final session = await _isar.studySessionModels .filter() .sessionIdEqualTo(sessionId) .findFirst(); if (session == null || !session.isActive) return; // End session with final statistics final endedSession = session.end( wordsLearned: wordsLearned, testsCompleted: testsCompleted, accuracy: accuracy, ); await _isar.writeTxn(() async { await _isar.studySessionModels.put(endedSession); }); // Clean up tracking final userId = session.userId; _activeSessions.remove(userId); _cancelSessionTimer(sessionId); } /// End session for a specific user Future endUserSession(int userId) async { final sessionId = _activeSessions[userId]; if (sessionId != null) { await endSession(sessionId); } } /// Get active session for a user Future getActiveSession(int userId) async { final sessionId = _activeSessions[userId]; if (sessionId == null) return null; return await _isar.studySessionModels .filter() .sessionIdEqualTo(sessionId) .findFirst(); } /// Get recent sessions for a user Future> getRecentSessions( int userId, { int limit = 10, }) async { return await _isar.studySessionModels .filter() .userIdEqualTo(userId) .sortByStartTimeDesc() .limit(limit) .findAll(); } /// Clean up expired sessions /// /// Should be called periodically (e.g., via cron job) Future cleanupExpiredSessions() async { final cutoffTime = DateTime.now().subtract(_sessionTimeout); // Find sessions that should have expired but are still marked as active final expiredSessions = await _isar.studySessionModels .filter() .endTimeIsNull() .startTimeLessThan(cutoffTime) .findAll(); for (final session in expiredSessions) { await endSession(session.sessionId!); } } /// Get session statistics for a user over a time period Future> getSessionStatistics( int userId, { DateTime? from, DateTime? to, }) async { final query = _isar.studySessionModels.filter().userIdEqualTo(userId); if (from != null) { query.startTimeGreaterThan(from); } if (to != null) { query.startTimeLessThan(to); } final sessions = await query.findAll(); if (sessions.isEmpty) { return { 'totalSessions': 0, 'totalDurationMinutes': 0, 'totalWordsLearned': 0, 'totalTestsCompleted': 0, 'averageAccuracy': 0.0, 'averageSessionLength': 0.0, }; } final completedSessions = sessions.where((s) => !s.isActive).toList(); final totalDuration = completedSessions.fold( 0, (sum, session) => sum + session.durationMinutes, ); final totalWordsLearned = completedSessions.fold( 0, (sum, session) => sum + session.wordsLearned, ); final totalTestsCompleted = completedSessions.fold( 0, (sum, session) => sum + session.testsCompleted, ); final averageAccuracy = completedSessions.isEmpty ? 0.0 : completedSessions.fold( 0, (sum, session) => sum + session.accuracy, ) / completedSessions.length; return { 'totalSessions': sessions.length, 'completedSessions': completedSessions.length, 'totalDurationMinutes': totalDuration, 'totalWordsLearned': totalWordsLearned, 'totalTestsCompleted': totalTestsCompleted, 'averageAccuracy': averageAccuracy, 'averageSessionLength': completedSessions.isEmpty ? 0.0 : totalDuration / completedSessions.length, }; } /// Generate a unique session ID String _generateSessionId() { final timestamp = DateTime.now().millisecondsSinceEpoch; final random = DateTime.now().microsecondsSinceEpoch % 10000; return 'session_${timestamp}_$random'; } /// Reset the timeout timer for a session void _resetSessionTimer(String sessionId) { _cancelSessionTimer(sessionId); _sessionTimers[sessionId] = Timer(_sessionTimeout, () { // Session timed out - end it endSession(sessionId); }); } /// Cancel the timeout timer for a session void _cancelSessionTimer(String sessionId) { final timer = _sessionTimers.remove(sessionId); timer?.cancel(); } /// Update session last activity time Future _updateSessionActivity(String sessionId) async { // For now, we just reset the timer // In the future, we could track last activity timestamps } /// Dispose of all timers (for cleanup) void dispose() { for (final timer in _sessionTimers.values) { timer.cancel(); } _sessionTimers.clear(); _activeSessions.clear(); } }