package handlers import ( "crypto/sha256" "encoding/hex" "fmt" "net/http" "strconv" "time" "github.com/breakpilot/ai-compliance-sdk/internal/llm" "github.com/breakpilot/ai-compliance-sdk/internal/rbac" "github.com/breakpilot/ai-compliance-sdk/internal/ucca" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // UCCAHandlers handles UCCA-related API endpoints type UCCAHandlers struct { store *ucca.Store escalationStore *ucca.EscalationStore policyEngine *ucca.PolicyEngine legacyRuleEngine *ucca.RuleEngine // Keep for backwards compatibility providerRegistry *llm.ProviderRegistry legalRAGClient *ucca.LegalRAGClient escalationTrigger *ucca.EscalationTrigger } // NewUCCAHandlers creates new UCCA handlers func NewUCCAHandlers(store *ucca.Store, escalationStore *ucca.EscalationStore, providerRegistry *llm.ProviderRegistry) *UCCAHandlers { // Try to create YAML-based policy engine first policyEngine, err := ucca.NewPolicyEngine() if err != nil { // Log warning but don't fail - fall back to legacy engine fmt.Printf("Warning: Could not load YAML policy engine: %v. Falling back to legacy rules.\n", err) } return &UCCAHandlers{ store: store, escalationStore: escalationStore, policyEngine: policyEngine, // May be nil if YAML loading failed legacyRuleEngine: ucca.NewRuleEngine(), providerRegistry: providerRegistry, legalRAGClient: ucca.NewLegalRAGClient(), escalationTrigger: ucca.DefaultEscalationTrigger(), } } // evaluateIntake runs evaluation using YAML engine or legacy fallback func (h *UCCAHandlers) evaluateIntake(intake *ucca.UseCaseIntake) (*ucca.AssessmentResult, string) { if h.policyEngine != nil { return h.policyEngine.Evaluate(intake), h.policyEngine.GetPolicyVersion() } return h.legacyRuleEngine.Evaluate(intake), "1.0.0-legacy" } // Assess evaluates a use case intake and creates an assessment func (h *UCCAHandlers) Assess(c *gin.Context) { tenantID := rbac.GetTenantID(c) userID := rbac.GetUserID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } var intake ucca.UseCaseIntake if err := c.ShouldBindJSON(&intake); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } result, policyVersion := h.evaluateIntake(&intake) // Calculate hash of use case text hash := sha256.Sum256([]byte(intake.UseCaseText)) hashStr := hex.EncodeToString(hash[:]) assessment := &ucca.Assessment{ TenantID: tenantID, Title: intake.Title, PolicyVersion: policyVersion, Status: "completed", Intake: intake, UseCaseTextStored: intake.StoreRawText, UseCaseTextHash: hashStr, Feasibility: result.Feasibility, RiskLevel: result.RiskLevel, Complexity: result.Complexity, RiskScore: result.RiskScore, TriggeredRules: result.TriggeredRules, RequiredControls: result.RequiredControls, RecommendedArchitecture: result.RecommendedArchitecture, ForbiddenPatterns: result.ForbiddenPatterns, ExampleMatches: result.ExampleMatches, DSFARecommended: result.DSFARecommended, Art22Risk: result.Art22Risk, TrainingAllowed: result.TrainingAllowed, Domain: intake.Domain, CreatedBy: userID, } if !intake.StoreRawText { assessment.Intake.UseCaseText = "" } if assessment.Title == "" { assessment.Title = fmt.Sprintf("Assessment vom %s", time.Now().Format("02.01.2006 15:04")) } if err := h.store.CreateAssessment(c.Request.Context(), assessment); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } escalation := h.createEscalationForAssessment(c, assessment, result, tenantID, userID) c.JSON(http.StatusCreated, ucca.AssessResponse{ Assessment: *assessment, Result: *result, Escalation: escalation, }) } // ListAssessments returns all assessments for a tenant func (h *UCCAHandlers) ListAssessments(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } filters := &ucca.AssessmentFilters{ Feasibility: c.Query("feasibility"), Domain: c.Query("domain"), RiskLevel: c.Query("risk_level"), Search: c.Query("search"), } if limit, err := strconv.Atoi(c.DefaultQuery("limit", "0")); err == nil { filters.Limit = limit } if offset, err := strconv.Atoi(c.DefaultQuery("offset", "0")); err == nil { filters.Offset = offset } assessments, total, err := h.store.ListAssessments(c.Request.Context(), tenantID, filters) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"assessments": assessments, "total": total}) } // GetAssessment returns a single assessment by ID func (h *UCCAHandlers) GetAssessment(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) return } assessment, err := h.store.GetAssessment(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if assessment == nil { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } c.JSON(http.StatusOK, assessment) } // DeleteAssessment deletes an assessment func (h *UCCAHandlers) DeleteAssessment(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) return } if err := h.store.DeleteAssessment(c.Request.Context(), id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "deleted"}) } // UpdateAssessment re-evaluates and updates an existing assessment func (h *UCCAHandlers) UpdateAssessment(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) return } var intake ucca.UseCaseIntake if err := c.ShouldBindJSON(&intake); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } result, policyVersion := h.evaluateIntake(&intake) hash := sha256.Sum256([]byte(intake.UseCaseText)) hashStr := hex.EncodeToString(hash[:]) updated := &ucca.Assessment{ Title: intake.Title, PolicyVersion: policyVersion, Intake: intake, UseCaseTextStored: intake.StoreRawText, UseCaseTextHash: hashStr, Feasibility: result.Feasibility, RiskLevel: result.RiskLevel, Complexity: result.Complexity, RiskScore: result.RiskScore, TriggeredRules: result.TriggeredRules, RequiredControls: result.RequiredControls, RecommendedArchitecture: result.RecommendedArchitecture, ForbiddenPatterns: result.ForbiddenPatterns, ExampleMatches: result.ExampleMatches, DSFARecommended: result.DSFARecommended, Art22Risk: result.Art22Risk, TrainingAllowed: result.TrainingAllowed, Domain: intake.Domain, } if !intake.StoreRawText { updated.Intake.UseCaseText = "" } if updated.Title == "" { updated.Title = fmt.Sprintf("Assessment vom %s", time.Now().Format("02.01.2006 15:04")) } if err := h.store.UpdateAssessment(c.Request.Context(), id, updated); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } assessment, err := h.store.GetAssessment(c.Request.Context(), id) if err != nil || assessment == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "assessment not found after update"}) return } c.JSON(http.StatusOK, assessment) } // GetStats returns UCCA statistics for a tenant func (h *UCCAHandlers) GetStats(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } stats, err := h.store.GetStats(c.Request.Context(), tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, stats) } // createEscalationForAssessment automatically creates an escalation based on assessment result func (h *UCCAHandlers) createEscalationForAssessment(c *gin.Context, assessment *ucca.Assessment, result *ucca.AssessmentResult, tenantID, userID uuid.UUID) *ucca.Escalation { if h.escalationStore == nil || h.escalationTrigger == nil { return nil } level, reason := h.escalationTrigger.DetermineEscalationLevel(result) responseHours, _ := ucca.GetDefaultSLA(level) var dueDate *time.Time if responseHours > 0 { due := time.Now().UTC().Add(time.Duration(responseHours) * time.Hour) dueDate = &due } escalation := &ucca.Escalation{ TenantID: tenantID, AssessmentID: assessment.ID, EscalationLevel: level, EscalationReason: reason, Status: ucca.EscalationStatusPending, DueDate: dueDate, } if level == ucca.EscalationLevelE0 { escalation.Status = ucca.EscalationStatusApproved approveDecision := ucca.EscalationDecisionApprove escalation.Decision = &approveDecision now := time.Now().UTC() escalation.DecisionAt = &now autoNotes := "Automatische Freigabe (E0)" escalation.DecisionNotes = &autoNotes } if err := h.escalationStore.CreateEscalation(c.Request.Context(), escalation); err != nil { fmt.Printf("Warning: Could not create escalation: %v\n", err) return nil } h.escalationStore.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{ EscalationID: escalation.ID, Action: "auto_created", NewStatus: string(escalation.Status), NewLevel: string(escalation.EscalationLevel), ActorID: userID, Notes: "Automatisch erstellt bei Assessment", }) if level != ucca.EscalationLevelE0 { role := ucca.GetRoleForLevel(level) reviewer, err := h.escalationStore.GetNextAvailableReviewer(c.Request.Context(), tenantID, role) if err == nil && reviewer != nil { h.escalationStore.AssignEscalation(c.Request.Context(), escalation.ID, reviewer.UserID, role) h.escalationStore.IncrementReviewerCount(c.Request.Context(), reviewer.UserID) h.escalationStore.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{ EscalationID: escalation.ID, Action: "auto_assigned", OldStatus: string(ucca.EscalationStatusPending), NewStatus: string(ucca.EscalationStatusAssigned), ActorID: userID, Notes: "Automatisch zugewiesen an: " + reviewer.UserName, }) } } return escalation }