package services import ( "context" "time" "github.com/breakpilot/school-service/internal/models" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) // ExamService handles exam-related operations type ExamService struct { db *pgxpool.Pool } // NewExamService creates a new ExamService func NewExamService(db *pgxpool.Pool) *ExamService { return &ExamService{db: db} } // CreateExam creates a new exam func (s *ExamService) CreateExam(ctx context.Context, teacherID string, req *models.CreateExamRequest) (*models.Exam, error) { var classID, subjectID *uuid.UUID if req.ClassID != "" { id, _ := uuid.Parse(req.ClassID) classID = &id } if req.SubjectID != "" { id, _ := uuid.Parse(req.SubjectID) subjectID = &id } var examDate *time.Time if req.ExamDate != "" { t, _ := time.Parse("2006-01-02", req.ExamDate) examDate = &t } var durationMinutes *int if req.DurationMinutes > 0 { durationMinutes = &req.DurationMinutes } var maxPoints *float64 if req.MaxPoints > 0 { maxPoints = &req.MaxPoints } var exam models.Exam err := s.db.QueryRow(ctx, ` INSERT INTO exams (teacher_id, class_id, subject_id, title, exam_type, topic, content, difficulty_level, duration_minutes, max_points, exam_date) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, teacher_id, class_id, subject_id, title, exam_type, topic, content, source_file_path, difficulty_level, duration_minutes, max_points, is_template, parent_exam_id, status, exam_date, created_at, updated_at `, teacherID, classID, subjectID, req.Title, req.ExamType, req.Topic, req.Content, req.DifficultyLevel, durationMinutes, maxPoints, examDate).Scan( &exam.ID, &exam.TeacherID, &exam.ClassID, &exam.SubjectID, &exam.Title, &exam.ExamType, &exam.Topic, &exam.Content, &exam.SourceFilePath, &exam.DifficultyLevel, &exam.DurationMinutes, &exam.MaxPoints, &exam.IsTemplate, &exam.ParentExamID, &exam.Status, &exam.ExamDate, &exam.CreatedAt, &exam.UpdatedAt, ) return &exam, err } // GetExams returns all exams for a teacher func (s *ExamService) GetExams(ctx context.Context, teacherID string) ([]models.Exam, error) { rows, err := s.db.Query(ctx, ` SELECT e.id, e.teacher_id, e.class_id, e.subject_id, e.title, e.exam_type, e.topic, e.content, e.source_file_path, e.difficulty_level, e.duration_minutes, e.max_points, e.is_template, e.parent_exam_id, e.status, e.exam_date, e.created_at, e.updated_at, COALESCE(c.name, '') as class_name, COALESCE(sub.name, '') as subject_name FROM exams e LEFT JOIN classes c ON e.class_id = c.id LEFT JOIN subjects sub ON e.subject_id = sub.id WHERE e.teacher_id = $1 ORDER BY e.created_at DESC `, teacherID) if err != nil { return nil, err } defer rows.Close() var exams []models.Exam for rows.Next() { var e models.Exam if err := rows.Scan(&e.ID, &e.TeacherID, &e.ClassID, &e.SubjectID, &e.Title, &e.ExamType, &e.Topic, &e.Content, &e.SourceFilePath, &e.DifficultyLevel, &e.DurationMinutes, &e.MaxPoints, &e.IsTemplate, &e.ParentExamID, &e.Status, &e.ExamDate, &e.CreatedAt, &e.UpdatedAt, &e.ClassName, &e.SubjectName); err != nil { return nil, err } exams = append(exams, e) } return exams, nil } // GetExam returns a single exam func (s *ExamService) GetExam(ctx context.Context, examID, teacherID string) (*models.Exam, error) { var exam models.Exam err := s.db.QueryRow(ctx, ` SELECT e.id, e.teacher_id, e.class_id, e.subject_id, e.title, e.exam_type, e.topic, e.content, e.source_file_path, e.difficulty_level, e.duration_minutes, e.max_points, e.is_template, e.parent_exam_id, e.status, e.exam_date, e.created_at, e.updated_at, COALESCE(c.name, '') as class_name, COALESCE(sub.name, '') as subject_name FROM exams e LEFT JOIN classes c ON e.class_id = c.id LEFT JOIN subjects sub ON e.subject_id = sub.id WHERE e.id = $1 AND e.teacher_id = $2 `, examID, teacherID).Scan( &exam.ID, &exam.TeacherID, &exam.ClassID, &exam.SubjectID, &exam.Title, &exam.ExamType, &exam.Topic, &exam.Content, &exam.SourceFilePath, &exam.DifficultyLevel, &exam.DurationMinutes, &exam.MaxPoints, &exam.IsTemplate, &exam.ParentExamID, &exam.Status, &exam.ExamDate, &exam.CreatedAt, &exam.UpdatedAt, &exam.ClassName, &exam.SubjectName, ) return &exam, err } // UpdateExam updates an exam func (s *ExamService) UpdateExam(ctx context.Context, examID, teacherID string, req *models.CreateExamRequest) (*models.Exam, error) { var classID, subjectID *uuid.UUID if req.ClassID != "" { id, _ := uuid.Parse(req.ClassID) classID = &id } if req.SubjectID != "" { id, _ := uuid.Parse(req.SubjectID) subjectID = &id } var examDate *time.Time if req.ExamDate != "" { t, _ := time.Parse("2006-01-02", req.ExamDate) examDate = &t } var exam models.Exam err := s.db.QueryRow(ctx, ` UPDATE exams SET class_id = $3, subject_id = $4, title = $5, exam_type = $6, topic = $7, content = $8, difficulty_level = $9, duration_minutes = $10, max_points = $11, exam_date = $12, updated_at = NOW() WHERE id = $1 AND teacher_id = $2 RETURNING id, teacher_id, class_id, subject_id, title, exam_type, topic, content, source_file_path, difficulty_level, duration_minutes, max_points, is_template, parent_exam_id, status, exam_date, created_at, updated_at `, examID, teacherID, classID, subjectID, req.Title, req.ExamType, req.Topic, req.Content, req.DifficultyLevel, req.DurationMinutes, req.MaxPoints, examDate).Scan( &exam.ID, &exam.TeacherID, &exam.ClassID, &exam.SubjectID, &exam.Title, &exam.ExamType, &exam.Topic, &exam.Content, &exam.SourceFilePath, &exam.DifficultyLevel, &exam.DurationMinutes, &exam.MaxPoints, &exam.IsTemplate, &exam.ParentExamID, &exam.Status, &exam.ExamDate, &exam.CreatedAt, &exam.UpdatedAt, ) return &exam, err } // DeleteExam deletes an exam func (s *ExamService) DeleteExam(ctx context.Context, examID, teacherID string) error { _, err := s.db.Exec(ctx, `DELETE FROM exams WHERE id = $1 AND teacher_id = $2`, examID, teacherID) return err } // CreateExamVariant creates a variant of an existing exam (for Nachschreiber) func (s *ExamService) CreateExamVariant(ctx context.Context, parentExamID, teacherID string, newContent string, variationType string) (*models.Exam, error) { parentID, _ := uuid.Parse(parentExamID) // Get parent exam parent, err := s.GetExam(ctx, parentExamID, teacherID) if err != nil { return nil, err } title := parent.Title + " (Nachschreiber)" if variationType == "alternative" { title = parent.Title + " (Alternativ)" } var exam models.Exam err = s.db.QueryRow(ctx, ` INSERT INTO exams (teacher_id, class_id, subject_id, title, exam_type, topic, content, difficulty_level, duration_minutes, max_points, is_template, parent_exam_id, status) SELECT teacher_id, class_id, subject_id, $3, exam_type, topic, $4, difficulty_level, duration_minutes, max_points, false, $2, 'draft' FROM exams WHERE id = $2 AND teacher_id = $1 RETURNING id, teacher_id, class_id, subject_id, title, exam_type, topic, content, source_file_path, difficulty_level, duration_minutes, max_points, is_template, parent_exam_id, status, exam_date, created_at, updated_at `, teacherID, parentID, title, newContent).Scan( &exam.ID, &exam.TeacherID, &exam.ClassID, &exam.SubjectID, &exam.Title, &exam.ExamType, &exam.Topic, &exam.Content, &exam.SourceFilePath, &exam.DifficultyLevel, &exam.DurationMinutes, &exam.MaxPoints, &exam.IsTemplate, &exam.ParentExamID, &exam.Status, &exam.ExamDate, &exam.CreatedAt, &exam.UpdatedAt, ) return &exam, err } // SaveExamResult saves or updates a student's exam result func (s *ExamService) SaveExamResult(ctx context.Context, examID string, req *models.UpdateExamResultRequest) (*models.ExamResult, error) { var result models.ExamResult err := s.db.QueryRow(ctx, ` INSERT INTO exam_results (exam_id, student_id, points_achieved, grade, notes, is_absent, needs_rewrite) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (exam_id, student_id) DO UPDATE SET points_achieved = EXCLUDED.points_achieved, grade = EXCLUDED.grade, notes = EXCLUDED.notes, is_absent = EXCLUDED.is_absent, needs_rewrite = EXCLUDED.needs_rewrite, updated_at = NOW() RETURNING id, exam_id, student_id, points_achieved, grade, percentage, notes, is_absent, needs_rewrite, approved_by_teacher, approved_at, created_at, updated_at `, examID, req.StudentID, req.PointsAchieved, req.Grade, req.Notes, req.IsAbsent, req.NeedsRewrite).Scan( &result.ID, &result.ExamID, &result.StudentID, &result.PointsAchieved, &result.Grade, &result.Percentage, &result.Notes, &result.IsAbsent, &result.NeedsRewrite, &result.ApprovedByTeacher, &result.ApprovedAt, &result.CreatedAt, &result.UpdatedAt, ) return &result, err } // GetExamResults returns all results for an exam func (s *ExamService) GetExamResults(ctx context.Context, examID string) ([]models.ExamResult, error) { rows, err := s.db.Query(ctx, ` SELECT er.id, er.exam_id, er.student_id, er.points_achieved, er.grade, er.percentage, er.notes, er.is_absent, er.needs_rewrite, er.approved_by_teacher, er.approved_at, er.created_at, er.updated_at, CONCAT(s.first_name, ' ', s.last_name) as student_name FROM exam_results er JOIN students s ON er.student_id = s.id WHERE er.exam_id = $1 ORDER BY s.last_name, s.first_name `, examID) if err != nil { return nil, err } defer rows.Close() var results []models.ExamResult for rows.Next() { var r models.ExamResult if err := rows.Scan(&r.ID, &r.ExamID, &r.StudentID, &r.PointsAchieved, &r.Grade, &r.Percentage, &r.Notes, &r.IsAbsent, &r.NeedsRewrite, &r.ApprovedByTeacher, &r.ApprovedAt, &r.CreatedAt, &r.UpdatedAt, &r.StudentName); err != nil { return nil, err } results = append(results, r) } return results, nil } // ApproveExamResult approves a result for transfer to grade overview func (s *ExamService) ApproveExamResult(ctx context.Context, examID, studentID string) error { _, err := s.db.Exec(ctx, ` UPDATE exam_results SET approved_by_teacher = true, approved_at = NOW(), updated_at = NOW() WHERE exam_id = $1 AND student_id = $2 `, examID, studentID) return err } // GetStudentsNeedingRewrite returns students who need to rewrite an exam func (s *ExamService) GetStudentsNeedingRewrite(ctx context.Context, examID string) ([]models.Student, error) { rows, err := s.db.Query(ctx, ` SELECT s.id, s.class_id, s.first_name, s.last_name, s.date_of_birth, s.student_number, s.created_at FROM students s JOIN exam_results er ON s.id = er.student_id WHERE er.exam_id = $1 AND (er.needs_rewrite = true OR er.is_absent = true) ORDER BY s.last_name, s.first_name `, examID) if err != nil { return nil, err } defer rows.Close() var students []models.Student for rows.Next() { var st models.Student if err := rows.Scan(&st.ID, &st.ClassID, &st.FirstName, &st.LastName, &st.BirthDate, &st.StudentNumber, &st.CreatedAt); err != nil { return nil, err } students = append(students, st) } return students, nil }