import 'dart:io'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_storage/firebase_storage.dart'; import '../utils/logger.dart'; /// Service for managing educational content from teachers class ContentService { static final FirebaseAuth _auth = FirebaseAuth.instance; static final FirebaseFirestore _firestore = FirebaseFirestore.instance; static final FirebaseStorage _storage = FirebaseStorage.instanceFor( bucket: 'teachit-app.firebasestorage.app', ); /// Upload and process content file static Future uploadContent({ required File file, required String title, required String subject, required String concept, required double difficulty, required int grade, Map? additionalMetadata, }) async { try { final user = _auth.currentUser; if (user == null) throw Exception('User not authenticated'); Logger.info('Starting content upload: $title'); // 1. Upload file to Firebase Storage final fileName = '${DateTime.now().millisecondsSinceEpoch}_${file.path.split('/').last}'; final storageRef = _storage.ref().child('content/${user.uid}/$fileName'); final uploadTask = await storageRef.putFile(file); final downloadUrl = await uploadTask.ref.getDownloadURL(); Logger.info('File uploaded to storage: $downloadUrl'); // 2. Create content document in Firestore final contentDoc = { 'title': title, 'subject': subject, 'concept': concept, 'difficulty': difficulty, 'grade': grade, 'teacherId': user.uid, 'teacherEmail': user.email, 'fileName': fileName, 'downloadUrl': downloadUrl, 'fileSize': await file.length(), 'uploadedAt': FieldValue.serverTimestamp(), 'status': 'processing', 'metadata': additionalMetadata ?? {}, 'chunkCount': 0, 'totalChunks': 0, }; final docRef = await _firestore.collection('content').add(contentDoc); final contentId = docRef.id; Logger.info('Content document created: $contentId'); // 3. Trigger content processing (this would be handled by a Cloud Function) await _triggerContentProcessing(contentId, file.path, downloadUrl); return contentId; } catch (e) { Logger.error('Error uploading content: $e'); throw Exception('Failed to upload content: $e'); } } /// Get content list for a teacher static Future>> getTeacherContent({ int limit = 20, DocumentSnapshot? lastDocument, }) async { try { final user = _auth.currentUser; if (user == null) throw Exception('User not authenticated'); Query query = _firestore .collection('content') .where('teacherId', isEqualTo: user.uid) .orderBy('uploadedAt', descending: true) .limit(limit); if (lastDocument != null) { query = query.startAfterDocument(lastDocument); } final querySnapshot = await query.get(); return querySnapshot.docs .map((doc) => {'id': doc.id, ...doc.data() as Map}) .toList(); } catch (e) { Logger.error('Error getting teacher content: $e'); throw Exception('Failed to get content: $e'); } } /// Get content details static Future?> getContentDetails( String contentId, ) async { try { final doc = await _firestore.collection('content').doc(contentId).get(); if (!doc.exists) return null; return {'id': doc.id, ...doc.data() as Map}; } catch (e) { Logger.error('Error getting content details: $e'); throw Exception('Failed to get content details: $e'); } } /// Get available content for students (filtered by school/grade) static Future>> getAvailableContent({ String? subject, String? concept, double? minDifficulty, double? maxDifficulty, int? grade, int limit = 50, }) async { try { final user = _auth.currentUser; if (user == null) throw Exception('User not authenticated'); // Get user's grade from their profile final userDoc = await _firestore.collection('users').doc(user.uid).get(); final userData = userDoc.data(); final userGrade = userData?['profile']?['gradeLevel'] ?? grade; Query query = _firestore .collection('content') .where('status', isEqualTo: 'processed') .where('grade', isEqualTo: userGrade) .orderBy('uploadedAt', descending: true) .limit(limit); // Apply filters if (subject != null) { query = query.where('subject', isEqualTo: subject); } if (concept != null) { query = query.where('concept', isEqualTo: concept); } if (minDifficulty != null) { query = query.where( 'difficulty', isGreaterThanOrEqualTo: minDifficulty, ); } if (maxDifficulty != null) { query = query.where('difficulty', isLessThanOrEqualTo: maxDifficulty); } final querySnapshot = await query.get(); return querySnapshot.docs .map((doc) => {'id': doc.id, ...doc.data() as Map}) .toList(); } catch (e) { Logger.error('Error getting available content: $e'); throw Exception('Failed to get available content: $e'); } } /// Delete content static Future deleteContent(String contentId) async { try { final user = _auth.currentUser; if (user == null) throw Exception('User not authenticated'); // Get content details final content = await getContentDetails(contentId); if (content == null) throw Exception('Content not found'); // Check ownership if (content['teacherId'] != user.uid) { throw Exception('Permission denied'); } // Delete from Firestore await _firestore.collection('content').doc(contentId).delete(); // Delete from Storage if (content['fileName'] != null) { final storageRef = _storage.ref().child( 'content/${user.uid}/${content['fileName']}', ); await storageRef.delete(); } // Delete associated chunks final chunksSnapshot = await _firestore .collection('contentChunks') .where('contentId', isEqualTo: contentId) .get(); final batch = _firestore.batch(); for (final doc in chunksSnapshot.docs) { batch.delete(doc.reference); } await batch.commit(); Logger.info('Content deleted successfully: $contentId'); } catch (e) { Logger.error('Error deleting content: $e'); throw Exception('Failed to delete content: $e'); } } /// Update content metadata static Future updateContentMetadata( String contentId, Map metadata, ) async { try { final user = _auth.currentUser; if (user == null) throw Exception('User not authenticated'); // Check ownership final content = await getContentDetails(contentId); if (content == null) throw Exception('Content not found'); if (content['teacherId'] != user.uid) { throw Exception('Permission denied'); } await _firestore.collection('content').doc(contentId).update({ 'metadata': metadata, 'updatedAt': FieldValue.serverTimestamp(), }); Logger.info('Content metadata updated: $contentId'); } catch (e) { Logger.error('Error updating content metadata: $e'); throw Exception('Failed to update content metadata: $e'); } } /// Get content statistics for a teacher static Future> getTeacherStats() async { try { final user = _auth.currentUser; if (user == null) throw Exception('User not authenticated'); final contentSnapshot = await _firestore .collection('content') .where('teacherId', isEqualTo: user.uid) .get(); final totalContent = contentSnapshot.docs.length; final processedContent = contentSnapshot.docs .where((doc) => doc['status'] == 'processed') .length; final totalChunks = contentSnapshot.docs.fold( 0, (sum, doc) => sum + (doc['chunkCount'] as int? ?? 0), ); final subjects = {}; for (final doc in contentSnapshot.docs) { final subject = doc['subject'] as String? ?? 'Unknown'; subjects[subject] = (subjects[subject] ?? 0) + 1; } return { 'totalContent': totalContent, 'processedContent': processedContent, 'totalChunks': totalChunks, 'subjects': subjects, 'processingContent': totalContent - processedContent, }; } catch (e) { Logger.error('Error getting teacher stats: $e'); throw Exception('Failed to get teacher stats: $e'); } } /// Trigger content processing (would be handled by Cloud Function) static Future _triggerContentProcessing( String contentId, String filePath, String downloadUrl, ) async { try { // This would typically trigger a Cloud Function // For now, we'll update the status to indicate processing should start await _firestore.collection('content').doc(contentId).update({ 'status': 'processing_started', 'processingStartedAt': FieldValue.serverTimestamp(), }); Logger.info('Content processing triggered: $contentId'); } catch (e) { Logger.error('Error triggering content processing: $e'); throw Exception('Failed to trigger content processing: $e'); } } /// Search content by text static Future>> searchContent({ required String searchQuery, String? subject, int? grade, int limit = 20, }) async { try { final user = _auth.currentUser; if (user == null) throw Exception('User not authenticated'); // Get user's grade if not specified final userDoc = await _firestore.collection('users').doc(user.uid).get(); final userData = userDoc.data(); final userGrade = grade ?? userData?['profile']?['gradeLevel']; // For now, we'll do a simple text search on title and concept // In a full implementation, this would use the vector search Query query = _firestore .collection('content') .where('status', isEqualTo: 'processed') .where('grade', isEqualTo: userGrade) .orderBy('uploadedAt', descending: true) .limit(limit); if (subject != null) { query = query.where('subject', isEqualTo: subject); } final querySnapshot = await query.get(); final searchQueryLower = searchQuery.toLowerCase(); final results = querySnapshot.docs .where((doc) { final data = doc.data() as Map; final title = (data['title'] as String? ?? '').toLowerCase(); final concept = (data['concept'] as String? ?? '').toLowerCase(); return title.contains(searchQueryLower) || concept.contains(searchQueryLower); }) .map((doc) => {'id': doc.id, ...doc.data() as Map}) .toList(); return results; } catch (e) { Logger.error('Error searching content: $e'); throw Exception('Failed to search content: $e'); } } }