import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../data/models/exam_model.dart'; import '../../data/repositories/app_repositories.dart'; abstract class ExamState {} class ExamInitial extends ExamState {} class ExamLoading extends ExamState { final String message; ExamLoading({this.message = 'جاري تحضير الامتحان والأسئلة التكيفية...'}); } class ExamLoaded extends ExamState { final ExamModel exam; final int currentQuestionIndex; final Map selectedAnswers; // questionId -> optionId final int remainingSeconds; final bool isSubmitting; ExamLoaded({ required this.exam, this.currentQuestionIndex = 0, this.selectedAnswers = const {}, required this.remainingSeconds, this.isSubmitting = false, }); QuestionModel? get currentQuestion { if (exam.questions.isEmpty || currentQuestionIndex >= exam.questions.length) return null; return exam.questions[currentQuestionIndex]; } bool get isLastQuestion => currentQuestionIndex == exam.questions.length - 1; int get answeredCount => selectedAnswers.length; ExamLoaded copyWith({ ExamModel? exam, int? currentQuestionIndex, Map? selectedAnswers, int? remainingSeconds, bool? isSubmitting, }) { return ExamLoaded( exam: exam ?? this.exam, currentQuestionIndex: currentQuestionIndex ?? this.currentQuestionIndex, selectedAnswers: selectedAnswers ?? this.selectedAnswers, remainingSeconds: remainingSeconds ?? this.remainingSeconds, isSubmitting: isSubmitting ?? this.isSubmitting, ); } } class ExamCompleted extends ExamState { final ExamModel exam; final ExamSubmissionResultModel result; final int timeSpentSeconds; ExamCompleted({ required this.exam, required this.result, required this.timeSpentSeconds, }); } class ExamError extends ExamState { final String message; ExamError(this.message); } class ExamCubit extends Cubit { final ExamRepository _repository; Timer? _countdownTimer; int _initialSeconds = 0; ExamCubit({ExamRepository? repository}) : _repository = repository ?? ExamRepository(), super(ExamInitial()); @override Future close() { _countdownTimer?.cancel(); return super.close(); } Future loadExam({int examId = 1, ExamModel? initialExam}) async { emit(ExamLoading()); try { ExamModel loadedExam; if (initialExam != null && initialExam.questions.isNotEmpty) { loadedExam = initialExam; } else { try { loadedExam = await _repository.getExamDetails(examId); } catch (_) { // If server call fails or exam not in DB, use curriculum-grounded default questions loadedExam = initialExam ?? _createDefaultExam(examId); } } if (loadedExam.questions.isEmpty) { loadedExam = _createDefaultExam(examId, title: loadedExam.title); } _initialSeconds = loadedExam.durationMinutes * 60; emit(ExamLoaded( exam: loadedExam, remainingSeconds: _initialSeconds, )); _startTimer(); } catch (e) { emit(ExamError('حدث خطأ أثناء تحميل الامتحان: ${e.toString()}')); } } void _startTimer() { _countdownTimer?.cancel(); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { final s = state; if (s is ExamLoaded) { if (s.remainingSeconds <= 1) { timer.cancel(); submitCurrentExam(); } else { emit(s.copyWith(remainingSeconds: s.remainingSeconds - 1)); } } else { timer.cancel(); } }); } void selectOption(int questionId, int optionId) { final s = state; if (s is ExamLoaded) { final updatedAnswers = Map.from(s.selectedAnswers); updatedAnswers[questionId] = optionId; emit(s.copyWith(selectedAnswers: updatedAnswers)); } } void goToQuestion(int index) { final s = state; if (s is ExamLoaded) { if (index >= 0 && index < s.exam.questions.length) { emit(s.copyWith(currentQuestionIndex: index)); } } } void nextQuestion() { final s = state; if (s is ExamLoaded && !s.isLastQuestion) { emit(s.copyWith(currentQuestionIndex: s.currentQuestionIndex + 1)); } } void previousQuestion() { final s = state; if (s is ExamLoaded && s.currentQuestionIndex > 0) { emit(s.copyWith(currentQuestionIndex: s.currentQuestionIndex - 1)); } } Future submitCurrentExam() async { final s = state; if (s is! ExamLoaded) return; _countdownTimer?.cancel(); emit(s.copyWith(isSubmitting: true)); final timeSpent = _initialSeconds - s.remainingSeconds; final formattedAnswers = s.selectedAnswers.entries.map((e) { return { 'question_id': e.key, 'selected_option_id': e.value, }; }).toList(); try { ExamSubmissionResultModel result; try { result = await _repository.submitExam( s.exam.id, answers: formattedAnswers, timeSpentSeconds: timeSpent, ); } catch (_) { // Safe offline evaluation fallback if API token not present result = _evaluateLocally(s.exam, s.selectedAnswers); } emit(ExamCompleted( exam: s.exam, result: result, timeSpentSeconds: timeSpent, )); } catch (e) { emit(ExamError('فشل تقديم الامتحان: ${e.toString()}')); } } ExamSubmissionResultModel _evaluateLocally(ExamModel exam, Map selectedAnswers) { int earnedScore = 0; int totalScore = 0; List weakTopics = []; List detailedAnswers = []; for (var q in exam.questions) { totalScore += q.points; final selectedOptId = selectedAnswers[q.id]; QuestionOptionModel? correctOption; try { correctOption = q.options.firstWhere((opt) => opt.isCorrect); } catch (_) { correctOption = q.options.isNotEmpty ? q.options.first : null; } final isCorrect = selectedOptId != null && correctOption != null && selectedOptId == correctOption.id; final points = isCorrect ? q.points : 0; earnedScore += points; if (!isCorrect) { if (!weakTopics.contains(q.topicTag)) { weakTopics.add(q.topicTag); } } detailedAnswers.add(DetailedAnswerModel( questionId: q.id, selectedOptionId: selectedOptId ?? 0, isCorrect: isCorrect, pointsAwarded: points, explanation: q.explanationText ?? 'راجع نص القاعدة في كتاب الوزارة', aiHint: q.aiHint, )); } final pct = totalScore > 0 ? (earnedScore / totalScore) * 100 : 0.0; final passed = pct >= exam.passingPercentage; final report = passed ? 'أداء ممتاز! حققت نسبة إتقان ${pct.toStringAsFixed(1)}%. لديك استيعاب عميق للمفاهيم الأساسية.' : 'تم رصد تعثر في مفاهيم: ${weakTopics.join('، ')}. ننصح بمشاهدة مقاطع الشرح المركزة لمعالجة الثغرات.'; return ExamSubmissionResultModel( attemptId: DateTime.now().millisecondsSinceEpoch ~/ 1000, score: earnedScore, totalScore: totalScore, percentage: pct, passed: passed, rewindSeconds: passed ? 0 : 45, aiDiagnosticReport: report, weakTopics: weakTopics, tawjihiReadinessScore: (pct * 0.7) + (passed ? 25.0 : 10.0), detailedAnswers: detailedAnswers, ); } static ExamModel _createDefaultExam(int examId, {String? title}) { return ExamModel( id: examId, uuid: 'exam-default-$examId', courseId: 1, title: title ?? 'اختبار الفهم والتشخيص السقراطي — المستوى المتقدم', description: 'اختبار تشخيصي تكيفي يقيس الفهم العميق والقدرة على تطبيق القوانين الوزارية.', scope: 'unit_exam', passingPercentage: 70.0, durationMinutes: 15, questionsCount: 3, questions: const [ QuestionModel( id: 101, questionText: 'ما هو المعنى الهندسي المباشر للمشتقة الأولى f\'(x) عند نقطة التماس؟', questionType: 'multiple_choice', points: 10, topicTag: 'المعنى الهندسي للاشتقاق', explanationText: 'المشتقة الأولى تمثل رياضياً وهندسياً ميل خط المماس لمنحنى الاقتران عند نقطة التماس.', options: [ QuestionOptionModel(id: 1, optionText: 'ميل خط المماس للمنحنى عند تلك النقطة', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 2, optionText: 'معادلة المستقيم القاطع المار بنقطتين', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 3, optionText: 'المساحة المحصورة تحت المنحنى ومحور السينات', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 4, optionText: 'طول المماس الأفقي عند النقطة الحرجة', isCorrect: false, sequenceOrder: 4), ], ), QuestionModel( id: 102, questionText: 'إذا كان f(x) = sin(4x)، فما هي قيمة مشتقته الأولى f\'(x) وفق قاعدة السلسلة؟', questionType: 'multiple_choice', points: 10, topicTag: 'مشتقات الاقترانات الدائرية وقاعدة السلسلة', explanationText: 'مشتقة sin(ax) هي a * cos(ax)؛ أي مشتقة الزاوية ضرب مشتقة الاقتران الخارجي.', options: [ QuestionOptionModel(id: 5, optionText: '4 cos(4x)', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 6, optionText: 'cos(4x)', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 7, optionText: '-4 cos(4x)', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 8, optionText: '4 sin(4x)', isCorrect: false, sequenceOrder: 4), ], ), QuestionModel( id: 103, questionText: 'ما هو الشرط الرياضي اللازم والكافي ليكون المماس أفقياً عند نقطة واقعة على المنحنى؟', questionType: 'multiple_choice', points: 10, topicTag: 'المماسات الأفقية والنقاط الحرجة', explanationText: 'يكون المماس أفقياً وموازياً لمحور السينات عندما يكون ميله مساوياً للصفر، أي f\'(x) = 0.', options: [ QuestionOptionModel(id: 9, optionText: 'أن تكون المشتقة الأولى مساوية للصفر f\'(x) = 0', isCorrect: true, sequenceOrder: 1), QuestionOptionModel(id: 10, optionText: 'أن تكون قيمة الاقتران مساوية للصفر f(x) = 0', isCorrect: false, sequenceOrder: 2), QuestionOptionModel(id: 11, optionText: 'أن تكون المشتقة غير معرّفة أو قيمة مطلقة', isCorrect: false, sequenceOrder: 3), QuestionOptionModel(id: 12, optionText: 'أن يتقاطع المماس مع نقطة الأصل دائماً', isCorrect: false, sequenceOrder: 4), ], ), ], ); } }