package handlers import ( "net/http" "strconv" "strings" "github.com/breakpilot/ai-compliance-sdk/internal/academy" "github.com/breakpilot/ai-compliance-sdk/internal/rbac" "github.com/breakpilot/ai-compliance-sdk/internal/training" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // AcademyHandlers handles academy HTTP requests type AcademyHandlers struct { store *academy.Store trainingStore *training.Store } // NewAcademyHandlers creates new academy handlers func NewAcademyHandlers(store *academy.Store, trainingStore *training.Store) *AcademyHandlers { return &AcademyHandlers{store: store, trainingStore: trainingStore} } // ============================================================================ // Course Management // ============================================================================ // CreateCourse creates a new compliance training course // POST /sdk/v1/academy/courses func (h *AcademyHandlers) CreateCourse(c *gin.Context) { var req academy.CreateCourseRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } tenantID := rbac.GetTenantID(c) course := &academy.Course{ TenantID: tenantID, Title: req.Title, Description: req.Description, Category: req.Category, DurationMinutes: req.DurationMinutes, RequiredForRoles: req.RequiredForRoles, IsActive: true, } if course.RequiredForRoles == nil { course.RequiredForRoles = []string{} } if err := h.store.CreateCourse(c.Request.Context(), course); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Create lessons if provided for i := range req.Lessons { lesson := &academy.Lesson{ CourseID: course.ID, Title: req.Lessons[i].Title, Description: req.Lessons[i].Description, LessonType: req.Lessons[i].LessonType, ContentURL: req.Lessons[i].ContentURL, DurationMinutes: req.Lessons[i].DurationMinutes, OrderIndex: req.Lessons[i].OrderIndex, QuizQuestions: req.Lessons[i].QuizQuestions, } if err := h.store.CreateLesson(c.Request.Context(), lesson); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } course.Lessons = append(course.Lessons, *lesson) } if course.Lessons == nil { course.Lessons = []academy.Lesson{} } c.JSON(http.StatusCreated, gin.H{"course": course}) } // GetCourse retrieves a course with its lessons // GET /sdk/v1/academy/courses/:id func (h *AcademyHandlers) GetCourse(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid course ID"}) return } course, err := h.store.GetCourse(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if course == nil { c.JSON(http.StatusNotFound, gin.H{"error": "course not found"}) return } c.JSON(http.StatusOK, gin.H{"course": course}) } // ListCourses lists courses for the current tenant // GET /sdk/v1/academy/courses func (h *AcademyHandlers) ListCourses(c *gin.Context) { tenantID := rbac.GetTenantID(c) filters := &academy.CourseFilters{ Limit: 50, } if category := c.Query("category"); category != "" { filters.Category = academy.CourseCategory(category) } if search := c.Query("search"); search != "" { filters.Search = search } if activeStr := c.Query("is_active"); activeStr != "" { active := activeStr == "true" filters.IsActive = &active } if limitStr := c.Query("limit"); limitStr != "" { if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 { filters.Limit = limit } } if offsetStr := c.Query("offset"); offsetStr != "" { if offset, err := strconv.Atoi(offsetStr); err == nil && offset >= 0 { filters.Offset = offset } } courses, total, err := h.store.ListCourses(c.Request.Context(), tenantID, filters) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, academy.CourseListResponse{ Courses: courses, Total: total, }) } // UpdateCourse updates a course // PUT /sdk/v1/academy/courses/:id func (h *AcademyHandlers) UpdateCourse(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid course ID"}) return } course, err := h.store.GetCourse(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if course == nil { c.JSON(http.StatusNotFound, gin.H{"error": "course not found"}) return } var req academy.UpdateCourseRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if req.Title != nil { course.Title = *req.Title } if req.Description != nil { course.Description = *req.Description } if req.Category != nil { course.Category = *req.Category } if req.DurationMinutes != nil { course.DurationMinutes = *req.DurationMinutes } if req.RequiredForRoles != nil { course.RequiredForRoles = req.RequiredForRoles } if req.IsActive != nil { course.IsActive = *req.IsActive } if err := h.store.UpdateCourse(c.Request.Context(), course); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"course": course}) } // DeleteCourse deletes a course // DELETE /sdk/v1/academy/courses/:id func (h *AcademyHandlers) DeleteCourse(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid course ID"}) return } if err := h.store.DeleteCourse(c.Request.Context(), id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "course deleted"}) } // estimateReadingTime estimates reading time in minutes from markdown content // Average reading speed: ~200 words per minute func estimateReadingTime(content string) int { words := len(strings.Fields(content)) minutes := words / 200 if minutes < 5 { minutes = 5 } return minutes }