import 'package:flutter_bloc/flutter_bloc.dart'; import 'dart:typed_data'; import '../../data/models/teacher_models.dart'; import '../../data/repositories/teacher_repository.dart'; import '../../core/utils/app_logger.dart'; /** * ============================================================================== * SAQEL TEACHER STUDIO - STUDIO UPLOAD CUBIT * ============================================================================== * * إدارة حالة استوديو رفع الحصص وفحص الجودة الإدراكي: * - التحقق من معيار معهد ماساتشوستس (20 - 25 دقيقة) * - استدعاء التدقيق الآلي للدرس ومخرجات المنهاج الوزاري. */ class TeacherStudioState { final String lessonTitle; final double durationMinutes; final String gradeLevel; final Map curriculumTree; final String gradeKey; final String subjectKey; final String semesterKey; final String unitKey; final String lessonKey; final bool isCurriculumLoading; final String? errorMessage; final String? selectedFileName; final double? selectedFileSizeMb; final String? selectedFilePath; final Uint8List? selectedFileBytes; final bool isAuditing; final TeacherLessonAuditModel? auditResult; final bool isUploading; final bool isUploaded; final String? uploadSuccessMessage; final double uploadProgress; final String uploadPhase; const TeacherStudioState({ required this.lessonTitle, required this.durationMinutes, this.gradeLevel = '', this.curriculumTree = const {}, this.gradeKey = '', this.subjectKey = '', this.semesterKey = '', this.unitKey = '', this.lessonKey = '', this.isCurriculumLoading = false, this.errorMessage, this.selectedFileName, this.selectedFileSizeMb, this.selectedFilePath, this.selectedFileBytes, required this.isAuditing, this.auditResult, this.isUploading = false, this.isUploaded = false, this.uploadSuccessMessage, this.uploadProgress = 0.0, this.uploadPhase = 'idle', }); TeacherStudioState copyWith({ String? lessonTitle, double? durationMinutes, String? gradeLevel, Map? curriculumTree, String? gradeKey, String? subjectKey, String? semesterKey, String? unitKey, String? lessonKey, bool? isCurriculumLoading, String? errorMessage, bool clearError = false, String? selectedFileName, double? selectedFileSizeMb, String? selectedFilePath, Uint8List? selectedFileBytes, bool? isAuditing, TeacherLessonAuditModel? auditResult, bool? isUploading, bool? isUploaded, String? uploadSuccessMessage, double? uploadProgress, String? uploadPhase, }) { return TeacherStudioState( lessonTitle: lessonTitle ?? this.lessonTitle, durationMinutes: durationMinutes ?? this.durationMinutes, gradeLevel: gradeLevel ?? this.gradeLevel, curriculumTree: curriculumTree ?? this.curriculumTree, gradeKey: gradeKey ?? this.gradeKey, subjectKey: subjectKey ?? this.subjectKey, semesterKey: semesterKey ?? this.semesterKey, unitKey: unitKey ?? this.unitKey, lessonKey: lessonKey ?? this.lessonKey, isCurriculumLoading: isCurriculumLoading ?? this.isCurriculumLoading, errorMessage: clearError ? null : errorMessage ?? this.errorMessage, selectedFileName: selectedFileName ?? this.selectedFileName, selectedFileSizeMb: selectedFileSizeMb ?? this.selectedFileSizeMb, selectedFilePath: selectedFilePath ?? this.selectedFilePath, selectedFileBytes: selectedFileBytes ?? this.selectedFileBytes, isAuditing: isAuditing ?? this.isAuditing, auditResult: auditResult ?? this.auditResult, isUploading: isUploading ?? this.isUploading, isUploaded: isUploaded ?? this.isUploaded, uploadSuccessMessage: uploadSuccessMessage ?? this.uploadSuccessMessage, uploadProgress: uploadProgress ?? this.uploadProgress, uploadPhase: uploadPhase ?? this.uploadPhase, ); } } class TeacherStudioCubit extends Cubit { final TeacherRepository repository; TeacherStudioCubit({required this.repository}) : super(const TeacherStudioState( lessonTitle: '', durationMinutes: 20.0, isAuditing: false, auditResult: null, )); void updateLessonTitle(String title) { emit(state.copyWith(lessonTitle: title)); } void updateDuration(double duration) { emit(state.copyWith(durationMinutes: duration)); } void updateGradeLevel(String grade) { emit(state.copyWith(gradeLevel: grade)); } Map _map(dynamic value) => value is Map ? Map.from(value) : {}; Map get _subjects => _map(state.curriculumTree[state.gradeKey]?['subjects']); Map get _semesters => _map(_subjects[state.subjectKey]?['semesters']); Map get _units => _map(_semesters[state.semesterKey]?['units']); Future loadCurriculum() async { if (state.curriculumTree.isNotEmpty || state.isCurriculumLoading) return; emit(state.copyWith(isCurriculumLoading: true, clearError: true)); try { final results = await Future.wait( [repository.getCurriculumTree(), repository.getProfileStatus()]); final rawTree = Map.from(results[0] as Map); final profile = results[1] as TeacherProfileModel; final allowedGrades = profile.gradesTaught .map(_normalized) .where((e) => e.isNotEmpty) .toList(); final specialization = _normalized(profile.specialization); final tree = {}; for (final gradeEntry in rawTree.entries) { final gradeNode = _map(gradeEntry.value); final gradeName = _normalized(gradeNode['name']?.toString() ?? gradeEntry.key); if (allowedGrades.isNotEmpty && !allowedGrades .any((g) => gradeName.contains(g) || g.contains(gradeName))) continue; final allSubjects = _map(gradeNode['subjects']); if (specialization.isNotEmpty) { final matched = Map.fromEntries( allSubjects.entries.where((entry) { final subjectName = _normalized('${entry.key} ${_map(entry.value)['name'] ?? ''}'); return subjectName.contains(specialization) || specialization.contains(subjectName) || specialization.split(' ').any( (word) => word.length > 3 && subjectName.contains(word)); })); if (matched.isNotEmpty) gradeNode['subjects'] = matched; } tree[gradeEntry.key] = gradeNode; } final key = tree.keys.isNotEmpty ? tree.keys.first : ''; emit(state.copyWith(curriculumTree: tree, isCurriculumLoading: false)); selectGrade(key); } catch (e) { AppLogger.error('Studio upload failed', error: e, tag: 'TEACHER_STUDIO'); emit(state.copyWith( isCurriculumLoading: false, errorMessage: e.toString())); } } String _normalized(String value) => value .toLowerCase() .replaceAll(RegExp(r'[^\u0600-\u06ffa-z0-9]+'), ' ') .trim(); void selectGrade(String key) { final node = state.curriculumTree[key]; final name = node is Map ? node['name']?.toString() ?? key : key; emit(state.copyWith( gradeKey: key, gradeLevel: name, subjectKey: '', semesterKey: '', unitKey: '', lessonKey: '', clearError: true)); final values = _map(state.curriculumTree[key]?['subjects']); selectSubject(values.keys.isNotEmpty ? values.keys.first : ''); } void selectSubject(String key) { emit(state.copyWith( subjectKey: key, semesterKey: '', unitKey: '', lessonKey: '', clearError: true)); final values = _map(_subjects[key]?['semesters']); selectSemester(values.keys.isNotEmpty ? values.keys.first : ''); } void selectSemester(String key) { emit(state.copyWith( semesterKey: key, unitKey: '', lessonKey: '', clearError: true)); final values = _map(_semesters[key]?['units']); selectUnit(values.keys.isNotEmpty ? values.keys.first : ''); } void selectUnit(String key) { emit(state.copyWith(unitKey: key, lessonKey: '', clearError: true)); final lessons = _units[key]?['lessons']; final id = lessons is List && lessons.isNotEmpty && lessons.first is Map ? (lessons.first as Map)['id']?.toString() ?? '' : ''; selectLesson(id); } void selectLesson(String key) { String title = ''; final lessons = _units[state.unitKey]?['lessons']; if (lessons is List) for (final item in lessons) { if (item is Map && item['id']?.toString() == key) title = item['title']?.toString() ?? ''; } emit(state.copyWith(lessonKey: key, lessonTitle: title, clearError: true)); } String get selectedSubjectName { final node = _subjects[state.subjectKey]; final name = node is Map ? node['name']?.toString() ?? state.subjectKey : state.subjectKey; return name.replaceFirst(RegExp(r'\s*\([^)]*\)\s*$'), '').trim(); } String get curriculumKey => [ state.gradeKey, state.subjectKey, state.semesterKey, state.unitKey, state.lessonKey ].where((e) => e.isNotEmpty).join('/'); void reportError(String message) => emit(state.copyWith(errorMessage: message)); void selectVideoFile( String fileName, double sizeMb, double durationMinutes, { String? path, Uint8List? bytes, }) { emit(state.copyWith( selectedFileName: fileName, selectedFileSizeMb: sizeMb, selectedFilePath: path, selectedFileBytes: bytes, durationMinutes: durationMinutes, isUploaded: false, uploadSuccessMessage: null, )); } Future runQualityGate() async { if (state.selectedFileName == null || (state.selectedFilePath == null && state.selectedFileBytes == null)) { emit(state.copyWith( errorMessage: 'اختر ملف فيديو فعلياً قبل بدء فحص الجودة.')); return; } if (curriculumKey.isEmpty || state.lessonTitle.trim().isEmpty) { emit(state.copyWith( errorMessage: 'اختر الصف والمبحث والفصل والوحدة والدرس أولاً.')); return; } await uploadAndPublishLesson(); } Future uploadAndPublishLesson() async { if (state.selectedFileName == null || (state.selectedFilePath == null && state.selectedFileBytes == null)) { return false; } emit(state.copyWith( isUploading: true, isAuditing: true, uploadProgress: 0.0, uploadPhase: 'uploading', clearError: true, )); try { final res = await repository.uploadLesson( title: state.lessonTitle, durationMinutes: state.durationMinutes, gradeLevel: state.gradeLevel, subject: selectedSubjectName, curriculumKey: curriculumKey, fileName: state.selectedFileName!, fileSizeMb: state.selectedFileSizeMb, filePath: state.selectedFilePath, fileBytes: state.selectedFileBytes, onProgress: (sent, total) { if (total > 0) { final p = (sent / total).clamp(0.0, 1.0); emit(state.copyWith( uploadProgress: p, uploadPhase: p >= 1.0 ? 'auditing' : 'uploading', )); } }, ); final status = res['status']?.toString(); final data = res['data'] is Map ? Map.from(res['data'] as Map) : {}; final report = data['preflight_report'] is Map ? Map.from(data['preflight_report'] as Map) : {}; final aiAnalysis = data['ai_analysis'] is Map ? Map.from(data['ai_analysis'] as Map) : {}; final result = TeacherLessonAuditModel.fromJson({ ...report, 'ai_analysis': aiAnalysis, 'lesson_title': state.lessonTitle, 'subject': selectedSubjectName }); if (status == 'needs_review' || report['decision'] == 'needs_manual_review' || report['decision'] == 'rejected') { final reason = report['reason']?.toString() ?? res['message']?.toString() ?? 'لم يجتز الفيديو تدقيق الجودة.'; emit(state.copyWith( isAuditing: false, isUploading: false, isUploaded: false, uploadProgress: 0.0, uploadPhase: 'idle', auditResult: result, errorMessage: reason, )); return false; } final msg = res['message']?.toString() ?? 'تم اعتماد الحصة ورفعها بنجاح.'; emit(state.copyWith( isAuditing: false, isUploading: false, isUploaded: true, uploadProgress: 1.0, uploadPhase: 'completed', auditResult: result, uploadSuccessMessage: msg, )); return true; } catch (e) { emit(state.copyWith( isAuditing: false, isUploading: false, uploadProgress: 0.0, uploadPhase: 'idle', errorMessage: e.toString().replaceFirst('Bad state: ', ''))); return false; } } }