import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import '../models/user_stats.dart'; import '../models/class_stats.dart'; import '../models/achievement.dart'; import '../utils/logger.dart'; /// Serviço para gerenciar gamificação e conquistas class GamificationService { static final FirebaseFirestore _firestore = FirebaseFirestore.instance; static final FirebaseAuth _auth = FirebaseAuth.instance; /// Atualizar streak diário do usuário static Future updateDailyStreak(String userId) async { try { final userStatsRef = _firestore.collection('users').doc(userId); final userStatsDoc = await userStatsRef.get(); if (!userStatsDoc.exists) { // Criar estatísticas iniciais await userStatsRef.set({ 'userId': userId, 'currentStreak': 1, 'longestStreak': 1, 'totalStudyTime': 0, 'lastActivityDate': Timestamp.now(), 'weeklyStudyTime': 0, 'monthlyStudyTime': 0, 'masteredConcepts': [], 'unlockedAchievements': [], }); return; } final userStats = UserStats.fromFirestore(userStatsDoc.data()!, userId); final now = DateTime.now(); final lastActivity = userStats.lastActivityDate; int newStreak = userStats.currentStreak; int newLongestStreak = userStats.longestStreak; Logger.info('=== UPDATE DAILY STREAK DEBUG ==='); Logger.info('Last activity: $lastActivity'); Logger.info('Current streak before update: $newStreak'); if (lastActivity == null) { // Primeira atividade - iniciar streak Logger.info('First activity detected - setting streak to 1'); newStreak = 1; newLongestStreak = 1; } else { // Normalizar para início do dia para comparação correta final today = DateTime(now.year, now.month, now.day); final lastDay = DateTime( lastActivity.year, lastActivity.month, lastActivity.day, ); final difference = today.difference(lastDay).inDays; if (difference == 0) { // Já ativou hoje, não alterar streak mas atualiza timestamp // Corrigir streak se estiver em 0 (erro de dados) if (newStreak == 0) { newStreak = 1; newLongestStreak = 1; Logger.info('Correcting invalid streak (0) to 1'); await userStatsRef.update({ 'currentStreak': 1, 'longestStreak': 1, 'lastActivityDate': Timestamp.now(), }); } else { Logger.info('Already active today, streak unchanged: $newStreak'); await userStatsRef.update({'lastActivityDate': Timestamp.now()}); } return; } else if (difference == 1) { Logger.info('Consecutive activity detected, incrementing streak'); // Atividade consecutiva newStreak++; newLongestStreak = newStreak > newLongestStreak ? newStreak : newLongestStreak; } else { // Quebrou o streak newStreak = 1; newLongestStreak = newStreak > newLongestStreak ? newStreak : newLongestStreak; } } Logger.info('Updating streak to: $newStreak'); await userStatsRef.update({ 'currentStreak': newStreak, 'longestStreak': newLongestStreak, 'lastActivityDate': Timestamp.now(), }); Logger.info('Streak updated successfully'); // Verificar conquistas de streak await _checkStreakAchievements(userId, newStreak); Logger.info('=== END UPDATE DAILY STREAK DEBUG ==='); } catch (e) { Logger.error('Error updating daily streak: $e'); } } /// Registrar tempo de estudo static Future recordStudyTime(String userId, int minutes) async { try { final userStatsRef = _firestore.collection('users').doc(userId); final userStatsDoc = await userStatsRef.get(); if (!userStatsDoc.exists) { await _createInitialUserStats(userId); } final userStats = UserStats.fromFirestore(userStatsDoc.data()!, userId); final newTotalTime = userStats.totalStudyTime + minutes; await userStatsRef.update({ 'totalStudyTime': newTotalTime, 'weeklyStudyTime': FieldValue.increment(minutes), 'monthlyStudyTime': FieldValue.increment(minutes), }); // Verificar conquistas de tempo de estudo await _checkStudyTimeAchievements(userId, newTotalTime); } catch (e) { Logger.error('Error recording study time: $e'); } } /// Registrar atividade de quiz static Future recordQuizActivity( String userId, { required int score, required int totalQuestions, required String materialName, }) async { try { final userStatsRef = _firestore.collection('users').doc(userId); // Atualizar streak await updateDailyStreak(userId); // Tempo de estudo agora é calculado em tempo real no quiz sheet // Não adicionamos tempo fixo aqui // Verificar conquistas de quiz await _checkQuizAchievements(userId, score, totalQuestions); // Incrementar contador de quizzes completos await userStatsRef.update({'completedQuizzes': FieldValue.increment(1)}); Logger.info('Incremented completed quizzes count'); // Atualizar conceitos dominados se score >= 50% Logger.info( 'Checking if score qualifies for mastered concept: ${score / totalQuestions >= 0.5}', ); if (score / totalQuestions >= 0.5) { Logger.info( 'Adding mastered concept: $materialName with score: $score', ); await _addMasteredConcept(userId, materialName, score); } else { Logger.info( 'Score too low for mastered concept: $score/$totalQuestions', ); } Logger.info( 'Quiz activity recorded for user $userId: $score/$totalQuestions', ); } catch (e) { Logger.error('Error recording quiz activity: $e'); } } /// Obter estatísticas do usuário static Future getUserStats(String userId) async { try { final doc = await _firestore.collection('users').doc(userId).get(); if (!doc.exists) { // Criar estatísticas iniciais se não existirem await _initializeUserStats(userId); return await getUserStats( userId, ); // Chamada recursiva após inicialização } final data = doc.data() as Map; // Garantir que completedQuizzes exista if (!data.containsKey('completedQuizzes')) { await _firestore.collection('users').doc(userId).update({ 'completedQuizzes': 0, }); data['completedQuizzes'] = 0; } return UserStats.fromFirestore(data, userId); } catch (e) { Logger.error('Error getting user stats: $e'); return null; } } /// Obter estatísticas da turma static Future getClassStats( String classId, { bool forceRefresh = false, }) async { try { if (forceRefresh) { // Forçar recálculo completo return await _calculateClassStats(classId); } final classStatsDoc = await _firestore .collection('classStats') .doc(classId) .get(); if (!classStatsDoc.exists) { return await _calculateClassStats(classId); } // Verificar se os dados estão desatualizados (mais de 1 hora) final data = classStatsDoc.data()!; final lastUpdated = data['lastUpdated'] as Timestamp?; if (lastUpdated == null || DateTime.now().difference(lastUpdated.toDate()).inHours > 1) { return await _calculateClassStats(classId); } return ClassStats.fromFirestore(data, classId); } catch (e) { Logger.error('Error getting class stats: $e'); return null; } } /// Forçar atualização de estatísticas de todas as turmas de um professor static Future refreshAllClassStats(String teacherId) async { try { final classesSnapshot = await _firestore .collection('classes') .where('teacherId', isEqualTo: teacherId) .get(); for (final classDoc in classesSnapshot.docs) { await _calculateClassStats(classDoc.id); } Logger.info('Refreshed stats for ${classesSnapshot.docs.length} classes'); } catch (e) { Logger.error('Error refreshing class stats: $e'); } } /// Obter ranking de alunos da turma static Future> getClassRanking(String classId) async { try { // Primeiro, obter todos os alunos matriculados na turma final enrollmentsSnapshot = await _firestore .collection('enrollments') .where('classId', isEqualTo: classId) .get(); if (enrollmentsSnapshot.docs.isEmpty) { Logger.info('No students enrolled in class $classId'); return []; } final studentIds = enrollmentsSnapshot.docs .map((doc) => doc['studentId'] as String) .toList(); final rankings = []; // Obter número real de quizzes disponíveis na turma final quizzesSnapshot = await _firestore .collection('teacherQuizzes') .where('classIds', arrayContains: classId) .get(); final totalAvailableQuizzes = quizzesSnapshot.docs.length; // Para cada aluno, obter suas estatísticas for (final studentId in studentIds) { try { final userStats = await getUserStats(studentId); if (userStats != null) { // Obter informações do usuário final userDoc = await _firestore .collection('users') .doc(studentId) .get(); final userData = userDoc.data() as Map?; // Calcular estatísticas para o ranking final completedQuizzes = userStats.completedQuizzes; final totalQuizzes = totalAvailableQuizzes > 0 ? totalAvailableQuizzes : 1; final quizCompletionRate = completedQuizzes / totalQuizzes; Logger.info('=== RANKING SCORE DEBUG ==='); Logger.info('Student ID: $studentId'); Logger.info('Completed quizzes: $completedQuizzes'); Logger.info('Total quizzes: $totalQuizzes'); Logger.info( 'Quiz completion rate: $quizCompletionRate (${(quizCompletionRate * 100).toInt()}%)', ); Logger.info('Current streak: ${userStats.currentStreak}'); Logger.info( 'Total study time: ${userStats.totalStudyTime} minutes', ); Logger.info( 'Mastered concepts: ${userStats.masteredConcepts.length}', ); Logger.info( 'Unlocked achievements: ${userStats.unlockedAchievements.length}', ); // Calcular score geral baseado em múltiplos fatores final overallScore = _calculateOverallScore( userStats, quizCompletionRate, ); Logger.info( 'Overall score calculated: $overallScore (${overallScore.toInt()}%)', ); Logger.info('=== END RANKING SCORE DEBUG ==='); // Tentar obter um nome melhor para o aluno String studentName = 'Aluno $studentId'; if (userData != null) { studentName = userData['displayName'] ?? userData['email']?.split('@')[0] ?? 'Aluno ${studentId.substring(0, 8)}...'; } rankings.add( StudentRanking( studentId: studentId, studentName: studentName, studentEmail: userData?['email'] ?? '', overallScore: overallScore, completedQuizzes: completedQuizzes, totalQuizzes: totalQuizzes, quizCompletionRate: quizCompletionRate, currentStreak: userStats.currentStreak, studyTimeMinutes: userStats.totalStudyTime, lastActivity: userStats.lastActivityDate ?? DateTime.now(), ), ); } } catch (e) { Logger.error('Error getting stats for student $studentId: $e'); continue; } } // Ordenar por score geral rankings.sort((a, b) => b.overallScore.compareTo(a.overallScore)); return rankings; } catch (e) { Logger.error('Error getting class ranking: $e'); return []; } } /// Calcular score geral para ranking static double _calculateOverallScore( UserStats userStats, double quizCompletionRate, ) { // Se completou 100% dos quizzes, score é 100% if (quizCompletionRate >= 1.0) { return 100.0; } // Para completion < 100%, calcular proporcionalmente double baseScore = quizCompletionRate * 90; // 90% baseado em completion // Bônus adicionais (máximo 10% extra) double bonusScore = 0.0; // 5% para conceitos dominados bonusScore += (userStats.masteredConcepts.length / 5.0 * 5).clamp(0.0, 5.0); // 3% para streak bonusScore += (userStats.currentStreak / 7.0 * 3).clamp(0.0, 3.0); // 2% para conquistas bonusScore += (userStats.unlockedAchievements.length / 10.0 * 2).clamp( 0.0, 2.0, ); final totalScore = baseScore + bonusScore; return totalScore.clamp(0.0, 100.0); } /// Criar conquista personalizada (professor) static Future createCustomAchievement({ required String teacherId, required String name, required String description, required String icon, required String category, required AchievementRequirement requirements, required int points, required String rarity, }) async { try { final achievementRef = await _firestore.collection('achievements').add({ 'name': name, 'description': description, 'icon': icon, 'category': category, 'requirements': requirements.toFirestore(), 'points': points, 'rarity': rarity, 'isActive': true, 'createdAt': Timestamp.now(), 'createdBy': teacherId, }); Logger.info('Custom achievement created: ${achievementRef.id}'); return achievementRef.id; } catch (e) { Logger.error('Error creating custom achievement: $e'); rethrow; } } /// Obter conquistas disponíveis static Future> getAvailableAchievements({ String? teacherId, }) async { try { // Sempre incluir conquistas do sistema List achievements = List.from( SystemAchievements.defaultAchievements, ); // Adicionar conquistas personalizadas do professor Query query = _firestore .collection('achievements') .where('isActive', isEqualTo: true); if (teacherId != null) { query = query.where('createdBy', isEqualTo: teacherId); } final snapshot = await query.get(); achievements.addAll( snapshot.docs .map( (doc) => Achievement.fromFirestore( doc.data() as Map, doc.id, ), ) .toList(), ); Logger.info('Total achievements loaded: ${achievements.length}'); return achievements; } catch (e) { Logger.error('Error getting available achievements: $e'); return SystemAchievements.defaultAchievements; } } /// Métodos privados static Future _createInitialUserStats(String userId) async { await _firestore.collection('users').doc(userId).set({ 'userId': userId, 'currentStreak': 0, 'longestStreak': 0, 'totalStudyTime': 0, 'lastActivityDate': null, // null para que primeira atividade inicie streak 'weeklyStudyTime': 0, 'monthlyStudyTime': 0, 'masteredConcepts': [], 'unlockedAchievements': [], }); } static Future _addMasteredConcept( String userId, String conceptName, int score, ) async { try { final userStatsRef = _firestore.collection('users').doc(userId); final userStatsDoc = await userStatsRef.get(); if (!userStatsDoc.exists) return; final userStats = UserStats.fromFirestore(userStatsDoc.data()!, userId); // Verificar se conceito já está dominado final existingConcept = userStats.masteredConcepts .where((c) => c.conceptName == conceptName) .firstOrNull; if (existingConcept != null) { // Atualizar nível de maestria se score maior if (score > existingConcept.masteryLevel) { final updatedConcepts = userStats.masteredConcepts.map((c) { if (c.conceptName == conceptName) { return MasteredConcept( conceptName: conceptName, masteredAt: DateTime.now(), masteryLevel: score, ); } return c; }).toList(); await userStatsRef.update({ 'masteredConcepts': updatedConcepts .map((c) => c.toFirestore()) .toList(), }); } } else { // Adicionar novo conceito final newConcept = MasteredConcept( conceptName: conceptName, masteredAt: DateTime.now(), masteryLevel: score, ); await userStatsRef.update({ 'masteredConcepts': FieldValue.arrayUnion([newConcept.toFirestore()]), }); } } catch (e) { Logger.error('Error adding mastered concept: $e'); } } static Future _calculateClassStats(String classId) async { try { // Obter informações da turma final classDoc = await _firestore .collection('classes') .doc(classId) .get(); if (!classDoc.exists) { throw Exception('Class not found'); } final className = classDoc.data()?['name'] ?? 'Unknown Class'; final teacherId = classDoc.data()?['teacherId'] ?? ''; // Obter alunos matriculados final enrollmentsSnapshot = await _firestore .collection('enrollments') .where('classId', isEqualTo: classId) .get(); final studentIds = enrollmentsSnapshot.docs .map((doc) => doc.data()['studentId'] as String) .toList(); // Calcular estatísticas int activeStudents = 0; double totalProgress = 0; List needingSupport = []; for (final studentId in studentIds) { final userStats = await getUserStats(studentId); // Verificar se está ativo (atividade nos últimos 30 dias - mais realista) int daysSinceLastActivity = 999; // Valor alto para inatividade bool hasStats = userStats != null; if (hasStats && userStats!.lastActivityDate != null) { daysSinceLastActivity = DateTime.now() .difference(userStats.lastActivityDate!) .inDays; if (daysSinceLastActivity <= 30) { activeStudents++; } } // Calcular progresso baseado em quizzes completos e conceitos dominados double progress = 0.0; if (hasStats) { final completedQuizzes = userStats!.completedQuizzes; final masteredConcepts = userStats.masteredConcepts.length; Logger.info('=== PROGRESS CALCULATION DEBUG ==='); Logger.info('Student ID: $studentId'); Logger.info('Completed quizzes: $completedQuizzes'); Logger.info('Mastered concepts: $masteredConcepts'); // Progresso mais representativo: 60% quizzes + 40% conceitos // Primeiro quiz já dá 30% de progresso (incentivo inicial) final quizProgress = completedQuizzes > 0 ? (0.3 + (completedQuizzes - 1) * 0.15).clamp(0.0, 0.6) : 0.0; // Primeiro conceito já dá 15% de progresso final conceptProgress = (masteredConcepts * 0.15).clamp(0.0, 0.4); progress = quizProgress + conceptProgress; Logger.info( 'Quiz progress: $quizProgress (${(quizProgress * 100).toInt()}%)', ); Logger.info( 'Concept progress: $conceptProgress (${(conceptProgress * 100).toInt()}%)', ); Logger.info( 'Total progress: $progress (${(progress * 100).toInt()}%)', ); Logger.info('=== END PROGRESS CALCULATION DEBUG ==='); } else { Logger.info('Student $studentId has no stats - progress = 0.0'); } totalProgress += progress; // Verificar se precisa de apoio (ajustado para nova fórmula) if (progress < 0.25 || daysSinceLastActivity > 30) { final userDoc = await _firestore .collection('users') .doc(studentId) .get(); final userData = userDoc.data(); // Tentar obter um nome melhor para o aluno String studentName = 'Aluno ${studentId.substring(0, 8)}...'; if (userData != null) { studentName = userData['displayName'] ?? userData['email']?.split('@')[0] ?? 'Aluno ${studentId.substring(0, 8)}...'; } needingSupport.add( StudentNeedingSupport( studentId: studentId, studentName: studentName, reason: progress < 0.3 ? 'low_scores' : 'inactivity', lastActivity: hasStats ? userStats!.lastActivityDate ?? DateTime.now() : DateTime.now().subtract(const Duration(days: 45)), averageScore: progress * 100, ), ); } } final averageProgress = studentIds.isEmpty ? 0.0 : totalProgress / studentIds.length; Logger.info('=== AVERAGE PROGRESS DEBUG ==='); Logger.info('Total students: ${studentIds.length}'); Logger.info('Total progress sum: $totalProgress'); Logger.info( 'Average progress: $averageProgress (${(averageProgress * 100).toInt()}%)', ); Logger.info('=== END AVERAGE PROGRESS DEBUG ==='); // Obter estatísticas de quizzes e conteúdos final quizzesSnapshot = await _firestore .collection('teacherQuizzes') .where('classIds', arrayContains: classId) .get(); // Obter materiais/conteúdos da turma final materialsSnapshot = await _firestore .collection('materials') .where('classId', isEqualTo: classId) .get(); // Contar quizzes ativos (últimos 30 dias) final thirtyDaysAgo = DateTime.now().subtract(const Duration(days: 30)); final activeQuizzesCount = quizzesSnapshot.docs.where((doc) { final createdAt = (doc.data()['createdAt'] as Timestamp?)?.toDate(); return createdAt != null && createdAt.isAfter(thirtyDaysAgo); }).length; final classStats = ClassStats( classId: classId, teacherId: teacherId, className: className, totalStudents: studentIds.length, activeStudents: activeStudents, averageProgress: averageProgress, totalQuizzes: quizzesSnapshot.docs.length, activeQuizzes: activeQuizzesCount, totalContent: materialsSnapshot.docs.length, weeklyStats: [], studentsNeedingSupport: needingSupport, lastUpdated: DateTime.now(), ); // Limpar cache primeiro e depois salvar estatísticas calculadas await _firestore.collection('classStats').doc(classId).delete(); await _firestore .collection('classStats') .doc(classId) .set(classStats.toFirestore()); Logger.info('Class stats refreshed and saved for class $classId'); return classStats; } catch (e) { Logger.error('Error calculating class stats: $e'); rethrow; } } static Future _checkStreakAchievements( String userId, int streakDays, ) async { final achievements = await getAvailableAchievements(); final streakAchievements = achievements.where( (a) => a.category == 'streak', ); for (final achievement in streakAchievements) { if (achievement.requirements.checkCondition(streakDays)) { await _unlockAchievement(userId, achievement.id); } } } static Future _checkStudyTimeAchievements( String userId, int totalMinutes, ) async { final achievements = await getAvailableAchievements(); final studyAchievements = achievements.where( (a) => a.category == 'study_time', ); for (final achievement in studyAchievements) { if (achievement.requirements.checkCondition(totalMinutes)) { await _unlockAchievement(userId, achievement.id); } } } static Future _checkQuizAchievements( String userId, int score, int totalQuestions, ) async { Logger.info('=== CHECKING QUIZ ACHIEVEMENTS ==='); Logger.info('Score: $score/$totalQuestions'); final achievements = await getAvailableAchievements(); final userStats = await getUserStats(userId); if (userStats == null) { Logger.error('User stats null for achievement checking'); return; } final percentage = (score / totalQuestions) * 100; // Usar contador real de quizzes completos final completedQuizzes = userStats.completedQuizzes; Logger.info('Percentage: $percentage%'); Logger.info('Completed quizzes: $completedQuizzes'); Logger.info('Mastered concepts: ${userStats.masteredConcepts.length}'); Logger.info('Available achievements: ${achievements.length}'); for (final achievement in achievements) { if (achievement.category == 'quiz' && achievement.requirements.type == 'quiz_score' && achievement.requirements.checkCondition(percentage)) { await _unlockAchievement(userId, achievement.id); } else if (achievement.category == 'quiz_count' && achievement.requirements.type == 'quiz_completion' && achievement.requirements.checkCondition(completedQuizzes)) { await _unlockAchievement(userId, achievement.id); } else if (achievement.category == 'quiz' && achievement.requirements.type == 'quiz_completion' && achievement.id == 'first_quiz' && achievement.requirements.checkCondition(1)) { await _unlockAchievement(userId, achievement.id); } else if (achievement.category == 'concept' && achievement.requirements.type == 'concepts_mastered' && achievement.requirements.checkCondition( userStats.masteredConcepts.length, )) { await _unlockAchievement(userId, achievement.id); } else { Logger.info( 'Achievement not matched: ${achievement.id} - category: ${achievement.category}, type: ${achievement.requirements.type}', ); } } Logger.info('=== END CHECKING QUIZ ACHIEVEMENTS ==='); } static Future _unlockAchievement( String userId, String achievementId, ) async { try { Logger.info('=== ATTEMPTING TO UNLOCK ACHIEVEMENT ==='); Logger.info('Achievement ID: $achievementId'); Logger.info('User ID: $userId'); final userStatsRef = _firestore.collection('users').doc(userId); final userStatsDoc = await userStatsRef.get(); if (!userStatsDoc.exists) { Logger.error('User stats document does not exist for user $userId'); return; } final userStats = UserStats.fromFirestore(userStatsDoc.data()!, userId); Logger.info( 'Current unlocked achievements count: ${userStats.unlockedAchievements.length}', ); // Verificar se já desbloqueou final alreadyUnlocked = userStats.unlockedAchievements.any( (a) => a.achievementId == achievementId, ); Logger.info('Already unlocked: $alreadyUnlocked'); if (!alreadyUnlocked) { final unlockedAchievement = UnlockedAchievement( achievementId: achievementId, unlockedAt: DateTime.now(), metadata: {}, ); Logger.info('Adding achievement to Firestore...'); await userStatsRef.update({ 'unlockedAchievements': FieldValue.arrayUnion([ unlockedAchievement.toFirestore(), ]), }); Logger.info( 'Achievement unlocked successfully: $achievementId for user $userId', ); } else { Logger.info( 'Achievement $achievementId already unlocked for user $userId', ); } } catch (e) { Logger.error('Error unlocking achievement: $e'); } } /// Inicializar estatísticas do usuário static Future _initializeUserStats(String userId) async { try { final userStatsRef = _firestore.collection('users').doc(userId); // Verificar se já existe final doc = await userStatsRef.get(); if (doc.exists) { // Apenas atualizar com completedQuizzes se não existir final data = doc.data() as Map; if (!data.containsKey('completedQuizzes')) { await userStatsRef.update({'completedQuizzes': 0}); } } else { // Criar documento inicial await userStatsRef.set({ 'completedQuizzes': 0, 'currentStreak': 0, 'longestStreak': 0, 'totalStudyTime': 0, 'weeklyStudyTime': 0, 'monthlyStudyTime': 0, 'masteredConcepts': [], 'unlockedAchievements': [], }); } Logger.info('User stats initialized for user $userId'); } catch (e) { Logger.error('Error initializing user stats: $e'); } } }