package handlers import ( "net/http" "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" ) // ============================================================================ // Training Block Endpoints (Controls → Schulungsmodule) // ============================================================================ // ListBlockConfigs returns all block configs for the tenant // GET /sdk/v1/training/blocks func (h *TrainingHandlers) ListBlockConfigs(c *gin.Context) { tenantID := rbac.GetTenantID(c) configs, err := h.store.ListBlockConfigs(c.Request.Context(), tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "blocks": configs, "total": len(configs), }) } // CreateBlockConfig creates a new block configuration // POST /sdk/v1/training/blocks func (h *TrainingHandlers) CreateBlockConfig(c *gin.Context) { tenantID := rbac.GetTenantID(c) var req training.CreateBlockConfigRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } config := &training.TrainingBlockConfig{ TenantID: tenantID, Name: req.Name, Description: req.Description, DomainFilter: req.DomainFilter, CategoryFilter: req.CategoryFilter, SeverityFilter: req.SeverityFilter, TargetAudienceFilter: req.TargetAudienceFilter, RegulationArea: req.RegulationArea, ModuleCodePrefix: req.ModuleCodePrefix, FrequencyType: req.FrequencyType, DurationMinutes: req.DurationMinutes, PassThreshold: req.PassThreshold, MaxControlsPerModule: req.MaxControlsPerModule, } if config.FrequencyType == "" { config.FrequencyType = training.FrequencyAnnual } if config.DurationMinutes == 0 { config.DurationMinutes = 45 } if config.PassThreshold == 0 { config.PassThreshold = 70 } if config.MaxControlsPerModule == 0 { config.MaxControlsPerModule = 20 } if err := h.store.CreateBlockConfig(c.Request.Context(), config); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, config) } // GetBlockConfig returns a single block config // GET /sdk/v1/training/blocks/:id func (h *TrainingHandlers) GetBlockConfig(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"}) return } config, err := h.store.GetBlockConfig(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if config == nil { c.JSON(http.StatusNotFound, gin.H{"error": "block config not found"}) return } c.JSON(http.StatusOK, config) } // UpdateBlockConfig updates a block config // PUT /sdk/v1/training/blocks/:id func (h *TrainingHandlers) UpdateBlockConfig(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"}) return } config, err := h.store.GetBlockConfig(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if config == nil { c.JSON(http.StatusNotFound, gin.H{"error": "block config not found"}) return } var req training.UpdateBlockConfigRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if req.Name != nil { config.Name = *req.Name } if req.Description != nil { config.Description = *req.Description } if req.DomainFilter != nil { config.DomainFilter = *req.DomainFilter } if req.CategoryFilter != nil { config.CategoryFilter = *req.CategoryFilter } if req.SeverityFilter != nil { config.SeverityFilter = *req.SeverityFilter } if req.TargetAudienceFilter != nil { config.TargetAudienceFilter = *req.TargetAudienceFilter } if req.MaxControlsPerModule != nil { config.MaxControlsPerModule = *req.MaxControlsPerModule } if req.DurationMinutes != nil { config.DurationMinutes = *req.DurationMinutes } if req.PassThreshold != nil { config.PassThreshold = *req.PassThreshold } if req.IsActive != nil { config.IsActive = *req.IsActive } if err := h.store.UpdateBlockConfig(c.Request.Context(), config); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, config) } // DeleteBlockConfig deletes a block config // DELETE /sdk/v1/training/blocks/:id func (h *TrainingHandlers) DeleteBlockConfig(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"}) return } if err := h.store.DeleteBlockConfig(c.Request.Context(), id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"status": "deleted"}) } // PreviewBlock performs a dry run showing matching controls and proposed roles // POST /sdk/v1/training/blocks/:id/preview func (h *TrainingHandlers) PreviewBlock(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"}) return } preview, err := h.blockGenerator.Preview(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, preview) } // GenerateBlock runs the full generation pipeline // POST /sdk/v1/training/blocks/:id/generate func (h *TrainingHandlers) GenerateBlock(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"}) return } var req training.GenerateBlockRequest if err := c.ShouldBindJSON(&req); err != nil { // Defaults are fine req.Language = "de" req.AutoMatrix = true } result, err := h.blockGenerator.Generate(c.Request.Context(), id, req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } // GetBlockControls returns control links for a block config // GET /sdk/v1/training/blocks/:id/controls func (h *TrainingHandlers) GetBlockControls(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid block config ID"}) return } links, err := h.store.GetControlLinksForBlock(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "controls": links, "total": len(links), }) } // ListCanonicalControls returns filtered canonical controls for browsing // GET /sdk/v1/training/canonical/controls func (h *TrainingHandlers) ListCanonicalControls(c *gin.Context) { domain := c.Query("domain") category := c.Query("category") severity := c.Query("severity") targetAudience := c.Query("target_audience") controls, err := h.store.QueryCanonicalControls(c.Request.Context(), domain, category, severity, targetAudience, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "controls": controls, "total": len(controls), }) } // GetCanonicalMeta returns aggregated metadata about canonical controls // GET /sdk/v1/training/canonical/meta func (h *TrainingHandlers) GetCanonicalMeta(c *gin.Context) { meta, err := h.store.GetCanonicalControlMeta(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, meta) }