import 'package:flutter_bloc/flutter_bloc.dart'; import '../../core/utils/app_logger.dart'; import '../../data/models/socratic_checkpoint_model.dart'; import '../../data/models/subject_model.dart'; import '../../data/repositories/curriculum_repository.dart'; abstract class VideoPlaybackState {} class VideoPlaybackInitial extends VideoPlaybackState {} class VideoPlaybackLoading extends VideoPlaybackState {} class VideoPlaybackReady extends VideoPlaybackState { final LessonPlaybackData playbackData; final CurriculumLessonItemModel? lessonItem; final SubjectModel? subject; final int currentPositionSeconds; final bool isPlaying; final SocraticCheckpointModel? activeCheckpoint; final bool isCheckpointPassed; final String? remediationNotice; final double readinessBonusAdded; final Set passedCheckpointIds; VideoPlaybackReady({ required this.playbackData, this.lessonItem, this.subject, this.currentPositionSeconds = 0, this.isPlaying = true, this.activeCheckpoint, this.isCheckpointPassed = false, this.remediationNotice, this.readinessBonusAdded = 0.0, this.passedCheckpointIds = const {}, }); VideoPlaybackReady copyWith({ LessonPlaybackData? playbackData, CurriculumLessonItemModel? lessonItem, SubjectModel? subject, int? currentPositionSeconds, bool? isPlaying, SocraticCheckpointModel? activeCheckpoint, bool? isCheckpointPassed, String? remediationNotice, double? readinessBonusAdded, Set? passedCheckpointIds, bool clearActiveCheckpoint = false, }) { return VideoPlaybackReady( playbackData: playbackData ?? this.playbackData, lessonItem: lessonItem ?? this.lessonItem, subject: subject ?? this.subject, currentPositionSeconds: currentPositionSeconds ?? this.currentPositionSeconds, isPlaying: isPlaying ?? this.isPlaying, activeCheckpoint: clearActiveCheckpoint ? null : (activeCheckpoint ?? this.activeCheckpoint), isCheckpointPassed: isCheckpointPassed ?? this.isCheckpointPassed, remediationNotice: remediationNotice, readinessBonusAdded: readinessBonusAdded ?? this.readinessBonusAdded, passedCheckpointIds: passedCheckpointIds ?? this.passedCheckpointIds, ); } } class VideoPlaybackError extends VideoPlaybackState { final String message; VideoPlaybackError(this.message); } class VideoPlaybackCubit extends Cubit { final CurriculumRepository _repo; int _lastObservedPosition = -1; VideoPlaybackCubit({CurriculumRepository? repo}) : _repo = repo ?? CurriculumRepository(), super(VideoPlaybackInitial()); Future loadLesson(CurriculumLessonItemModel lesson, {SubjectModel? subject}) async { AppLogger.log('Loading Socratic playback for lesson: ${lesson.title}', tag: 'VIDEO_CUBIT'); _lastObservedPosition = -1; emit(VideoPlaybackLoading()); try { final playback = await _repo.getLessonPlayback(lesson.markdownFilePath ?? lesson.id, subjectId: subject?.id); emit(VideoPlaybackReady( playbackData: playback, lessonItem: lesson, subject: subject, currentPositionSeconds: 0, isPlaying: true, )); } catch (e) { AppLogger.log('Playback API failed for ${lesson.title}: $e', tag: 'VIDEO_CUBIT'); emit(VideoPlaybackError('لا يوجد فيديو منشور لهذا الدرس حاليًا. يرجى المحاولة لاحقًا.')); } } void updatePosition(int seconds) { final currentState = state; if (currentState is VideoPlaybackReady && currentState.activeCheckpoint == null) { // Check if natural forward playback reaches a Socratic checkpoint for (var cp in currentState.playbackData.checkpoints) { final hitCheckpoint = !currentState.passedCheckpointIds.contains(cp.id) && _lastObservedPosition < cp.timestampSeconds && seconds >= cp.timestampSeconds && (seconds - _lastObservedPosition).abs() <= 3; // Natural sequential playback if (hitCheckpoint) { AppLogger.log('🚨 Socratic Checkpoint Triggered at ${cp.timestampSeconds}s! (${cp.questionText})', tag: 'SOCRATIC_ENGINE'); _lastObservedPosition = cp.timestampSeconds; emit(currentState.copyWith( currentPositionSeconds: cp.timestampSeconds, isPlaying: false, // Freeze video playback activeCheckpoint: cp, )); return; } } _lastObservedPosition = seconds; emit(currentState.copyWith(currentPositionSeconds: seconds)); } } void togglePlayPause() { final currentState = state; if (currentState is VideoPlaybackReady && currentState.activeCheckpoint == null) { emit(currentState.copyWith(isPlaying: !currentState.isPlaying)); } } void seekTo(int seconds) { final currentState = state; if (currentState is VideoPlaybackReady) { // Find the earliest unpassed checkpoint before or at this target SocraticCheckpointModel? blockingCheckpoint; for (var cp in currentState.playbackData.checkpoints) { if (!currentState.passedCheckpointIds.contains(cp.id) && seconds > cp.timestampSeconds) { if (blockingCheckpoint == null || cp.timestampSeconds < blockingCheckpoint.timestampSeconds) { blockingCheckpoint = cp; } } } // If user tries to skip past an unpassed checkpoint, snap precisely to that checkpoint final actualSeek = blockingCheckpoint != null ? blockingCheckpoint.timestampSeconds : seconds; _lastObservedPosition = actualSeek; emit(currentState.copyWith( currentPositionSeconds: actualSeek, activeCheckpoint: blockingCheckpoint, isPlaying: blockingCheckpoint == null ? currentState.isPlaying : false, )); } } Future saveProgress({required int positionSeconds, required int watchedSeconds}) async { final currentState = state; if (currentState is! VideoPlaybackReady || currentState.playbackData.lessonId <= 0) return; try { await _repo.saveProgress( lessonId: currentState.playbackData.lessonId, positionSeconds: positionSeconds, watchedSeconds: watchedSeconds, ); } catch (e) { AppLogger.log('Progress sync deferred: $e', tag: 'VIDEO_CUBIT'); } } /// Submit Socratic Checkpoint Answer bool submitCheckpointAnswer(SocraticOptionModel selectedOption) { final currentState = state; if (currentState is VideoPlaybackReady && currentState.activeCheckpoint != null) { final cp = currentState.activeCheckpoint!; if (selectedOption.isCorrect) { // Correct Answer -> Reward readiness score (+0.5%) & resume video AppLogger.log('✅ Correct answer! Rewarding +0.5% readiness.', tag: 'SOCRATIC_ENGINE'); final updatedPassed = Set.from(currentState.passedCheckpointIds)..add(cp.id); _lastObservedPosition = cp.timestampSeconds; emit(currentState.copyWith( clearActiveCheckpoint: true, isPlaying: true, readinessBonusAdded: currentState.readinessBonusAdded + 0.5, passedCheckpointIds: updatedPassed, remediationNotice: 'إجابة نموذجية ممتازة! تم تعزيز مؤشر الجاهزية (+0.5%) 🚀', )); if (cp.id > 0) { _repo.submitCheckpoint(examId: cp.id, questionId: cp.questionId, optionId: selectedOption.id).catchError((e) { AppLogger.log('Checkpoint sync deferred: $e', tag: 'VIDEO_CUBIT'); }); } return true; } else { // Wrong Answer -> Socratic Productive Struggle: Rewind video by N seconds from checkpoint final rewindTo = (cp.timestampSeconds - cp.rewindSecondsOnFail).clamp(0, currentState.playbackData.durationSeconds); _lastObservedPosition = rewindTo; AppLogger.log('❌ Incorrect answer. Socratic remediation: Rewinding to ${rewindTo}s.', tag: 'SOCRATIC_ENGINE'); emit(currentState.copyWith( clearActiveCheckpoint: true, currentPositionSeconds: rewindTo, isPlaying: true, remediationNotice: 'تعثرت في هذا المفهوم. تم إرجاع الفيديو ${cp.rewindSecondsOnFail} ثانية لإعادة الاستماع بتركيز 🔄', )); if (cp.id > 0 && cp.questionId > 0) { _repo.submitCheckpoint(examId: cp.id, questionId: cp.questionId, optionId: selectedOption.id).catchError((e) { AppLogger.log('Checkpoint sync deferred: $e', tag: 'VIDEO_CUBIT'); }); } return false; } } return false; } }