package sfcrmappservice

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"strconv"
	"strings"
	"sync/atomic"
	"time"

	"github.com/pingcap-inc/nexuscrm-api-host/runtimegrpc"
	querycodec "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/querycodec"
	crmwire "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/wire"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	permissionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/effectivepermissionsv1"
	semantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordaccessv1"
	relatedsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/relatedcountsv1"
)

const (
	AppID                                  = "io.nexuscrm.sfcrm"
	CapabilityID                           = "io.nexuscrm.sfcrm.ui.record-access.v1"
	QueryCapabilityID                      = "io.nexuscrm.sfcrm.query.v1"
	RelatedCountCapabilityID               = "io.nexuscrm.sfcrm.related-list-counts.v1"
	RecycleBinCapabilityID                 = "io.nexuscrm.sfcrm.recycle-bin-read.v1"
	RecycleBinMutationCapabilityID         = "io.nexuscrm.sfcrm.recycle-bin-mutation.v1"
	RecordViewCapabilityID                 = "io.nexuscrm.sfcrm.ui.record-view-read.v1"
	RecordViewAttachmentSourceCapabilityID = "io.nexuscrm.sfcrm.ui.record-view-attachments.v1"
	MutationCapabilityID                   = "io.nexuscrm.sfcrm.record-mutation.v1"
	FilesCapabilityID                      = "io.nexuscrm.sfcrm.record-files.v1"
	BulkIngestCapabilityID                 = "io.nexuscrm.sfcrm.bulk-ingest.v1"
	RecordDefaultsCapabilityID             = "io.nexuscrm.sfcrm.ui.record-defaults.v1"
	EffectivePermissionsCapabilityID       = "io.nexuscrm.sfcrm.effective-permissions.v1"
	FlowActionsCapabilityID                = "io.nexuscrm.sfcrm.flow-actions.v1"
	AnalyticsReadCapabilityID              = "io.nexuscrm.sfcrm.analytics-read.v1"
	AuditLogReadCapabilityID               = "io.nexuscrm.sfcrm.audit-log-read.v1"
	ApprovalLifecycleCapabilityID          = "io.nexuscrm.sfcrm.approval-lifecycle.v1"
	ApprovalReadCapabilityID               = "io.nexuscrm.sfcrm.approval-read.v1"
	ApprovalNotificationFlowsCapabilityID  = "io.nexuscrm.sfcrm.approval-notification-flows.v1"
	PrivateListViewCapabilityID            = "io.nexuscrm.sfcrm.private-list-views.v1"
	UIAppsCapabilityID                     = "io.nexuscrm.sfcrm.ui-apps.v1"
	RecordAccessPath                       = "/services/data/v63.0/ui-api/record-access"
	RelatedCountPath                       = "/services/data/v63.0/ui-api/related-list-counts"
	RecycleBinPath                         = "/services/data/v63.0/recyclebin"
)

var supportedRecordAccessPaths = map[string]struct{}{
	"/services/data/v59.0/ui-api/record-access": {},
	"/services/data/v60.0/ui-api/record-access": {},
	"/services/data/v61.0/ui-api/record-access": {},
	"/services/data/v62.0/ui-api/record-access": {},
	RecordAccessPath: {},
}

var supportedRelatedCountPaths = map[string]struct{}{
	"/services/data/v59.0/ui-api/related-list-counts": {},
	"/services/data/v60.0/ui-api/related-list-counts": {},
	"/services/data/v61.0/ui-api/related-list-counts": {},
	"/services/data/v62.0/ui-api/related-list-counts": {},
	RelatedCountPath: {},
}

var supportedRecycleBinPaths = map[string]struct{}{
	"/services/data/v59.0/recyclebin": {},
	"/services/data/v60.0/recyclebin": {},
	"/services/data/v61.0/recyclebin": {},
	"/services/data/v62.0/recyclebin": {},
	RecycleBinPath:                    {},
}

type ServiceConfig struct {
	RuntimeReadApplicationNavigation    RuntimeReadApplicationNavigationFunc
	RuntimeResolveApplicationNavigation RuntimeResolveApplicationNavigationFunc
	RuntimeSaveApplicationNavigation    RuntimeSaveApplicationNavigationFunc
	// Session activates the five existing sfcrm-owned login/session routes and
	// browser renewal against the same private Runtime target. A nil config keeps
	// the optional source composition inert; the production AppService installs
	// the complete capability with no process-local auth fallback.
	Session             *SessionHTTPConfig
	RuntimeEvaluate     RuntimeEvaluateFunc
	RuntimeReady        func() bool
	RuntimeMetrics      func() runtimegrpc.RuntimeClientMetrics
	AppMetadataMetrics  func() RuntimeAppMetadataV3CacheMetrics
	RuntimeQuery        RuntimeQueryFunc
	RuntimeHistoryQuery RuntimeHistoryQueryFunc
	// RuntimeRelatedCount is optional until the D2A related-count ownership
	// flip. A nil port keeps the related-count paths outside this production
	// composition; tests and the release-matched distribution may install it.
	RuntimeRelatedCount RuntimeRelatedCountFunc
	// RuntimeRecycleBin is optional until this D2A ownership flip is released.
	// A nil port leaves every recycle-bin read path outside this AppService.
	RuntimeRecycleBin RuntimeRecycleBinFunc
	// Restore and purge are one write lifecycle and must be installed together.
	// The read-only recycle-bin port remains independent and gains no mutation
	// authority when these ports are absent.
	RuntimeRestoreRecycleBin RuntimeRecycleBinMutationFunc
	RuntimePurgeRecycleBin   RuntimeRecycleBinMutationFunc
	// RuntimeReadRecordView and RuntimeResolveAttachmentSourceProofs are
	// installed together by the D2A record-view ownership slice. The proof port
	// has a distinct Runtime and L1 capability; neither port can fall back to
	// the historical source-value-only attachment RPC.
	RuntimeReadRecordView                RuntimeRecordViewReadFunc
	RuntimeResolveAttachmentSourceProofs RuntimeAttachmentSourceProofResolveFunc
	// RuntimeMutate is optional until the D2B ownership flip. A nil port keeps
	// mutation paths outside this production composition; tests and the D2B
	// distribution may explicitly install it.
	RuntimeMutate RuntimeMutateFunc
	// The three Runtime Files ports are installed as one capability. Partial
	// installation would make one Salesforce file route silently diverge from
	// the others, so construction rejects it.
	RuntimeBeginFileCreate RuntimeBeginFileCreateFunc
	RuntimeOpenFile        RuntimeOpenFileFunc
	RuntimeDeleteFile      RuntimeDeleteFileFunc
	// Bulk Ingest is one lifecycle. Partial installation would expose jobs that
	// cannot receive data or reach the durable worker, so all four ports switch
	// ownership together.
	RuntimeCreateBulkIngestJob RuntimeCreateBulkIngestJobFunc
	RuntimeReadBulkIngestJob   RuntimeReadBulkIngestJobFunc
	RuntimeCloseBulkIngestJob  RuntimeCloseBulkIngestJobFunc
	RuntimeBeginBulkUpload     RuntimeBeginBulkIngestUploadFunc
	// RuntimePreviewCreateDefaults supplies Runtime-owned dynamic defaults.
	// Catalog-authored static defaults remain projected by the L1 handler.
	RuntimePreviewCreateDefaults RuntimePreviewCreateDefaultsFunc
	// Effective permissions and AppMetadata V3 activate together. Permission
	// and search-scope projections assemble effective metadata with current
	// Runtime decisions at one Catalog/Security authority. The Runtime
	// Search port additionally activates Salesforce /search/scopeOrder without
	// restoring the historical process-local MRU/permission composition.
	RuntimeReadEffectivePermissions RuntimeReadEffectivePermissionsFunc
	// RuntimeAuthorizeCapabilities exchanges one request-scoped opaque
	// delegation for a composed L1 route. It does not cache authority across
	// HTTP requests; each Runtime data method still performs normal admission.
	RuntimeAuthorizeCapabilities RuntimeAuthorizeCapabilitiesFunc
	RuntimeReadSearchScopeOrder  RuntimeReadSearchScopeOrderFunc
	// RuntimeSearch activates the remaining current launch Search routes only
	// when the same composition also supplies AppMetadata and Query. This keeps
	// L1 field/object assembly revision-bound and leaves no local search path.
	RuntimeSearch               RuntimeSearchFunc
	RuntimeGetAppMetadataViewV3 RuntimeGetAppMetadataViewV3Func
	// RuntimeResolvePageLayout is the only caller-effective layout selection
	// path. L1 keeps object/field assembly and Salesforce wire projection, but
	// never reimplements Profile/Role/RecordType assignment ranking.
	RuntimeResolvePageLayout RuntimeResolvePageLayoutFunc
	// RuntimeReadObjectListFields returns tenant-current ordered list
	// presentation. It activates with PageLayout resolution so every editable
	// Layout fact has one Runtime read path.
	RuntimeReadObjectListFields RuntimeReadObjectListFieldsFunc
	// RuntimeReconcileAppMetadataViewV3 is installed only with the watched V3
	// cache. It performs a bounded authoritative refresh when SecurityEpoch
	// advances independently of CatalogRevision; nil preserves strict
	// fail-closed behavior for direct clients and tests without that cache.
	RuntimeReconcileAppMetadataViewV3 RuntimeReconcileAppMetadataV3ReceiptsFunc
	// RuntimeExecuteFlow activates only the bounded synchronous Flow Actions
	// surface. AppMetadataViewV3 supplies the Catalog-derived effective Flow
	// version and behavior at one immutable revision; effective permissions
	// supply caller authority. There is no process-local Flow catalog or
	// executor fallback.
	RuntimeExecuteFlow RuntimeExecuteFlowFunc
	// RuntimeExecuteCatalogAction is distinct from ordinary ExecuteFlow so L1
	// cannot supply an independent Runtime Flow identity for a Catalog action.
	RuntimeExecuteCatalogAction RuntimeExecuteCatalogActionFunc
	// RuntimePreviewCatalogAction activates the complete Catalog-bound Standard
	// Actions surface together with RuntimeExecuteFlow. Runtime owns action
	// admission and no-effect preview execution; sfcrm owns only the v63 wire.
	RuntimePreviewCatalogAction RuntimePreviewCatalogActionFunc
	// EnableUIApps activates UI Apps and the installed application definition
	// administration read. Definitions use Catalog and current setup permission;
	// only the serving UI Apps projection resolves current Runtime navigation.
	EnableUIApps bool
	// RuntimeReadReport closes saved-report/dashboard reads. Query and
	// HistoryQuery also supply the L1-owned dashboard and CRM virtual history
	// report projections. AppMetadataViewV3 supplies immutable field labels,
	// types and report-source configuration; effective permissions preserve the
	// RunReports gate. The AppService never installs a database, report
	// repository or legacy process-local dashboard cache.
	RuntimeReadReport RuntimeReadReportFunc
	// RuntimeReadAuditLog activates the Salesforce admin audit route. L1 owns
	// its permission name and wire shape; Runtime owns privileged storage read.
	RuntimeReadAuditLog RuntimeReadAuditLogFunc
	// RuntimeExecuteApproval and RuntimeReadApproval activate the existing
	// Salesforce approval lifecycle routes as one unit. Runtime remains the
	// transaction, authority and durable replay owner; AppService owns only the
	// request and response dialect.
	RuntimeExecuteApproval RuntimeExecuteApprovalFunc
	RuntimeReadApproval    RuntimeReadApprovalFunc
	// RuntimeReadNotificationFlows activates the administrator-only
	// selector route over an independently admitted neutral descriptor read.
	// RuntimeReadEffectivePermissions supplies the existing ModifyMetadata gate.
	RuntimeReadNotificationFlows RuntimeReadApprovalNotificationFlowsFunc
	RuntimeReadPrivateListViews  RuntimeReadPrivateListViewsFunc
	RuntimeMutatePrivateListView RuntimeMutatePrivateListViewFunc
	// Permission Authoring is one aggregate lifecycle. Partial installation could
	// expose a Save whose ambiguous outcome cannot be reconciled, so all three
	// ports switch ownership together.
	RuntimeReadPermissionAuthoringSnapshot RuntimeReadPermissionAuthoringSnapshotFunc
	RuntimeSavePermissionAuthoring         RuntimeSavePermissionAuthoringFunc
	RuntimeGetPermissionAuthoringStatus    RuntimeGetPermissionAuthoringStatusFunc
	// Metadata administration reads are one coarse neutral Runtime capability.
	// The four ports activate together so the Object, Layout, and Validation Rule managers can
	// never mix a new index with a legacy stored-layout or assignment path.
	RuntimeReadObjectAdminIndex         RuntimeReadObjectAdminIndexFunc
	RuntimeReadValidationRuleAdminIndex RuntimeReadValidationRuleAdminIndexFunc
	// Layout authoring is one synchronous Runtime transaction surface. The
	// three ports activate together; there is no command queue or status route.
	RuntimeReadLayoutAuthoringWorkspace RuntimeReadLayoutAuthoringWorkspaceFunc
	RuntimeReadLayoutAuthoringStored    RuntimeReadLayoutAuthoringStoredLayoutFunc
	RuntimeMutateLayoutAuthoring        RuntimeMutateLayoutAuthoringFunc
	RequestTimeout                      time.Duration
	MaxRequestBytes                     int64
}

// Service owns only CRM route semantics and product metrics. Generic process
// lifecycle and operational probes are owned by processhttp.Process.
type Service struct {
	applicationNavigation       *RuntimeApplicationNavigationClient
	sessions                    *sessionHTTP
	client                      *RuntimeRecordAccessClient
	query                       *querycodec.Handler
	runtimeQuery                RuntimeQueryFunc
	relatedCount                RuntimeRelatedCountFunc
	recycleBin                  RuntimeRecycleBinFunc
	restoreRecycleBin           RuntimeRecycleBinMutationFunc
	purgeRecycleBin             RuntimeRecycleBinMutationFunc
	recordView                  *RuntimeRecordViewClient
	mutation                    *RuntimeMutationClient
	files                       *RuntimeFilesClient
	bulkIngest                  *RuntimeBulkIngestClient
	previewCreateDefaults       RuntimePreviewCreateDefaultsFunc
	permissionMetadata          *RuntimePermissionMetadataClient
	authorizeRuntime            RuntimeAuthorizeCapabilitiesFunc
	appMetadataV3               RuntimeGetAppMetadataViewV3Func
	resolvePageLayoutRuntime    RuntimeResolvePageLayoutFunc
	readObjectListFieldsRuntime RuntimeReadObjectListFieldsFunc
	reconcileAppMetadataV3      RuntimeReconcileAppMetadataV3ReceiptsFunc
	search                      *RuntimeSearchClient
	flowActions                 *RuntimeFlowActionsClient
	standardActions             *RuntimeStandardActionsClient
	analytics                   *RuntimeAnalyticsClient
	auditLog                    *RuntimeAuditLogClient
	approvals                   *RuntimeApprovalsClient
	notificationFlows           *RuntimeApprovalNotificationFlowsClient
	listViews                   *RuntimePrivateListViewClient
	permissionAuthoring         *RuntimePermissionAuthoringClient
	metadataAdmin               *RuntimeMetadataAdminReadClient
	layoutAuthoring             *runtimeLayoutAuthoringClient
	uiApps                      *RuntimeUIAppsClient
	runtimeReady                func() bool
	runtimeMetrics              func() runtimegrpc.RuntimeClientMetrics
	appMetadataMetrics          func() RuntimeAppMetadataV3CacheMetrics
	requestTimeout              time.Duration
	maxRequestBytes             int64
	draining                    atomic.Bool
	inFlight                    atomic.Int64
	requests                    atomic.Uint64
	successes                   atomic.Uint64
	failures                    atomic.Uint64
	drainRejected               atomic.Uint64
	localRejected               atomic.Uint64
}

func NewService(config ServiceConfig) (*Service, error) {
	if config.RuntimeEvaluate == nil || config.RuntimeQuery == nil || config.RuntimeHistoryQuery == nil || config.RuntimeReady == nil || config.RuntimeMetrics == nil ||
		config.RequestTimeout <= 0 || config.MaxRequestBytes <= 0 {
		return nil, errors.New("sfcrm AppService: Runtime, request timeout and body bound are required")
	}
	if (config.RuntimeReadRecordView == nil) != (config.RuntimeResolveAttachmentSourceProofs == nil) {
		return nil, errors.New("sfcrm AppService: record-view read and attachment proof resolver ports must be installed together")
	}
	filePortCount := 0
	for _, installed := range []bool{
		config.RuntimeBeginFileCreate != nil,
		config.RuntimeOpenFile != nil,
		config.RuntimeDeleteFile != nil,
	} {
		if installed {
			filePortCount++
		}
	}
	if filePortCount != 0 && filePortCount != 3 {
		return nil, errors.New("sfcrm AppService: create, open and delete Runtime Files ports must be installed together")
	}
	bulkIngestPortCount := 0
	for _, installed := range []bool{
		config.RuntimeCreateBulkIngestJob != nil,
		config.RuntimeReadBulkIngestJob != nil,
		config.RuntimeCloseBulkIngestJob != nil,
		config.RuntimeBeginBulkUpload != nil,
	} {
		if installed {
			bulkIngestPortCount++
		}
	}
	if bulkIngestPortCount != 0 && bulkIngestPortCount != 4 {
		return nil, errors.New("sfcrm AppService: create, read, close and upload Runtime Bulk Ingest ports must be installed together")
	}
	if config.RuntimeReadEffectivePermissions != nil &&
		config.RuntimeGetAppMetadataViewV3 == nil {
		return nil, errors.New(
			"sfcrm AppService: effective permissions requires AppMetadataViewV3",
		)
	}
	if config.RuntimeReadSearchScopeOrder != nil &&
		(config.RuntimeGetAppMetadataViewV3 == nil || config.RuntimeReadEffectivePermissions == nil) {
		return nil, errors.New(
			"sfcrm AppService: search scope order requires AppMetadataViewV3 and effective permissions",
		)
	}
	if config.RuntimeSearch != nil &&
		config.RuntimeGetAppMetadataViewV3 == nil {
		return nil, errors.New(
			"sfcrm AppService: Search requires AppMetadataViewV3",
		)
	}
	if (config.RuntimeResolvePageLayout == nil) != (config.RuntimeReadObjectListFields == nil) {
		return nil, errors.New(
			"sfcrm AppService: PageLayout resolution and current ListFields must activate together",
		)
	}
	if config.RuntimeResolvePageLayout != nil &&
		(config.RuntimeGetAppMetadataViewV3 == nil ||
			config.RuntimeReadEffectivePermissions == nil) {
		return nil, errors.New(
			"sfcrm AppService: PageLayout resolution requires AppMetadataViewV3 and effective permissions",
		)
	}
	if config.RuntimeExecuteFlow != nil &&
		(config.RuntimeGetAppMetadataViewV3 == nil ||
			config.RuntimeReadEffectivePermissions == nil) {
		return nil, errors.New(
			"sfcrm AppService: Flow Actions require AppMetadataViewV3 and effective permissions",
		)
	}
	if config.RuntimePreviewCatalogAction != nil &&
		(config.RuntimeExecuteCatalogAction == nil ||
			config.RuntimeGetAppMetadataViewV3 == nil ||
			config.RuntimeReadEffectivePermissions == nil) {
		return nil, errors.New(
			"sfcrm AppService: Standard Actions require execute, preview, AppMetadata and effective permissions",
		)
	}
	if config.EnableUIApps &&
		(config.RuntimeGetAppMetadataViewV3 == nil ||
			config.RuntimeReadEffectivePermissions == nil) {
		return nil, errors.New(
			"sfcrm AppService: UI Apps requires AppMetadataViewV3 and effective permissions",
		)
	}
	if config.RuntimeReadReport != nil &&
		(config.RuntimeReadEffectivePermissions == nil ||
			config.RuntimeGetAppMetadataViewV3 == nil) {
		return nil, errors.New(
			"sfcrm AppService: analytics reads require effective permissions and AppMetadataViewV3",
		)
	}
	if config.RuntimeReadAuditLog != nil && config.RuntimeReadEffectivePermissions == nil {
		return nil, errors.New("sfcrm AppService: audit log requires effective permissions")
	}
	if (config.RuntimeExecuteApproval == nil) != (config.RuntimeReadApproval == nil) {
		return nil, errors.New(
			"sfcrm AppService: Approval execute and read ports must be installed together",
		)
	}
	if config.RuntimeReadNotificationFlows != nil &&
		config.RuntimeReadEffectivePermissions == nil {
		return nil, errors.New(
			"sfcrm AppService: approval notification Flow reads require effective permissions",
		)
	}
	if (config.RuntimeReadPrivateListViews == nil) != (config.RuntimeMutatePrivateListView == nil) {
		return nil, errors.New("sfcrm AppService: private list-view read and mutation ports must be installed together")
	}
	permissionAuthoringPortCount := 0
	for _, installed := range []bool{
		config.RuntimeReadPermissionAuthoringSnapshot != nil,
		config.RuntimeSavePermissionAuthoring != nil,
		config.RuntimeGetPermissionAuthoringStatus != nil,
	} {
		if installed {
			permissionAuthoringPortCount++
		}
	}
	if permissionAuthoringPortCount != 0 && permissionAuthoringPortCount != 3 {
		return nil, errors.New("sfcrm AppService: snapshot, save and status Runtime Permission Authoring ports must be installed together")
	}
	if permissionAuthoringPortCount == 3 && config.RuntimeGetAppMetadataViewV3 == nil {
		return nil, errors.New("sfcrm AppService: Permission Authoring requires AppMetadataViewV3 for complete object and field catalogs")
	}
	if (config.RuntimeRestoreRecycleBin == nil) != (config.RuntimePurgeRecycleBin == nil) {
		return nil, errors.New("sfcrm AppService: recycle-bin restore and purge ports must be installed together")
	}
	metadataAdminPortCount := 0
	for _, installed := range []bool{
		config.RuntimeReadObjectAdminIndex != nil,
		config.RuntimeReadValidationRuleAdminIndex != nil,
	} {
		if installed {
			metadataAdminPortCount++
		}
	}
	if metadataAdminPortCount != 0 && metadataAdminPortCount != 2 {
		return nil, errors.New("sfcrm AppService: object index and validation-rule Runtime reads must be installed together")
	}
	layoutAuthoringPortCount := 0
	for _, installed := range []bool{
		config.RuntimeReadLayoutAuthoringWorkspace != nil,
		config.RuntimeReadLayoutAuthoringStored != nil,
		config.RuntimeMutateLayoutAuthoring != nil,
	} {
		if installed {
			layoutAuthoringPortCount++
		}
	}
	if layoutAuthoringPortCount != 0 && layoutAuthoringPortCount != 3 {
		return nil, errors.New("sfcrm AppService: workspace, stored-layout and mutation Runtime Layout authoring ports must be installed together")
	}
	if filePortCount == 3 && config.MaxRequestBytes < minRecordFileRequestBytes {
		return nil, errors.New("sfcrm AppService: request body bound must preserve the 25 MiB CRM file contract plus multipart framing")
	}
	var sessions *sessionHTTP
	if config.Session != nil {
		var sessionErr error
		sessions, sessionErr = newSessionHTTP(*config.Session)
		if sessionErr != nil {
			return nil, sessionErr
		}
	}
	var recordView *RuntimeRecordViewClient
	if config.RuntimeReadRecordView != nil {
		var err error
		recordView, err = NewRuntimeRecordViewClient(config.RuntimeReadRecordView, config.RuntimeResolveAttachmentSourceProofs)
		if err != nil {
			return nil, err
		}
	}
	client, err := NewRuntimeRecordAccessClient(config.RuntimeEvaluate)
	if err != nil {
		return nil, err
	}
	query, err := NewRuntimeQueryHandler(config.RuntimeQuery, config.RuntimeHistoryQuery, &runtimeQueryDates{query: config.RuntimeQuery, metadata: config.RuntimeGetAppMetadataViewV3, reconcile: config.RuntimeReconcileAppMetadataViewV3})
	if err != nil {
		return nil, err
	}
	service := &Service{
		sessions: sessions, client: client, query: query, runtimeQuery: config.RuntimeQuery, relatedCount: config.RuntimeRelatedCount, recycleBin: config.RuntimeRecycleBin,
		restoreRecycleBin: config.RuntimeRestoreRecycleBin, purgeRecycleBin: config.RuntimePurgeRecycleBin,
		runtimeReady: config.RuntimeReady, runtimeMetrics: config.RuntimeMetrics, appMetadataMetrics: config.AppMetadataMetrics,
		recordView:                  recordView,
		appMetadataV3:               config.RuntimeGetAppMetadataViewV3,
		resolvePageLayoutRuntime:    config.RuntimeResolvePageLayout,
		readObjectListFieldsRuntime: config.RuntimeReadObjectListFields,
		reconcileAppMetadataV3:      config.RuntimeReconcileAppMetadataViewV3,
		authorizeRuntime:            config.RuntimeAuthorizeCapabilities,
		previewCreateDefaults:       config.RuntimePreviewCreateDefaults,
		requestTimeout:              config.RequestTimeout,
		maxRequestBytes:             config.MaxRequestBytes,
	}
	if config.RuntimeMutate != nil {
		service.mutation, err = NewRuntimeMutationClient(config.RuntimeMutate)
		if err != nil {
			return nil, err
		}
	}
	if filePortCount == 3 {
		service.files, err = NewRuntimeFilesClient(
			config.RuntimeBeginFileCreate,
			config.RuntimeOpenFile,
			config.RuntimeDeleteFile,
		)
		if err != nil {
			return nil, err
		}
	}
	if bulkIngestPortCount == 4 {
		service.bulkIngest, err = NewRuntimeBulkIngestClient(
			config.RuntimeCreateBulkIngestJob,
			config.RuntimeReadBulkIngestJob,
			config.RuntimeCloseBulkIngestJob,
			config.RuntimeBeginBulkUpload,
		)
		if err != nil {
			return nil, err
		}
	}
	if config.RuntimeReadEffectivePermissions != nil {
		service.permissionMetadata, err = NewRuntimePermissionMetadataClient(
			config.RuntimeReadEffectivePermissions,
			config.RuntimeGetAppMetadataViewV3,
			config.RuntimeReadSearchScopeOrder,
		)
		if err != nil {
			return nil, err
		}
		service.permissionMetadata.reconcileAppMetadata = config.RuntimeReconcileAppMetadataViewV3
	}
	if config.RuntimeSearch != nil {
		service.search, err = NewRuntimeSearchClient(
			config.RuntimeSearch,
			config.RuntimeQuery,
			config.RuntimeGetAppMetadataViewV3,
		)
		if err != nil {
			return nil, err
		}
		service.search.reconcileAppMetadata = config.RuntimeReconcileAppMetadataViewV3
	}
	if config.RuntimeExecuteFlow != nil {
		service.flowActions, err = NewRuntimeFlowActionsClient(
			config.RuntimeExecuteFlow,
			config.RuntimeGetAppMetadataViewV3,
			config.RuntimeReadEffectivePermissions,
		)
		if err != nil {
			return nil, err
		}
		service.flowActions.reconcileAppMetadata = config.RuntimeReconcileAppMetadataViewV3
	}
	if config.RuntimePreviewCatalogAction != nil {
		service.standardActions, err = NewRuntimeStandardActionsClient(
			config.RuntimeExecuteCatalogAction,
			config.RuntimePreviewCatalogAction,
			config.RuntimeGetAppMetadataViewV3,
			config.RuntimeReadEffectivePermissions,
		)
		if err != nil {
			return nil, err
		}
		service.standardActions.reconcileAppMetadata = config.RuntimeReconcileAppMetadataViewV3
	}
	if config.RuntimeReadReport != nil {
		service.analytics, err = NewRuntimeAnalyticsClient(
			config.RuntimeQuery,
			config.RuntimeHistoryQuery,
			config.RuntimeReadReport,
			config.RuntimeReadEffectivePermissions,
			config.RuntimeGetAppMetadataViewV3,
		)
		if err != nil {
			return nil, err
		}
		service.analytics.reconcileAppMetadata = config.RuntimeReconcileAppMetadataViewV3
	}
	if config.RuntimeReadAuditLog != nil {
		service.auditLog, err = NewRuntimeAuditLogClient(
			config.RuntimeReadAuditLog, config.RuntimeReadEffectivePermissions,
		)
		if err != nil {
			return nil, err
		}
	}
	if config.RuntimeExecuteApproval != nil {
		service.approvals, err = NewRuntimeApprovalsClient(
			config.RuntimeExecuteApproval,
			config.RuntimeReadApproval,
		)
		if err != nil {
			return nil, err
		}
	}
	if config.RuntimeReadNotificationFlows != nil {
		service.notificationFlows, err = NewRuntimeApprovalNotificationFlowsClient(
			config.RuntimeReadNotificationFlows,
			config.RuntimeReadEffectivePermissions,
		)
		if err != nil {
			return nil, err
		}
	}
	if config.RuntimeReadPrivateListViews != nil {
		service.listViews, err = NewRuntimePrivateListViewClient(
			config.RuntimeReadPrivateListViews, config.RuntimeMutatePrivateListView,
		)
		if err != nil {
			return nil, err
		}
	}
	if permissionAuthoringPortCount == 3 {
		service.permissionAuthoring, err = NewRuntimePermissionAuthoringClient(
			config.RuntimeReadPermissionAuthoringSnapshot,
			config.RuntimeSavePermissionAuthoring,
			config.RuntimeGetPermissionAuthoringStatus,
		)
		if err != nil {
			return nil, err
		}
	}
	if metadataAdminPortCount == 2 {
		service.metadataAdmin, err = NewRuntimeMetadataAdminReadClient(
			config.RuntimeReadObjectAdminIndex,
			config.RuntimeReadValidationRuleAdminIndex,
		)
		if err != nil {
			return nil, err
		}
	}
	if layoutAuthoringPortCount == 3 {
		service.layoutAuthoring, err = newRuntimeLayoutAuthoringClient(
			config.RuntimeReadLayoutAuthoringWorkspace,
			config.RuntimeReadLayoutAuthoringStored,
			config.RuntimeMutateLayoutAuthoring,
		)
		if err != nil {
			return nil, err
		}
	}
	if config.RuntimeReadApplicationNavigation != nil || config.RuntimeSaveApplicationNavigation != nil {
		service.applicationNavigation, err = NewRuntimeApplicationNavigationClient(config.RuntimeReadApplicationNavigation, config.RuntimeResolveApplicationNavigation, config.RuntimeSaveApplicationNavigation)
		if err != nil {
			return nil, err
		}
	}

	if config.EnableUIApps {
		service.uiApps, err = NewRuntimeUIAppsClient(
			config.RuntimeGetAppMetadataViewV3,
			config.RuntimeReadEffectivePermissions,
			config.RuntimeResolveApplicationNavigation,
		)
		if err != nil {
			return nil, err
		}
		service.uiApps.reconcileAppMetadata = config.RuntimeReconcileAppMetadataViewV3
	}
	return service, nil
}

func (s *Service) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
	// Preserve caller correlation across the private Runtime transport so one
	// HTTP request can be joined to its L0 phase and SQL ledger. The transport
	// helper validates and bounds the value; it is diagnostic-only authority.
	request = request.WithContext(runtimegrpc.WithRequestID(
		request.Context(), request.Header.Get("X-Request-ID"),
	))
	if request.URL.Path == "/metrics" {
		if request.Method != http.MethodGet && request.Method != http.MethodHead {
			writer.Header().Set("Allow", "GET, HEAD")
			http.Error(writer, "method not allowed", http.StatusMethodNotAllowed)
			return
		}
		s.writeMetrics(writer)
		return
	}
	if s.sessions != nil && isSessionRoute(request.URL.Path) {
		if s.draining.Load() {
			s.drainRejected.Add(1)
			s.failures.Add(1)
			writeOAuthError(
				writer, http.StatusServiceUnavailable, "temporarily_unavailable",
				"Service is draining",
			)
			return
		}
		if s.sessions.serve(writer, request) {
			return
		}
	}
	if redirectLegacyQuerySlash(writer, request) {
		return
	}
	if s.sessions != nil && !s.sessions.prepareControlledRequest(writer, request) {
		return
	}
	if s.serveDiscovery(writer, request) {
		return
	}
	if _, ok := supportedRecordAccessPaths[request.URL.Path]; ok {
		if request.Method != http.MethodPost {
			writer.Header().Set("Allow", http.MethodPost)
			writeCRMError(writer, http.StatusMethodNotAllowed, crmwire.SFCodeInvalidRequest, "Method not allowed")
			return
		}
		s.serveRecordAccess(writer, request)
		return
	}
	if s.relatedCount != nil {
		if _, ok := supportedRelatedCountPaths[request.URL.Path]; ok {
			if request.Method != http.MethodPost {
				writer.Header().Set("Allow", http.MethodPost)
				writeCRMError(writer, http.StatusMethodNotAllowed, crmwire.SFCodeInvalidRequest, "Method not allowed")
				return
			}
			s.serveRelatedCounts(writer, request)
			return
		}
	}
	if s.recycleBin != nil {
		if _, ok := supportedRecycleBinPaths[request.URL.Path]; ok {
			if request.Method != http.MethodGet {
				writer.Header().Set("Allow", http.MethodGet)
				writeCRMErrorWith(writer, http.StatusMethodNotAllowed, crmwire.SFCodeInvalidRequest, "Method not allowed", writeLegacyJSON)
				return
			}
			s.serveRecycleBin(writer, request)
			return
		}
	}
	if s.restoreRecycleBin != nil {
		if operation, recordID, ok := matchRecycleBinMutationRoute(request.URL.EscapedPath()); ok {
			expectedMethod := http.MethodDelete
			if operation == recycleBinMutationRestore {
				expectedMethod = http.MethodPost
			}
			if request.Method != expectedMethod {
				writer.Header().Set("Allow", expectedMethod)
				writeCRMErrorWith(writer, http.StatusMethodNotAllowed, crmwire.SFCodeInvalidRequest, "Method not allowed", writeLegacyJSON)
				return
			}
			s.serveRecycleBinMutation(writer, request, operation, recordID)
			return
		}
	}
	if s.serveRecordUI(writer, request) {
		return
	}
	if s.serveRecordUIMetadata(writer, request) {
		return
	}
	if s.serveRecordDefaults(writer, request) {
		return
	}
	if s.serveObjectQuickActions(writer, request) {
		return
	}
	if s.recordView != nil && request.Method == http.MethodGet {
		if route, ok := matchRecordViewRoute(request.URL.EscapedPath()); ok {
			s.serveRecordView(writer, request, route)
			return
		}
	}
	if s.files != nil && s.serveRecordFiles(writer, request) {
		return
	}
	if s.bulkIngest != nil && s.serveBulkIngest(writer, request) {
		return
	}
	if s.permissionAuthoring != nil && s.servePermissionAuthoring(writer, request) {
		return
	}
	if s.permissionMetadata != nil &&
		s.servePermissionMetadata(writer, request) {
		return
	}
	if s.serveAdminAuditLog(writer, request) {
		return
	}
	if s.serveLayoutAuthoring(writer, request) {
		return
	}
	if s.serveRelatedListCandidates(writer, request) {
		return
	}
	if s.serveMetadataAdminRead(writer, request) {
		return
	}
	if s.serveObjectDescribe(writer, request) {
		return
	}
	if s.serveObjectLayouts(writer, request) {
		return
	}
	if s.listViews != nil && s.servePrivateListViews(writer, request) {
		return
	}
	if s.search != nil && s.serveSearch(writer, request) {
		return
	}
	if s.flowActions != nil && s.serveFlowActions(writer, request) {
		return
	}
	if s.standardActions != nil && s.serveStandardActions(writer, request) {
		return
	}
	if s.analytics != nil && s.serveAnalytics(writer, request) {
		return
	}
	if s.notificationFlows != nil &&
		s.serveApprovalNotificationFlows(writer, request) {
		return
	}
	if s.approvals != nil && s.serveApprovals(writer, request) {
		return
	}
	if s.applicationNavigation != nil && s.serveApplicationNavigation(writer, request) {
		return
	}

	if s.uiApps != nil && s.serveUIApps(writer, request) {
		return
	}
	if s.serveComposite(writer, request) {
		return
	}
	if request.URL.Path == ProductNotificationPath {
		if request.Method != http.MethodGet {
			writer.Header().Set("Allow", http.MethodGet)
			writeCRMError(writer, http.StatusMethodNotAllowed, crmwire.SFCodeInvalidRequest, "Method not allowed")
			return
		}
		s.serveNotifications(writer, request, false, "")
		return
	}
	if version, ok := supportedConnectNotificationPaths[request.URL.Path]; ok {
		if request.Method != http.MethodGet {
			writer.Header().Set("Allow", http.MethodGet)
			writeCRMError(writer, http.StatusMethodNotAllowed, crmwire.SFCodeInvalidRequest, "Method not allowed")
			return
		}
		s.serveNotifications(writer, request, true, version)
		return
	}
	queryRoute, ok := querycodec.Match(request.URL.Path)
	if ok {
		if request.Method != http.MethodGet {
			writer.Header().Set("Allow", http.MethodGet)
			writeCRMError(writer, http.StatusMethodNotAllowed, crmwire.SFCodeInvalidRequest, "Method not allowed")
			return
		}
		s.serveQuery(writer, request, queryRoute)
		return
	}
	if s.mutation != nil && s.serveRecordMutation(writer, request) {
		return
	}
	http.NotFound(writer, request)
}

func (s *Service) serveRecordView(writer http.ResponseWriter, request *http.Request, route recordViewRoute) {
	s.requests.Add(1)
	if s.draining.Load() {
		s.drainRejected.Add(1)
		s.failures.Add(1)
		writeCRMErrorWith(writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service is draining", writeLegacyJSON)
		return
	}
	bearer, ok := endUserBearer(request.Header.Values("Authorization"))
	if !ok {
		s.localRejected.Add(1)
		s.failures.Add(1)
		writeCRMErrorWith(writer, http.StatusUnauthorized, crmwire.SFCodeInvalidSessionID, "Session expired or invalid", writeLegacyJSON)
		return
	}
	s.inFlight.Add(1)
	defer s.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), s.requestTimeout)
	defer cancel()
	record, err := readCRMRecordView(ctx, s.recordView, bearer, route)
	if err != nil {
		s.failures.Add(1)
		writeRecordViewFailure(writer, route, err)
		return
	}
	s.successes.Add(1)
	writeLegacyJSON(writer, http.StatusOK, record)
}

func (s *Service) serveRecycleBin(writer http.ResponseWriter, request *http.Request) {
	s.requests.Add(1)
	if s.draining.Load() {
		s.drainRejected.Add(1)
		s.failures.Add(1)
		writeCRMErrorWith(writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service is draining", writeLegacyJSON)
		return
	}
	bearer, ok := endUserBearer(request.Header.Values("Authorization"))
	if !ok {
		s.localRejected.Add(1)
		s.failures.Add(1)
		writeCRMErrorWith(writer, http.StatusUnauthorized, crmwire.SFCodeInvalidSessionID, "Session expired or invalid", writeLegacyJSON)
		return
	}
	scope, err := recycleBinScope(request.URL.Query().Get("scope"))
	if err != nil {
		s.localRejected.Add(1)
		s.failures.Add(1)
		writeCRMErrorWith(writer, http.StatusBadRequest, crmwire.SFCodeInvalidRequest, err.Error(), writeLegacyJSON)
		return
	}
	s.inFlight.Add(1)
	defer s.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), s.requestTimeout)
	defer cancel()
	result, err := listRecycleBin(ctx, s.recycleBin, bearer, scope)
	if err != nil {
		s.failures.Add(1)
		writeRecycleBinRuntimeFailure(writer, scope, err)
		return
	}
	s.successes.Add(1)
	writeLegacyJSON(writer, http.StatusOK, result)
}

func (s *Service) serveRecycleBinMutation(writer http.ResponseWriter, request *http.Request, operation recycleBinMutationOperation, recordID string) {
	s.requests.Add(1)
	if s.draining.Load() {
		s.drainRejected.Add(1)
		s.failures.Add(1)
		writeCRMErrorWith(writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service is draining", writeLegacyJSON)
		return
	}
	bearer, ok := endUserBearer(request.Header.Values("Authorization"))
	if !ok {
		s.localRejected.Add(1)
		s.failures.Add(1)
		writeCRMErrorWith(writer, http.StatusUnauthorized, crmwire.SFCodeInvalidSessionID, "Session expired or invalid", writeLegacyJSON)
		return
	}
	s.inFlight.Add(1)
	defer s.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), s.requestTimeout)
	defer cancel()
	var runtime RuntimeRecycleBinMutationFunc
	if operation == recycleBinMutationRestore {
		runtime = s.restoreRecycleBin
	} else {
		runtime = s.purgeRecycleBin
	}
	if err := mutateRecycleBin(ctx, runtime, bearer, recordID); err != nil {
		s.failures.Add(1)
		writeRuntimeFailureWith(writer, err, writeLegacyJSON)
		return
	}
	s.successes.Add(1)
	writer.WriteHeader(http.StatusNoContent)
}

func (s *Service) serveNotifications(writer http.ResponseWriter, request *http.Request, unreadOnly bool, version string) {
	s.requests.Add(1)
	if s.draining.Load() {
		s.drainRejected.Add(1)
		s.failures.Add(1)
		writeCRMError(writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service is draining")
		return
	}
	bearer, ok := endUserBearer(request.Header.Values("Authorization"))
	if !ok {
		s.localRejected.Add(1)
		s.failures.Add(1)
		writeCRMError(writer, http.StatusUnauthorized, crmwire.SFCodeInvalidSessionID, "Session expired or invalid")
		return
	}
	limit := uint32(connectNotificationLimit)
	if !unreadOnly {
		var err error
		limit, err = notificationLimit(request.URL.Query().Get("limit"))
		if err != nil {
			s.localRejected.Add(1)
			s.failures.Add(1)
			writeJSON(writer, http.StatusBadRequest, map[string]any{"error": "invalid limit"})
			return
		}
	}
	s.inFlight.Add(1)
	defer s.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), s.requestTimeout)
	defer cancel()
	notifications, unreadCount, err := queryNotifications(ctx, s.runtimeQuery, bearer, unreadOnly, limit)
	if err != nil {
		// The Salesforce-compatible Connect surface historically masks a
		// notification-query failure as an empty result. Preserve that public
		// behavior while the product route keeps its existing typed failure.
		if unreadOnly {
			s.successes.Add(1)
			writeJSON(writer, http.StatusOK, map[string]any{"notifications": []connectNotificationProjection{}, "unreadCount": 0})
			return
		}
		s.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return
	}
	s.successes.Add(1)
	if unreadOnly {
		writeJSON(writer, http.StatusOK, map[string]any{
			"notifications": connectNotificationProjections(notifications), "unreadCount": unreadCount,
			"currentPageUrl": "/services/data/" + version + "/connect/notifications", "nextPageUrl": nil,
		})
		return
	}
	writeJSON(writer, http.StatusOK, map[string]any{"notifications": productNotificationProjections(notifications), "unreadCount": unreadCount})
}

func (s *Service) serveRelatedCounts(writer http.ResponseWriter, request *http.Request) {
	s.requests.Add(1)
	if s.draining.Load() {
		s.drainRejected.Add(1)
		s.failures.Add(1)
		writeCRMError(writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service is draining")
		return
	}
	bearer, ok := endUserBearer(request.Header.Values("Authorization"))
	if !ok {
		s.localRejected.Add(1)
		s.failures.Add(1)
		writeCRMError(writer, http.StatusUnauthorized, crmwire.SFCodeInvalidSessionID, "Session expired or invalid")
		return
	}
	s.inFlight.Add(1)
	defer s.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), s.requestTimeout)
	defer cancel()
	request.Body = http.MaxBytesReader(writer, request.Body, s.maxRequestBytes)
	var counts []relatedListCountRequest
	decoder := json.NewDecoder(request.Body)
	if err := decoder.Decode(&counts); err != nil || requireJSONEOF(decoder) != nil {
		s.localRejected.Add(1)
		s.failures.Add(1)
		writeJSON(writer, http.StatusBadRequest, map[string]any{"error": "invalid request body"})
		return
	}
	if len(counts) > relatedsemantic.MaxCounts {
		s.localRejected.Add(1)
		s.failures.Add(1)
		writeJSON(writer, http.StatusBadRequest, map[string]any{"error": "max 50 count requests per batch"})
		return
	}
	if len(counts) == 0 {
		s.successes.Add(1)
		writeJSON(writer, http.StatusOK, map[string]any{"counts": []relatedListCountResponse{}})
		return
	}
	results, err := countRelatedRecords(ctx, s.relatedCount, bearer, counts)
	if err != nil {
		s.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return
	}
	s.successes.Add(1)
	writeJSON(writer, http.StatusOK, map[string]any{"counts": results})
}

func (s *Service) serveQuery(writer http.ResponseWriter, request *http.Request, route querycodec.Route) {
	s.requests.Add(1)
	if s.draining.Load() {
		s.drainRejected.Add(1)
		s.failures.Add(1)
		writeCRMError(writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service is draining")
		return
	}
	bearer, ok := endUserBearer(request.Header.Values("Authorization"))
	if !ok {
		s.localRejected.Add(1)
		s.failures.Add(1)
		writeCRMError(writer, http.StatusUnauthorized, crmwire.SFCodeInvalidSessionID, "Session expired or invalid")
		return
	}
	s.inFlight.Add(1)
	defer s.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(withQueryBearer(request.Context(), bearer), s.requestTimeout)
	defer cancel()
	var toolingPermissions commonsemantic.Receipts
	var toolingAuthority runtimeQueryAuthority
	if route.Kind == querycodec.RouteToolingQuery {
		if s.permissionMetadata == nil {
			s.failures.Add(1)
			writeCRMError(writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service temporarily unavailable")
			return
		}
		permissions, err := s.permissionMetadata.readEffective(
			ctx, bearer, systemPermissionAuthorityRequest(),
		)
		if err != nil {
			s.failures.Add(1)
			writeRuntimeFailure(writer, err)
			return
		}
		if err := permissionsemantic.ValidateResponse(permissions); err != nil {
			s.failures.Add(1)
			writeRuntimeFailure(writer, err)
			return
		}
		if !toolingQueryPermissionGranted(permissions.SystemPermissions) {
			s.localRejected.Add(1)
			s.failures.Add(1)
			writeCRMError(writer, http.StatusForbidden, crmwire.SFCodeForbidden, "Insufficient access")
			return
		}
		toolingPermissions = permissions.Receipts
		ctx = withQueryAuthority(ctx, &toolingAuthority)
	}
	response := s.query.Handle(ctx, querycodec.Request{Route: route, Query: request.URL.Query(), Header: request.Header.Clone()})
	if route.Kind == querycodec.RouteToolingQuery && response.Status < http.StatusBadRequest &&
		!toolingAuthority.matches(toolingPermissions) {
		s.failures.Add(1)
		writeCRMError(writer, http.StatusInternalServerError, crmwire.SFCodeInternalError, crmwire.SFGenericInternalErrorMessage)
		return
	}
	for key, values := range response.Header {
		for _, value := range values {
			writer.Header().Add(key, value)
		}
	}
	if response.Status >= http.StatusBadRequest {
		s.failures.Add(1)
	} else {
		s.successes.Add(1)
	}
	writeJSON(writer, response.Status, response.Body)
}

func toolingQueryPermissionGranted(grants []permissionsemantic.PermissionGrant) bool {
	for _, grant := range grants {
		if grant.Granted && (grant.PermissionID == "ViewSetup" ||
			grant.PermissionID == "CustomizeApplication") {
			return true
		}
	}
	return false
}

func (s *Service) serveRecordAccess(writer http.ResponseWriter, request *http.Request) {
	s.requests.Add(1)
	if s.draining.Load() {
		s.drainRejected.Add(1)
		s.failures.Add(1)
		writeCRMError(writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service is draining")
		return
	}
	s.inFlight.Add(1)
	defer s.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), s.requestTimeout)
	defer cancel()

	var body struct {
		ObjectAPIName string   `json:"objectApiName"`
		RecordIDs     []string `json:"recordIds"`
	}
	request.Body = http.MaxBytesReader(writer, request.Body, s.maxRequestBytes)
	decoder := json.NewDecoder(request.Body)
	if err := decoder.Decode(&body); err != nil || requireJSONEOF(decoder) != nil {
		s.localRejected.Add(1)
		s.failures.Add(1)
		writeCRMError(writer, http.StatusBadRequest, crmwire.SFCodeInvalidRequest, "Invalid request body")
		return
	}
	if len(body.RecordIDs) > semantic.MaxRecords {
		s.localRejected.Add(1)
		s.failures.Add(1)
		writeCRMError(writer, http.StatusBadRequest, crmwire.SFCodeLimitExceeded, "Too many record IDs (max 200)")
		return
	}
	bearer, ok := endUserBearer(request.Header.Values("Authorization"))
	if !ok {
		s.localRejected.Add(1)
		s.failures.Add(1)
		writeCRMError(writer, http.StatusUnauthorized, crmwire.SFCodeInvalidSessionID, "Session expired or invalid")
		return
	}
	access, _, err := s.client.Evaluate(ctx, bearer, body.ObjectAPIName, body.RecordIDs)
	if err != nil {
		s.failures.Add(1)
		writeRuntimeFailure(writer, err)
		return
	}
	s.successes.Add(1)
	writeJSON(writer, http.StatusOK, struct {
		RecordAccess map[string]RecordAccess `json:"recordAccess"`
	}{RecordAccess: access})
}

func (s *Service) BeginDrain() {
	if s != nil {
		s.draining.Store(true)
	}
}

func (s *Service) Ready() bool {
	return s != nil && !s.draining.Load() && s.runtimeReady != nil && s.runtimeReady()
}

func endUserBearer(values []string) (string, bool) {
	if len(values) != 1 || !strings.HasPrefix(values[0], "Bearer ") {
		return "", false
	}
	bearer := strings.TrimPrefix(values[0], "Bearer ")
	return bearer, bearer != "" && strings.TrimSpace(bearer) == bearer && !strings.ContainsAny(bearer, "\r\n")
}

func requireJSONEOF(decoder *json.Decoder) error {
	var extra any
	if err := decoder.Decode(&extra); err != io.EOF {
		if err == nil {
			return errors.New("multiple JSON values")
		}
		return err
	}
	return nil
}

// normalizeRuntimeError preserves semantic failures and classifies local context
// errors that can return before the Runtime transport maps them.
func normalizeRuntimeError(err error) error {
	var failure *commonsemantic.Failure
	if errors.As(err, &failure) {
		return err
	}
	if errors.Is(err, context.DeadlineExceeded) {
		return &commonsemantic.Failure{Category: commonsemantic.FailureDeadlineExceeded}
	}
	if errors.Is(err, context.Canceled) {
		return &commonsemantic.Failure{Category: commonsemantic.FailureCanceled}
	}
	return err
}

func writeRuntimeFailure(writer http.ResponseWriter, err error) {
	writeRuntimeFailureWith(writer, err, writeJSON)
}

func writeRuntimeFailureWith(writer http.ResponseWriter, err error, write func(http.ResponseWriter, int, any)) {
	err = normalizeRuntimeError(err)
	var failure *commonsemantic.Failure
	if !errors.As(err, &failure) {
		log.Printf("sfcrm runtime failure: category=UNCLASSIFIED")
		writeCRMErrorWith(writer, http.StatusInternalServerError, crmwire.SFCodeInternalError, crmwire.SFGenericInternalErrorMessage, write)
		return
	}
	switch failure.Category {
	case commonsemantic.FailureDeadlineExceeded,
		commonsemantic.FailureIncompatible,
		commonsemantic.FailureStaleState,
		commonsemantic.FailureUnavailable,
		commonsemantic.FailureInternal:
		log.Printf(
			"sfcrm runtime failure: category=%s stale_axis=%s required_revision=%d served_revision=%d limit_kind=%s retry_after_ms=%d request_id=%s",
			failure.Category,
			failure.StaleAxis,
			failure.RequiredRevision,
			failure.ServedRevision,
			failure.LimitKind,
			failure.RetryAfterMillis,
			failure.RequestID,
		)
	}
	switch failure.Category {
	case commonsemantic.FailureInvalidRequest:
		writeCRMErrorWith(writer, http.StatusBadRequest, crmwire.SFCodeInvalidRequest, "Invalid request", write)
	case commonsemantic.FailureResourceExhausted:
		if failure.LimitKind == commonsemantic.LimitAdmission {
			setRetryAfterHeader(writer, failure.RetryAfterMillis)
			writeCRMErrorWith(writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service temporarily unavailable", write)
			return
		}
		writeCRMErrorWith(writer, http.StatusBadRequest, crmwire.SFCodeLimitExceeded, "Request exceeds a supported limit", write)
	case commonsemantic.FailureUnauthenticated:
		writeCRMErrorWith(writer, http.StatusUnauthorized, crmwire.SFCodeInvalidSessionID, "Session expired or invalid", write)
	case commonsemantic.FailureForbidden:
		writeCRMErrorWith(writer, http.StatusForbidden, crmwire.SFCodeForbidden, "Insufficient access", write)
	case commonsemantic.FailureMissing:
		writeCRMErrorWith(writer, http.StatusNotFound, crmwire.SFCodeNotFound, "Record not found", write)
	case commonsemantic.FailureConflict:
		message := "Record conflicts with current state"
		switch failure.ConflictReason {
		case commonsemantic.ConflictRevisionMismatch:
			message = "The configuration has changed. Reload the latest version before saving."
		case commonsemantic.ConflictReferenced:
			message = "This configuration is still referenced. Remove its references before deleting it."
		case commonsemantic.ConflictDefaultRequired:
			message = "Choose another default before deleting this configuration."
		}
		writeCRMErrorWith(writer, http.StatusConflict, crmwire.SFCodeConflict, message, write)
	case commonsemantic.FailureCanceled:
		writeCRMErrorWith(writer, 499, crmwire.SFCodeUnknownException, "Request canceled", write)
	case commonsemantic.FailureDeadlineExceeded:
		writeCRMErrorWith(writer, http.StatusGatewayTimeout, crmwire.SFCodeUnknownException, "Request deadline exceeded", write)
	case commonsemantic.FailureIncompatible, commonsemantic.FailureStaleState, commonsemantic.FailureUnavailable:
		// These failures do not prove whether Runtime began or committed the
		// operation. Keep Retry-After reserved for the admission rejection above,
		// which is guaranteed to occur before the Runtime handler executes.
		writeCRMErrorWith(writer, http.StatusServiceUnavailable, crmwire.SFCodeUnknownException, "Service temporarily unavailable", write)
	default:
		writeCRMErrorWith(writer, http.StatusInternalServerError, crmwire.SFCodeInternalError, crmwire.SFGenericInternalErrorMessage, write)
	}
}

func setRetryAfterHeader(writer http.ResponseWriter, milliseconds uint64) {
	if milliseconds == 0 {
		return
	}
	seconds := 1 + (milliseconds-1)/1000
	writer.Header().Set("Retry-After", strconv.FormatUint(seconds, 10))
}

func writeCRMError(writer http.ResponseWriter, status int, code, message string) {
	writeCRMErrorWith(writer, status, code, message, writeJSON)
}

func writeCRMErrorWith(writer http.ResponseWriter, status int, code, message string, write func(http.ResponseWriter, int, any)) {
	write(writer, status, crmwire.SFErrorResponse(code, message))
}

func writeJSON(writer http.ResponseWriter, status int, value any) {
	writer.Header().Set("Content-Type", "application/json")
	writer.WriteHeader(status)
	_ = json.NewEncoder(writer).Encode(value)
}

// writeLegacyJSON preserves the existing Gin route's byte framing during the
// recycle-read ownership flip. json.Encoder.Encode adds a trailing newline;
// the legacy route's json.Marshal + Write path does not.
func writeLegacyJSON(writer http.ResponseWriter, status int, value any) {
	payload, err := json.Marshal(value)
	writer.Header().Set("Content-Type", "application/json")
	writer.WriteHeader(status)
	if err == nil {
		_, _ = writer.Write(payload)
	}
}

func (s *Service) writeMetrics(writer http.ResponseWriter) {
	writer.Header().Set("Content-Type", "text/plain; version=0.0.4")
	runtimeMetrics := runtimegrpc.RuntimeClientMetrics{}
	appMetadataMetrics := RuntimeAppMetadataV3CacheMetrics{}
	if s != nil && s.runtimeMetrics != nil {
		runtimeMetrics = s.runtimeMetrics()
	}
	if s != nil && s.appMetadataMetrics != nil {
		appMetadataMetrics = s.appMetadataMetrics()
	}
	metrics := []struct {
		name  string
		value uint64
	}{
		{"sfcrm_appservice_requests_total", s.requests.Load()},
		{"sfcrm_appservice_successes_total", s.successes.Load()},
		{"sfcrm_appservice_failures_total", s.failures.Load()},
		{"sfcrm_appservice_local_rejections_total", s.localRejected.Load()},
		{"sfcrm_appservice_drain_rejections_total", s.drainRejected.Load()},
		{"sfcrm_appservice_runtime_identity_rpc_total", runtimeMetrics.IdentityCalls},
		{"sfcrm_appservice_runtime_records_rpc_total", runtimeMetrics.RecordsCalls},
		{"sfcrm_appservice_app_metadata_requests_total", appMetadataMetrics.Requests},
		{"sfcrm_appservice_app_metadata_full_reads_total", appMetadataMetrics.FullReads},
		{"sfcrm_appservice_app_metadata_watch_attempts_total", appMetadataMetrics.WatchAttempts},
		{"sfcrm_appservice_app_metadata_watch_updates_total", appMetadataMetrics.WatchUpdates},
		{"sfcrm_appservice_app_metadata_watch_failures_total", appMetadataMetrics.WatchFailures},
	}
	for _, metric := range metrics {
		_, _ = fmt.Fprintf(writer, "%s %d\n", metric.name, metric.value)
	}
	_, _ = fmt.Fprintf(writer, "sfcrm_appservice_in_flight %d\n", s.inFlight.Load())
}
