package multitenant import ( "context" "fmt" "log" "time" "github.com/breakpilot/ai-compliance-sdk/internal/rbac" "github.com/breakpilot/ai-compliance-sdk/internal/reporting" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) // Store provides aggregated multi-tenant views by combining data from the // existing RBAC store, reporting store, and direct SQL queries for module highlights. type Store struct { pool *pgxpool.Pool rbacStore *rbac.Store reportingStore *reporting.Store } // NewStore creates a new multi-tenant store. func NewStore(pool *pgxpool.Pool, rbacStore *rbac.Store, reportingStore *reporting.Store) *Store { return &Store{ pool: pool, rbacStore: rbacStore, reportingStore: reportingStore, } } // GetOverview retrieves all tenants with their compliance scores and module highlights. // It aggregates data from the RBAC tenant list, the reporting compliance score, // and direct SQL counts for namespaces, incidents, reports, DSRs, training, and vendors. // Individual query failures are tolerated and result in zero-value defaults. func (s *Store) GetOverview(ctx context.Context) (*MultiTenantOverviewResponse, error) { tenants, err := s.rbacStore.ListTenants(ctx) if err != nil { return nil, fmt.Errorf("failed to list tenants: %w", err) } overviews := make([]TenantOverview, 0, len(tenants)) totalScore := 0 for _, tenant := range tenants { overview := s.buildTenantOverview(ctx, tenant) totalScore += overview.ComplianceScore overviews = append(overviews, overview) } averageScore := 0 if len(overviews) > 0 { averageScore = totalScore / len(overviews) } return &MultiTenantOverviewResponse{ Tenants: overviews, Total: len(overviews), AverageScore: averageScore, GeneratedAt: time.Now().UTC(), }, nil } // GetTenantDetail returns detailed compliance info for a specific tenant. func (s *Store) GetTenantDetail(ctx context.Context, tenantID uuid.UUID) (*TenantOverview, error) { tenant, err := s.rbacStore.GetTenant(ctx, tenantID) if err != nil { return nil, fmt.Errorf("failed to get tenant: %w", err) } overview := s.buildTenantOverview(ctx, tenant) return &overview, nil } // buildTenantOverview constructs a TenantOverview by fetching compliance scores // and module highlights for a single tenant. Errors are logged but do not // propagate -- missing data defaults to zero values. func (s *Store) buildTenantOverview(ctx context.Context, tenant *rbac.Tenant) TenantOverview { overview := TenantOverview{ ID: tenant.ID, Name: tenant.Name, Slug: tenant.Slug, Status: string(tenant.Status), MaxUsers: tenant.MaxUsers, LLMQuotaMonthly: tenant.LLMQuotaMonthly, CreatedAt: tenant.CreatedAt, UpdatedAt: tenant.UpdatedAt, } // Compliance score and risk level derived from an executive report. // GenerateReport computes the compliance score and risk overview internally. report, err := s.reportingStore.GenerateReport(ctx, tenant.ID) if err != nil { log.Printf("multitenant: failed to generate report for tenant %s: %v", tenant.ID, err) } else { overview.ComplianceScore = report.ComplianceScore overview.RiskLevel = report.RiskOverview.OverallLevel } // Namespace count overview.NamespaceCount = s.countSafe(ctx, tenant.ID, "SELECT COUNT(*) FROM compliance_namespaces WHERE tenant_id = $1") // Open incidents overview.OpenIncidents = s.countSafe(ctx, tenant.ID, "SELECT COUNT(*) FROM incidents WHERE tenant_id = $1 AND status IN ('new', 'investigating', 'containment')") // Open whistleblower reports overview.OpenReports = s.countSafe(ctx, tenant.ID, "SELECT COUNT(*) FROM whistleblower_reports WHERE tenant_id = $1 AND status IN ('new', 'acknowledged', 'investigating')") // Pending DSR requests overview.PendingDSRs = s.countSafe(ctx, tenant.ID, "SELECT COUNT(*) FROM dsr_requests WHERE tenant_id = $1 AND status IN ('new', 'in_progress')") // Training completion rate (average progress, 0-100) overview.TrainingRate = s.avgSafe(ctx, tenant.ID, "SELECT COALESCE(AVG(CASE WHEN status = 'completed' THEN 100.0 ELSE progress END), 0) FROM academy_enrollments WHERE tenant_id = $1") // High-risk vendors overview.VendorRiskHigh = s.countSafe(ctx, tenant.ID, "SELECT COUNT(*) FROM vendors WHERE tenant_id = $1 AND risk_level = 'high'") return overview } // countSafe executes a COUNT(*) query that takes a single tenant_id parameter. // If the query fails for any reason (e.g. table does not exist), it returns 0. func (s *Store) countSafe(ctx context.Context, tenantID uuid.UUID, query string) int { var count int err := s.pool.QueryRow(ctx, query, tenantID).Scan(&count) if err != nil { // Tolerate errors -- table may not exist or query may fail return 0 } return count } // avgSafe executes an AVG query that takes a single tenant_id parameter. // If the query fails for any reason, it returns 0. func (s *Store) avgSafe(ctx context.Context, tenantID uuid.UUID, query string) float64 { var avg float64 err := s.pool.QueryRow(ctx, query, tenantID).Scan(&avg) if err != nil { return 0 } return avg }