package main

import (
	"os"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/backend/internal/application/services"
	filefeature "github.com/nexuscrm/backend/internal/features/files"
	"github.com/nexuscrm/backend/internal/features/mru"
	"github.com/nexuscrm/backend/internal/features/streaming"
	"github.com/nexuscrm/backend/internal/features/uiapi"
	"github.com/nexuscrm/backend/internal/infrastructure/database"
	"github.com/nexuscrm/backend/internal/infrastructure/persistence"
	"github.com/nexuscrm/backend/internal/infrastructure/storage"
	"github.com/nexuscrm/backend/internal/interfaces/middleware"
	"github.com/nexuscrm/backend/internal/interfaces/rest"
	"github.com/nexuscrm/shared/pkg/constants"
)

type routeDeps struct {
	eventBridge    *streaming.EventBridge
	sessionManager *streaming.SessionManager
	uiAPISvc       *uiapi.Service
}

// aliasParam copies a Gin route param under a second name so handlers that
// expect a different key still work (avoids wildcard name conflicts in the trie).
func aliasParam(src, dst string, h gin.HandlerFunc) gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Params = append(c.Params, gin.Param{Key: dst, Value: c.Param(src)})
		h(c)
	}
}

func injectParam(key, value string, h gin.HandlerFunc) gin.HandlerFunc {
	return func(c *gin.Context) {
		c.Params = append(c.Params, gin.Param{Key: key, Value: value})
		h(c)
	}
}

func registerRoutes(router *gin.Engine, svcMgr *services.ServiceManager, db *database.TiDBConnection, requireAuth gin.HandlerFunc, permLoader gin.HandlerFunc, permCache *middleware.PermissionCache, scopeRegistry *middleware.ScopeRegistry) routeDeps {
	// MRU service (not part of ServiceManager)
	mruRepo := persistence.NewMRURepository(db.DB())
	mruService := mru.NewService(mruRepo)

	fileStorageCfg := storage.LoadConfigFromEnv()
	fileStore, err := storage.NewStore(fileStorageCfg)
	if err != nil {
		panic(err)
	}

	// Streaming infrastructure
	sessionManager := streaming.DefaultSessionManager()
	eventBridge := streaming.NewEventBridge(svcMgr.EventBus, sessionManager)
	eventBridge.Start()

	// ── OAuth routes (no auth required) ──
	oauthHandler := rest.NewOAuthHandler(svcMgr)
	oauthHandler.SetPermCache(permCache)
	// H9 fix: dedicated auth rate limiter (5 req/s, burst 3) to prevent credential brute-forcing.
	// Token endpoints share this stricter limiter instead of the global 100 req/s.
	// Burst=3 ensures rapid brute-force attempts are blocked after just a few tries.
	authLimiter := middleware.NewRateLimiter(5, 3)
	oauth := router.Group("/services/oauth2")
	{
		oauth.GET("/authorize", requireAuth, oauthHandler.Authorize)
		oauth.POST("/token", authLimiter.Middleware(), oauthHandler.Token)
		oauth.POST("/revoke", oauthHandler.Revoke)
		oauth.POST("/introspect", requireAuth, oauthHandler.Introspect)
		oauth.GET("/userinfo", requireAuth, middleware.ScopeEnforcer(scopeRegistry), oauthHandler.UserInfo)
		oauth.POST("/google/token", authLimiter.Middleware(), oauthHandler.GoogleToken)
		oauth.POST("/change-password", requireAuth, oauthHandler.ChangePassword)
		oauth.POST("/forgot-password", authLimiter.Middleware(), oauthHandler.ForgotPassword)
		oauth.POST("/reset-password", authLimiter.Middleware(), oauthHandler.ResetPassword)
	}

	// ── Per-Connected App rate limiter ──
	// Runs after ScopeEnforcer (which sets connected_app_id) to enforce
	// hourly/daily request quotas for OAuth Connected Apps.
	// Defaults: 1000/hour, 10000/day. Phase 2 will read limits from the
	// ConnectedApp table (DailyApiLimit, HourlyApiLimit columns).
	appLimiter := middleware.NewAppRateLimiter()

	// ── Salesforce-compatible Data API ──
	// PermissionLoader runs globally after auth: loads effective system+custom
	// permissions into request context (cached in-memory, Salesforce-style).
	api := router.Group("/services/data/:version", requireAuth, middleware.ScopeEnforcer(scopeRegistry), appLimiter.Middleware(1000, 10000), permLoader, middleware.RequirePermission(constants.PermApiEnabled))
	{
		// System
		sysHandler := rest.NewSystemHandler(svcMgr)
		api.GET("/", sysHandler.ListResources)
		api.GET("/limits", sysHandler.Limits)
		api.GET("/id/:orgId/:userId", sysHandler.GetIdentity)

		// File Upload/Download (before generic SObject routes to avoid being caught by /sobjects/:sobject)
		fileHandler := rest.NewFileUploadHandler(fileStore, fileStorageCfg.Provider, fileStorageCfg.Bucket, fileStorageCfg.LegacyUploadRoot, svcMgr.Persistence, svcMgr.Permissions)
		fileHandler.SetDurableUploadService(filefeature.NewArchivedOrderUploadService(db.DB(), fileStore))
		sobjectHandler := rest.NewSObjectHandler(svcMgr, mruService)
		api.POST("/files", fileHandler.UploadRecordFile)
		api.GET("/files/:id/download", fileHandler.DownloadRecordFile)
		api.DELETE("/files/:id", fileHandler.DeleteRecordFile)
		// Compatibility aliases while callers migrate off internal table-shaped URLs.
		// Keep describe/layout routes on the static _System_File branch so Gin does
		// not stop at /sobjects/_System_File/:id and shadow generic describe routes.
		api.POST("/record-files", fileHandler.UploadRecordFile)
		api.GET("/sobjects/_System_File/describe", injectParam("sobject", "_System_File", sobjectHandler.DescribeObject))
		api.GET("/sobjects/_System_File/describe/layouts", injectParam("sobject", "_System_File", sobjectHandler.DescribeLayouts))
		api.GET("/sobjects/_System_File/:id/download", fileHandler.DownloadRecordFile)
		api.DELETE("/sobjects/_System_File/:id", fileHandler.DeleteRecordFile)
		api.POST("/sobjects/ContentVersion", fileHandler.Upload)
		api.GET("/sobjects/ContentVersion/:fileId/VersionData", fileHandler.Download)

		// SObject
		api.GET("/sobjects", sobjectHandler.ListObjects)
		api.GET("/sobjects/:sobject/describe", sobjectHandler.DescribeObject)
		api.GET("/sobjects/:sobject/describe/layouts", sobjectHandler.DescribeLayouts)
		api.GET("/sobjects/:sobject/describe/layouts/:recordTypeId", sobjectHandler.DescribeLayoutByRecordType)
		api.POST("/sobjects/:sobject", sobjectHandler.CreateRecord)
		api.GET("/sobjects/:sobject/:id", sobjectHandler.GetRecord)
		api.PATCH("/sobjects/:sobject/:id", sobjectHandler.UpdateRecord)
		api.DELETE("/sobjects/:sobject/:id", sobjectHandler.DeleteRecord)
		api.GET("/sobjects/:sobject/listviews", sobjectHandler.ListViews)
		api.GET("/sobjects/:sobject/listviews/:listViewId/describe", sobjectHandler.ListViewDescribe)
		api.PATCH("/sobjects/:sobject/:id/:fieldValue", aliasParam("id", "fieldName", sobjectHandler.UpsertByExternalID))
		api.GET("/sobjects/:sobject/:id/:fieldValue", aliasParam("id", "fieldName", sobjectHandler.GetByExternalID))

		// Relevant Items
		relevantHandler := rest.NewRelevantItemsHandler(mruService)
		api.GET("/sobjects/relevantItems", relevantHandler.GetRelevantItems)

		// Query
		queryHandler := rest.NewQueryHandler(svcMgr)
		api.GET("/query", queryHandler.ExecuteQuery)
		api.GET("/queryAll", queryHandler.ExecuteQueryAll)
		api.GET("/query/:locator", queryHandler.QueryMore)
		api.POST("/ui-api/related-list-counts", queryHandler.BatchRelatedListCounts)
		api.GET("/ui-api/related-list-candidates/:objectApiName", sobjectHandler.GetRelatedListCandidates)

		// Object Permissions
		permHandler := rest.NewPermissionsHandler(svcMgr)
		api.GET("/objectpermissions", permHandler.ListObjectPermissions)

		// System Permissions — catalog (read-only), per-PermissionSet grants, and effective
		api.GET("/permissions/system/catalog", permHandler.SystemPermissionCatalog)
		api.GET("/permissions/system/permissionset/:id", permHandler.GetPermissionSetSystemGrants)
		api.PATCH("/permissions/system/permissionset/:id", middleware.RequirePermission(constants.PermManageProfilesPermSets), permHandler.SavePermissionSetSystemGrants)
		api.GET("/permissions/effective", permHandler.GetEffectivePermissions)

		recycleBinHandler := rest.NewRecycleBinHandler(svcMgr)
		api.GET("/recyclebin", recycleBinHandler.List)
		api.POST("/recyclebin/restore/:id", recycleBinHandler.Restore)
		api.DELETE("/recyclebin/:id", recycleBinHandler.Purge)

		// Search
		searchHandler := rest.NewSearchHandler(svcMgr)
		api.GET("/search", searchHandler.SearchSOSL)
		api.GET("/parameterizedSearch", searchHandler.SearchParameterized)
		api.POST("/parameterizedSearch", searchHandler.SearchParameterizedPOST)

		searchLayoutHandler := rest.NewSearchLayoutHandler(svcMgr)
		api.GET("/search/layout", searchLayoutHandler.GetLayout)

		searchScopeHandler := rest.NewSearchScopeHandler(mruService)
		api.GET("/search/scopeOrder", searchScopeHandler.GetScopeOrder)

		// Composite
		compositeHandler := rest.NewCompositeHandler(router)
		api.POST("/composite", compositeHandler.HandleComposite)

		batchHandler := rest.NewCompositeBatchHandler(router)
		api.POST("/composite/batch", batchHandler.HandleBatch)

		collectionsHandler := rest.NewCompositeCollectionsHandler(svcMgr.Persistence, svcMgr.Metadata)
		api.GET("/composite/sobjects", collectionsHandler.HandleCollectionsRetrieve)
		api.POST("/composite/sobjects", collectionsHandler.HandleCollectionsCreate)
		api.PATCH("/composite/sobjects", collectionsHandler.HandleCollectionsUpdate)
		api.DELETE("/composite/sobjects", collectionsHandler.HandleCollectionsDelete)
		api.PATCH("/composite/sobjects/:sObjectName/:externalIdFieldName", collectionsHandler.HandleCollectionsUpsert)

		treeHandler := rest.NewCompositeTreeHandler(svcMgr.Persistence)
		api.POST("/composite/tree/:objectName", treeHandler.HandleTreeCreate)

		// Metadata — read routes (any authenticated user)
		metaHandler := rest.NewMetadataHandler(svcMgr)
		api.GET("/metadata/deployRequest/:id", metaHandler.GetDeployStatus)
		api.GET("/metadata/objects", metaHandler.GetSchemas)
		api.GET("/metadata/objects/:apiName", metaHandler.GetSchema)
		api.GET("/metadata/fieldtypes", metaHandler.GetFieldTypes)
		api.GET("/metadata/validation-rules", metaHandler.GetValidationRules)

		// Metadata — write routes (requires ModifyMetadata)
		metaWrite := api.Group("", middleware.RequirePermission(constants.PermModifyMetadata))
		metaWrite.POST("/metadata/deployRequest", metaHandler.DeployRequest)
		metaWrite.POST("/metadata/objects", metaHandler.CreateSchema)
		metaWrite.PATCH("/metadata/objects/:apiName", metaHandler.UpdateSchema)
		metaWrite.DELETE("/metadata/objects/:apiName", metaHandler.DeleteSchema)
		metaWrite.POST("/metadata/objects/:apiName/fields", metaHandler.CreateField)
		metaWrite.POST("/metadata/objects/:apiName/fields/batch", metaHandler.BatchCreateFields)
		metaWrite.PATCH("/metadata/objects/:apiName/fields/:fieldApiName", metaHandler.UpdateField)
		metaWrite.DELETE("/metadata/objects/:apiName/fields/:fieldApiName", metaHandler.DeleteField)
		metaWrite.POST("/metadata/refresh-cache", metaHandler.RefreshCache)
		metaWrite.POST("/metadata/validation-rules", metaHandler.CreateValidationRule)
		metaWrite.PATCH("/metadata/validation-rules/:id", metaHandler.UpdateValidationRule)
		metaWrite.DELETE("/metadata/validation-rules/:id", metaHandler.DeleteValidationRule)

		// Formula Views — read (any user), write (requires ModifyMetadata)
		formulaViewHandler := rest.NewFormulaViewHandler(svcMgr)
		api.GET("/metadata/formula-views/:objectName/status", formulaViewHandler.ViewStatus)
		api.GET("/metadata/formula-views/:objectName/ddl", formulaViewHandler.PreviewDDL)
		metaWrite.POST("/metadata/formula-views/:objectName/rebuild", formulaViewHandler.RebuildView)
		metaWrite.POST("/metadata/formula-views/rebuild-all", formulaViewHandler.RebuildAllViews)
		metaWrite.POST("/metadata/formula-views/:objectName/validate", formulaViewHandler.ValidateFormula)

		// Analytics
		dashboardHandler := rest.NewDashboardHandler(svcMgr)
		analyticsHandler := rest.NewAnalyticsHandler(svcMgr.ReportSvc, dashboardHandler, svcMgr)
		analyticsGroup := api.Group("/analytics", middleware.RequirePermission(constants.PermRunReports))
		analyticsHandler.RegisterRoutes(analyticsGroup)
		dashboardHandler.RegisterSalesforceRoutes(analyticsGroup)

		// Bulk API
		bulkHandler := rest.NewBulkHandler(
			svcMgr.BulkSvc,
			svcMgr.Persistence,
			svcMgr.Metadata,
			svcMgr.Permissions,
			svcMgr.EventBus,
			svcMgr.QuerySvc,
		)
		bulkGroup := api.Group("/jobs")
		bulkHandler.RegisterRoutes(bulkGroup)

		// Folders — read requires RunReports, write requires either report OR dashboard folder permission
		// Per-type enforcement (Report vs Dashboard) is done inside the handlers based on folder Type.
		folderHandler := rest.NewFolderHandler(svcMgr)
		folderRead := api.Group("/folders", middleware.RequirePermission(constants.PermRunReports))
		folderHandler.RegisterReadRoutes(folderRead)
		folderWrite := api.Group("/folders", middleware.RequireAnyPermission(constants.PermManageReportsInPubFolders, constants.PermManageDashbdsInPubFolders))
		folderHandler.RegisterWriteRoutes(folderWrite)
		// Admin-only folder routes (ownership transfer, etc.)
		folderAdmin := api.Group("/folders", middleware.RequirePermission(constants.PermManageUsers))
		folderHandler.RegisterAdminRoutes(folderAdmin)

		// Connect
		connectHandler := rest.NewConnectHandler(svcMgr)
		connectGroup := api.Group("/connect")
		connectHandler.RegisterRoutes(connectGroup)

		// Standard Actions legacy handler — owns changeOwner + createRecord
		// until their M5 migration. convertLead now dispatches through the
		// registry-driven StandardActionDispatcher, not this handler.
		standardActionsHandler := rest.NewStandardActionsHandler(
			svcMgr.Persistence,
			svcMgr.Permissions,
			svcMgr.Metadata,
			svcMgr.QuerySvc,
			svcMgr.EmailSender,
			os.Getenv("FRONTEND_URL"),
		)
		standardActionDispatcher := rest.NewStandardActionDispatcher(
			db.DB(),
			svcMgr.FlowExecutor,
			svcMgr.Permissions,
			standardActionsHandler,
			svcMgr.Metadata,
		)
		api.POST("/actions/standard/:name/preview/record-create-fields", standardActionDispatcher.PreviewRecordCreateFields)
		api.POST("/actions/standard/:name", standardActionDispatcher.Dispatch)
		// Part E: metadata-driven button discovery. Frontend lists active
		// _System_StandardAction rows (filtered to the subset the caller is
		// permitted to invoke) and renders buttons accordingly. Admin
		// toggles visibility via IsActive=0 — no code change required.
		api.GET("/actions/standard", standardActionDispatcher.List)

		// Flows — execution (requires RunFlow), definition management (requires ManageFlows)
		flowHandler := rest.NewFlowHandler(svcMgr.FlowExecutor, svcMgr.FlowInterviewSvc, svcMgr.Metadata, db.DB())
		actionsGroup := api.Group("/actions/custom", middleware.RequirePermission(constants.PermRunFlow))
		flowHandler.RegisterRoutes(actionsGroup)
		flowHandler.RegisterSObjectRoutes(api.Group("/sobjects"))

		// Tooling Query — read (requires ViewSetup or CustomizeApplication)
		toolingQueryHandler := rest.NewToolingQueryHandler(svcMgr)
		api.GET("/tooling/query", middleware.RequireAnyPermission(constants.PermViewSetup, constants.PermCustomizeApplication), toolingQueryHandler.HandleQuery)

		// Tooling sObjects — write routes require CustomizeApplication
		toolingGroup := api.Group("/tooling/sobjects", middleware.RequirePermission(constants.PermCustomizeApplication))
		flowHandler.RegisterToolingRoutes(toolingGroup)

		toolingValHandler := rest.NewToolingValidationHandler(svcMgr)
		toolingGroup.POST("/ValidationRule", toolingValHandler.CreateValidationRule)
		toolingGroup.PATCH("/ValidationRule/:id", toolingValHandler.UpdateValidationRule)
		toolingGroup.DELETE("/ValidationRule/:id", toolingValHandler.DeleteValidationRule)

		toolingMetaHandler := rest.NewToolingMetadataHandler(svcMgr)
		toolingGroup.POST("/CustomObject", toolingMetaHandler.CreateCustomObject)
		toolingGroup.PATCH("/CustomObject/:id", toolingMetaHandler.UpdateCustomObject)
		toolingGroup.POST("/CustomField", toolingMetaHandler.CreateCustomField)
		toolingGroup.POST("/ProfileLayout", toolingMetaHandler.CreateProfileLayout)

		actionHandler := rest.NewActionHandler(svcMgr)
		toolingGroup.POST("/QuickActionDefinition", actionHandler.CreateAction)

		// Quick Actions
		quickActionHandler := rest.NewQuickActionHandler(svcMgr)
		api.GET("/quickActions", quickActionHandler.ListGlobalQuickActions)
		api.GET("/sobjects/:sobject/quickActions", quickActionHandler.ListObjectQuickActions)
		api.GET("/quickActions/:actionName/describe", quickActionHandler.DescribeQuickAction)
		api.POST("/quickActions/:actionName", quickActionHandler.ExecuteQuickAction)

		// Approvals
		approvalHandler := rest.NewApprovalHandler(svcMgr.Approval)
		approvalReadGroup := api.Group("/process")
		approvalHandler.RegisterReadRoutes(approvalReadGroup)
		approvalWriteGroup := api.Group("/process", middleware.RequirePermission(constants.PermModifyMetadata))
		approvalHandler.RegisterWriteRoutes(approvalWriteGroup)

		// UI API
		uiAPIHandler := rest.NewUIAPIHandler(svcMgr.UIAPI, mruService, svcMgr.HrefFieldResolver)
		api.GET("/ui-api/record-ui/:recordIds", uiAPIHandler.HandleRecordUI)
		api.POST("/ui-api/record-access", uiAPIHandler.HandleBatchRecordAccess)
		api.GET("/ui-api/object-info/:objectApiName", uiAPIHandler.HandleObjectInfo)
		api.GET("/ui-api/layout/:objectApiName", uiAPIHandler.HandleLayout)
		api.GET("/ui-api/layouts/:objectApiName", uiAPIHandler.HandleListLayouts)
		api.GET("/ui-api/approval-layouts/:objectApiName", uiAPIHandler.HandleListApprovalLayouts) // Approval layouts list
		api.GET("/ui-api/record-defaults/create/:objectApiName", uiAPIHandler.HandleRecordDefaultsCreate)
		api.GET("/ui-api/record-defaults/clone/:recordId", uiAPIHandler.HandleRecordDefaultsClone)
		api.GET("/ui-api/list-info/:objectApiName/:listViewApiName", uiAPIHandler.HandleListInfo)
		// Layout mutation — requires CustomizeApplication permission
		adminLayout := api.Group("", middleware.RequirePermission(constants.PermCustomizeApplication))
		adminLayout.POST("/ui-api/layout/Global", uiAPIHandler.HandleSaveLayout)
		adminLayout.POST("/ui-api/layout/:layoutId/clone", uiAPIHandler.HandleCloneLayout)
		adminLayout.PATCH("/ui-api/layout/:layoutId/default", uiAPIHandler.HandleSetDefaultLayout)
		adminLayout.DELETE("/ui-api/layout/:layoutId", uiAPIHandler.HandleDeleteLayout)
		adminLayout.POST("/ui-api/layout/:layoutId/restore/:versionNumber", uiAPIHandler.HandleRestoreLayout)
		adminLayout.POST("/ui-api/layout-assignment", uiAPIHandler.HandleCreateAssignment)
		adminLayout.DELETE("/ui-api/layout-assignment/:assignmentId", uiAPIHandler.HandleDeleteAssignment)

		// Approval layout admin endpoints - also require CustomizeApplication permission
		adminLayout.POST("/ui-api/approval-layout/Global", uiAPIHandler.HandleSaveApprovalLayout)
		adminLayout.POST("/ui-api/approval-layout/:layoutId/clone", uiAPIHandler.HandleCloneApprovalLayout)
		adminLayout.PATCH("/ui-api/approval-layout/:layoutId/default", uiAPIHandler.HandleSetDefaultApprovalLayout)
		adminLayout.DELETE("/ui-api/approval-layout/:layoutId", uiAPIHandler.HandleDeleteApprovalLayout)

		// Layout read — any authenticated user
		api.GET("/ui-api/layout-assignments/:objectApiName", uiAPIHandler.HandleListAssignments)

		// UI Handler (Apps) — read (any user), write (requires CustomizeApplication)
		uiHandler := rest.NewUIHandler(svcMgr)
		api.GET("/ui-api/apps", uiHandler.GetAppsUIAPI)
		api.GET("/tooling/sobjects/CustomApplication", uiHandler.GetApps)
		api.GET("/tooling/sobjects/CustomApplication/:id", uiHandler.GetAppByID)
		appAdmin := api.Group("", middleware.RequirePermission(constants.PermCustomizeApplication))
		appAdmin.POST("/tooling/sobjects/CustomApplication", uiHandler.CreateApp)
		appAdmin.PATCH("/tooling/sobjects/CustomApplication/:id", uiHandler.UpdateApp)
		appAdmin.DELETE("/tooling/sobjects/CustomApplication/:id", uiHandler.DeleteApp)

		// Autonumber metadata maintenance — Round-6 R5-H1. Forces the
		// running process to reload AutoNumberMetadata from _System_Field.
		// Call this (per pod) after `cmd/setup apply` writes new
		// AutoNumberFormat / TokenRefs rows, or rely on rolling restart.
		adminAutoNumber := rest.NewAdminAutoNumberHandler(svcMgr)
		appAdmin.POST("/admin/autonumber/refresh-metadata", adminAutoNumber.RefreshMetadata)

		// Audit log admin read API (§5.2). Gated by ViewFieldAuditTrail —
		// the SF-native permission for privileged audit access. Returns
		// full AuditLog metadata (Entrypoint/TraceId/ClientIp/etc.) that
		// <Object>History deliberately hides.
		auditAdminGroup := api.Group("", middleware.RequirePermission(constants.PermViewFieldAuditTrail))
		adminAudit := rest.NewAdminAuditHandler(svcMgr)
		auditAdminGroup.GET("/admin/audit/log", adminAudit.GetAuditLog)
	}

	// ── Versions endpoint (no auth) ──
	sysHandler := rest.NewSystemHandler(svcMgr)
	router.GET("/services/data", sysHandler.Versions)

	// ── CometD Streaming ──
	streamingHandler := rest.NewStreamingHandler(svcMgr)
	cometd := router.Group("/cometd/:version", requireAuth, middleware.ScopeEnforcer(scopeRegistry))
	{
		cometd.POST("/handshake", streamingHandler.Handshake)
		cometd.POST("/connect", streamingHandler.Connect)
		cometd.POST("/subscribe", streamingHandler.Subscribe)
		cometd.POST("/disconnect", streamingHandler.Disconnect)
	}

	// ── WebSocket ──
	wsHandler := rest.NewWebSocketHandler(svcMgr, sessionManager)
	router.GET("/ws", requireAuth, middleware.ScopeEnforcer(scopeRegistry), wsHandler.HandleConnection)

	// ── Internal ops metrics (Sentry dashboard) ──
	// Fail-closed: only register when SENTRY_INTERNAL_TOKEN is set, so we never
	// expose this endpoint without authentication.
	if token := os.Getenv("SENTRY_INTERNAL_TOKEN"); token != "" {
		version := os.Getenv("APP_VERSION")
		if version == "" {
			version = "dev"
		}
		sentryMetricsHandler := rest.NewSentryMetricsHandler(db.DB(), version)
		internal := router.Group("/internal", middleware.InternalTokenAuth(token))
		internal.GET("/sentry-metrics", sentryMetricsHandler.Get)
	}

	return routeDeps{
		eventBridge:    eventBridge,
		sessionManager: sessionManager,
		uiAPISvc:       svcMgr.UIAPI,
	}
}
