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); // Registrar tempo de estudo (estimado) await recordStudyTime(userId, 15); // 15 minutos por quiz // 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) async { try { final classStatsDoc = await _firestore.collection('classStats').doc(classId).get(); if (!classStatsDoc.exists) { return await _calculateClassStats(classId); } return ClassStats.fromFirestore(classStatsDoc.data()!, classId); } catch (e) { Logger.error('Error getting class stats: $e'); return null; } } /// 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 = []; // 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 // Usar contador real de quizzes completos final completedQuizzes = userStats.completedQuizzes; final totalQuizzes = completedQuizzes + 5; // Estimativa de quizzes disponíveis final quizCompletionRate = totalQuizzes > 0 ? completedQuizzes / totalQuizzes : 0.0; // Calcular score geral baseado em múltiplos fatores final overallScore = _calculateOverallScore(userStats, quizCompletionRate); rankings.add(StudentRanking( studentId: studentId, studentName: userData?['displayName'] ?? 'Aluno $studentId', 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) { double score = 0.0; // Peso de 40% para completion rate de quizzes score += quizCompletionRate * 40; // Peso de 20% para streak (máximo 20 pontos) score += (userStats.currentStreak / 30.0 * 20).clamp(0.0, 20.0); // Peso de 20% para tempo de estudo (máximo 20 pontos para 10 horas) score += (userStats.totalStudyTime / 600.0 * 20).clamp(0.0, 20.0); // Peso de 20% para conceitos dominados (máximo 20 pontos para 10 conceitos) score += (userStats.masteredConcepts.length / 10.0 * 20).clamp(0.0, 20.0); return score; } /// 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étodo público para inicializar dados de gamificação (para testes) static Future initializeGamificationData(String userId) async { try { final userStats = await getUserStats(userId); if (userStats == null) { await _createInitialUserStats(userId); Logger.info('Gamification data initialized for user $userId'); } } catch (e) { Logger.error('Error initializing gamification data: $e'); } } /// Método público para simular quiz completion (para testes) static Future simulateQuizCompletion(String userId, { required int score, required int totalQuestions, required String materialName, }) async { Logger.info('=== SIMULATING QUIZ COMPLETION ==='); Logger.info('User: $userId'); Logger.info('Score: $score/$totalQuestions'); Logger.info('Material: $materialName'); await recordQuizActivity( userId, score: score, totalQuestions: totalQuestions, materialName: materialName, ); // Verificar estado após simulação await debugUserStats(userId); } /// Método de debugging para verificar estado completo do usuário static Future debugUserStats(String userId) async { try { Logger.info('=== DEBUGGING USER STATS ==='); final userStats = await getUserStats(userId); if (userStats == null) { Logger.error('User stats not found for $userId'); return; } Logger.info('Current Streak: ${userStats.currentStreak}'); Logger.info('Longest Streak: ${userStats.longestStreak}'); Logger.info('Total Study Time: ${userStats.totalStudyTime}'); Logger.info('Weekly Study Time: ${userStats.weeklyStudyTime}'); Logger.info('Monthly Study Time: ${userStats.monthlyStudyTime}'); Logger.info('Mastered Concepts: ${userStats.masteredConcepts.length}'); Logger.info('Unlocked Achievements: ${userStats.unlockedAchievements.length}'); for (final concept in userStats.masteredConcepts) { Logger.info(' - Concept: ${concept.conceptName}, Level: ${concept.masteryLevel}'); } for (final achievement in userStats.unlockedAchievements) { Logger.info(' - Achievement: ${achievement.achievementId}, Unlocked: ${achievement.unlockedAt}'); } Logger.info('=== END DEBUG ==='); } catch (e) { Logger.error('Error debugging user stats: $e'); } } /// 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); if (userStats != null) { // Verificar se está ativo (atividade nos últimos 7 dias) int daysSinceLastActivity = 999; // Valor alto para inatividade if (userStats.lastActivityDate != null) { daysSinceLastActivity = DateTime.now().difference(userStats.lastActivityDate!).inDays; if (daysSinceLastActivity <= 7) { activeStudents++; } } // Calcular progresso baseado em conceitos dominados final progress = userStats.masteredConcepts.isEmpty ? 0.0 : userStats.masteredConcepts.map((c) => c.masteryLevel).reduce((a, b) => a + b) / userStats.masteredConcepts.length / 100; totalProgress += progress; // Verificar se precisa de apoio if (progress < 0.5 || daysSinceLastActivity > 14) { final userDoc = await _firestore.collection('users').doc(studentId).get(); final userName = userDoc.data()?['displayName'] ?? 'Unknown'; needingSupport.add(StudentNeedingSupport( studentId: studentId, studentName: userName, reason: progress < 0.5 ? 'low_scores' : 'inactivity', lastActivity: userStats.lastActivityDate ?? DateTime.now(), averageScore: progress * 100, )); } } } final averageProgress = studentIds.isEmpty ? 0.0 : totalProgress / studentIds.length; // Obter estatísticas de quizzes final quizzesSnapshot = await _firestore .collection('teacherQuizzes') .where('classIds', arrayContains: classId) .get(); final classStats = ClassStats( classId: classId, teacherId: teacherId, className: className, totalStudents: studentIds.length, activeStudents: activeStudents, averageProgress: averageProgress, totalQuizzes: quizzesSnapshot.docs.length, activeQuizzes: quizzesSnapshot.docs.length, // Simplificado totalContent: 0, // Implementar depois weeklyStats: [], studentsNeedingSupport: needingSupport, ); // Salvar estatísticas calculadas await _firestore.collection('classStats').doc(classId).set(classStats.toFirestore()); 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.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 ==='); // Verificar conquistas genéricas de número de quizzes await _checkQuizCountAchievements(userId, completedQuizzes); } static Future _checkQuizCountAchievements(String userId, int completedQuizzes) async { final achievements = await getAvailableAchievements(); final quizCountAchievements = achievements.where((a) => a.category == 'quiz_count'); for (final achievement in quizCountAchievements) { if (achievement.requirements.type == 'quiz_completion' && achievement.requirements.checkCondition(completedQuizzes)) { await _unlockAchievement(userId, achievement.id); } } } 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'); } } }