package handlers import ( "net/http" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" "github.com/breakpilot/ai-compliance-sdk/internal/llm" "github.com/breakpilot/ai-compliance-sdk/internal/usecase" ) // UseCaseHandler handles use-case compiler endpoints. type UseCaseHandler struct { store *usecase.Store compiler *usecase.Compiler gapDetector *usecase.GapDetector } // NewUseCaseHandler creates a new UseCaseHandler. func NewUseCaseHandler(pool *pgxpool.Pool, registry *llm.ProviderRegistry) *UseCaseHandler { store := usecase.NewStore(pool) llmGen := usecase.NewLLMQuestionGenerator(registry) return &UseCaseHandler{ store: store, compiler: usecase.NewCompiler(store, llmGen), gapDetector: usecase.NewGapDetector(store), } } // GetTemplates returns all available use-case templates. // GET /sdk/v1/use-case/templates func (h *UseCaseHandler) GetTemplates(c *gin.Context) { templates := usecase.TemplateList() c.JSON(http.StatusOK, gin.H{"templates": templates, "total": len(templates)}) } // GetTemplate returns a specific template with compiled questions. // GET /sdk/v1/use-case/templates/:id func (h *UseCaseHandler) GetTemplate(c *gin.Context) { id := c.Param("id") tmpl, ok := usecase.Templates[id] if !ok { c.JSON(http.StatusNotFound, gin.H{"error": "template not found"}) return } questions, err := h.compiler.Compile(&tmpl) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } tmpl.Questions = questions c.JSON(http.StatusOK, gin.H{"template": tmpl}) } // Compile generates questions from MC filters ad-hoc. // POST /sdk/v1/use-case/compile // Uses the full pipeline: doc_check → LLM → deterministic fallback func (h *UseCaseHandler) Compile(c *gin.Context) { var req struct { MCFilters []string `json:"mc_filters" binding:"required"` Regulations []string `json:"regulations"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } tmpl := &usecase.Template{ ID: "custom", MCFilters: req.MCFilters, Regulations: req.Regulations, } questions, err := h.compiler.Compile(tmpl) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"questions": questions, "total": len(questions)}) } // CreateAudit starts a new audit from a template. // POST /sdk/v1/use-case/audits func (h *UseCaseHandler) CreateAudit(c *gin.Context) { var input usecase.CreateAuditInput if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } tenantID, err := uuid.Parse(c.GetHeader("X-Tenant-ID")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "X-Tenant-ID required"}) return } tmpl, ok := usecase.Templates[input.TemplateID] if !ok { c.JSON(http.StatusBadRequest, gin.H{"error": "unknown template_id"}) return } questions, err := h.compiler.Compile(&tmpl) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } audit := &usecase.Audit{ TenantID: tenantID, TemplateID: input.TemplateID, Name: input.Name, TargetName: input.TargetName, TotalQuestions: len(questions), Questions: questions, } if err := h.store.CreateAudit(audit); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create audit"}) return } c.JSON(http.StatusCreated, gin.H{"audit": audit}) } // ListAudits returns all audits for a tenant. // GET /sdk/v1/use-case/audits func (h *UseCaseHandler) ListAudits(c *gin.Context) { tenantID, err := uuid.Parse(c.GetHeader("X-Tenant-ID")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "X-Tenant-ID required"}) return } audits, err := h.store.ListAudits(tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list audits"}) return } c.JSON(http.StatusOK, gin.H{"audits": audits, "total": len(audits)}) } // GetAudit returns an audit with questions and answers. // GET /sdk/v1/use-case/audits/:id func (h *UseCaseHandler) GetAudit(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid audit ID"}) return } audit, err := h.store.GetAudit(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "audit not found"}) return } answers, err := h.store.ListAnswers(id) if err != nil { answers = nil } c.JSON(http.StatusOK, gin.H{"audit": audit, "answers": answers}) } // AnswerQuestion saves an answer for a question in an audit. // POST /sdk/v1/use-case/audits/:id/answer func (h *UseCaseHandler) AnswerQuestion(c *gin.Context) { auditID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid audit ID"}) return } var input usecase.AnswerInput if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Find MC ID from the question audit, err := h.store.GetAudit(auditID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "audit not found"}) return } var mcID string for _, q := range audit.Questions { if q.ID == input.QuestionID { mcID = q.MCID break } } status := usecase.AnswerStatusAnswered if input.Status == "skipped" { status = usecase.AnswerStatusSkipped } else if input.Status == "escalated" { status = usecase.AnswerStatusEscalated } answer := &usecase.Answer{ AuditID: auditID, QuestionID: input.QuestionID, MCID: mcID, Value: input.Value, Comment: input.Comment, EvidenceIDs: input.EvidenceIDs, Status: status, } if err := h.store.SaveAnswer(answer); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save answer"}) return } // Update audit counters answers, _ := h.store.ListAnswers(auditID) score := usecase.Score(audit, answers) auditStatus := usecase.StatusInProgress if score.Answered >= audit.TotalQuestions { auditStatus = usecase.StatusCompleted } h.store.UpdateAuditScore(auditID, score.Answered, score.ComplianceScore, auditStatus) c.JSON(http.StatusOK, gin.H{"answer": answer, "progress": score}) } // GetScore calculates and returns the compliance score. // GET /sdk/v1/use-case/audits/:id/score func (h *UseCaseHandler) GetScore(c *gin.Context) { auditID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid audit ID"}) return } audit, err := h.store.GetAudit(auditID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "audit not found"}) return } answers, err := h.store.ListAnswers(auditID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load answers"}) return } score := usecase.Score(audit, answers) c.JSON(http.StatusOK, score) } // GetGaps returns missing regulation sources for an audit. // GET /sdk/v1/use-case/audits/:id/gaps func (h *UseCaseHandler) GetGaps(c *gin.Context) { auditID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid audit ID"}) return } audit, err := h.store.GetAudit(auditID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "audit not found"}) return } tmpl, ok := usecase.Templates[audit.TemplateID] if !ok { c.JSON(http.StatusOK, gin.H{"gaps": []interface{}{}, "audit_gaps": []interface{}{}}) return } // Missing regulation sources (from MC analysis) missingRegs, err := h.gapDetector.DetectMissingRegulations(&tmpl) if err != nil { missingRegs = nil } // Audit-specific gaps (from answer analysis) answers, _ := h.store.ListAnswers(auditID) auditGaps := h.gapDetector.DetectAuditGaps(audit, answers) c.JSON(http.StatusOK, gin.H{ "missing_sources": missingRegs, "audit_gaps": auditGaps, }) }