package httpapi

import (
	"context"
	"errors"
	"log/slog"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/config"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/identity"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/identityruntime"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/oidcsession"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/runner"
	ssfreceiver "github.com/easyai/easyai-ai-gateway/apps/api/internal/securityevents"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

type Server struct {
	ctx                        context.Context
	cfg                        config.Config
	store                      *store.Store
	coordinationStore          *store.Store
	riverStore                 *store.Store
	oidcUserResolver           oidcUserResolver
	auth                       *auth.Authenticator
	oidcClient                 oidcPublicClient
	oidcSessions               oidcSessionManager
	oidcSessionCipher          *oidcsession.Cipher
	runner                     *runner.Service
	logger                     *slog.Logger
	geminiUploadSessions       sync.Map
	requestAssetLocksMu        sync.Mutex
	requestAssetLocks          map[string]*requestAssetLock
	mediaRequestSlots          chan struct{}
	mediaRequestBodySlots      chan struct{}
	securityEventReceiver      http.Handler
	securityEventManager       *ssfreceiver.ConnectionManager
	identityRuntime            *identityruntime.Manager
	identityPairing            *identity.PairingService
	identityManagementMu       sync.Mutex
	securityEventManagementMu  sync.Mutex
	identityPairingWorkers     sync.Map
	identityCleanupWorkers     sync.Map
	identityRestoredPairings   sync.Map
	identityTestRevision       identity.Revision
	identityTestCookieSecure   bool
	identityTestBrowserEnabled bool
	billingMetrics             *ssfreceiver.Metrics
	taskTrafficCacheMu         sync.Mutex
	taskTrafficCache           map[taskTrafficAuthorizationCacheKey]taskTrafficAuthorizationCacheEntry
	taskTrafficInflight        map[taskTrafficAuthorizationCacheKey]*taskTrafficAuthorizationCall
}

type oidcPublicClient interface {
	AuthorizationURL(context.Context, string, string, string, string, string) (string, error)
	ExchangeCode(context.Context, string, string) (auth.OIDCTokenResponse, error)
	VerifyIDToken(context.Context, string, string) (string, error)
	Refresh(context.Context, string) (auth.OIDCTokenResponse, error)
	RevokeRefreshToken(context.Context, string) error
	EndSessionURL(context.Context, string) (string, error)
}

type oidcSessionManager interface {
	Create(context.Context, oidcsession.TokenBundle, *auth.User) (string, error)
	Resolve(context.Context, string) (*auth.User, error)
	Delete(context.Context, string) (oidcsession.TokenBundle, error)
	Cleanup(context.Context) (int64, error)
}

func NewServer(cfg config.Config, db *store.Store, logger *slog.Logger) http.Handler {
	return NewServerWithContext(context.Background(), cfg, db, logger)
}

func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Store, logger *slog.Logger) http.Handler {
	return NewServerWithContextAndCoordinationStore(ctx, cfg, db, db, logger)
}

func NewServerWithContextAndCoordinationStore(
	ctx context.Context,
	cfg config.Config,
	db *store.Store,
	coordinationDB *store.Store,
	logger *slog.Logger,
) http.Handler {
	return NewServerWithStores(ctx, cfg, db, coordinationDB, db, logger)
}

func NewServerWithStores(
	ctx context.Context,
	cfg config.Config,
	db *store.Store,
	coordinationDB *store.Store,
	riverDB *store.Store,
	logger *slog.Logger,
) http.Handler {
	if cfg.MediaMaterializationConcurrency == 0 {
		cfg.MediaMaterializationConcurrency = 8
	}
	if cfg.MediaRequestConcurrency == 0 {
		cfg.MediaRequestConcurrency = 16
	}
	securityEventMetrics := &ssfreceiver.Metrics{}
	server := &Server{
		ctx:                   ctx,
		cfg:                   cfg,
		store:                 db,
		coordinationStore:     coordinationDB,
		riverStore:            riverDB,
		oidcUserResolver:      db,
		auth:                  auth.New(cfg.JWTSecret, cfg.ServerMainBaseURL, cfg.ServerMainInternalToken),
		runner:                runner.NewWithStores(cfg, db, coordinationDB, riverDB, logger, securityEventMetrics),
		logger:                logger,
		billingMetrics:        securityEventMetrics,
		requestAssetLocks:     map[string]*requestAssetLock{},
		mediaRequestSlots:     make(chan struct{}, cfg.MediaMaterializationConcurrency),
		mediaRequestBodySlots: make(chan struct{}, cfg.MediaRequestConcurrency),
	}
	server.auth.ServerMainInternalKey = cfg.ServerMainInternalKey
	server.auth.ServerMainInternalSecret = cfg.ServerMainInternalSecret
	if cfg.RunsPublicHTTP() {
		server.initializeIdentityRuntime(ctx, db, securityEventMetrics)
	}
	server.auth.LocalAPIKeyVerifier = db.VerifyLocalAPIKey
	server.runner.StartAdmissionNotifier(ctx)
	if cfg.RunsPublicHTTP() {
		server.runner.StartTaskCompletionWaiter(ctx)
	}
	if cfg.RunsAsyncExecutionWorker() {
		server.runner.StartAsyncQueueWorker(ctx)
	} else {
		server.runner.StartAsyncQueueClient(ctx)
		if logger != nil {
			logger.Info("asynchronous queue worker disabled for this process")
		}
	}
	server.runner.StartRouteHealthProber(ctx)
	if cfg.RunsAsyncAdmissionDispatcher() {
		server.runner.StartAsyncAdmissionDispatcher(ctx)
	}
	if cfg.RunsBackgroundWorkers() {
		server.runner.StartBillingSettlementWorker(ctx)
		server.runner.StartTaskHistoryWorkers(ctx)
		server.startLocalTempAssetCleanup(ctx)
		server.startOIDCSessionCleanup(ctx)
	}

	mux := http.NewServeMux()
	mux.HandleFunc("GET /healthz", server.health)
	mux.HandleFunc("GET /readyz", server.ready)
	mux.HandleFunc("GET /api/v1/healthz", server.health)
	mux.HandleFunc("GET /api/v1/readyz", server.ready)
	mux.Handle("GET /metrics", securityEventMetrics.DynamicHandler(postgresMetricsProvider{
		Store: db, critical: coordinationDB, river: riverDB,
	}))
	if cfg.RunsAsyncExecutionWorker() {
		mux.HandleFunc("POST /internal/v1/executions", server.internalExecution)
	}
	if !cfg.RunsPublicHTTP() {
		return server.recover(mux)
	}
	mux.HandleFunc("GET /static/simulation/{asset}", serveSimulationAsset)
	mux.HandleFunc("GET /static/generated/{asset}", server.serveGeneratedStaticAsset)
	mux.HandleFunc("GET /static/uploaded/{asset}", server.serveUploadedStaticAsset)
	mux.HandleFunc("GET /api-docs-json", server.apiDocsJSON)
	mux.HandleFunc("GET /api-docs-yaml", server.apiDocsYAML)
	mux.HandleFunc("GET /api/v1/openapi.json", server.apiDocsJSON)
	mux.HandleFunc("GET /api/v1/openapi.yaml", server.apiDocsYAML)
	mux.HandleFunc("GET /api/v1/public/skills/ai-gateway-ops-management/metadata", server.getOpsManagementSkillMetadata)
	mux.HandleFunc("GET /api/v1/public/skills/ai-gateway-ops-management/download", server.downloadOpsManagementSkill)

	mux.Handle("POST /api/v1/auth/register", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.register)))
	mux.Handle("POST /api/v1/auth/login", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.login)))
	mux.HandleFunc("GET /api/v1/auth/oidc/login", server.startOIDCLogin)
	mux.HandleFunc("GET /api/v1/auth/oidc/callback", server.completeOIDCLogin)
	mux.HandleFunc("POST /api/v1/auth/oidc/logout", server.logoutOIDCSession)
	mux.HandleFunc("DELETE /api/v1/auth/oidc/session", server.deleteOIDCBrowserSession)
	mux.HandleFunc("GET /api/v1/public/identity", server.getPublicIdentityConfiguration)
	mux.HandleFunc("POST /api/v1/security-events/ssf", server.receiveSecurityEvent)
	mux.Handle("GET /api/v1/me", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.me)))
	mux.Handle("GET /api/v1/public/catalog/providers", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.listCatalogProviders)))
	mux.Handle("GET /api/v1/public/catalog/base-models", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.listBaseModels)))
	mux.Handle("GET /api/v1/public/client-customization", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.getPublicClientCustomizationSettings)))
	mux.Handle("GET /api/admin/catalog/providers", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listCatalogProviders)))
	mux.Handle("POST /api/admin/catalog/providers", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createCatalogProvider)))
	mux.Handle("PATCH /api/admin/catalog/providers/{providerID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateCatalogProvider)))
	mux.Handle("DELETE /api/admin/catalog/providers/{providerID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteCatalogProvider)))
	mux.Handle("GET /api/admin/catalog/base-models", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listBaseModels)))
	mux.Handle("POST /api/admin/catalog/base-models", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createBaseModel)))
	mux.Handle("POST /api/admin/catalog/base-models/reset-all", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.resetAllBaseModels)))
	mux.Handle("PATCH /api/admin/catalog/base-models/{baseModelID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateBaseModel)))
	mux.Handle("POST /api/admin/catalog/base-models/{baseModelID}/reset", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.resetBaseModel)))
	mux.Handle("DELETE /api/admin/catalog/base-models/{baseModelID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteBaseModel)))
	mux.Handle("GET /api/admin/tenants", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listTenants)))
	mux.Handle("POST /api/admin/tenants", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createTenant)))
	mux.Handle("PATCH /api/admin/tenants/{tenantID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateTenant)))
	mux.Handle("DELETE /api/admin/tenants/{tenantID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteTenant)))
	mux.Handle("GET /api/admin/users", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listUsers)))
	mux.Handle("POST /api/admin/users", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createGatewayUser)))
	mux.Handle("POST /api/admin/users/batch", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.batchGatewayUsers)))
	mux.Handle("PATCH /api/admin/users/{userID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateGatewayUser)))
	mux.Handle("PATCH /api/admin/users/{userID}/wallet", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.setUserWalletBalance)))
	mux.Handle("POST /api/admin/users/{userID}/wallet/recharge", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.rechargeUserWalletBalance)))
	mux.Handle("DELETE /api/admin/users/{userID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteGatewayUser)))
	mux.Handle("GET /api/admin/audit-logs", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listAuditLogs)))
	mux.Handle("GET /api/admin/user-groups", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listUserGroups)))
	mux.Handle("POST /api/admin/user-groups", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createUserGroup)))
	mux.Handle("POST /api/admin/user-groups/batch", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.batchUserGroups)))
	mux.Handle("PATCH /api/admin/user-groups/{groupID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateUserGroup)))
	mux.Handle("DELETE /api/admin/user-groups/{groupID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteUserGroup)))
	mux.Handle("GET /api/admin/access-rules", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listAccessRules)))
	mux.Handle("POST /api/admin/access-rules", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createAccessRule)))
	mux.Handle("POST /api/admin/access-rules/batch", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.batchAccessRules)))
	mux.Handle("PATCH /api/admin/access-rules/{ruleID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateAccessRule)))
	mux.Handle("DELETE /api/admin/access-rules/{ruleID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteAccessRule)))
	mux.Handle("GET /api/admin/tasks", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listAdminTasks)))
	mux.Handle("GET /api/admin/tasks/{taskID}", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getAdminTask)))
	mux.Handle("GET /api/admin/tasks/{taskID}/param-preprocessing", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.adminTaskParamPreprocessing)))
	mux.Handle("GET /api/v1/api-keys", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listAPIKeys)))
	mux.Handle("POST /api/v1/api-keys", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.createAPIKey)))
	mux.Handle("GET /api/v1/api-keys/access-rules", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listAPIKeyAccessRules)))
	mux.Handle("POST /api/v1/api-keys/access-rules/batch", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.batchAPIKeyAccessRules)))
	mux.Handle("GET /api/v1/api-keys/assignable-models", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listAPIKeyAssignableModels)))
	mux.Handle("GET /api/v1/api-keys/{apiKeyID}/assignable-models", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listAPIKeyAssignableModelsForKey)))
	mux.Handle("PATCH /api/v1/api-keys/{apiKeyID}/scopes", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.updateAPIKeyScopes)))
	mux.Handle("PATCH /api/v1/api-keys/{apiKeyID}/disable", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.disableAPIKey)))
	mux.Handle("DELETE /api/v1/api-keys/{apiKeyID}", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.deleteAPIKey)))
	mux.Handle("GET /api/playground/api-keys", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listPlayableAPIKeys)))
	mux.Handle("GET /api/workspace/desktop-config", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.getDesktopConfig)))
	mux.Handle("GET /api/workspace/user-groups", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listCurrentUserGroups)))
	mux.Handle("GET /api/workspace/wallet", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.getWallet)))
	mux.Handle("GET /api/workspace/wallet/transactions", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listWalletTransactions)))
	mux.Handle("GET /api/workspace/token-usage/daily", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.dailyTokenUsage)))
	mux.Handle("GET /api/workspace/tasks", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listTasks)))
	mux.Handle("GET /api/workspace/tasks/{taskID}", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.getTask)))
	mux.Handle("POST /api/workspace/tasks/{taskID}/cancel", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.cancelTask)))
	mux.Handle("GET /api/workspace/tasks/{taskID}/param-preprocessing", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.taskParamPreprocessing)))
	mux.Handle("GET /api/workspace/tasks/{taskID}/events", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.taskEvents)))
	mux.Handle("GET /api/admin/pricing/rules", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listPricingRules)))
	mux.Handle("GET /api/admin/pricing/rule-sets", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listPricingRuleSets)))
	mux.Handle("POST /api/admin/pricing/rule-sets", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createPricingRuleSet)))
	mux.Handle("PATCH /api/admin/pricing/rule-sets/{ruleSetID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updatePricingRuleSet)))
	mux.Handle("DELETE /api/admin/pricing/rule-sets/{ruleSetID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deletePricingRuleSet)))
	mux.Handle("POST /api/v1/pricing/estimate", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.estimatePricing)))
	mux.Handle("GET /api/admin/runtime/policy-sets", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listRuntimePolicySets)))
	mux.Handle("POST /api/admin/runtime/policy-sets", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createRuntimePolicySet)))
	mux.Handle("PATCH /api/admin/runtime/policy-sets/{policySetID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateRuntimePolicySet)))
	mux.Handle("DELETE /api/admin/runtime/policy-sets/{policySetID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteRuntimePolicySet)))
	mux.Handle("GET /api/admin/runtime/runner-policy", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getRunnerPolicy)))
	mux.Handle("GET /api/admin/runtime/execution-pools", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listExecutionPools)))
	mux.Handle("PATCH /api/admin/runtime/runner-policy", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateRunnerPolicy)))
	mux.Handle("GET /api/admin/runtime/billing-settlements", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listBillingSettlements)))
	mux.Handle("POST /api/admin/runtime/billing-settlements/{settlementId}/retry", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.retryBillingSettlement)))
	mux.Handle("POST /api/admin/runtime/model-rate-limits/{platformModelID}/restore", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.restorePlatformModelRuntimeStatus)))
	mux.Handle("GET /api/admin/config/network-proxy", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getNetworkProxyConfig)))
	mux.Handle("GET /api/admin/system/file-storage/settings", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getFileStorageSettings)))
	mux.Handle("PATCH /api/admin/system/file-storage/settings", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateFileStorageSettings)))
	mux.Handle("GET /api/admin/system/client-customization/settings", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getClientCustomizationSettings)))
	mux.Handle("PATCH /api/admin/system/client-customization/settings", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateClientCustomizationSettings)))
	mux.Handle("GET /api/admin/system/acceptance/traffic-mode", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getGatewayTrafficMode)))
	mux.Handle("POST /api/admin/system/acceptance/traffic-mode/pause", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.pauseGatewayTraffic)))
	mux.Handle("POST /api/admin/system/acceptance/runs", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createAcceptanceRun)))
	mux.Handle("GET /api/admin/system/acceptance/runs/{runID}", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getAcceptanceRun)))
	mux.Handle("POST /api/admin/system/acceptance/runs/{runID}/activate", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.activateAcceptanceRun)))
	mux.Handle("POST /api/admin/system/acceptance/runs/{runID}/capacity-profiles", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.stageAcceptanceCapacityProfiles)))
	mux.Handle("POST /api/admin/system/acceptance/runs/{runID}/finish", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.finishAcceptanceRun)))
	mux.Handle("POST /api/admin/system/acceptance/runs/{runID}/retry", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.retryAcceptanceRun)))
	mux.Handle("POST /api/admin/system/acceptance/runs/{runID}/promote", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.promoteAcceptanceRun)))
	mux.Handle("POST /api/admin/system/acceptance/runs/{runID}/abort", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.abortAcceptanceRun)))
	mux.Handle("GET /api/admin/system/acceptance/capacity-profiles", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listCapacityProfiles)))
	mux.Handle("GET /api/admin/runtime/workers", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getWorkerClusterRuntime)))
	mux.Handle("GET /api/admin/system/identity/configuration", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getIdentityConfiguration)))
	mux.Handle("POST /api/admin/system/identity/pairings", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.startIdentityPairing)))
	mux.Handle("GET /api/admin/system/identity/pairings/{pairingID}", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getIdentityPairing)))
	mux.Handle("POST /api/admin/system/identity/pairings/{pairingID}/cancel", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.cancelIdentityPairing)))
	mux.Handle("POST /api/admin/system/identity/pairings/{pairingID}/retire-conflicting-security-event", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.retireIdentityPairingSecurityEventConflict)))
	mux.Handle("PATCH /api/admin/system/identity/revisions/{revisionID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateIdentityDraftPolicy)))
	mux.Handle("POST /api/admin/system/identity/revisions/{revisionID}/validate", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.validateIdentityRevision)))
	mux.Handle("POST /api/admin/system/identity/revisions/{revisionID}/activate", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.activateIdentityRevision)))
	mux.Handle("POST /api/admin/system/identity/revisions/{revisionID}/rollback", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.rollbackIdentityRevision)))
	mux.Handle("POST /api/admin/system/identity/disable", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.disableIdentityConfiguration)))
	mux.Handle("GET /api/admin/system/identity/security-events/connection", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getSecurityEventConnection)))
	mux.Handle("PUT /api/admin/system/identity/security-events/connection", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.putSecurityEventConnection)))
	mux.Handle("POST /api/admin/system/identity/security-events/connection/verify", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.verifySecurityEventConnection)))
	mux.Handle("POST /api/admin/system/identity/security-events/connection/rotate-credential", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.rotateSecurityEventConnectionCredential)))
	mux.Handle("DELETE /api/admin/system/identity/security-events/connection", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteSecurityEventConnection)))
	mux.Handle("GET /api/admin/system/file-storage/channels", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listFileStorageChannels)))
	mux.Handle("POST /api/admin/system/file-storage/channels", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createFileStorageChannel)))
	mux.Handle("PATCH /api/admin/system/file-storage/channels/{channelID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateFileStorageChannel)))
	mux.Handle("POST /api/admin/system/file-storage/channels/{channelID}/test", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.testFileStorageChannel)))
	mux.Handle("DELETE /api/admin/system/file-storage/channels/{channelID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteFileStorageChannel)))
	mux.Handle("GET /api/admin/platforms", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listPlatforms)))
	mux.Handle("POST /api/admin/platforms", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createPlatform)))
	mux.Handle("PATCH /api/admin/platforms/{platformID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updatePlatform)))
	mux.Handle("PATCH /api/admin/platforms/{platformID}/dynamic-priority", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updatePlatformDynamicPriority)))
	mux.Handle("DELETE /api/admin/platforms/{platformID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deletePlatform)))
	mux.Handle("PUT /api/admin/platforms/{platformID}/models", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.replacePlatformModels)))
	mux.Handle("POST /api/admin/platforms/{platformID}/models", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createPlatformModel)))
	mux.Handle("POST /api/admin/platform-models", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createPlatformModel)))
	mux.Handle("DELETE /api/admin/platform-models/{modelID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deletePlatformModel)))
	mux.Handle("GET /api/admin/models", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listModels)))
	mux.Handle("GET /api/v1/model-catalog", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listModelCatalog)))
	mux.Handle("GET /api/v1/platforms", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listPlayablePlatforms)))
	mux.Handle("GET /api/v1/platform-models", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listPlayableModels)))
	mux.Handle("GET /api/v1/models", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listLegacyPlayableModels)))
	mux.Handle("GET /api/v1/playground/models", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listPlayableModels)))
	mux.Handle("GET /v1/models", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listOpenAIModels)))
	mux.Handle("GET /api/admin/runtime/rate-limit-windows", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listRateLimitWindows)))
	mux.Handle("GET /api/admin/runtime/model-rate-limits", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listModelRateLimitStatuses)))
	mux.Handle("POST /api/v1/chat/completions", server.requireProtocolUser(clients.ProtocolOpenAIChatCompletions, server.createAPIV1ChatCompletions()))
	mux.Handle("POST /api/v1/responses", server.requireProtocolUser(clients.ProtocolOpenAIResponses, server.createTask("responses", true)))
	mux.Handle("POST /api/v1/embeddings", server.requireProtocolUser(clients.ProtocolOpenAIEmbeddings, server.createTask("embeddings", true)))
	mux.Handle("POST /api/v1/reranks", server.requireUser(auth.PermissionBasic, server.createTask("reranks", true)))
	mux.Handle("POST /api/v1/images/generations", server.requireProtocolUser(clients.ProtocolOpenAIImages, server.createTask("images.generations", true)))
	mux.Handle("POST /api/v1/images/edits", server.requireProtocolUser(clients.ProtocolOpenAIImages, server.createTask("images.edits", true)))
	mux.Handle("POST /api/v1/images/vectorize", server.requireUser(auth.PermissionBasic, server.createImageVectorizeTask()))
	mux.Handle("POST /api/v1/videos/generations", server.requireUser(auth.PermissionBasic, server.createTask("videos.generations", true)))
	mux.Handle("POST /api/v1/videos/upscales", server.requireUser(auth.PermissionBasic, server.createVideoUpscaleTask()))
	mux.Handle("POST /api/v1/video/upscale", server.requireUser(auth.PermissionBasic, server.createVideoUpscaleTask()))
	mux.Handle("POST /api/v1/video/generations", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.createLegacyVolcesVideoGeneration)))
	mux.Handle("GET /api/v1/ai/result/{taskID}", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.getEasyAITaskResult)))
	mux.Handle("POST /api/v1/song/generations", server.requireUser(auth.PermissionBasic, server.createTask("song.generations", true)))
	mux.Handle("POST /api/v1/music/generations", server.requireUser(auth.PermissionBasic, server.createTask("music.generations", true)))
	mux.Handle("POST /api/v1/speech/generations", server.requireUser(auth.PermissionBasic, server.createTask("speech.generations", true)))
	mux.Handle("POST /api/v1/voice_clone", server.requireUser(auth.PermissionBasic, server.createTask("voice.clone", true)))
	mux.Handle("GET /api/v1/voice_clone/voices", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listClonedVoices)))
	mux.Handle("DELETE /api/v1/voice_clone/voices/{voiceID}", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.deleteClonedVoice)))
	mux.Handle("POST /api/v1/files/upload", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.uploadFile)))
	mux.Handle("GET /api/v1/resource/material/seedance-portrait-assets/capability", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.getSeedancePortraitAssetCapability)))
	mux.Handle("GET /api/v1/resource/material/user/materials", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listSeedancePortraitAssets)))
	mux.Handle("POST /api/v1/resource/material", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.createSeedancePortraitAsset)))
	mux.Handle("POST /api/v1/resource/material/seedance-portrait-assets/sync", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.syncSeedancePortraitAssets)))
	mux.Handle("POST /api/v3/contents/generations/tasks", server.requireProtocolUser(clients.ProtocolVolcesContents, http.HandlerFunc(server.createVolcesContentsGenerationTask)))
	mux.Handle("GET /api/v3/contents/generations/tasks", server.requireProtocolUser(clients.ProtocolVolcesContents, http.HandlerFunc(server.listVolcesContentsGenerationTasks)))
	mux.Handle("GET /api/v3/contents/generations/tasks/{taskID}", server.requireProtocolUser(clients.ProtocolVolcesContents, http.HandlerFunc(server.getVolcesContentsGenerationTask)))
	mux.Handle("DELETE /api/v3/contents/generations/tasks/{taskID}", server.requireProtocolUser(clients.ProtocolVolcesContents, http.HandlerFunc(server.deleteVolcesContentsGenerationTask)))
	mux.Handle("POST /api/v1/contents/generations/tasks", server.requireProtocolUser(clients.ProtocolVolcesContents, http.HandlerFunc(server.createVolcesContentsGenerationTask)))
	mux.Handle("GET /api/v1/contents/generations/tasks", server.requireProtocolUser(clients.ProtocolVolcesContents, http.HandlerFunc(server.listVolcesContentsGenerationTasks)))
	mux.Handle("GET /api/v1/contents/generations/tasks/{taskID}", server.requireProtocolUser(clients.ProtocolVolcesContents, http.HandlerFunc(server.getVolcesContentsGenerationTask)))
	mux.Handle("DELETE /api/v1/contents/generations/tasks/{taskID}", server.requireProtocolUser(clients.ProtocolVolcesContents, http.HandlerFunc(server.deleteVolcesContentsGenerationTask)))
	server.registerGeminiGenerateContentRoutes(mux)
	server.registerKlingCompatibilityRoutes(mux)
	mux.Handle("POST /upload/{version}/files", server.requireProtocolUser(clients.ProtocolGeminiGenerateContent, http.HandlerFunc(server.geminiFilesUpload)))
	mux.Handle("POST /upload/{version}/files/{uploadID}", server.requireProtocolUser(clients.ProtocolGeminiGenerateContent, http.HandlerFunc(server.geminiFilesUploadFinalize)))
	mux.Handle("POST /api/v1/gemini/upload/{version}/files", server.requireProtocolUser(clients.ProtocolGeminiGenerateContent, http.HandlerFunc(server.geminiFilesUpload)))
	mux.Handle("POST /api/v1/gemini/upload/{version}/files/{uploadID}", server.requireProtocolUser(clients.ProtocolGeminiGenerateContent, http.HandlerFunc(server.geminiFilesUploadFinalize)))
	mux.Handle("GET /api/v1/tasks", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listTasks)))
	mux.Handle("GET /api/v1/tasks/{taskID}", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.getTask)))
	mux.Handle("POST /api/v1/tasks/{taskID}/cancel", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.cancelTask)))
	mux.Handle("GET /api/v1/tasks/{taskID}/param-preprocessing", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.taskParamPreprocessing)))
	mux.Handle("GET /api/v1/tasks/{taskID}/events", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.taskEvents)))
	mux.Handle("GET /tasks", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listTasks)))
	mux.Handle("GET /tasks/{taskID}", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.getTask)))
	mux.Handle("POST /tasks/{taskID}/cancel", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.cancelTask)))
	mux.Handle("GET /tasks/{taskID}/param-preprocessing", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.taskParamPreprocessing)))
	mux.Handle("GET /tasks/{taskID}/events", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.taskEvents)))
	mux.Handle("POST /chat/completions", server.requireProtocolUser(clients.ProtocolOpenAIChatCompletions, server.createTask("chat.completions", true)))
	mux.Handle("POST /v1/chat/completions", server.requireProtocolUser(clients.ProtocolOpenAIChatCompletions, server.createTask("chat.completions", true)))
	mux.Handle("POST /responses", server.requireProtocolUser(clients.ProtocolOpenAIResponses, server.createTask("responses", true)))
	mux.Handle("POST /v1/responses", server.requireProtocolUser(clients.ProtocolOpenAIResponses, server.createTask("responses", true)))
	mux.Handle("POST /embeddings", server.requireProtocolUser(clients.ProtocolOpenAIEmbeddings, server.createTask("embeddings", true)))
	mux.Handle("POST /v1/embeddings", server.requireProtocolUser(clients.ProtocolOpenAIEmbeddings, server.createTask("embeddings", true)))
	mux.Handle("POST /reranks", server.requireUser(auth.PermissionBasic, server.createTask("reranks", true)))
	mux.Handle("POST /v1/reranks", server.requireUser(auth.PermissionBasic, server.createTask("reranks", true)))
	mux.Handle("POST /images/generations", server.requireProtocolUser(clients.ProtocolOpenAIImages, server.createTask("images.generations", true)))
	mux.Handle("POST /v1/images/generations", server.requireProtocolUser(clients.ProtocolOpenAIImages, server.createTask("images.generations", true)))
	mux.Handle("POST /images/edits", server.requireProtocolUser(clients.ProtocolOpenAIImages, server.createTask("images.edits", true)))
	mux.Handle("POST /v1/images/edits", server.requireProtocolUser(clients.ProtocolOpenAIImages, server.createTask("images.edits", true)))
	mux.Handle("POST /images/vectorize", server.requireUser(auth.PermissionBasic, server.createImageVectorizeTask()))
	mux.Handle("POST /v1/images/vectorize", server.requireUser(auth.PermissionBasic, server.createImageVectorizeTask()))
	mux.Handle("POST /video/upscale", server.requireUser(auth.PermissionBasic, server.createVideoUpscaleTask()))
	mux.Handle("POST /v1/video/upscale", server.requireUser(auth.PermissionBasic, server.createVideoUpscaleTask()))
	mux.Handle("POST /v1/videos/omni-video", server.requireKelingAPIKey(http.HandlerFunc(server.createKelingOmniVideo)))
	mux.Handle("GET /v1/videos/omni-video/{taskID}", server.requireKelingAPIKey(http.HandlerFunc(server.getKelingOmniVideo)))
	mux.Handle("POST /api/v1/videos/omni-video", server.requireKelingAPIKey(http.HandlerFunc(server.createKelingOmniVideo)))
	mux.Handle("GET /api/v1/videos/omni-video/{taskID}", server.requireKelingAPIKey(http.HandlerFunc(server.getKelingOmniVideo)))
	mux.Handle("POST /song/generations", server.requireUser(auth.PermissionBasic, server.createTask("song.generations", true)))
	mux.Handle("POST /v1/song/generations", server.requireUser(auth.PermissionBasic, server.createTask("song.generations", true)))
	mux.Handle("POST /music/generations", server.requireUser(auth.PermissionBasic, server.createTask("music.generations", true)))
	mux.Handle("POST /v1/music/generations", server.requireUser(auth.PermissionBasic, server.createTask("music.generations", true)))
	mux.Handle("POST /speech/generations", server.requireUser(auth.PermissionBasic, server.createTask("speech.generations", true)))
	mux.Handle("POST /v1/speech/generations", server.requireUser(auth.PermissionBasic, server.createTask("speech.generations", true)))
	mux.Handle("POST /voice_clone", server.requireUser(auth.PermissionBasic, server.createTask("voice.clone", true)))
	mux.Handle("POST /v1/voice_clone", server.requireUser(auth.PermissionBasic, server.createTask("voice.clone", true)))
	mux.Handle("GET /voice_clone/voices", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listClonedVoices)))
	mux.Handle("GET /v1/voice_clone/voices", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.listClonedVoices)))
	mux.Handle("DELETE /voice_clone/voices/{voiceID}", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.deleteClonedVoice)))
	mux.Handle("DELETE /v1/voice_clone/voices/{voiceID}", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.deleteClonedVoice)))
	mux.Handle("POST /v1/files/upload", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.uploadFile)))
	mux.Handle("POST /v1/tasks/{taskID}/cancel", server.requireUser(auth.PermissionBasic, http.HandlerFunc(server.cancelTask)))

	return server.recover(server.cors(server.protectOIDCSessionCookie(mux)))
}

type postgresMetricsProvider struct {
	*store.Store
	critical *store.Store
	river    *store.Store
}

func (provider postgresMetricsProvider) RiverPostgresPoolMetrics() store.PostgresPoolMetricsSnapshot {
	if provider.river == nil || provider.river == provider.Store {
		return store.PostgresPoolMetricsSnapshot{}
	}
	return provider.river.PostgresPoolMetrics()
}

func (provider postgresMetricsProvider) CriticalPostgresPoolMetrics() store.PostgresPoolMetricsSnapshot {
	if provider.critical == nil || provider.critical == provider.Store {
		return store.PostgresPoolMetricsSnapshot{}
	}
	return provider.critical.PostgresPoolMetrics()
}

func (server *Server) initializeIdentityRuntime(
	ctx context.Context,
	db *store.Store,
	securityEventMetrics *ssfreceiver.Metrics,
) {
	secretStore, err := identitySecretStore(server.cfg)
	if err != nil {
		panic("invalid identity SecretStore: " + err.Error())
	}
	go ssfreceiver.RunSecurityEventRetirementWorker(ctx, db)
	go ssfreceiver.RunIdentitySecretCleanupWorker(ctx, db, secretStore)
	runtimeBuilder := identityruntime.NewRuntimeBuilder(ctx, db, secretStore, identityruntime.RuntimeBuilderConfig{
		AppEnv: server.cfg.AppEnv, JWKSCacheTTL: 5 * time.Minute,
		HeartbeatInterval: time.Duration(server.cfg.IdentitySecurityEventHeartbeatIntervalSeconds) * time.Second,
		StaleAfter:        time.Duration(server.cfg.IdentitySecurityEventStaleAfterSeconds) * time.Second,
		ClockSkew:         time.Duration(server.cfg.IdentitySecurityEventClockSkewSeconds) * time.Second,
	}, securityEventMetrics)
	server.identityRuntime = identityruntime.NewManager(db, runtimeBuilder)
	if err := server.identityRuntime.LoadActive(ctx); err != nil && server.logger != nil {
		server.logger.Error(
			"load active identity runtime failed; local management login remains available",
			"error_category", "identity_runtime_load_failed",
			"error", err,
		)
	}
	server.identityPairing = identity.NewPairingService(db, secretStore, func(baseURL string) (identity.OnboardingRemote, error) {
		return identity.NewOnboardingClient(baseURL, nil, server.cfg.AppEnv)
	}, server.identityRuntime, server.cfg.AppEnv)
	go server.runIdentityPairingCoordinator()
	server.auth.OIDCVerifierProvider = func() *auth.OIDCVerifier {
		if runtime := server.identityRuntime.Current(); runtime != nil {
			return runtime.Verifier
		}
		return nil
	}
	server.auth.OIDCSessionResolverProvider = func(ctx context.Context, sessionID string) (*auth.User, error) {
		runtime := server.identityRuntime.Current()
		if runtime == nil || runtime.Sessions == nil {
			return nil, auth.ErrUnauthorized
		}
		user, resolveErr := runtime.Sessions.Resolve(ctx, sessionID)
		return user, oidcSessionRequestError(resolveErr)
	}
	server.auth.LegacyJWTEnabledProvider = func() bool {
		return server.identityRuntime.LegacyJWTEnabled()
	}
}

func identitySecretStore(cfg config.Config) (ssfreceiver.SecretStore, error) {
	switch strings.ToLower(strings.TrimSpace(cfg.IdentitySecretStore)) {
	case "", "file":
		directory := cfg.IdentitySecretDir
		if directory == "" {
			directory = ".local-secrets/identity"
		}
		return ssfreceiver.NewFileSecretStore(directory)
	case "kubernetes":
		return ssfreceiver.NewKubernetesSecretStore(ssfreceiver.KubernetesSecretStoreConfig{
			Namespace: cfg.IdentityKubernetesNamespace, SecretName: cfg.IdentityKubernetesSecretName,
			APIServer: cfg.IdentityKubernetesAPIServer, TokenFile: cfg.IdentityKubernetesTokenFile,
			CAFile: cfg.IdentityKubernetesCAFile,
		})
	default:
		return nil, errors.New("unsupported identity SecretStore")
	}
}

func oidcSessionRequestError(err error) error {
	switch {
	case err == nil:
		return nil
	case errors.Is(err, oidcsession.ErrSessionExpired):
		return auth.NewRequestAuthError(http.StatusUnauthorized, "OIDC_SESSION_EXPIRED", "登录会话已过期，请重新登录")
	case errors.Is(err, oidcsession.ErrSessionRefreshUnavailable):
		return auth.NewRequestAuthError(http.StatusServiceUnavailable, "OIDC_SESSION_REFRESH_UNAVAILABLE", "认证中心暂时不可用，请稍后重试")
	case errors.Is(err, oidcsession.ErrSessionStoreUnavailable):
		return auth.NewRequestAuthError(http.StatusServiceUnavailable, "OIDC_SESSION_STORE_UNAVAILABLE", "登录会话存储暂时不可用")
	case errors.Is(err, oidcsession.ErrSecurityStateUnavailable):
		return auth.NewRequestAuthError(http.StatusServiceUnavailable, "OIDC_SECURITY_EVENT_STATE_UNAVAILABLE", "认证撤销状态暂时不可用")
	case errors.Is(err, oidcsession.ErrGatewayUserDisabled):
		return auth.NewRequestAuthError(http.StatusForbidden, "GATEWAY_USER_DISABLED", "该 Gateway 账号已停用，请联系管理员")
	default:
		return auth.NewRequestAuthError(http.StatusUnauthorized, "OIDC_SESSION_INVALID", "登录会话无效，请重新登录")
	}
}

func (s *Server) requireAdmin(permission auth.Permission, next http.Handler) http.Handler {
	return s.requireUser(permission, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		user, _ := auth.UserFromContext(r.Context())
		if user != nil && strings.TrimSpace(user.APIKeyID) != "" {
			writeError(w, http.StatusForbidden, "admin api does not accept api key credentials")
			return
		}
		next.ServeHTTP(w, r)
	}))
}

func (s *Server) cors(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		origin := r.Header.Get("Origin")
		if origin != "" && s.corsOriginAllowed(origin) {
			w.Header().Set("Access-Control-Allow-Origin", origin)
			w.Header().Set("Vary", "Origin")
			w.Header().Set("Access-Control-Allow-Credentials", "true")
			w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Idempotency-Key, If-Match, X-Comfy-Api-Key, X-Goog-Api-Key, X-Goog-Upload-Protocol, X-Goog-Upload-Command, X-Goog-Upload-Header-Content-Length, X-Goog-Upload-Header-Content-Type, X-Goog-Upload-Offset, X-Async, X-EasyAI-Conversation-ID")
			w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
		}
		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusNoContent)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func (s *Server) corsOriginAllowed(origin string) bool {
	if originAllowed(origin, s.cfg.CORSAllowedOrigin) {
		return true
	}
	if s.identityRuntime != nil {
		return originMatchesBaseURL(origin, s.identityRuntime.TrustedWebBaseURL())
	}
	runtime := s.currentIdentityRuntime()
	return runtime != nil && originMatchesBaseURL(origin, runtime.Revision.WebBaseURL)
}

func originAllowed(origin string, allowed string) bool {
	for _, item := range strings.Split(allowed, ",") {
		item = strings.TrimSpace(item)
		if item != "*" && strings.EqualFold(origin, item) {
			return true
		}
	}
	return false
}

func (s *Server) recover(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				s.logger.Error("panic recovered", "error", err, "path", r.URL.Path)
				writeError(w, http.StatusInternalServerError, "internal server error")
			}
		}()
		next.ServeHTTP(w, r)
	})
}
