package handlers import ( "encoding/json" "fmt" "strings" "github.com/breakpilot/ai-compliance-sdk/internal/iace" "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" ) // ============================================================================ // Handler Struct & Constructor // ============================================================================ // IACEHandler handles HTTP requests for the IACE module (Inherent-risk Adjusted // Control Effectiveness). It provides endpoints for project management, component // onboarding, regulatory classification, hazard/risk analysis, evidence management, // CE technical file generation, and post-market monitoring. type IACEHandler struct { store *iace.Store engine *iace.RiskEngine classifier *iace.Classifier checker *iace.CompletenessChecker ragClient *ucca.LegalRAGClient techFileGen *iace.TechFileGenerator exporter *iace.DocumentExporter } // NewIACEHandler creates a new IACEHandler with all required dependencies. func NewIACEHandler(store *iace.Store, providerRegistry *llm.ProviderRegistry) *IACEHandler { ragClient := ucca.NewLegalRAGClient() return &IACEHandler{ store: store, engine: iace.NewRiskEngine(), classifier: iace.NewClassifier(), checker: iace.NewCompletenessChecker(), ragClient: ragClient, techFileGen: iace.NewTechFileGenerator(providerRegistry, ragClient, store), exporter: iace.NewDocumentExporter(), } } // ============================================================================ // Helper: Tenant ID extraction // ============================================================================ // getTenantID extracts the tenant UUID from the X-Tenant-Id header. // It first checks the rbac middleware context; if not present, falls back to the // raw header value. func getTenantID(c *gin.Context) (uuid.UUID, error) { // Prefer value set by RBAC middleware tid := rbac.GetTenantID(c) if tid != uuid.Nil { return tid, nil } tenantStr := c.GetHeader("X-Tenant-Id") if tenantStr == "" { return uuid.Nil, fmt.Errorf("X-Tenant-Id header required") } return uuid.Parse(tenantStr) } // ============================================================================ // Internal Helpers // ============================================================================ // buildCompletenessContext constructs the CompletenessContext needed by the checker // by loading all related entities for a project. func (h *IACEHandler) buildCompletenessContext( c *gin.Context, project *iace.Project, components []iace.Component, classifications []iace.RegulatoryClassification, ) *iace.CompletenessContext { projectID := project.ID hazards, _ := h.store.ListHazards(c.Request.Context(), projectID) var allAssessments []iace.RiskAssessment var allMitigations []iace.Mitigation for _, hazard := range hazards { assessments, _ := h.store.ListAssessments(c.Request.Context(), hazard.ID) allAssessments = append(allAssessments, assessments...) mitigations, _ := h.store.ListMitigations(c.Request.Context(), hazard.ID) allMitigations = append(allMitigations, mitigations...) } evidence, _ := h.store.ListEvidence(c.Request.Context(), projectID) techFileSections, _ := h.store.ListTechFileSections(c.Request.Context(), projectID) hasAI := false for _, comp := range components { if comp.ComponentType == iace.ComponentTypeAIModel { hasAI = true break } } return &iace.CompletenessContext{ Project: project, Components: components, Classifications: classifications, Hazards: hazards, Assessments: allAssessments, Mitigations: allMitigations, Evidence: evidence, TechFileSections: techFileSections, HasAI: hasAI, } } // containsString checks if a string slice contains the given value. func containsString(slice []string, val string) bool { for _, s := range slice { if s == val { return true } } return false } // componentTypeKeys extracts keys from a map[string]bool and returns them as a sorted slice. func componentTypeKeys(m map[string]bool) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } // Sort for deterministic output sortStrings(keys) return keys } // sortStrings sorts a slice of strings in place using a simple insertion sort. func sortStrings(s []string) { for i := 1; i < len(s); i++ { for j := i; j > 0 && strings.Compare(s[j-1], s[j]) > 0; j-- { s[j-1], s[j] = s[j], s[j-1] } } } // mustMarshalJSON marshals the given value to json.RawMessage. func mustMarshalJSON(v interface{}) json.RawMessage { data, err := json.Marshal(v) if err != nil { return nil } return data }