// Package router composes profile-scoped AgentTeam routes and embedded resources.
// Resource services provide dependencies; HTTP route ownership remains here.
package router

import (
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/admin"
	"github.com/tidbcloud/AgentTeam/internal/api/agent"
	"github.com/tidbcloud/AgentTeam/internal/api/app"
	"github.com/tidbcloud/AgentTeam/internal/api/auth"
	"github.com/tidbcloud/AgentTeam/internal/api/billing"
	"github.com/tidbcloud/AgentTeam/internal/api/channel"
	"github.com/tidbcloud/AgentTeam/internal/api/daemon"
	"github.com/tidbcloud/AgentTeam/internal/api/identity/lookup"
	"github.com/tidbcloud/AgentTeam/internal/api/im"
	"github.com/tidbcloud/AgentTeam/internal/api/knowledgeqa"
	"github.com/tidbcloud/AgentTeam/internal/api/llm"
	"github.com/tidbcloud/AgentTeam/internal/api/media"
	"github.com/tidbcloud/AgentTeam/internal/api/onboarding"
	"github.com/tidbcloud/AgentTeam/internal/api/org"
	"github.com/tidbcloud/AgentTeam/internal/api/provider"
	"github.com/tidbcloud/AgentTeam/internal/api/public"
	"github.com/tidbcloud/AgentTeam/internal/api/schedule"
	"github.com/tidbcloud/AgentTeam/internal/api/serviceaccount"
	"github.com/tidbcloud/AgentTeam/internal/api/task"
	"github.com/tidbcloud/AgentTeam/internal/api/team"
	"github.com/tidbcloud/AgentTeam/internal/api/teamworkflow"
	"github.com/tidbcloud/AgentTeam/internal/api/trigger"
	"github.com/tidbcloud/AgentTeam/internal/api/webagent"
	"github.com/tidbcloud/AgentTeam/internal/api/workflow/binding"
	"github.com/tidbcloud/AgentTeam/internal/api/workflow/invocation"
	"github.com/tidbcloud/AgentTeam/internal/api/workflow/plan"
	"github.com/tidbcloud/AgentTeam/internal/api/workflow/run"
	"github.com/tidbcloud/AgentTeam/internal/api/workspace"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail/mailapi"
	"github.com/tidbcloud/AgentTeam/internal/forwarder"
	"github.com/tidbcloud/AgentTeam/internal/observability/debughttp"
	storeagent "github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/versioning"
	"github.com/tidbcloud/AgentTeam/internal/workspace/owner"
)

// Profile identifies a route-ownership slice for the HTTP server.
type Profile string

const (
	ProfileAll       Profile = "all"
	ProfileCentral   Profile = "central"
	ProfileWorkspace Profile = "workspace"
	// ProfileExtension registers ONLY the Extension runtime routes plus the
	// common health/observability routes (spec §6.1). It does not register Web,
	// IM, daemon WebSocket, LLM, workflow, or ordinary Workspace routes, and it
	// does not inherit Workspace background workers.
	ProfileExtension Profile = "extension"
)

// Dependencies holds the HTTP-layer collaborators.
type Dependencies struct {
	Logger            *slog.Logger
	HTTPMetrics       HTTPMetrics
	HTTPTracing       HTTPTracing
	Health            HealthDependencies
	API               APIDependencies
	Agent             agent.Dependencies
	LiveEmitter       *im.LiveEventEmitter
	Daemon            DaemonDependencies
	ManagedWorkspaces owner.ManagedWorkspaceSet
	Web               WebDependencies
	Extension         ExtensionDependencies
}

// WebDependencies controls optional static web serving from the API server.
type WebDependencies struct {
	ServeWeb bool
	DistDir  string
}

type HTTPMetrics interface {
	Handler(skipPaths ...string) gin.HandlerFunc
	RecordPanic(method string, route string)
}

type HTTPTracing interface {
	Handler() gin.HandlerFunc
}

// HealthDependencies contains readiness-check dependencies.
type HealthDependencies struct {
	CoreDB                 coredb.AdminDB
	CoreDBAdmin            coredb.Admin
	DependencyCheckTimeout time.Duration
	AdditionalReady        func(context.Context) error
}

const defaultReadyzDependencyCheckTimeout = 2 * time.Second

// APIDependencies groups browser-facing domain route dependencies.
type APIDependencies struct {
	Public             public.Dependencies
	Auth               auth.Dependencies
	Billing            billing.Dependencies
	Admin              admin.Dependencies
	Workspace          workspace.Dependencies
	IM                 im.Dependencies
	Org                org.Dependencies
	Media              media.Dependencies
	Onboarding         onboarding.Dependencies
	Channel            channel.Dependencies
	APIForwarder       forwarder.Dependencies
	MeshXInternalAdmin http.Handler
	MeshXWorkspaceAGS  http.Handler
	RuntimeIdentity    lookup.Dependencies
	Task               task.Dependencies
	Schedule           schedule.Dependencies
	ServiceAccount     serviceaccount.Dependencies
	Trigger            trigger.Dependencies
	WorkflowBinding    binding.Dependencies
	WorkflowInvocation invocation.Dependencies
	WorkflowPlan       plan.Dependencies
	WorkflowRun        run.Dependencies
	Team               team.Dependencies
	TeamWorkflow       teamworkflow.Dependencies
	App                app.Dependencies
	Mail               mailapi.Dependencies
	Provider           provider.Dependencies
	LLM                llm.Dependencies
	KnowledgeQA        knowledgeqa.Dependencies
	WebAgent           webagent.Dependencies
}

// DaemonDependencies contains daemon websocket route dependencies.
type DaemonDependencies struct {
	Hub                  *daemon.Hub
	Auth                 daemon.MachineAuthenticator
	SessionIssuer        daemon.SessionTokenIssuer
	LatestDesktopVersion versioning.Provider
	MinDesktopVersion    string
}

// Validate verifies production hard dependencies before serving traffic.
func (dependencies Dependencies) Validate() error {
	return dependencies.ValidateForProfile(ProfileAll)
}

// ValidateForProfile verifies production hard dependencies for a selected profile.
func (dependencies Dependencies) ValidateForProfile(profile Profile) error {
	var missing []string
	dependencies.validateCommonDependencies(&missing)
	dependencies.validateRouteDependencies(profile, &missing)
	if len(missing) > 0 {
		return fmt.Errorf("router dependencies missing: %s", strings.Join(missing, ", "))
	}
	return nil
}

func (dependencies Dependencies) validateCommonDependencies(missing *[]string) {
	if dependencies.Health.CoreDB == nil {
		*missing = append(*missing, "Health.CoreDB")
	}
	if dependencies.Health.CoreDBAdmin == nil {
		*missing = append(*missing, "Health.CoreDBAdmin")
	}
}

func (dependencies Dependencies) validateRouteDependencies(profile Profile, missing *[]string) {
	switch profile {
	case ProfileAll:
		dependencies.validateControlDependencies(missing)
		dependencies.validateWorkspaceDependencies(missing)
		dependencies.validateRuntimeCompatibilityDependencies(missing)
		dependencies.validateExtensionDependencies(missing)
	case ProfileCentral:
		dependencies.validateControlDependencies(missing)
	case ProfileWorkspace:
		dependencies.validateWorkspaceDependencies(missing)
		dependencies.validateRuntimeCompatibilityDependencies(missing)
	case ProfileExtension:
		// The Extension profile is a narrow runtime: it requires only the Agent
		// Runtime principal authenticator and the Extension Catalog registry. It
		// MUST NOT require control/workspace/runtime-compatibility dependencies
		// (spec §6.1).
		dependencies.validateExtensionDependencies(missing)
	default:
		// Fail-closed: an unknown profile registers NO domain routes, so it has
		// no route dependencies to validate. Common health/observability
		// dependencies are validated separately by validateCommonDependencies.
	}
}

func (dependencies Dependencies) validateControlDependencies(missing *[]string) {
	api := dependencies.API
	if api.Public.AuthStore == nil {
		*missing = append(*missing, "API.Public.AuthStore")
	}
	if api.Public.MessageSearchStore == nil {
		*missing = append(*missing, "API.Public.MessageSearchStore")
	}
	if api.Public.ContentReports == nil {
		*missing = append(*missing, "API.Public.ContentReports")
	}
	if api.Auth.Store == nil {
		*missing = append(*missing, "API.Auth.Store")
	}
	if api.Auth.Organizations == nil {
		*missing = append(*missing, "API.Auth.Organizations")
	}
	if api.Billing.Enabled {
		if api.Billing.SessionAuth == nil {
			*missing = append(*missing, "API.Billing.SessionAuth")
		}
		if api.Billing.Organizations == nil {
			*missing = append(*missing, "API.Billing.Organizations")
		}
		if api.Billing.BillingClient == nil {
			*missing = append(*missing, "API.Billing.BillingClient")
		}
		if api.Billing.SeatBilling == nil {
			*missing = append(*missing, "API.Billing.SeatBilling")
		}
		if api.Billing.Alipay == nil {
			*missing = append(*missing, "API.Billing.Alipay")
		}
	}
	if api.Admin.Store == nil && api.Admin.AuthStore == nil {
		*missing = append(*missing, "API.Admin.AuthStore")
	}
	if api.Admin.Store == nil && api.Admin.AdminStore == nil {
		*missing = append(*missing, "API.Admin.AdminStore")
	}
	if api.Admin.Store == nil && api.Admin.RegistrationAllowlist == nil {
		*missing = append(*missing, "API.Admin.RegistrationAllowlist")
	}
	validateWorkspaceAdminDependencies(api, missing)
	if api.Org.Store == nil {
		*missing = append(*missing, "API.Org.Store")
	}
	if api.Org.Organizations == nil {
		*missing = append(*missing, "API.Org.Organizations")
	}
	if api.Org.PlanStore == nil {
		*missing = append(*missing, "API.Org.PlanStore")
	}
	validateMediaDependencies(api, missing)
	if api.App.AppStore == nil {
		*missing = append(*missing, "API.App.AppStore")
	}
	if api.Provider.Store == nil {
		*missing = append(*missing, "API.Provider.Store")
	}
}

func (dependencies Dependencies) validateWorkspaceDependencies(missing *[]string) {
	api := dependencies.API
	if api.Workspace.WorkspaceService == nil {
		*missing = append(*missing, "API.Workspace.WorkspaceService")
	}
	validateMediaDependencies(api, missing)
	validateWorkspaceIMDependencies(api, missing)
	validateWorkspaceChannelDependencies(api, missing)
	validateWorkspaceTaskDependencies(api, missing)
	validateWorkspaceTeamDependencies(api, missing)
	validateWorkspaceScheduleDependencies(api, missing)
	validateWorkspaceAppDependencies(api, missing)
	validateWorkspaceWebAgentDependencies(api, missing)
}

func validateWorkspaceIMDependencies(api APIDependencies, missing *[]string) {
	if api.IM.SessionAuth == nil {
		*missing = append(*missing, "API.IM.SessionAuth")
	}
	if api.IM.WorkspaceStore == nil {
		*missing = append(*missing, "API.IM.WorkspaceStore")
	}
	if api.IM.Messages == nil {
		*missing = append(*missing, "API.IM.Messages")
	}
	if api.IM.Channels == nil {
		*missing = append(*missing, "API.IM.Channels")
	}
	if api.IM.ChannelAccess == nil {
		*missing = append(*missing, "API.IM.ChannelAccess")
	}
	if api.IM.DMs == nil {
		*missing = append(*missing, "API.IM.DMs")
	}
	if api.IM.Conversations == nil {
		*missing = append(*missing, "API.IM.Conversations")
	}
	if api.IM.Reactions == nil {
		*missing = append(*missing, "API.IM.Reactions")
	}
	if api.IM.Pins == nil {
		*missing = append(*missing, "API.IM.Pins")
	}
	if api.IM.StreamEvents == nil {
		*missing = append(*missing, "API.IM.StreamEvents")
	}
	if api.IM.VisibleScopes == nil {
		*missing = append(*missing, "API.IM.VisibleScopes")
	}
	if api.IM.StreamSubscriptions == nil {
		*missing = append(*missing, "API.IM.StreamSubscriptions")
	}
	if api.IM.StreamCursors == nil {
		*missing = append(*missing, "API.IM.StreamCursors")
	}
	if api.IM.ConversationIndex == nil {
		*missing = append(*missing, "API.IM.ConversationIndex")
	}
}

func validateWorkspaceChannelDependencies(api APIDependencies, missing *[]string) {
	if api.Channel.AuthStore == nil {
		*missing = append(*missing, "API.Channel.AuthStore")
	}
	if api.Channel.WorkspaceStore == nil {
		*missing = append(*missing, "API.Channel.WorkspaceStore")
	}
	if api.Channel.Channels == nil {
		*missing = append(*missing, "API.Channel.Channels")
	}
	if api.Channel.AppStore == nil {
		*missing = append(*missing, "API.Channel.AppStore")
	}
	if api.Channel.ThreadStore == nil {
		*missing = append(*missing, "API.Channel.ThreadStore")
	}
	if api.Channel.ThreadReviewStore == nil {
		*missing = append(*missing, "API.Channel.ThreadReviewStore")
	}
}

func validateWorkspaceTaskDependencies(api APIDependencies, missing *[]string) {
	if api.Task.AuthStore == nil {
		*missing = append(*missing, "API.Task.AuthStore")
	}
	if api.Task.WorkspaceStore == nil {
		*missing = append(*missing, "API.Task.WorkspaceStore")
	}
	if api.Task.Store == nil {
		*missing = append(*missing, "API.Task.Store")
	}
}

func validateWorkspaceTeamDependencies(api APIDependencies, missing *[]string) {
	if api.Team.AuthStore == nil {
		*missing = append(*missing, "API.Team.AuthStore")
	}
	if api.Team.WorkspaceStore == nil {
		*missing = append(*missing, "API.Team.WorkspaceStore")
	}
	if api.Team.Teams == nil {
		*missing = append(*missing, "API.Team.Teams")
	}
	if api.TeamWorkflow.AuthStore == nil {
		*missing = append(*missing, "API.TeamWorkflow.AuthStore")
	}
	if api.TeamWorkflow.WorkspaceStore == nil {
		*missing = append(*missing, "API.TeamWorkflow.WorkspaceStore")
	}
	if api.TeamWorkflow.Service == nil {
		*missing = append(*missing, "API.TeamWorkflow.Service")
	}
}

func validateWorkspaceScheduleDependencies(api APIDependencies, missing *[]string) {
	if api.Schedule.AuthStore == nil {
		*missing = append(*missing, "API.Schedule.AuthStore")
	}
	if api.Schedule.WorkspaceStore == nil {
		*missing = append(*missing, "API.Schedule.WorkspaceStore")
	}
	if api.Schedule.ScheduleService == nil {
		*missing = append(*missing, "API.Schedule.ScheduleService")
	}
	if api.Trigger.AuthStore == nil {
		*missing = append(*missing, "API.Trigger.AuthStore")
	}
	if api.Trigger.WorkspaceStore == nil {
		*missing = append(*missing, "API.Trigger.WorkspaceStore")
	}
	if api.Trigger.TriggerService == nil {
		*missing = append(*missing, "API.Trigger.TriggerService")
	}
	if api.WorkflowBinding.AuthStore == nil {
		*missing = append(*missing, "API.WorkflowBinding.AuthStore")
	}
	if api.WorkflowBinding.WorkspaceStore == nil {
		*missing = append(*missing, "API.WorkflowBinding.WorkspaceStore")
	}
	if api.WorkflowBinding.Service == nil {
		*missing = append(*missing, "API.WorkflowBinding.Service")
	}
	if api.WorkflowInvocation.AuthStore == nil {
		*missing = append(*missing, "API.WorkflowInvocation.AuthStore")
	}
	if api.WorkflowInvocation.WorkspaceStore == nil {
		*missing = append(*missing, "API.WorkflowInvocation.WorkspaceStore")
	}
	if api.WorkflowInvocation.Service == nil {
		*missing = append(*missing, "API.WorkflowInvocation.Service")
	}
	if api.WorkflowPlan.AuthStore == nil {
		*missing = append(*missing, "API.WorkflowPlan.AuthStore")
	}
	if api.WorkflowPlan.WorkspaceStore == nil {
		*missing = append(*missing, "API.WorkflowPlan.WorkspaceStore")
	}
	if api.WorkflowPlan.Service == nil {
		*missing = append(*missing, "API.WorkflowPlan.Service")
	}
	if api.WorkflowRun.AuthStore == nil {
		*missing = append(*missing, "API.WorkflowRun.AuthStore")
	}
	if api.WorkflowRun.WorkspaceStore == nil {
		*missing = append(*missing, "API.WorkflowRun.WorkspaceStore")
	}
	if api.WorkflowRun.Service == nil {
		*missing = append(*missing, "API.WorkflowRun.Service")
	}
	if api.WorkflowRun.Calendar == nil {
		*missing = append(*missing, "API.WorkflowRun.Calendar")
	}
}

func validateWorkspaceAppDependencies(api APIDependencies, missing *[]string) {
	if api.App.AppStore == nil {
		*missing = append(*missing, "API.App.AppStore")
	}
	if api.Provider.Store == nil {
		*missing = append(*missing, "API.Provider.Store")
	}
	if api.ServiceAccount.Service == nil {
		*missing = append(*missing, "API.ServiceAccount.Service")
	}
	if api.ServiceAccount.PendingRequestProjector == nil {
		*missing = append(*missing, "API.ServiceAccount.PendingRequestProjector")
	}
	if api.ServiceAccount.SessionAuth == nil {
		*missing = append(*missing, "API.ServiceAccount.SessionAuth")
	}
	if api.ServiceAccount.WorkspaceStore == nil {
		*missing = append(*missing, "API.ServiceAccount.WorkspaceStore")
	}
}

func validateWorkspaceWebAgentDependencies(api APIDependencies, missing *[]string) {
	if api.WebAgent.AgentStore == nil {
		*missing = append(*missing, "API.WebAgent.AgentStore")
	}
	if api.WebAgent.ModelProfiles == nil {
		*missing = append(*missing, "API.WebAgent.ModelProfiles")
	}
	if api.WebAgent.SessionAuth == nil {
		*missing = append(*missing, "API.WebAgent.SessionAuth")
	}
	if api.WebAgent.WorkspaceStore == nil {
		*missing = append(*missing, "API.WebAgent.WorkspaceStore")
	}
}

func (dependencies Dependencies) validateRuntimeCompatibilityDependencies(missing *[]string) {
	api := dependencies.API
	if api.App.AppStore == nil {
		*missing = append(*missing, "API.App.AppStore")
	}
	if api.LLM.APRDispatcher == nil {
		*missing = append(*missing, "API.LLM.APRDispatcher")
	}
	if dependencies.Agent.Auth == nil {
		*missing = append(*missing, "Agent.Auth")
	}
	if dependencies.Agent.WorkflowPlans == nil {
		*missing = append(*missing, "Agent.WorkflowPlans")
	}
	if dependencies.Agent.WorkflowPlanAgentEditGuard == nil {
		*missing = append(*missing, "Agent.WorkflowPlanAgentEditGuard")
	}
	if dependencies.Agent.WorkflowBindings == nil {
		*missing = append(*missing, "Agent.WorkflowBindings")
	}
	if dependencies.Agent.WorkflowBindingOwners == nil {
		*missing = append(*missing, "Agent.WorkflowBindingOwners")
	}
	if dependencies.Agent.WorkflowBindingWorkspace == nil {
		*missing = append(*missing, "Agent.WorkflowBindingWorkspace")
	}
	if dependencies.Agent.WorkflowInvocations == nil {
		*missing = append(*missing, "Agent.WorkflowInvocations")
	}
	if dependencies.Agent.WorkflowInvocationOwners == nil {
		*missing = append(*missing, "Agent.WorkflowInvocationOwners")
	}
	if dependencies.Agent.WorkflowInvocationWorkspace == nil {
		*missing = append(*missing, "Agent.WorkflowInvocationWorkspace")
	}
	if dependencies.Agent.WorkflowRuns == nil {
		*missing = append(*missing, "Agent.WorkflowRuns")
	}
	if dependencies.Agent.WorkflowRunCallbacks == nil {
		*missing = append(*missing, "Agent.WorkflowRunCallbacks")
	}
	if dependencies.LiveEmitter == nil {
		*missing = append(*missing, "LiveEmitter")
	}
	if dependencies.Daemon.Hub == nil {
		*missing = append(*missing, "Daemon.Hub")
	}
	if dependencies.Daemon.Auth == nil {
		*missing = append(*missing, "Daemon.Auth")
	}
	if dependencies.Daemon.SessionIssuer == nil {
		*missing = append(*missing, "Daemon.SessionIssuer")
	}
}

func validateWorkspaceAdminDependencies(api APIDependencies, missing *[]string) {
	if api.Workspace.WorkspaceService == nil {
		*missing = append(*missing, "API.Workspace.WorkspaceService")
	}
	if api.Workspace.PlanStore == nil {
		*missing = append(*missing, "API.Workspace.PlanStore")
	}
	if api.Workspace.Organizations == nil {
		*missing = append(*missing, "API.Workspace.Organizations")
	}
	if api.Workspace.MachineRuntime == nil {
		*missing = append(*missing, "API.Workspace.MachineRuntime")
	}
	if api.Workspace.LatestDaemonVersion == nil {
		*missing = append(*missing, "API.Workspace.LatestDaemonVersion")
	}
}

func validateMediaDependencies(api APIDependencies, missing *[]string) {
	if api.Media.AuthStore == nil {
		*missing = append(*missing, "API.Media.AuthStore")
	}
	if api.Media.WorkspaceStore == nil {
		*missing = append(*missing, "API.Media.WorkspaceStore")
	}
	if api.Media.AgentTokenStore == nil {
		*missing = append(*missing, "API.Media.AgentTokenStore")
	}
	if api.Media.AgentStore == nil {
		*missing = append(*missing, "API.Media.AgentStore")
	}
	if api.Media.AttachmentStore == nil {
		*missing = append(*missing, "API.Media.AttachmentStore")
	}
	if api.Media.PersonalStickerStore == nil {
		*missing = append(*missing, "API.Media.PersonalStickerStore")
	}
}

// NewRouter builds the HTTP router for the AgentTeam server.
func NewRouter(dependencies Dependencies) *gin.Engine {
	return NewRouterForProfile(ProfileAll, dependencies)
}

// setReleaseModeOnce ensures gin's process-global release mode is set exactly
// once. gin.SetMode writes a package global without synchronization, so calling
// it on every NewRouterForProfile raced when tests build routers concurrently.
// The mode is a process-level setting and setting it more than once is a no-op,
// so a sync.Once is both safe and sufficient. The Do call is inlined into
// NewRouterForProfile (rather than a one-line helper) to avoid adding a thin
// wrapper.
var setReleaseModeOnce sync.Once

// NewRouterForProfile builds the HTTP router for a selected route profile.
func NewRouterForProfile(profile Profile, dependencies Dependencies) *gin.Engine {
	setReleaseModeOnce.Do(func() { gin.SetMode(gin.ReleaseMode) })

	router := gin.New()
	router.HandleMethodNotAllowed = true
	router.Use(recoveryMiddleware(dependencies.Logger, dependencies.HTTPMetrics))
	router.Use(requestIDMiddleware())
	router.Use(gzipCompressionMiddleware())
	if dependencies.HTTPTracing != nil {
		router.Use(dependencies.HTTPTracing.Handler())
	}
	if dependencies.HTTPMetrics != nil {
		router.Use(dependencies.HTTPMetrics.Handler("/healthz", "/readyz"))
	}
	router.Use(requestLogger(dependencies.Logger))

	router.GET("/healthz", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, gin.H{"status": "ok"})
	})

	debughttp.RegisterPprofRoutes(router)

	router.GET("/readyz", func(ctx *gin.Context) {
		checkTimeout := dependencies.Health.DependencyCheckTimeout
		if checkTimeout <= 0 {
			checkTimeout = defaultReadyzDependencyCheckTimeout
		}
		pingCtx, cancel := context.WithTimeout(ctx.Request.Context(), checkTimeout)
		defer cancel()
		if dependencies.Health.CoreDBAdmin == nil {
			ctx.JSON(http.StatusServiceUnavailable, gin.H{"status": "degraded", "error": "core database admin is not configured"})
			return
		}
		if dependencies.Health.CoreDB == nil {
			ctx.JSON(http.StatusServiceUnavailable, gin.H{"status": "degraded", "error": "core database is not configured"})
			return
		}
		if err := dependencies.Health.CoreDB.Ping(pingCtx); err != nil {
			ctx.JSON(http.StatusServiceUnavailable, gin.H{"status": "degraded", "error": err.Error()})
			return
		}
		if err := dependencies.Health.CoreDBAdmin.RequireSchemaCurrent(pingCtx, dependencies.Health.CoreDB); err != nil {
			ctx.JSON(http.StatusServiceUnavailable, gin.H{"status": "degraded", "error": err.Error()})
			return
		}
		if dependencies.Health.AdditionalReady != nil {
			if err := dependencies.Health.AdditionalReady(pingCtx); err != nil {
				ctx.JSON(http.StatusServiceUnavailable, gin.H{"status": "degraded", "error": err.Error()})
				return
			}
		}
		ctx.JSON(http.StatusOK, gin.H{"status": "ready", "profile": string(profile)})
	})

	registerRoutesForProfile(router, profile, dependencies)

	notFound := newNoRouteHandler(profile, dependencies.Web)
	// Only the all-in-one profile retains the unqualified AGS Smart-HTTP
	// compatibility surface. Split Central and Workspace profiles keep their SPA
	// behavior but fail unmatched Git paths closed until a product workspace
	// locator is activated.
	if profile == ProfileAll {
		router.NoRoute(forwarder.SmartHTTPNoRouteHandler(dependencies.API.APIForwarder, notFound))
	} else {
		router.NoRoute(notFound)
	}
	router.NoMethod(func(ctx *gin.Context) {
		ctx.JSON(http.StatusMethodNotAllowed, gin.H{"error": "method not allowed"})
	})

	return router
}

func jsonNotFound(ctx *gin.Context) {
	ctx.JSON(http.StatusNotFound, gin.H{"error": "not found"})
}

type webDistServer struct {
	distDir   string
	indexPath string
	files     http.Handler
}

func newWebDistServer(distDir string) (*webDistServer, error) {
	trimmed := strings.TrimSpace(distDir)
	if trimmed == "" {
		return nil, fmt.Errorf("AGENTTEAM_WEB_DIST_DIR is required when AGENTTEAM_WEB_SERVE_ENABLED=true")
	}
	info, err := os.Stat(trimmed)
	if err != nil {
		return nil, fmt.Errorf("stat AGENTTEAM_WEB_DIST_DIR %q: %w", trimmed, err)
	}
	if !info.IsDir() {
		return nil, fmt.Errorf("AGENTTEAM_WEB_DIST_DIR %q is not a directory", trimmed)
	}
	indexPath := filepath.Join(trimmed, "index.html")
	if indexInfo, err := os.Stat(indexPath); err != nil {
		return nil, fmt.Errorf("stat web index %q: %w", indexPath, err)
	} else if indexInfo.IsDir() {
		return nil, fmt.Errorf("web index %q is a directory", indexPath)
	}
	return &webDistServer{
		distDir:   trimmed,
		indexPath: indexPath,
		files:     http.FileServer(http.Dir(trimmed)),
	}, nil
}

func (server *webDistServer) Serve(ctx *gin.Context) {
	requestPath := ctx.Request.URL.Path
	if ctx.Request.Method != http.MethodGet && ctx.Request.Method != http.MethodHead {
		jsonNotFound(ctx)
		return
	}
	if isBackendReservedPath(requestPath) {
		jsonNotFound(ctx)
		return
	}
	if server.hasStaticFile(requestPath) {
		setWebDistCacheHeader(ctx, requestPath)
		server.files.ServeHTTP(ctx.Writer, ctx.Request)
		return
	}
	if isWebDistImmutableAssetPath(requestPath) {
		ctx.Header("Cache-Control", "no-cache")
		jsonNotFound(ctx)
		return
	}
	ctx.Header("Cache-Control", "no-cache")
	ctx.File(server.indexPath)
}

func setWebDistCacheHeader(ctx *gin.Context, requestPath string) {
	if isWebDistImmutableAssetPath(requestPath) {
		ctx.Header("Cache-Control", "public, max-age=31536000, immutable")
		return
	}
	ctx.Header("Cache-Control", "no-cache")
}

func isWebDistImmutableAssetPath(requestPath string) bool {
	return requestPath == "/assets" || strings.HasPrefix(requestPath, "/assets/")
}

func (server *webDistServer) hasStaticFile(requestPath string) bool {
	cleanPath := filepath.Clean("/" + requestPath)
	filePath := filepath.Join(server.distDir, strings.TrimPrefix(cleanPath, "/"))
	info, err := os.Stat(filePath)
	return err == nil && !info.IsDir()
}

func registerRoutesForProfile(router *gin.Engine, profile Profile, dependencies Dependencies) {
	switch profile {
	case ProfileAll:
		dependencies.API.APIForwarder.EnforceRouteWorkspaceOwner = false
		registerWorkspaceAGSRoute(router, dependencies)
		registerControlRoutesWithoutForwarder(router, dependencies)
		registerWorkspaceRoutes(router, dependencies)
		registerRuntimeCompatibilityRoutes(router, dependencies)
		registerExtensionRoutes(router, dependencies)
	case ProfileCentral:
		registerControlRoutesWithoutForwarder(router, dependencies)
	case ProfileWorkspace:
		// Profile selection, not caller-supplied dependency state, owns whether
		// unqualified AGS compatibility routes exist. This keeps route inventory
		// and production wiring aligned and makes the split profile fail closed.
		dependencies.API.APIForwarder.EnforceRouteWorkspaceOwner = true
		registerMeshXInternalAdminRoutes(router, dependencies.API.MeshXInternalAdmin)
		registerWorkspaceAGSRoute(router, dependencies)
		registerWorkspaceRoutes(router, dependencies)
		registerRuntimeCompatibilityRoutes(router, dependencies)
	case ProfileExtension:
		// The Extension profile registers ONLY the Extension runtime routes.
		// Common health/observability routes are already registered by
		// NewRouterForProfile. It MUST NOT register Web, IM, daemon WebSocket,
		// LLM, workflow, or Workspace routes (spec §6.1).
		registerExtensionRoutes(router, dependencies)
	default:
		// Fail-closed: an unknown profile registers NO domain routes. Only the
		// common health/observability routes registered in NewRouterForProfile
		// serve. This prevents a typo or future profile from accidentally
		// inheriting the full route set.
	}
}

func registerWorkspaceAGSRoute(router *gin.Engine, dependencies Dependencies) {
	handler := dependencies.API.MeshXWorkspaceAGS
	if handler == nil {
		return
	}
	handler = forwarder.WorkspaceAGSSessionBridge(dependencies.API.APIForwarder, handler)
	router.Any("/api/v1/workspaces/:workspaceId/ags/*path", func(ctx *gin.Context) {
		handler.ServeHTTP(ctx.Writer, ctx.Request)
		ctx.Abort()
	})
}

func registerControlRoutesWithoutForwarder(router *gin.Engine, dependencies Dependencies) {
	public.RegisterRoutes(router, dependencies.API.Public)
	auth.RegisterRoutes(router, dependencies.API.Auth)
	billing.RegisterRoutes(router, dependencies.API.Billing)
	admin.RegisterRoutes(router, dependencies.API.Admin)
	admin.RegisterInternalRoutes(router, dependencies.API.Admin)
	registerMeshXInternalAdminRoutes(router, dependencies.API.MeshXInternalAdmin)
	workspace.RegisterControlRoutes(router, dependencies.API.Workspace)
	org.RegisterRoutes(router, dependencies.API.Org)
	media.RegisterControlRoutes(router, dependencies.API.Media)
	provider.RegisterControlRoutes(router, dependencies.API.Provider)
	webagent.RegisterControlRoutes(router, dependencies.API.WebAgent)
}

func registerMeshXInternalAdminRoutes(router *gin.Engine, handler http.Handler) {
	if router == nil || handler == nil {
		return
	}
	wrapped := func(ctx *gin.Context) {
		handler.ServeHTTP(ctx.Writer, ctx.Request)
		ctx.Abort()
	}
	router.GET("/internal/admin/public-repository-caches", wrapped)
	router.POST("/internal/admin/public-repository-caches", wrapped)
	router.PATCH("/internal/admin/public-repository-caches/:id", wrapped)
	router.POST("/internal/admin/public-repository-caches/:id/build", wrapped)
	router.POST("/internal/admin/public-repository-caches/:id/cancel", wrapped)
	router.DELETE("/internal/admin/public-repository-caches/:id", wrapped)
}

func registerWorkspaceRoutes(router *gin.Engine, dependencies Dependencies) {
	lookup.RegisterRoutes(router, dependencies.API.RuntimeIdentity)
	workspace.RegisterWorkspaceRoutes(router, dependencies.API.Workspace)
	media.RegisterWorkspaceRoutes(router, dependencies.API.Media)
	onboarding.RegisterRoutes(router, dependencies.API.Onboarding)
	channel.RegisterRoutes(router, dependencies.API.Channel)
	im.RegisterRoutes(router, dependencies.API.IM)
	im.RegisterWebSocketRoutes(router, dependencies.API.IM)
	task.RegisterRoutes(router, dependencies.API.Task)
	team.RegisterRoutes(router, dependencies.API.Team)
	teamworkflow.RegisterRoutes(router, dependencies.API.TeamWorkflow)
	schedule.RegisterRoutes(router, dependencies.API.Schedule)
	serviceaccount.RegisterRoutes(router, dependencies.API.ServiceAccount)
	trigger.RegisterRoutes(router, dependencies.API.Trigger)
	binding.RegisterRoutes(router, dependencies.API.WorkflowBinding)
	invocation.RegisterRoutes(router, dependencies.API.WorkflowInvocation)
	plan.RegisterRoutes(router, dependencies.API.WorkflowPlan)
	run.RegisterRoutes(router, dependencies.API.WorkflowRun)
	app.RegisterWorkspaceRoutes(router, dependencies.API.App)
	mailapi.RegisterWorkspaceRoutes(router, dependencies.API.Mail)
	provider.RegisterWorkspaceRoutes(router, dependencies.API.Provider)
	webagent.RegisterWorkspaceRoutes(router, dependencies.API.WebAgent)
	knowledgeqa.RegisterRoutes(router, dependencies.API.KnowledgeQA)
	forwarder.RegisterRoutes(router, dependencies.API.APIForwarder)
}

func registerRuntimeCompatibilityRoutes(router *gin.Engine, dependencies Dependencies) {
	app.RegisterIngressRoutes(router, dependencies.API.App)
	llm.RegisterRoutes(router, dependencies.API.LLM)
	agentDeps := dependencies.Agent
	agentDeps.Auth = managedAgentAuthenticator{next: agentDeps.Auth, managed: dependencies.ManagedWorkspaces}
	agent.RegisterRoutes(router, agentDeps)
	daemon.RegisterRoutes(router, dependencies.Daemon.Hub, managedMachineAuthenticator{next: dependencies.Daemon.Auth, managed: dependencies.ManagedWorkspaces}, dependencies.Daemon.SessionIssuer, daemon.RegisterRoutesOptions{
		LatestDesktopVersion: dependencies.Daemon.LatestDesktopVersion,
		MinDesktopVersion:    dependencies.Daemon.MinDesktopVersion,
	})
}

type managedAgentAuthenticator struct {
	next    agent.AgentAuthenticator
	managed owner.ManagedWorkspaceSet
}

func (auth managedAgentAuthenticator) AuthenticateAgent(ctx context.Context, agentID string, bearerToken string) (storeagent.AuthenticatedAgent, error) {
	if auth.next == nil {
		return storeagent.AuthenticatedAgent{}, api.ErrUnauthorized
	}
	authenticated, err := auth.next.AuthenticateAgent(ctx, agentID, bearerToken)
	if err != nil {
		return storeagent.AuthenticatedAgent{}, err
	}
	if err := api.RequireWorkspaceOwner(auth.managed, authenticated.WorkspaceID); err != nil {
		return storeagent.AuthenticatedAgent{}, err
	}
	return authenticated, nil
}

type managedMachineAuthenticator struct {
	next    daemon.MachineAuthenticator
	managed owner.ManagedWorkspaceSet
}

func (auth managedMachineAuthenticator) AuthenticateMachine(ctx context.Context, apiKey string) (storeagent.MachineIdentity, error) {
	if auth.next == nil {
		return storeagent.MachineIdentity{}, api.ErrUnauthorized
	}
	identity, err := auth.next.AuthenticateMachine(ctx, apiKey)
	if err != nil {
		return storeagent.MachineIdentity{}, err
	}
	if err := api.RequireWorkspaceOwner(auth.managed, identity.WorkspaceID); err != nil {
		return storeagent.MachineIdentity{}, err
	}
	return identity, nil
}

func (auth managedMachineAuthenticator) IssueAgentAuthTokenForMachine(ctx context.Context, machineID string, agentID string, allowWorkspaceWrite bool) (string, error) {
	if auth.next == nil {
		return "", api.ErrUnauthorized
	}
	return auth.next.IssueAgentAuthTokenForMachine(ctx, machineID, agentID, allowWorkspaceWrite)
}

func requestLogger(logger *slog.Logger) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		startedAt := time.Now()
		defer func() {
			recovered := recover()
			status := ctx.Writer.Status()
			if recovered != nil {
				status = http.StatusInternalServerError
			}
			if logger != nil {
				duration := time.Since(startedAt)
				actualPath := ""
				if ctx.Request != nil && ctx.Request.URL != nil {
					actualPath = ctx.Request.URL.Path
				}
				logAttrs := []slog.Attr{
					slog.String("method", ctx.Request.Method),
					slog.String("path", actualPath),
					slog.Int("status", status),
					slog.Duration("duration", duration),
				}
				logger.LogAttrs(
					requestContext(ctx),
					slog.LevelInfo,
					"http request",
					logAttrs...,
				)
				if observed, ok := api.ObservedServerErrorFromContext(ctx); ok && recovered == nil {
					failedAttrs := []slog.Attr{
						slog.String("method", ctx.Request.Method),
						slog.String("path", actualPath),
						slog.Int("status", observed.Status),
						slog.Duration("duration", duration),
						slog.Any("error", observed.Err),
					}
					logger.LogAttrs(
						requestContext(ctx),
						slog.LevelError,
						"http request failed",
						failedAttrs...,
					)
				}
			}
			if recovered != nil {
				panic(recovered)
			}
		}()
		ctx.Next()
	}
}

func recoveryMiddleware(logger *slog.Logger, httpMetrics HTTPMetrics) gin.HandlerFunc {
	return gin.CustomRecovery(func(ctx *gin.Context, recovered any) {
		if httpMetrics != nil {
			httpMetrics.RecordPanic(ctx.Request.Method, ctx.FullPath())
		}
		if logger != nil {
			logger.ErrorContext(requestContext(ctx), "http request panic recovered", slog.Any("panic", recovered))
		}
		ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
	})
}
