package dialectsfcrm

import (
	"context"
	"crypto/rand"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"reflect"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	sharederrors "github.com/nexuscrm/shared/pkg/errors"
	"github.com/nexuscrm/shared/pkg/layoutfls"
	"github.com/nexuscrm/shared/pkg/models"
	"golang.org/x/sync/errgroup"
	"golang.org/x/sync/singleflight"
)

const uiAPIInternalErrorMessage = "An internal server error occurred. Contact your system administrator."
const uiAPIRelatedListTransformConcurrency = 8

type uiAPIService struct {
	records      contract.Records
	recordAccess contract.RecordAccessReader
	defaults     contract.RecordDefaultPreviewer
	metadata     contract.Metadata
	layouts      contract.LayoutMetadataReader
	catalog      contract.ObjectMetadataCatalogReader
	policy       contract.Policy
	schemaCache  *uiAPISchemaCache
	decorator    recordReadDecorator
	fileResolver recordAttachmentSourceResolver
	fileLookup   contract.RecordAttachmentSourceLookup
}

type uiAPIAccessCache struct {
	inner   contract.Policy
	mu      sync.Mutex
	entries map[uiAPIAccessCacheKey]*contract.EffectiveObjectAccess
	group   singleflight.Group
}

type uiAPIAccessCacheKey struct {
	userID             string
	profileID          string
	objectName         string
	includeFields      bool
	includeRecordTypes bool
}

func newUIAPIAccessCache(inner contract.Policy) contract.Policy {
	if inner == nil {
		return nil
	}
	if _, ok := inner.(*uiAPIAccessCache); ok {
		return inner
	}
	return &uiAPIAccessCache{
		inner:   inner,
		entries: make(map[uiAPIAccessCacheKey]*contract.EffectiveObjectAccess),
	}
}

func (c *uiAPIAccessCache) Access(ctx context.Context, user *models.UserSession, objectName string, req contract.AccessRequest) (*contract.EffectiveObjectAccess, error) {
	if c == nil || c.inner == nil {
		return nil, nil
	}
	key := uiAPIAccessCacheKey{
		objectName:         strings.ToLower(strings.TrimSpace(objectName)),
		includeFields:      req.IncludeFields,
		includeRecordTypes: req.IncludeRecordTypes,
	}
	if user != nil {
		key.userID = user.ID
		key.profileID = user.ProfileID
	}
	c.mu.Lock()
	if cached, ok := c.entries[key]; ok {
		out := cloneEffectiveObjectAccess(cached)
		c.mu.Unlock()
		return out, nil
	}
	c.mu.Unlock()

	value, err, _ := c.group.Do(uiAPIAccessCacheKeyString(key), func() (any, error) {
		c.mu.Lock()
		if cached, ok := c.entries[key]; ok {
			c.mu.Unlock()
			return cached, nil
		}
		c.mu.Unlock()

		access, err := c.inner.Access(ctx, user, objectName, req)
		if err != nil {
			return nil, err
		}
		stored := cloneEffectiveObjectAccess(access)
		c.mu.Lock()
		c.entries[key] = stored
		c.mu.Unlock()
		return stored, nil
	})
	if err != nil {
		return nil, err
	}
	if stored, ok := value.(*contract.EffectiveObjectAccess); ok {
		return cloneEffectiveObjectAccess(stored), nil
	}
	return nil, nil
}

func uiAPIAccessCacheKeyString(key uiAPIAccessCacheKey) string {
	return fmt.Sprintf("%s|%s|%s|fields=%t|recordTypes=%t", key.userID, key.profileID, key.objectName, key.includeFields, key.includeRecordTypes)
}

func cloneEffectiveObjectAccess(access *contract.EffectiveObjectAccess) *contract.EffectiveObjectAccess {
	if access == nil {
		return nil
	}
	out := *access
	if access.Fields != nil {
		out.Fields = make(map[string]contract.FieldAccess, len(access.Fields))
		for key, field := range access.Fields {
			out.Fields[key] = field
		}
	}
	if access.RecordTypes != nil {
		out.RecordTypes = append([]contract.RecordTypeAvailability(nil), access.RecordTypes...)
	}
	return &out
}

type uiAPISchemaCache struct {
	metadata contract.Metadata
	mu       sync.Mutex
	entries  map[string]*models.ObjectMetadata
	group    singleflight.Group
}

func newUIAPISchemaCache(metadata contract.Metadata) *uiAPISchemaCache {
	if metadata == nil {
		return nil
	}
	return &uiAPISchemaCache{
		metadata: metadata,
		entries:  make(map[string]*models.ObjectMetadata),
	}
}

func (c *uiAPISchemaCache) Describe(ctx context.Context, objectName string) (*models.ObjectMetadata, error) {
	if c == nil || c.metadata == nil {
		return nil, nil
	}
	key := strings.ToLower(strings.TrimSpace(objectName))
	if key == "" {
		return c.metadata.Describe(ctx, objectName)
	}
	c.mu.Lock()
	if schema, ok := c.entries[key]; ok {
		c.mu.Unlock()
		return schema, nil
	}
	c.mu.Unlock()

	value, err, _ := c.group.Do(key, func() (any, error) {
		c.mu.Lock()
		if schema, ok := c.entries[key]; ok {
			c.mu.Unlock()
			return schema, nil
		}
		c.mu.Unlock()
		schema, err := c.metadata.Describe(ctx, objectName)
		if err != nil {
			return nil, err
		}
		c.mu.Lock()
		c.entries[key] = schema
		c.mu.Unlock()
		return schema, nil
	})
	if err != nil {
		return nil, err
	}
	if schema, ok := value.(*models.ObjectMetadata); ok {
		return schema, nil
	}
	return nil, nil
}

type uiAPIRecordUI struct {
	Records     map[string]uiAPIRecord            `json:"records"`
	Layouts     map[string]map[string]uiAPILayout `json:"layouts"`
	ObjectInfos map[string]uiAPIObjectInfo        `json:"objectInfos"`
}

type uiAPIRecordAccess struct {
	HasReadAccess   bool `json:"hasReadAccess"`
	HasEditAccess   bool `json:"hasEditAccess"`
	HasDeleteAccess bool `json:"hasDeleteAccess"`
	IsLocked        bool `json:"isLocked"`
}

type uiAPIRecord struct {
	ApiName          string                     `json:"apiName"`
	Id               string                     `json:"id"`
	Fields           map[string]uiAPIFieldValue `json:"fields"`
	RecordAccess     *uiAPIRecordAccess         `json:"recordAccess,omitempty"`
	LastModifiedDate string                     `json:"lastModifiedDate,omitempty"`
	SystemModstamp   string                     `json:"systemModstamp,omitempty"`
	RecordTypeId     string                     `json:"recordTypeId,omitempty"`
}

type uiAPIFieldValue struct {
	DisplayValue *string `json:"displayValue"`
	Value        any     `json:"value"`
}

type uiAPIObjectInfo struct {
	ApiName             string                         `json:"apiName"`
	Label               string                         `json:"label"`
	LabelPlural         string                         `json:"labelPlural"`
	KeyPrefix           string                         `json:"keyPrefix"`
	ThemeInfo           *uiAPIThemeInfo                `json:"themeInfo,omitempty"`
	Layoutable          bool                           `json:"layoutable"`
	Queryable           bool                           `json:"queryable"`
	Createable          bool                           `json:"createable"`
	Updateable          bool                           `json:"updateable"`
	Deletable           bool                           `json:"deletable"`
	Retrieveable        bool                           `json:"retrieveable"`
	Searchable          bool                           `json:"searchable"`
	Triggerable         bool                           `json:"triggerable"`
	Undeletable         bool                           `json:"undeletable"`
	Fields              map[string]uiAPIFieldInfo      `json:"fields,omitempty"`
	DependentFields     map[string]any                 `json:"dependentFields,omitempty"`
	DefaultRecordTypeId string                         `json:"defaultRecordTypeId,omitempty"`
	RecordTypeInfos     map[string]uiAPIRecordTypeInfo `json:"recordTypeInfos,omitempty"`
}

type uiAPIRecordTypeInfo struct {
	Name                     string `json:"name"`
	RecordTypeId             string `json:"recordTypeId"`
	Available                bool   `json:"available"`
	DefaultRecordTypeMapping bool   `json:"defaultRecordTypeMapping"`
	Master                   bool   `json:"master"`
}

// uiAPIFieldInfo is the v63 UI API FieldInfo. Every attribute here exists in that shape; none is
// invented, and none is a rename of a describe attribute.
//
// The attributes below Length were added because the CRM frontend cannot leave the raw describe
// endpoint without them — it reads custom, calculated, unique, precision, scale, sortable,
// filterable, inlineHelpText and controllerName from the object metadata it renders. Serving them
// here is what lets a browser read object metadata through the UI API instead of /sobjects/*/describe
// (#1867 §12 slice 1).
//
// Notably absent: picklistValues and nillable. Neither is a v63 UI API FieldInfo attribute —
// picklist values have their own versioned endpoint, and nillable is describe's spelling of what this
// shape calls required. Adding either here would be a parity break dressed up as a convenience.
type uiAPIFieldInfo struct {
	ApiName          string                 `json:"apiName"`
	Label            string                 `json:"label"`
	DataType         string                 `json:"dataType"`
	Required         bool                   `json:"required"`
	Createable       bool                   `json:"createable"`
	Updateable       bool                   `json:"updateable"`
	Length           int                    `json:"length,omitempty"`
	Reference        bool                   `json:"reference"`
	ReferenceToInfos []uiAPIReferenceToInfo `json:"referenceToInfos,omitempty"`
	RelationshipName string                 `json:"relationshipName,omitempty"`
	NameField        bool                   `json:"nameField"`
	Custom           bool                   `json:"custom"`
	Calculated       bool                   `json:"calculated"`
	Unique           bool                   `json:"unique"`
	Precision        int                    `json:"precision"`
	Scale            int                    `json:"scale"`
	Sortable         bool                   `json:"sortable"`
	Filterable       bool                   `json:"filterable"`
	HtmlFormatted    bool                   `json:"htmlFormatted"`
	InlineHelpText   *string                `json:"inlineHelpText"`
	ControllerName   *string                `json:"controllerName"`
}

type uiAPIReferenceToInfo struct {
	ApiName    string   `json:"apiName"`
	NameFields []string `json:"nameFields"`
}

type uiAPIThemeInfo struct {
	Color   string `json:"color"`
	IconUrl string `json:"iconUrl"`
}

type uiAPILayout struct {
	Id                    string                   `json:"id"`
	LayoutName            string                   `json:"layoutName,omitempty"`
	ObjectApiName         string                   `json:"objectApiName,omitempty"`
	LayoutType            string                   `json:"layoutType,omitempty"`
	Mode                  string                   `json:"mode"`
	Sections              []uiAPILayoutSection     `json:"sections"`
	CompactLayout         []string                 `json:"compactLayout,omitempty"`
	Tabs                  []string                 `json:"tabs,omitempty"`
	RelatedLists          []uiAPIRelatedListInfo   `json:"relatedLists"`
	RecordPanels          []uiAPIRecordPanelInfo   `json:"recordPanels,omitempty"`
	HeaderActions         []models.ActionConfig    `json:"headerActions"`
	QuickActions          []models.ActionConfig    `json:"quickActions"`
	HeaderActionsAuthored bool                     `json:"headerActionsAuthored,omitempty"`
	QuickActionsAuthored  bool                     `json:"quickActionsAuthored,omitempty"`
	RecordStates          []models.RecordStateRule `json:"recordStates,omitempty"`
	PrimaryHeaderActions  []string                 `json:"primaryHeaderActions,omitempty"`
}

type uiAPIRelatedListInfo struct {
	Id              string                          `json:"id"`
	Label           string                          `json:"label"`
	SecondaryLabel  string                          `json:"secondaryLabel,omitempty"`
	ObjectApiName   string                          `json:"objectApiName"`
	LookupField     string                          `json:"lookupField"`
	Fields          []string                        `json:"fields"`
	PrefillMappings map[string]string               `json:"prefillMappings,omitempty"`
	Resolution      *contract.RelatedListResolution `json:"resolution,omitempty"`
}

type uiAPIRecordPanelInfo struct {
	Id                  string                 `json:"id"`
	RendererID          string                 `json:"rendererId"`
	Position            string                 `json:"position,omitempty"`
	TabID               string                 `json:"tabId,omitempty"`
	TabLabel            string                 `json:"tabLabel,omitempty"`
	Order               int                    `json:"order,omitempty"`
	VisibilityCondition string                 `json:"visibilityCondition,omitempty"`
	Config              map[string]interface{} `json:"config,omitempty"`
}

type uiAPILayoutSection struct {
	Id          string            `json:"id"`
	Heading     string            `json:"heading"`
	Tab         string            `json:"tab,omitempty"`
	Columns     int               `json:"columns"`
	LayoutRows  []uiAPILayoutRow  `json:"layoutRows"`
	UseHeading  bool              `json:"useHeading"`
	Collapsible bool              `json:"collapsible"`
	IsCollapsed bool              `json:"isCollapsed"`
	FieldLabels map[string]string `json:"fieldLabels,omitempty"`
}

type uiAPILayoutRow struct {
	LayoutItems []uiAPILayoutItem `json:"layoutItems"`
}

type uiAPILayoutItem struct {
	Label             string                 `json:"label"`
	LayoutComponents  []uiAPILayoutComponent `json:"layoutComponents"`
	Required          bool                   `json:"required"`
	EditableForNew    bool                   `json:"editableForNew"`
	EditableForUpdate bool                   `json:"editableForUpdate"`
}

type uiAPILayoutComponent struct {
	ComponentType string         `json:"componentType"`
	ApiName       string         `json:"apiName,omitempty"`
	Value         string         `json:"value,omitempty"`
	Details       map[string]any `json:"details,omitempty"`
}

type uiAPIRecordDefaults struct {
	ObjectApiName             string                  `json:"objectApiName"`
	RecordTypeId              string                  `json:"recordTypeId,omitempty"`
	Record                    uiAPIRecordWithDefaults `json:"record"`
	ObjectInfo                *uiAPIObjectInfo        `json:"objectInfo,omitempty"`
	Layout                    *uiAPILayout            `json:"layout,omitempty"`
	SiblingMaxDefaultedFields []string                `json:"siblingMaxDefaultedFields,omitempty"`
}

type uiAPIRecordWithDefaults struct {
	ApiName string                     `json:"apiName"`
	Fields  map[string]uiAPIFieldValue `json:"fields"`
}

type uiAPIListInfo struct {
	ApiName        string               `json:"apiName"`
	Id             string               `json:"id"`
	Label          string               `json:"label"`
	DeveloperName  string               `json:"developerName"`
	ObjectApiName  string               `json:"objectApiName"`
	Visibility     string               `json:"visibility"`
	DisplayColumns []uiAPIDisplayColumn `json:"displayColumns,omitempty"`
	OrderedByInfo  []uiAPIOrderInfo     `json:"orderedByInfo,omitempty"`
	FilterLogic    string               `json:"filterLogic,omitempty"`
}

type uiAPIDisplayColumn struct {
	FieldApiName string `json:"fieldApiName"`
	Label        string `json:"label"`
	Sortable     bool   `json:"sortable"`
}

type uiAPIOrderInfo struct {
	FieldApiName string `json:"fieldApiName"`
	IsAscending  bool   `json:"isAscending"`
}

type uiAPILayoutAssignmentResponse struct {
	ID               string    `json:"id"`
	ObjectAPIName    string    `json:"objectApiName"`
	LayoutID         string    `json:"layoutId"`
	RoleID           *string   `json:"roleId,omitempty"`
	ProfileID        *string   `json:"profileId,omitempty"`
	RecordTypeID     *string   `json:"recordTypeId,omitempty"`
	Specificity      int       `json:"specificity"`
	CreatedDate      time.Time `json:"createdDate,omitempty"`
	LastModifiedDate time.Time `json:"lastModifiedDate,omitempty"`
	CreatedByID      *string   `json:"createdById,omitempty"`
}

type uiAPILayoutSummary struct {
	ID               string `json:"id"`
	LayoutName       string `json:"layoutName"`
	ObjectAPIName    string `json:"objectApiName"`
	Type             string `json:"type"`
	IsDefault        bool   `json:"isDefault"`
	CreatedDate      string `json:"createdDate,omitempty"`
	LastModifiedDate string `json:"lastModifiedDate,omitempty"`
}

type uiAPIApprovalLayoutSummary struct {
	ID               string `json:"id"`
	LayoutName       string `json:"layoutName"`
	ObjectAPIName    string `json:"objectApiName"`
	Type             string `json:"type"`
	IsDefault        bool   `json:"isDefault"`
	CreatedDate      string `json:"createdDate,omitempty"`
	LastModifiedDate string `json:"lastModifiedDate,omitempty"`
}

type uiAPIStatusError struct {
	status  int
	message string
}

func (e uiAPIStatusError) Error() string   { return e.message }
func (e uiAPIStatusError) HTTPStatus() int { return e.status }

func UIAPIRecordUINeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	return uiAPIRecordUINeutralHandler(ctx, req, deps, nil, nil)
}

func (p CompositeTreePartition) UIAPIRecordUINeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	return uiAPIRecordUINeutralHandler(ctx, req, deps, p.recordDecorator, p.recordAttachmentLookup)
}

func uiAPIRecordUINeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps, decorator recordReadDecorator, lookup contract.RecordAttachmentSourceLookup) (*dialect.Response, error) {
	service, resp := uiAPIServiceFromDepsWithDecoration(deps, decorator, lookup)
	if resp != nil {
		return resp, nil
	}
	recordIDsParam := strings.TrimSpace(req.Params["recordIds"])
	ids := strings.Split(recordIDsParam, ",")
	if len(ids) > 200 {
		return uiAPIError(http.StatusBadRequest, wire.SFCodeLimitExceeded, "Too many record IDs (max 200)"), nil
	}

	typeHint := queryValue(req, "type")
	if typeHint == "" && len(ids) > 0 {
		resolvedType, err := service.resolveObjectType(ctx, ids[0], "")
		if err != nil {
			return uiAPIServiceErrorResponse(req, err), nil
		}
		typeHint = resolvedType
	}
	if typeHint == "" {
		return uiAPIError(http.StatusBadRequest, wire.SFCodeInvalidField, "Unable to resolve object type from record ID. Provide the 'type' query parameter."), nil
	}

	originalTypeHint := typeHint
	typeHint = strings.ToLower(typeHint)
	recordUI, err := service.getRecordUI(ctx, ids, typeHint, req.User, originalTypeHint)
	if err != nil {
		return uiAPIServiceErrorResponse(req, err), nil
	}
	service.decorateRecordReadsInRecordUI(ctx, uiAPIFilesBasePath(req.Version), recordUI, req.User)
	for _, recordID := range ids {
		trackRecordAccess(deps.Contracts.Activity, req.User, typeHint, recordID)
	}
	return jsonResponse(http.StatusOK, recordUI), nil
}

func UIAPIBatchRecordAccessNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	service, resp := uiAPIServiceFromDeps(deps)
	if resp != nil {
		return resp, nil
	}
	var body struct {
		ObjectAPIName string   `json:"objectApiName"`
		RecordIDs     []string `json:"recordIds"`
	}
	if err := json.Unmarshal(req.Body, &body); err != nil {
		return uiAPIError(http.StatusBadRequest, wire.SFCodeInvalidRequest, "Invalid request body"), nil
	}
	if len(body.RecordIDs) == 0 {
		return jsonResponse(http.StatusOK, map[string]any{"recordAccess": map[string]any{}}), nil
	}
	if len(body.RecordIDs) > 200 {
		return uiAPIError(http.StatusBadRequest, wire.SFCodeLimitExceeded, "Too many record IDs (max 200)"), nil
	}
	accessMap, err := service.getBatchRecordAccess(ctx, body.ObjectAPIName, body.RecordIDs, req.User)
	if err != nil {
		return uiAPIServiceErrorResponse(req, err), nil
	}
	return jsonResponse(http.StatusOK, map[string]any{"recordAccess": accessMap}), nil
}

func UIAPIObjectInfoNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	service, resp := uiAPIServiceFromDeps(deps)
	if resp != nil {
		return resp, nil
	}
	info, err := service.getObjectInfo(ctx, strings.TrimSpace(req.Params["objectApiName"]), req.User)
	if err != nil {
		return uiAPIServiceErrorResponse(req, err), nil
	}
	if info == nil {
		return uiAPIError(http.StatusNotFound, wire.SFCodeNotFound, "Object not found"), nil
	}
	return jsonResponse(http.StatusOK, info), nil
}

func UIAPIListLayoutsNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	service, resp := uiAPIServiceFromDeps(deps)
	if resp != nil {
		return resp, nil
	}
	layouts, err := service.layouts.ListLayouts(ctx, strings.TrimSpace(req.Params["objectApiName"]))
	if err != nil {
		return uiAPIServiceErrorResponse(req, err), nil
	}
	result := uiAPILayoutSummaries(layouts)
	return jsonResponse(http.StatusOK, map[string]any{"layouts": result, "totalSize": len(result)}), nil
}

func UIAPIListApprovalLayoutsNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	service, resp := uiAPIServiceFromDeps(deps)
	if resp != nil {
		return resp, nil
	}
	layouts, err := service.layouts.ListApprovalLayouts(ctx, strings.TrimSpace(req.Params["objectApiName"]))
	if err != nil {
		return uiAPIServiceErrorResponse(req, err), nil
	}
	result := uiAPIApprovalLayoutSummaries(layouts)
	return jsonResponse(http.StatusOK, map[string]any{"approvalLayouts": result, "totalSize": len(result)}), nil
}

func UIAPILayoutNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	service, resp := uiAPIServiceFromDeps(deps)
	if resp != nil {
		return resp, nil
	}
	layoutType := queryValue(req, "type")
	switch layoutType {
	case "", "Detail", "Create", "Edit":
	default:
		return uiAPIError(http.StatusBadRequest, wire.SFCodeInvalidRequest, "Invalid layout type. Must be one of: Create, Edit, Detail"), nil
	}
	layoutID := queryValue(req, "layoutId")
	objectName := strings.TrimSpace(req.Params["objectApiName"])
	var layout *uiAPILayout
	var err error
	if layoutID != "" {
		mode := uiAPILayoutModeView
		switch layoutType {
		case "Create":
			mode = "Create"
		case "Edit":
			mode = "Edit"
		}
		layout, err = service.getLayoutByID(ctx, layoutID, objectName, mode, req.User)
	} else {
		layout, err = service.getLayoutWithOptions(ctx, objectName, req.User, queryValue(req, "recordTypeId"), layoutType)
	}
	if err != nil {
		return uiAPIServiceErrorResponse(req, err), nil
	}
	if layout == nil {
		return uiAPIError(http.StatusNotFound, wire.SFCodeNotFound, "Layout not found"), nil
	}
	return jsonResponse(http.StatusOK, layout), nil
}

func UIAPIListAssignmentsNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	service, resp := uiAPIServiceFromDeps(deps)
	if resp != nil {
		return resp, nil
	}
	assignments, err := service.layouts.ListLayoutAssignments(ctx, strings.TrimSpace(req.Params["objectApiName"]))
	if err != nil {
		return uiAPIServiceErrorResponse(req, err), nil
	}
	if assignments == nil {
		assignments = []*models.LayoutAssignment{}
	}
	out := make([]uiAPILayoutAssignmentResponse, 0, len(assignments))
	for _, assignment := range assignments {
		out = append(out, uiAPILayoutAssignmentResponseFromModel(assignment))
	}
	return jsonResponse(http.StatusOK, out), nil
}

func UIAPIRecordDefaultsCreateNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	service, resp := uiAPIServiceFromDeps(deps)
	if resp != nil {
		return resp, nil
	}
	defaults, err := service.getRecordDefaults(ctx, strings.TrimSpace(req.Params["objectApiName"]), queryValue(req, "recordTypeId"), req.User, uiAPIRecordDefaultsOptions{
		ParentFieldAPIName: strings.TrimSpace(queryValue(req, "parentFieldApiName")),
		ParentRecordID:     strings.TrimSpace(queryValue(req, "parentRecordId")),
	})
	if err != nil {
		return uiAPIServiceErrorResponse(req, err), nil
	}
	return jsonResponse(http.StatusOK, defaults), nil
}

func UIAPIRecordDefaultsCloneNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	service, resp := uiAPIServiceFromDeps(deps)
	if resp != nil {
		return resp, nil
	}
	defaults, err := service.getCloneDefaults(ctx, strings.TrimSpace(req.Params["recordId"]), queryValue(req, "type"), queryValue(req, "recordTypeId"), req.User)
	if err != nil {
		return uiAPIServiceErrorResponse(req, err), nil
	}
	return jsonResponse(http.StatusOK, defaults), nil
}

func UIAPIListInfoNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	service, resp := uiAPIServiceFromDeps(deps)
	if resp != nil {
		return resp, nil
	}
	info, err := service.getListInfo(ctx, strings.TrimSpace(req.Params["objectApiName"]), strings.TrimSpace(req.Params["listViewApiName"]), req.User)
	if err != nil {
		return uiAPIServiceErrorResponse(req, err), nil
	}
	return jsonResponse(http.StatusOK, info), nil
}

func uiAPIServiceFromDeps(deps dialect.Deps) (*uiAPIService, *dialect.Response) {
	return uiAPIServiceFromDepsWithDecoration(deps, nil, nil)
}

func uiAPIServiceFromDepsWithDecoration(deps dialect.Deps, decorator recordReadDecorator, lookup contract.RecordAttachmentSourceLookup) (*uiAPIService, *dialect.Response) {
	contracts := deps.Contracts
	service := &uiAPIService{
		records:      contracts.Records,
		metadata:     contracts.Metadata,
		policy:       newUIAPIAccessCache(contracts.Policy),
		schemaCache:  newUIAPISchemaCache(contracts.Metadata),
		decorator:    decorator,
		fileResolver: contracts.Files,
		fileLookup:   lookup,
	}
	if service.records != nil {
		service.recordAccess, _ = service.records.(contract.RecordAccessReader)
		service.defaults, _ = service.records.(contract.RecordDefaultPreviewer)
	}
	if service.metadata != nil {
		service.layouts, _ = service.metadata.(contract.LayoutMetadataReader)
		service.catalog, _ = service.metadata.(contract.ObjectMetadataCatalogReader)
	}
	if service.records == nil ||
		service.recordAccess == nil ||
		service.defaults == nil ||
		service.metadata == nil ||
		service.layouts == nil ||
		service.catalog == nil ||
		service.policy == nil {
		log.Printf("ui api: required neutral contracts unavailable")
		return nil, uiAPIError(http.StatusInternalServerError, wire.SFCodeInternalError, uiAPIInternalErrorMessage)
	}
	return service, nil
}

func (s *uiAPIService) describe(ctx context.Context, objectName string) (*models.ObjectMetadata, error) {
	if s == nil {
		return nil, nil
	}
	if s.schemaCache != nil {
		return s.schemaCache.Describe(ctx, objectName)
	}
	if s.metadata == nil {
		return nil, nil
	}
	return s.metadata.Describe(ctx, objectName)
}

func (s *uiAPIService) resolveObjectType(ctx context.Context, recordID string, hintType string) (string, error) {
	if strings.TrimSpace(hintType) != "" {
		return hintType, nil
	}
	recordID = strings.TrimSpace(recordID)
	if recordID == "" {
		return "", nil
	}
	objectType, err := s.records.ObjectTypeForID(ctx, recordID)
	if err != nil {
		return "", err
	}
	objectType = strings.TrimSpace(objectType)
	if objectType == "" {
		return "", uiAPIStatusError{status: http.StatusNotFound, message: fmt.Sprintf("record %s not found", recordID)}
	}
	return objectType, nil
}

func (s *uiAPIService) getRecordUI(ctx context.Context, recordIDs []string, typeHint string, user *models.UserSession, originalTypeHints ...string) (*uiAPIRecordUI, error) {
	response := &uiAPIRecordUI{
		Records:     make(map[string]uiAPIRecord),
		Layouts:     make(map[string]map[string]uiAPILayout),
		ObjectInfos: make(map[string]uiAPIObjectInfo),
	}
	if len(recordIDs) == 0 {
		return response, nil
	}
	if typeHint == "" {
		return nil, uiAPIStatusError{status: http.StatusBadRequest, message: "object type hint is required for NexusCRM RecordUI"}
	}
	if err := s.requireObjectAccess(ctx, user, typeHint, constants.PermRead); err != nil {
		return nil, err
	}

	var mu sync.Mutex
	g1, g1Ctx := errgroup.WithContext(ctx)
	for _, id := range recordIDs {
		recordID := id
		g1.Go(func() error {
			uiRecord, err := s.processRecordForUI(g1Ctx, typeHint, recordID, user)
			if err != nil {
				return err
			}
			mu.Lock()
			response.Records[recordID] = *uiRecord
			mu.Unlock()
			return nil
		})
	}
	g1.Go(func() error {
		objInfo, err := s.getObjectInfo(g1Ctx, typeHint, user)
		if err != nil || objInfo == nil {
			return err
		}
		mu.Lock()
		response.ObjectInfos[typeHint] = *objInfo
		mu.Unlock()
		return nil
	})
	if err := g1.Wait(); err != nil {
		return nil, err
	}

	responseKey := typeHint
	schema, err := s.describe(ctx, typeHint)
	if err == nil && schema != nil {
		responseKey = schema.APIName
	} else if len(originalTypeHints) > 0 && originalTypeHints[0] != "" {
		responseKey = originalTypeHints[0]
	}
	if responseKey != typeHint {
		if oi, ok := response.ObjectInfos[typeHint]; ok {
			delete(response.ObjectInfos, typeHint)
			response.ObjectInfos[responseKey] = oi
		}
	}

	recordTypeID := ""
	if len(recordIDs) > 0 {
		if rec, ok := response.Records[recordIDs[0]]; ok {
			if rtField, ok := rec.Fields[constants.FieldRecordTypeID]; ok {
				if rtID, ok := rtField.Value.(string); ok && rtID != "" {
					recordTypeID = rtID
				}
			}
		}
	}
	uiLayout, err := s.getLayoutWithRecordType(ctx, typeHint, user, recordTypeID)
	if err != nil {
		return nil, err
	}
	if uiLayout != nil {
		response.Layouts[responseKey] = map[string]uiAPILayout{uiLayout.Id: *uiLayout}
	}
	return response, nil
}

func (s *uiAPIService) processRecordForUI(ctx context.Context, objectType, recordID string, user *models.UserSession) (*uiAPIRecord, error) {
	rec, access, err := s.recordAccess.RetrieveWithAccess(ctx, objectType, recordID, user)
	if err != nil {
		return nil, err
	}
	if rec == nil {
		return nil, uiAPIStatusError{status: http.StatusNotFound, message: fmt.Sprintf("record %s not found", recordID)}
	}
	schema, err := s.describe(ctx, objectType)
	if err != nil {
		return nil, err
	}
	if schema == nil {
		return nil, uiAPIStatusError{status: http.StatusNotFound, message: "schema " + objectType + " not found"}
	}
	fieldIndex := make(map[string]*models.FieldMetadata, len(schema.Fields))
	for i := range schema.Fields {
		fieldIndex[schema.Fields[i].APIName] = &schema.Fields[i]
	}
	uiRecord := uiAPIRecord{
		ApiName: schema.APIName,
		Id:      recordID,
		Fields:  make(map[string]uiAPIFieldValue),
	}
	if lmd, ok := rec[constants.FieldLastModifiedDate].(string); ok {
		uiRecord.LastModifiedDate = lmd
	}
	if sms, ok := rec[constants.FieldSystemModstamp].(string); ok {
		uiRecord.SystemModstamp = sms
	}
	fieldAccess := fieldAccessMap(ctx, s.policy, user, objectType)
	synthesizeVirtualCompoundFields(schema, models.SObject(rec), func(fieldAPIName string) bool {
		if fieldAccess == nil {
			return true
		}
		field, ok := fieldAccess[fieldAPIName]
		return ok && field.Readable
	})
	lookups := s.mapRecordFieldsToUI(models.SObject(rec), fieldIndex, &uiRecord)
	if err := s.resolveRecordLookups(ctx, lookups, &uiRecord, user); err != nil {
		return nil, err
	}
	if access != nil {
		uiRecord.RecordAccess = &uiAPIRecordAccess{
			HasReadAccess:   access.Read,
			HasEditAccess:   access.Update,
			HasDeleteAccess: access.Delete,
			IsLocked:        access.Locked,
		}
	}
	return &uiRecord, nil
}

func (s *uiAPIService) getBatchRecordAccess(ctx context.Context, objectName string, recordIDs []string, user *models.UserSession) (map[string]uiAPIRecordAccess, error) {
	if strings.TrimSpace(objectName) != "" {
		objectName = strings.ToLower(objectName)
		if err := s.requireObjectAccess(ctx, user, objectName, constants.PermRead); err != nil {
			return nil, err
		}
		access, err := s.recordAccess.BatchRecordAccess(ctx, objectName, recordIDs, user)
		return uiAPIRecordAccessMap(access), err
	}
	resolutionIDs := make([]string, 0, len(recordIDs))
	seenIDs := make(map[string]struct{}, len(recordIDs))
	for _, recordID := range recordIDs {
		recordID = strings.TrimSpace(recordID)
		if recordID == "" {
			continue
		}
		if _, seen := seenIDs[recordID]; seen {
			continue
		}
		seenIDs[recordID] = struct{}{}
		resolutionIDs = append(resolutionIDs, recordID)
	}
	if len(resolutionIDs) == 0 {
		return map[string]uiAPIRecordAccess{}, nil
	}
	idTypeMap, err := s.recordAccess.ObjectTypesForIDs(ctx, resolutionIDs)
	if err != nil {
		return nil, err
	}
	groupedIDs := make(map[string][]string)
	for _, recordID := range resolutionIDs {
		objectType := strings.TrimSpace(idTypeMap[recordID])
		if objectType == "" {
			continue
		}
		groupedIDs[objectType] = append(groupedIDs[objectType], recordID)
	}
	result := make(map[string]uiAPIRecordAccess)
	for objectType, ids := range groupedIDs {
		accessMap, err := s.recordAccess.BatchRecordAccess(ctx, objectType, ids, user)
		if err != nil {
			if status, ok := httpStatusFromError(err); ok && status == http.StatusForbidden {
				continue
			}
			return nil, err
		}
		for recordID, access := range uiAPIRecordAccessMap(accessMap) {
			result[recordID] = access
		}
	}
	return result, nil
}

func uiAPIRecordAccessMap(access map[string]contract.RecordAccess) map[string]uiAPIRecordAccess {
	out := make(map[string]uiAPIRecordAccess, len(access))
	for id, entry := range access {
		out[id] = uiAPIRecordAccess{
			HasReadAccess:   entry.Read,
			HasEditAccess:   entry.Update,
			HasDeleteAccess: entry.Delete,
			IsLocked:        entry.Locked,
		}
	}
	return out
}

func (s *uiAPIService) getObjectInfo(ctx context.Context, objectType string, user *models.UserSession) (*uiAPIObjectInfo, error) {
	access, err := s.effectiveAccess(ctx, user, objectType)
	if err != nil {
		return nil, err
	}
	if err := requireObjectInfoReadAccess(access, objectType); err != nil {
		return nil, err
	}
	schema, err := s.describe(ctx, objectType)
	if err != nil {
		if isNotFoundError(err) {
			return nil, nil
		}
		return nil, err
	}
	if schema == nil {
		return nil, nil
	}
	keyPrefix := ""
	if prefix, ok := s.metadata.KeyPrefix(ctx, schema.APIName); ok {
		keyPrefix = prefix
	}
	objInfo := &uiAPIObjectInfo{
		ApiName:      schema.APIName,
		Label:        schema.Label,
		LabelPlural:  schema.PluralLabel,
		KeyPrefix:    keyPrefix,
		Queryable:    true,
		Createable:   access.Object.Create,
		Updateable:   access.Object.Update,
		Deletable:    access.Object.Delete,
		Retrieveable: true,
		Searchable:   true,
		Triggerable:  !schema.IsSystem,
		Undeletable:  access.Object.Delete,
		Layoutable:   !schema.IsSystem,
		Fields:       make(map[string]uiAPIFieldInfo),
	}
	// Projected from describe's own field result rather than derived again from the schema.
	//
	// The two shapes answer the same questions about a field, and this used to answer them twice —
	// once here from models.FieldMetadata and once in describeObjectResponse. Two derivations of one
	// fact drift: object-info reported the resolved scalar type only because someone remembered to
	// mirror describe's rule here, and every attribute the frontend needs would have arrived with the
	// same obligation. Describe owns the field facts; this maps them into the v63 UI API spelling.
	//
	// It costs what describe costs, which is the point: the browser calls describe for exactly this
	// today, and #1867 §12 replaces that call with this one rather than adding a second.
	described, err := describeObjectResponse(ctx, "", s.metadata, access, schema)
	if err != nil {
		return nil, err
	}
	// Requiredness is the one field fact describe cannot own, because its shape has no attribute for
	// it. `nillable` is not that attribute inverted: describe reports every boolean as non-nillable
	// (a checkbox is false, never null), so inverting it would mark every optional checkbox required
	// — PricebookEntry.IsActive, Contact.DoNotCall — and put false required markers in the wire
	// contract. It comes from the schema's own bit instead.
	requiredByField := make(map[string]bool, len(schema.Fields))
	for _, f := range schema.Fields {
		requiredByField[f.APIName] = f.Required
	}

	for _, f := range described.Fields {
		// Describe already applied field-level security; this asserts it rather than assuming, so a
		// change there cannot silently widen what object-info exposes.
		fieldAccess, ok := uiAPIFieldAccess(access, f.Name)
		if !ok || !fieldAccess.Readable || !f.Readable {
			continue
		}
		fi := uiAPIFieldInfo{
			ApiName:        f.Name,
			Label:          f.Label,
			DataType:       f.Type,
			Required:       requiredByField[f.Name],
			Createable:     f.Createable,
			Updateable:     f.Updateable,
			Length:         f.Length,
			Reference:      len(f.ReferenceTo) > 0,
			NameField:      f.NameField,
			Custom:         f.Custom,
			Calculated:     f.Calculated,
			Unique:         f.Unique,
			Precision:      f.Precision,
			Scale:          f.Scale,
			Sortable:       f.Sortable,
			Filterable:     f.Filterable,
			HtmlFormatted:  f.HtmlFormatted,
			InlineHelpText: f.InlineHelpText,
			ControllerName: f.ControllerName,
		}
		for _, refTo := range f.ReferenceTo {
			fi.ReferenceToInfos = append(fi.ReferenceToInfos, uiAPIReferenceToInfo{ApiName: refTo, NameFields: []string{constants.FieldName}})
		}
		if len(f.ReferenceTo) > 0 && f.RelationshipName != nil {
			fi.RelationshipName = *f.RelationshipName
		}
		objInfo.Fields[f.Name] = fi
	}
	rtInfos := make(map[string]uiAPIRecordTypeInfo, len(access.RecordTypes))
	for _, entry := range access.RecordTypes {
		if entry.Master && access.HasConcreteRecordTypes {
			continue
		}
		rtInfos[entry.ID] = uiAPIRecordTypeInfo{
			Name:                     entry.Name,
			RecordTypeId:             entry.ID,
			Available:                entry.Available,
			DefaultRecordTypeMapping: entry.Default,
			Master:                   entry.Master,
		}
	}
	objInfo.RecordTypeInfos = rtInfos
	objInfo.DefaultRecordTypeId = access.DefaultRecordTypeID
	return objInfo, nil
}

func (s *uiAPIService) getLayoutWithRecordType(ctx context.Context, objectType string, user *models.UserSession, recordTypeID string) (*uiAPILayout, error) {
	return s.getLayoutWithOptions(ctx, objectType, user, recordTypeID, "")
}

func (s *uiAPIService) getLayoutWithOptions(ctx context.Context, objectType string, user *models.UserSession, recordTypeID string, layoutType string) (*uiAPILayout, error) {
	recordTypeID = strings.TrimSpace(recordTypeID)
	if err := s.requireObjectAccess(ctx, user, objectType, constants.PermRead); err != nil {
		return nil, err
	}
	if err := s.ensureRecordTypeVisibleForObject(ctx, objectType, user, recordTypeID); err != nil {
		return nil, err
	}
	internalLayout, err := s.metadata.ResolveLayout(ctx, objectType, user, recordTypeID)
	if err != nil || internalLayout == nil {
		return nil, err
	}
	mode := uiAPILayoutModeView
	switch layoutType {
	case "Create":
		mode = "Create"
	case "Edit":
		mode = "Edit"
	}
	return s.transformLayout(ctx, internalLayout, objectType, mode, user)
}

func (s *uiAPIService) getLayoutByID(ctx context.Context, layoutID string, objectType string, mode string, user *models.UserSession) (*uiAPILayout, error) {
	internalLayout, err := s.layouts.GetLayoutByID(ctx, layoutID)
	if err != nil {
		return nil, err
	}
	if internalLayout == nil && objectType != "" && layoutID == "default_"+objectType {
		if err := s.requireObjectAccess(ctx, user, objectType, constants.PermRead); err != nil {
			return nil, err
		}
		resolved, err := s.metadata.ResolveLayout(ctx, objectType, nil, "")
		if err != nil {
			return nil, err
		}
		if resolved != nil && resolved.ID == layoutID {
			internalLayout = resolved
		}
	}
	if internalLayout == nil {
		return nil, nil
	}
	if objectType != "" && internalLayout.ObjectAPIName != objectType {
		return nil, nil
	}
	if err := s.requireObjectAccess(ctx, user, internalLayout.ObjectAPIName, constants.PermRead); err != nil {
		return nil, err
	}
	return s.transformLayout(ctx, internalLayout, objectType, mode, user)
}

func (s *uiAPIService) transformLayout(ctx context.Context, internalLayout *models.PageLayout, objectType string, mode string, user *models.UserSession) (*uiAPILayout, error) {
	if internalLayout == nil {
		return nil, nil
	}
	if mode == "" {
		mode = uiAPILayoutModeView
	}
	layoutObjectType := strings.TrimSpace(objectType)
	if layoutObjectType == "" {
		layoutObjectType = strings.TrimSpace(internalLayout.ObjectAPIName)
	}
	access, err := s.effectiveAccess(ctx, user, layoutObjectType)
	if err != nil {
		return nil, err
	}
	uiLayout := &uiAPILayout{
		Id:            internalLayout.ID,
		LayoutName:    internalLayout.LayoutName,
		ObjectApiName: layoutObjectType,
		LayoutType:    internalLayout.Type,
		Mode:          mode,
		CompactLayout: uiAPIReadableFieldNames(access, internalLayout.CompactLayout),
		Tabs:          internalLayout.Tabs,
		// Initialized, like its siblings below, so an empty result marshals as
		// [] rather than null. This used to be unreachable in practice --
		// sections were emitted unconditionally -- and dropping the ones with
		// nothing to show makes "no sections at all" an ordinary response.
		Sections:     make([]uiAPILayoutSection, 0, len(internalLayout.Sections)),
		RelatedLists: make([]uiAPIRelatedListInfo, 0, len(internalLayout.RelatedLists)),
		RecordPanels: make([]uiAPIRecordPanelInfo, 0, len(internalLayout.RecordPanels)),
	}
	schema, err := s.describe(ctx, layoutObjectType)
	if err != nil && !isNotFoundError(err) {
		return nil, err
	}
	fieldIndex := make(map[string]*models.FieldMetadata)
	if schema != nil {
		for i := range schema.Fields {
			fieldIndex[schema.Fields[i].APIName] = &schema.Fields[i]
		}
	}
	layoutFLS := layoutfls.New(ctx, layoutObjectType, access, schema,
		func(ctx context.Context, objectName string) (*models.ObjectMetadata, error) {
			schema, err := s.describe(ctx, objectName)
			if err != nil {
				if isNotFoundError(err) {
					return nil, nil
				}
				return nil, err
			}
			return schema, nil
		},
		func(ctx context.Context, objectName string) (*contract.EffectiveObjectAccess, error) {
			return s.effectiveAccess(ctx, user, objectName)
		},
	)
	sections := internalLayout.Sections
	if mode == uiAPILayoutModeCreate {
		sections = sectionsSupplyingCreateDemandedFields(schema, sections, access)
	}
	for _, sec := range sections {
		fieldLabels, err := uiAPIReadableFieldLabels(access, sec.FieldLabels, layoutFLS)
		if err != nil {
			return nil, err
		}
		uiSec := uiAPILayoutSection{
			Id:          sec.ID,
			Heading:     sec.Label,
			Columns:     sec.Columns,
			UseHeading:  true,
			Collapsible: true,
			FieldLabels: fieldLabels,
		}
		if sec.Tab != nil {
			uiSec.Tab = *sec.Tab
		}
		cols := sec.Columns
		if cols < 1 {
			cols = 1
		}
		var currentRow uiAPILayoutRow
		appendItem := func(item uiAPILayoutItem) {
			currentRow.LayoutItems = append(currentRow.LayoutItems, item)
			if len(currentRow.LayoutItems) == cols {
				uiSec.LayoutRows = append(uiSec.LayoutRows, currentRow)
				currentRow = uiAPILayoutRow{}
			}
		}
		for _, fieldName := range sec.Fields {
			item := uiAPILayoutItem{}
			if dotIdx := strings.IndexByte(fieldName, '.'); dotIdx > 0 {
				parentFieldName := fieldName[:dotIdx]
				childFieldName := fieldName[dotIdx+1:]
				readable, err := layoutFLS.LookupExpansionReadable(fieldName)
				if err != nil {
					return nil, err
				}
				if !readable {
					continue
				}
				// A lookup expansion is a layout entry like any other, so
				// authored hiding applies to it. Without this the whole dotted
				// arm would silently ignore FieldBehaviors: an admin hides
				// Owner.Name, nothing happens, and nothing reports why.
				//
				// Only hidden. "required" stays inert here because an expansion
				// is a projection of the parent's record, not a writable field
				// of this one — marking it required would render a control the
				// save path never validates.
				//
				// Keyed by the layout entry verbatim. FLS resolves a dotted path
				// through several spellings (FK name, relationship name, either
				// case), so OwnerId.Name and Owner.Name reach the same field — but
				// behavior authored under one spelling does not apply to an entry
				// written as the other. Same exact-match rule the plain arm and the
				// sibling FieldLabels lookup already use: a key matching no entry is
				// inert, the ruled fall-through for unrecognized authoring.
				//
				// No clamp either: the create path
				// demands fields, never expansions, so hiding one cannot
				// produce a save that fails naming something invisible.
				if authoredBehaviorFor(sec.FieldBehaviors, fieldName, mode) == authoredBehaviorHidden {
					delete(fieldLabels, fieldName)
					continue
				}
				parentLabel := parentFieldName
				if fMeta, ok := fieldIndex[parentFieldName]; ok {
					parentLabel = fMeta.Label
				}
				label := parentLabel + " → " + childFieldName
				if customLabel, ok := fieldLabels[fieldName]; ok && customLabel != "" {
					label = customLabel
				}
				isEditableExpansion := childFieldName == constants.FieldName
				details := map[string]any{
					"lookupExpansion": true,
					"parentField":     parentFieldName,
					"displayField":    childFieldName,
				}
				if isEditableExpansion {
					details["editable"] = true
				}
				item = uiAPILayoutItem{
					Label: label,
					LayoutComponents: []uiAPILayoutComponent{{
						ComponentType: "Field",
						ApiName:       fieldName,
						Details:       details,
					}},
					EditableForNew:    isEditableExpansion,
					EditableForUpdate: isEditableExpansion,
				}
			} else {
				fieldAccess, ok := uiAPIFieldAccess(access, fieldName)
				if !ok || !fieldAccess.Readable {
					continue
				}
				fieldLabel := fieldName
				required := false
				editableForNew := fieldAccess.Createable
				editableForUpdate := fieldAccess.Updateable
				fMeta, hasMeta := fieldIndex[fieldName]
				if hasMeta {
					fieldLabel = fMeta.Label
					required = fMeta.Required
				}

				// Authored behavior INTERSECT field-level security. FLS is the
				// floor and is applied above; authoring can only narrow what
				// survives it, never widen it — editableForNew and
				// editableForUpdate stay FLS-derived below.
				//
				// The response is fetched per mode, so a single Required bool
				// resolves per mode without a wire change.
				authored := authoredBehaviorFor(sec.FieldBehaviors, fieldName, mode)
				switch authored {
				case authoredBehaviorHidden:
					// Hidden is the item's ABSENCE, the same exit an
					// FLS-unreadable field takes above, so appendItem re-packs
					// the grid and a create form with most fields hidden stays
					// dense. Setting editableForNew=false instead would leave
					// half-empty rows.
					//
					// Clamped: a field the create path will demand is never
					// hidden, or the save fails naming a field the user cannot
					// see. Roll-forward only, so that error would need a
					// hand-written repair.
					if mode != uiAPILayoutModeCreate || !fieldRequiresUserInputOnCreate(schema, fMeta) {
						// Take the custom label with it. The FLS path already
						// drops labels for fields it removes, and a label for an
						// item that is not in the response is a dangling half of
						// "hidden = absence".
						delete(fieldLabels, fieldName)
						continue
					}
				case authoredBehaviorRequired:
					// Narrowing-only applies on this axis too. Demanding a
					// field the caller is not permitted to write renders an
					// obligation they cannot discharge, so authored required
					// only reaches a field FLS lets them edit in this mode. A
					// field the SCHEMA requires keeps its required flag either
					// way — that is the schema's fact, not the layout's.
					//
					// Scoped to the authored axis, deliberately. The clamp above can
					// still emit required=true with editableForNew=false, for a
					// schema-required field FLS forbids creating. That pairing is
					// honest rather than broken: validateRequiredForCreate really will
					// demand it and never consults FLS, so the response is reporting a
					// misconfiguration, not creating one.
					switch mode {
					case uiAPILayoutModeCreate:
						required = required || editableForNew
					case uiAPILayoutModeEdit:
						required = required || editableForUpdate
					}
				}
				if customLabel, ok := fieldLabels[fieldName]; ok && customLabel != "" {
					fieldLabel = customLabel
				}
				item = uiAPILayoutItem{
					Label: fieldLabel,
					LayoutComponents: []uiAPILayoutComponent{{
						ComponentType: "Field",
						ApiName:       fieldName,
					}},
					Required:          required,
					EditableForNew:    editableForNew,
					EditableForUpdate: editableForUpdate,
				}
			}
			appendItem(item)
		}
		if len(currentRow.LayoutItems) > 0 {
			uiSec.LayoutRows = append(uiSec.LayoutRows, currentRow)
		}
		// Absence carries to the section, not just to the item.
		//
		// "Hidden is the item's ABSENCE" is the rule every exit above already
		// follows -- authored hidden, FLS-unreadable, unknown to the effective
		// access projection (the field no longer exists), and unreadable
		// expansions all `continue` rather than emit a disabled control. The
		// third of those is probably the one that fires most in production: a
		// layout section outliving the fields it lists. The section was
		// the one place the rule stopped: a section whose every field took one
		// of those exits still shipped, and the renderer draws a heading bar
		// and an empty box for it, because it has no empty-section guard.
		//
		// Two populations hit this, and the second one needs no authoring at
		// all: an admin who hides a section's fields for one form, and any user
		// whose FLS leaves them nothing readable in that section. The latter is
		// live today on the detail page.
		//
		// Component sections are exempt. They legitimately carry no fields, and
		// this path drops their componentName on the floor already -- they
		// render as empty boxes for a different reason, which is a defect of
		// its own rather than something to sweep up under "hidden".
		// Only a section that HAD fields and lost them all. A section authored
		// with none is a different thing -- an empty one the admin made, or a
		// component's home -- and today's behavior for those is out of scope
		// here; conflating the two would delete an arrangement rather than
		// tidy a projection.
		if len(sec.Fields) > 0 && len(uiSec.LayoutRows) == 0 && !isComponentSection(sec) {
			continue
		}
		uiLayout.Sections = append(uiLayout.Sections, uiSec)
	}
	relatedLists, err := s.transformRelatedLists(ctx, user, layoutObjectType, access, internalLayout.RelatedLists)
	if err != nil {
		return nil, err
	}
	uiLayout.RelatedLists = relatedLists
	for _, panel := range internalLayout.RecordPanels {
		hiddenRef, err := layoutFLS.TextReferencesHiddenField(panel.VisibilityCondition)
		if err != nil {
			return nil, err
		}
		if hiddenRef {
			continue
		}
		uiLayout.RecordPanels = append(uiLayout.RecordPanels, uiAPIRecordPanelInfo{
			Id:                  panel.ID,
			RendererID:          panel.RendererID,
			Position:            panel.Position,
			TabID:               panel.TabID,
			TabLabel:            panel.TabLabel,
			Order:               panel.Order,
			VisibilityCondition: panel.VisibilityCondition,
			Config:              panel.Config,
		})
	}
	headerActionsAuthored := internalLayout.HeaderActionsAuthored || len(internalLayout.HeaderActions) > 0
	if internalLayout.HeaderActions != nil || headerActionsAuthored {
		sourceActions := internalLayout.HeaderActions
		if sourceActions == nil {
			sourceActions = []models.ActionConfig{}
		}
		headerActions, err := layoutFLS.FilterActions(sourceActions)
		if err != nil {
			return nil, err
		}
		uiLayout.HeaderActions = headerActions
		uiLayout.HeaderActionsAuthored = headerActionsAuthored
	}
	quickActionsAuthored := internalLayout.QuickActionsAuthored || len(internalLayout.QuickActions) > 0
	if internalLayout.QuickActions != nil || quickActionsAuthored {
		sourceActions := internalLayout.QuickActions
		if sourceActions == nil {
			sourceActions = []models.ActionConfig{}
		}
		quickActions, err := layoutFLS.FilterActions(sourceActions)
		if err != nil {
			return nil, err
		}
		uiLayout.QuickActions = quickActions
		uiLayout.QuickActionsAuthored = quickActionsAuthored
	}
	if len(internalLayout.RecordStates) > 0 {
		recordStates, err := layoutFLS.FilterRecordStateRules(internalLayout.RecordStates)
		if err != nil {
			return nil, err
		}
		uiLayout.RecordStates = recordStates
	}
	// Primary-pin keys are presentation config (stable action keys), not
	// field-bearing metadata — no FLS filtering applies.
	if len(internalLayout.PrimaryHeaderActions) > 0 {
		uiLayout.PrimaryHeaderActions = append([]string(nil), internalLayout.PrimaryHeaderActions...)
	}
	return uiLayout, nil
}

func (s *uiAPIService) transformRelatedLists(ctx context.Context, user *models.UserSession, parentObjectAPIName string, parentAccess *contract.EffectiveObjectAccess, relatedLists []models.RelatedListConfig) ([]uiAPIRelatedListInfo, error) {
	if len(relatedLists) == 0 {
		return nil, nil
	}
	results := make([]*uiAPIRelatedListInfo, len(relatedLists))
	g, groupCtx := errgroup.WithContext(ctx)
	limit := uiAPIRelatedListTransformConcurrency
	if len(relatedLists) < limit {
		limit = len(relatedLists)
	}
	g.SetLimit(limit)
	for i := range relatedLists {
		index := i
		rl := relatedLists[i]
		g.Go(func() error {
			var err error
			var childAccess *contract.EffectiveObjectAccess
			if isHistoryObject(rl.ObjectAPIName) {
				childAccess = uiAPIFieldHistoryAccess(parentObjectAPIName, parentAccess, rl.ObjectAPIName)
			} else {
				childAccess, err = s.effectiveAccess(groupCtx, user, rl.ObjectAPIName)
				if err != nil {
					return err
				}
			}
			if childAccess == nil || !childAccess.Object.Read || !uiAPIFieldReadable(childAccess, rl.LookupField) {
				return nil
			}
			relatedFields := uiAPIReadableFieldNames(childAccess, rl.Fields)
			if len(rl.Fields) > 0 && len(relatedFields) == 0 {
				return nil
			}
			var resolution *contract.RelatedListResolution
			if res, ok := s.metadata.ResolveRelatedListResolution(groupCtx, rl.ObjectAPIName, rl.LookupField); ok {
				resolution, err = s.filterRelatedListResolution(groupCtx, user, parentAccess, childAccess, res)
				if err != nil {
					return err
				}
			}
			results[index] = &uiAPIRelatedListInfo{
				Id:              rl.ID,
				Label:           rl.Label,
				SecondaryLabel:  rl.SecondaryLabel,
				ObjectApiName:   rl.ObjectAPIName,
				LookupField:     rl.LookupField,
				Fields:          relatedFields,
				PrefillMappings: uiAPIFilterPrefillMappings(parentAccess, childAccess, rl.PrefillMappings),
				Resolution:      resolution,
			}
			return nil
		})
	}
	if err := g.Wait(); err != nil {
		return nil, err
	}
	out := make([]uiAPIRelatedListInfo, 0, len(relatedLists))
	for _, result := range results {
		if result != nil {
			out = append(out, *result)
		}
	}
	return out, nil
}

func uiAPIFieldHistoryAccess(parentObjectAPIName string, parentAccess *contract.EffectiveObjectAccess, objectAPIName string) *contract.EffectiveObjectAccess {
	if !isHistoryObject(objectAPIName) || !strings.EqualFold(parentObjectFromHistoryName(objectAPIName), parentObjectAPIName) || parentAccess == nil || !parentAccess.Object.Read {
		return nil
	}
	readableValue := false
	for _, access := range parentAccess.Fields {
		if access.Readable {
			readableValue = true
			break
		}
	}
	readable := contract.FieldAccess{Readable: true}
	return &contract.EffectiveObjectAccess{
		Object: contract.ObjectAccess{Read: true},
		Fields: map[string]contract.FieldAccess{
			"Id":                                  readable,
			historyParentFieldName(objectAPIName): readable,
			"Field":                               readable,
			"OldValue":                            {Readable: readableValue},
			"NewValue":                            {Readable: readableValue},
			"DataType":                            readable,
			"CreatedDate":                         readable,
			"CreatedById":                         readable,
		},
	}
}

type uiAPIRecordDefaultsOptions struct {
	ParentFieldAPIName string
	ParentRecordID     string
}

func (s *uiAPIService) getRecordDefaults(ctx context.Context, objectType string, recordTypeID string, user *models.UserSession, opts uiAPIRecordDefaultsOptions) (*uiAPIRecordDefaults, error) {
	recordTypeID = strings.TrimSpace(recordTypeID)
	if err := s.requireObjectAccess(ctx, user, objectType, constants.PermCreate); err != nil {
		return nil, err
	}
	schema, err := s.describe(ctx, objectType)
	if err != nil {
		if isNotFoundError(err) {
			return nil, nil
		}
		return nil, err
	}
	if schema == nil {
		return nil, nil
	}
	objInfo, err := s.getObjectInfo(ctx, objectType, user)
	if recordTypeID == "" && err == nil && objInfo != nil {
		recordTypeID = strings.TrimSpace(objInfo.DefaultRecordTypeId)
	}
	if recordTypeID == "" {
		access, accessErr := s.effectiveAccess(ctx, user, objectType)
		if accessErr != nil {
			return nil, accessErr
		}
		if requiresExplicitRecordTypeSelection(access) {
			return nil, noAvailableRecordTypeError(objectType)
		}
	}
	defaults := &uiAPIRecordDefaults{
		ObjectApiName: objectType,
		RecordTypeId:  recordTypeID,
		Record: uiAPIRecordWithDefaults{
			ApiName: objectType,
			Fields:  make(map[string]uiAPIFieldValue),
		},
	}
	if err == nil && objInfo != nil {
		defaults.ObjectInfo = objInfo
	}
	createLayout, err := s.getLayoutWithOptions(ctx, objectType, user, recordTypeID, "Create")
	if err != nil {
		return nil, err
	}
	if createLayout != nil {
		defaults.Layout = recordDefaultsLayout(createLayout)
	}
	access, err := s.effectiveAccess(ctx, user, objectType)
	if err != nil {
		return nil, err
	}
	for _, f := range schema.Fields {
		fieldAccess, ok := uiAPIFieldAccess(access, f.APIName)
		if !ok || !fieldAccess.Readable || !fieldAccess.Createable {
			continue
		}
		var defaultValue any
		var displayValue *string
		parentField := ""
		if f.SiblingMaxParentField != nil {
			parentField = strings.TrimSpace(*f.SiblingMaxParentField)
		}
		if parentField != "" {
			if err := s.defaults.ValidateSiblingMaxDefaultConfig(ctx, objectType, f.APIName); err != nil {
				return nil, err
			}
		}
		if f.DefaultValue != nil {
			valStr := *f.DefaultValue
			defaultValue = valStr
			switch f.Type {
			case "boolean":
				if b, err := strconv.ParseBool(valStr); err == nil {
					defaultValue = b
				}
			case "number", "double", "currency", "percent":
				if parsed, err := strconv.ParseFloat(valStr, 64); err == nil {
					defaultValue = parsed
				}
			case "int", "integer":
				if parsed, err := strconv.ParseInt(valStr, 10, 64); err == nil {
					defaultValue = parsed
				}
			}
		}
		if f.APIName == constants.FieldOwnerID && user != nil {
			defaultValue = user.ID
			displayValue = &user.Name
		}
		if f.APIName == constants.FieldRecordTypeID && recordTypeID != "" {
			defaultValue = recordTypeID
			recordTypeName := s.recordTypeNameForObject(ctx, objectType, recordTypeID)
			if recordTypeName != "" {
				displayValue = &recordTypeName
				defaults.Record.Fields[constants.ObjectRecordType] = relatedRecordFieldValue(constants.ObjectRecordType, recordTypeID, recordTypeName)
			}
		}
		if parentField != "" &&
			opts.ParentRecordID != "" &&
			strings.EqualFold(parentField, strings.TrimSpace(opts.ParentFieldAPIName)) &&
			defaultValue == nil {
			parentFieldMetadata := models.FindField(schema, parentField)
			if parentFieldMetadata == nil {
				return nil, NewValidationError("siblingMaxParentField", fmt.Sprintf("parent field %s.%s not found", objectType, parentField))
			}
			parentAccess, ok := uiAPIFieldAccess(access, parentFieldMetadata.APIName)
			if !ok || !parentAccess.Readable || !parentAccess.Createable {
				continue
			}
			if err := s.defaults.ValidateSiblingMaxDefaultParentForCreate(ctx, objectType, parentField, opts.ParentRecordID, user); err != nil {
				return nil, err
			}
			next, err := s.defaults.PreviewSiblingMaxDefault(ctx, objectType, f.APIName, parentField, opts.ParentRecordID)
			if err != nil {
				return nil, err
			}
			defaultValue = next
			defaults.SiblingMaxDefaultedFields = append(defaults.SiblingMaxDefaultedFields, f.APIName)
		}
		defaults.Record.Fields[f.APIName] = uiAPIFieldValue{Value: defaultValue, DisplayValue: displayValue}
	}
	return defaults, nil
}

func (s *uiAPIService) getCloneDefaults(ctx context.Context, recordID string, typeHint string, recordTypeID string, user *models.UserSession) (*uiAPIRecordDefaults, error) {
	recordTypeID = strings.TrimSpace(recordTypeID)
	objectType, err := s.resolveObjectType(ctx, recordID, typeHint)
	if err != nil {
		return nil, err
	}
	if objectType == "" {
		return nil, uiAPIStatusError{status: http.StatusNotFound, message: fmt.Sprintf("record %s not found", recordID)}
	}
	sourceRecord, err := s.records.Retrieve(ctx, objectType, recordID, user)
	if err != nil {
		return nil, err
	}
	if sourceRecord == nil {
		return nil, nil
	}
	if recordTypeID == "" {
		if sourceRecordTypeID, ok := sourceRecord[constants.FieldRecordTypeID].(string); ok {
			recordTypeID = strings.TrimSpace(sourceRecordTypeID)
		}
	}
	if err := s.requireObjectAccess(ctx, user, objectType, constants.PermCreate); err != nil {
		return nil, err
	}
	schema, err := s.describe(ctx, objectType)
	if err != nil {
		if isNotFoundError(err) {
			return nil, nil
		}
		return nil, err
	}
	if schema == nil {
		return nil, nil
	}
	defaults := &uiAPIRecordDefaults{
		ObjectApiName: objectType,
		RecordTypeId:  recordTypeID,
		Record: uiAPIRecordWithDefaults{
			ApiName: objectType,
			Fields:  make(map[string]uiAPIFieldValue),
		},
	}
	objInfo, err := s.getObjectInfo(ctx, objectType, user)
	if recordTypeID == "" && err == nil && objInfo != nil {
		recordTypeID = strings.TrimSpace(objInfo.DefaultRecordTypeId)
		defaults.RecordTypeId = recordTypeID
	}
	if recordTypeID == "" {
		access, accessErr := s.effectiveAccess(ctx, user, objectType)
		if accessErr != nil {
			return nil, accessErr
		}
		if requiresExplicitRecordTypeSelection(access) {
			return nil, noAvailableRecordTypeError(objectType)
		}
	}
	if err == nil && objInfo != nil {
		defaults.ObjectInfo = objInfo
	}
	// A clone is a CREATE: this payload seeds a form whose save is
	// POST /sobjects/:sobject, which runs validateRequiredForCreate. Projecting
	// Edit here skipped the create-path clamp entirely, so a field the create
	// path demands could be authored hidden and the save would then fail naming
	// a field the form never rendered. The sibling record-defaults/create path
	// already projects Create; this one was the outlier.
	createLayout, err := s.getLayoutWithOptions(ctx, objectType, user, recordTypeID, "Create")
	if err != nil {
		return nil, err
	}
	if createLayout != nil {
		defaults.Layout = recordDefaultsLayout(createLayout)
	}
	access, err := s.effectiveAccess(ctx, user, objectType)
	if err != nil {
		return nil, err
	}
	for _, f := range schema.Fields {
		if isSystemAuditField(f.APIName) {
			continue
		}
		fieldAccess, ok := uiAPIFieldAccess(access, f.APIName)
		if !ok || !fieldAccess.Readable || !fieldAccess.Createable {
			continue
		}
		value := sourceRecord[f.APIName]
		var displayValue *string
		if value != nil {
			if dv := valueToDisplayString(value); dv != "" {
				displayValue = &dv
			}
		}
		if f.APIName == constants.FieldRecordTypeID && recordTypeID != "" {
			value = recordTypeID
			recordTypeName := s.recordTypeNameForObject(ctx, objectType, recordTypeID)
			if recordTypeName != "" {
				displayValue = &recordTypeName
				defaults.Record.Fields[constants.ObjectRecordType] = relatedRecordFieldValue(constants.ObjectRecordType, recordTypeID, recordTypeName)
			}
		}
		if f.APIName == constants.FieldOwnerID && user != nil {
			value = user.ID
			displayValue = &user.Name
		}
		defaults.Record.Fields[f.APIName] = uiAPIFieldValue{Value: value, DisplayValue: displayValue}
	}
	return defaults, nil
}

func (s *uiAPIService) getListInfo(ctx context.Context, objectType string, listViewName string, user *models.UserSession) (*uiAPIListInfo, error) {
	if err := s.requireObjectAccess(ctx, user, objectType, constants.PermRead); err != nil {
		return nil, err
	}
	listViews, err := s.catalog.ListViews(ctx, objectType, user)
	if err != nil || listViews == nil {
		return nil, err
	}
	var listView *models.ListView
	for i := range listViews {
		if listViews[i].ID == listViewName || listViews[i].Label == listViewName {
			listView = listViews[i]
			break
		}
	}
	if listView == nil {
		return nil, nil
	}
	access, err := s.effectiveAccess(ctx, user, objectType)
	if err != nil {
		return nil, err
	}
	info := &uiAPIListInfo{
		ApiName:       listView.ID,
		Id:            listView.ID,
		Label:         listView.Label,
		DeveloperName: listView.ID,
		ObjectApiName: objectType,
		Visibility:    "Public",
	}
	schema, err := s.describe(ctx, objectType)
	if err != nil && !isNotFoundError(err) {
		return nil, err
	}
	fieldLabels := make(map[string]string)
	if schema != nil {
		for _, f := range schema.Fields {
			if !uiAPIFieldReadable(access, f.APIName) {
				continue
			}
			fieldLabels[f.APIName] = f.Label
		}
	}
	for _, col := range listView.Fields {
		if !uiAPIFieldReadable(access, col) {
			continue
		}
		label := col
		if l, ok := fieldLabels[col]; ok {
			label = l
		}
		info.DisplayColumns = append(info.DisplayColumns, uiAPIDisplayColumn{
			FieldApiName: col,
			Label:        label,
			Sortable:     true,
		})
	}
	return info, nil
}

func uiAPIFieldAccess(access *contract.EffectiveObjectAccess, fieldName string) (contract.FieldAccess, bool) {
	if access == nil || access.Fields == nil || fieldName == "" {
		return contract.FieldAccess{}, false
	}
	fieldAccess, ok := access.Fields[fieldName]
	return fieldAccess, ok
}

func uiAPIFieldReadable(access *contract.EffectiveObjectAccess, fieldName string) bool {
	fieldAccess, ok := uiAPIFieldAccess(access, fieldName)
	return ok && fieldAccess.Readable
}

func uiAPIFieldReadableAndCreateable(access *contract.EffectiveObjectAccess, fieldName string) bool {
	fieldAccess, ok := uiAPIFieldAccess(access, fieldName)
	return ok && fieldAccess.Readable && fieldAccess.Createable
}

func uiAPIReadableFieldNames(access *contract.EffectiveObjectAccess, fieldNames []string) []string {
	if len(fieldNames) == 0 {
		return nil
	}
	out := make([]string, 0, len(fieldNames))
	for _, fieldName := range fieldNames {
		if uiAPIFieldReadable(access, fieldName) {
			out = append(out, fieldName)
		}
	}
	return out
}

func uiAPIReadableFieldLabels(access *contract.EffectiveObjectAccess, labels map[string]string, layoutFLS *layoutfls.Filter) (map[string]string, error) {
	if len(labels) == 0 {
		return nil, nil
	}
	out := make(map[string]string, len(labels))
	for fieldName, label := range labels {
		readable := false
		if strings.Contains(fieldName, ".") && layoutFLS != nil {
			var err error
			readable, err = layoutFLS.LookupExpansionReadable(fieldName)
			if err != nil {
				return nil, err
			}
		} else {
			readable = uiAPIFieldReadable(access, fieldName)
		}
		if !readable {
			continue
		}
		if layoutFLS != nil {
			objectHints, err := layoutFLS.FieldPathObjectHints(fieldName)
			if err != nil {
				return nil, err
			}
			hiddenRef, err := layoutFLS.TextReferencesHiddenField(label, objectHints...)
			if err != nil {
				return nil, err
			}
			if hiddenRef {
				continue
			}
		}
		out[fieldName] = label
	}
	if len(out) == 0 {
		return nil, nil
	}
	return out, nil
}

func uiAPIFilterPrefillMappings(parentAccess, targetAccess *contract.EffectiveObjectAccess, mappings map[string]string) map[string]string {
	if len(mappings) == 0 {
		return nil
	}
	out := make(map[string]string, len(mappings))
	for targetField, parentField := range mappings {
		if uiAPIFieldReadableAndCreateable(targetAccess, targetField) && uiAPIFieldReadable(parentAccess, parentField) {
			out[targetField] = parentField
		}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func (s *uiAPIService) filterRelatedListResolution(ctx context.Context, user *models.UserSession, parentAccess, childAccess *contract.EffectiveObjectAccess, res *contract.RelatedListResolution) (*contract.RelatedListResolution, error) {
	if res == nil {
		return nil, nil
	}
	filtered := *res
	if !uiAPIResolvedReadTargetVisible(ctx, user, s, childAccess, filtered.Query.ObjectApiName, filtered.Query.LookupField) ||
		(filtered.Query.ParentFieldApiName != "" && !uiAPIFieldReadable(parentAccess, filtered.Query.ParentFieldApiName)) {
		return nil, nil
	}
	if !uiAPIResolvedReadTargetVisible(ctx, user, s, childAccess, filtered.List.ObjectApiName, filtered.List.FilterField) ||
		(filtered.List.ParentFieldApiName != "" && !uiAPIFieldReadable(parentAccess, filtered.List.ParentFieldApiName)) {
		return nil, nil
	}
	if filtered.Create.Kind != "" && filtered.Create.Kind != "none" && filtered.Create.ObjectApiName != "" {
		createAccess, err := s.accessForResolvedObject(ctx, user, childAccess, filtered.Create.ObjectApiName)
		if err != nil {
			return nil, err
		}
		if createAccess == nil || !createAccess.Object.Create {
			filtered.Create = contract.ResolvedCreateTarget{Kind: "none"}
		} else {
			filtered.Create.PrefillMappings = uiAPIFilterPrefillMappings(parentAccess, createAccess, filtered.Create.PrefillMappings)
		}
	}
	return &filtered, nil
}

func uiAPIResolvedReadTargetVisible(ctx context.Context, user *models.UserSession, s *uiAPIService, fallbackAccess *contract.EffectiveObjectAccess, objectName string, fieldName string) bool {
	if objectName == "" && fieldName == "" {
		return true
	}
	access, err := s.accessForResolvedObject(ctx, user, fallbackAccess, objectName)
	if err != nil || access == nil || !access.Object.Read {
		return false
	}
	return fieldName == "" || uiAPIFieldReadable(access, fieldName)
}

func (s *uiAPIService) accessForResolvedObject(ctx context.Context, user *models.UserSession, fallbackAccess *contract.EffectiveObjectAccess, objectName string) (*contract.EffectiveObjectAccess, error) {
	objectName = strings.TrimSpace(objectName)
	if objectName == "" {
		return fallbackAccess, nil
	}
	return s.effectiveAccess(ctx, user, objectName)
}

func (s *uiAPIService) effectiveAccess(ctx context.Context, user *models.UserSession, objectType string) (*contract.EffectiveObjectAccess, error) {
	if s.policy == nil {
		return nil, uiAPIStatusError{status: http.StatusInternalServerError, message: "effective access policy unavailable"}
	}
	return s.policy.Access(ctx, user, objectType, contract.AccessRequest{IncludeFields: true, IncludeRecordTypes: true})
}

func (s *uiAPIService) requireObjectAccess(ctx context.Context, user *models.UserSession, objectType string, operation constants.Permission) error {
	access, err := s.policy.Access(ctx, user, objectType, contract.AccessRequest{})
	if err != nil {
		return err
	}
	if access == nil {
		return uiAPIStatusError{status: http.StatusInternalServerError, message: "effective access policy unavailable"}
	}
	allowed := false
	switch operation {
	case constants.PermCreate:
		allowed = access.Object.Create
	case constants.PermRead:
		allowed = access.Object.Read
	case constants.PermEdit:
		allowed = access.Object.Update
	case constants.PermDelete:
		allowed = access.Object.Delete
	}
	if !allowed {
		return uiAPIStatusError{status: http.StatusForbidden, message: fmt.Sprintf("permission denied: cannot %s %s", operation, objectType)}
	}
	return nil
}

func requireObjectInfoReadAccess(access *contract.EffectiveObjectAccess, objectType string) error {
	if access == nil {
		return uiAPIStatusError{status: http.StatusInternalServerError, message: "effective access policy unavailable"}
	}
	if !access.Object.Read {
		return uiAPIStatusError{status: http.StatusForbidden, message: fmt.Sprintf("permission denied: cannot %s %s", constants.PermRead, objectType)}
	}
	return nil
}

func (s *uiAPIService) ensureRecordTypeVisibleForObject(ctx context.Context, objectType string, user *models.UserSession, recordTypeID string) error {
	recordTypeID = strings.TrimSpace(recordTypeID)
	if recordTypeID == "" {
		return nil
	}
	access, err := s.effectiveAccess(ctx, user, objectType)
	if err != nil {
		return err
	}
	if access == nil {
		return nil
	}
	if recordTypeID == constants.SyntheticMasterRecordTypeID {
		if access.HasConcreteRecordTypes {
			return noAvailableRecordTypeError(objectType)
		}
		return nil
	}
	if !recordTypeAvailable(access, recordTypeID) {
		return uiAPIStatusError{status: http.StatusForbidden, message: fmt.Sprintf("record type '%s' is not available for object '%s'", recordTypeID, objectType)}
	}
	return nil
}

func recordTypeAvailable(access *contract.EffectiveObjectAccess, recordTypeID string) bool {
	if access == nil {
		return true
	}
	for _, rt := range access.RecordTypes {
		if rt.ID == recordTypeID {
			return rt.Available
		}
	}
	return true
}

func requiresExplicitRecordTypeSelection(access *contract.EffectiveObjectAccess) bool {
	return access != nil && access.HasConcreteRecordTypes && access.AvailableConcreteCount == 0
}

func noAvailableRecordTypeError(objectType string) error {
	return uiAPIStatusError{status: http.StatusForbidden, message: fmt.Sprintf("no available record type for object '%s'", objectType)}
}

func (s *uiAPIService) recordTypeNameForObject(ctx context.Context, objectType string, recordTypeID string) string {
	recordTypeID = strings.TrimSpace(recordTypeID)
	if objectType == "" || recordTypeID == "" {
		return ""
	}
	if recordTypeID == constants.SyntheticMasterRecordTypeID {
		return "Master"
	}
	recordTypes, err := s.catalog.RecordTypes(ctx, objectType)
	if err != nil {
		return ""
	}
	for _, rt := range recordTypes {
		if rt != nil && rt.ID == recordTypeID {
			return strings.TrimSpace(rt.Name)
		}
	}
	return ""
}

// lookupTypeSuffix is the record-field companion key ("<field>_type") that
// publishes a polymorphic lookup's concrete target object type to the frontend
// reference renderer, which reads it to turn the lookup into a navigable link.
// Must match CRM_LOOKUP_TYPE_SUFFIX in the frontend pack
// (salesforce-crm-standard/frontend/src/ui-field-surface-policy.ts).
const lookupTypeSuffix = "_type"

type uiAPILookupTarget struct {
	fieldName      string
	relName        string
	sourceObj      string
	targetObj      string
	targetID       string
	polymorphic    bool
	allowedTargets []string
	resolvedType   string
}

func (s *uiAPIService) mapRecordFieldsToUI(rec models.SObject, fieldIndex map[string]*models.FieldMetadata, uiRecord *uiAPIRecord) []uiAPILookupTarget {
	var lookups []uiAPILookupTarget
	recordCurrency := recordCurrencyISOCode(rec)
	for k, v := range rec {
		fieldDef := fieldIndex[k]
		var displayValue *string
		if fieldDef != nil {
			fieldType := effectiveUIRecordFieldType(fieldDef)
			switch fieldType {
			case constants.FieldTypeDouble, constants.FieldTypeCurrency, constants.FieldTypePercent:
				if s, ok := v.(string); ok && s != "" {
					if f, err := strconv.ParseFloat(s, 64); err == nil {
						v = f
					}
				}
			}
			switch fieldType {
			case constants.FieldTypeDate:
				if t, ok := v.(string); ok && len(t) >= 10 {
					dv := t[:10]
					displayValue = &dv
				}
			case constants.FieldTypeDatetime:
				if t, ok := v.(string); ok && len(t) >= 19 {
					dv := t[:19]
					displayValue = &dv
				}
			case constants.FieldTypeCurrency:
				if f, ok := toFloat64(v); ok {
					dv := formatCurrencyDisplayValue(f, recordCurrency)
					displayValue = &dv
				}
			case constants.FieldTypePercent:
				if f, ok := toFloat64(v); ok {
					// Same scale authority the describe projection reports, so a
					// field cannot advertise one number of decimals and render
					// another (pack#4831 seam 1: this was a hardcoded "%.0f%%",
					// which turned 67.08333 into "67%").
					dv := fmt.Sprintf("%.*f%%", fieldScale(*fieldDef), f)
					displayValue = &dv
				}
			}
		}
		uiRecord.Fields[k] = uiAPIFieldValue{DisplayValue: displayValue, Value: v}
		if fieldDef == nil || !constants.IsReferenceType(fieldDef.Type) {
			continue
		}
		idStr, ok := v.(string)
		if !ok || idStr == "" || len(fieldDef.ReferenceTo) == 0 {
			continue
		}
		relName := inferLookupRelationshipName(fieldDef)
		if relName == "" {
			continue
		}
		targetObj := fieldDef.ReferenceTo[0]
		polymorphic := false
		var allowedTargets []string
		if fieldDef.IsPolymorphic {
			// Owner is polymorphic (User/Group) but is pinned to User for
			// display, keeping its existing dedicated treatment. Every other
			// polymorphic reference (Task.WhatId "Related To", Task.WhoId
			// "Name", …) has a per-record concrete target that ReferenceTo[0]
			// cannot express, so its real object is resolved — against the
			// declared referenceTo allowlist — in resolveRecordLookups.
			if fieldDef.APIName == constants.FieldOwnerID {
				targetObj = constants.TableUser
			} else {
				polymorphic = true
				allowedTargets = fieldDef.ReferenceTo
			}
		}
		lookups = append(lookups, uiAPILookupTarget{fieldName: k, relName: relName, sourceObj: uiRecord.ApiName, targetObj: targetObj, targetID: idStr, polymorphic: polymorphic, allowedTargets: allowedTargets})
	}
	return lookups
}

func (s *uiAPIService) resolveRecordLookups(ctx context.Context, lookups []uiAPILookupTarget, uiRecord *uiAPIRecord, user *models.UserSession) error {
	type resolveKey struct{ obj, id string }
	resolvedCache := make(map[resolveKey]map[string]any)
	targetIDsByObject := make(map[string][]string)
	targetObjectOrder := make([]string, 0)
	lookupCtx := constants.WithSecurityContext(ctx, constants.ElevatedContext, "uiapi: resolve lookup display names")
	// Polymorphic references (Task.WhatId "Related To", Task.WhoId "Name", …)
	// carry many possible referenceTo targets; the concrete object differs per
	// record, so ReferenceTo[0] is usually the wrong target. Resolve each
	// polymorphic target's real object here — before the display-name retrieval
	// below groups by object — so hydration queries the right object. Prefer a
	// type the record already carries; otherwise resolve it from the target id,
	// and accept it only if the field declares it (metadata allowlist), so a
	// stale/unsupported id-type never drives elevated hydration or a link to an
	// object outside referenceTo. The "<field>_type" companion is NOT published
	// here: it is cleared now and (re)published in the resolution loop only once
	// the target record and its schema actually resolve, so a failed or
	// unsupported lookup renders plain text rather than a link to the raw id.
	for i := range lookups {
		if !lookups[i].polymorphic {
			continue
		}
		typeKey := lookups[i].fieldName + lookupTypeSuffix
		concreteType, _ := uiRecord.Fields[typeKey].Value.(string)
		delete(uiRecord.Fields, typeKey)
		concreteType = strings.TrimSpace(concreteType)
		if concreteType == "" {
			if resolved, err := s.records.ObjectTypeForID(lookupCtx, lookups[i].targetID); err == nil {
				concreteType = strings.TrimSpace(resolved)
			}
		}
		concreteType = matchDeclaredTarget(concreteType, lookups[i].allowedTargets)
		if concreteType == "" {
			continue
		}
		lookups[i].targetObj = concreteType
		lookups[i].resolvedType = concreteType
	}
	for _, lt := range lookups {
		key := resolveKey{lt.targetObj, lt.targetID}
		if _, exists := resolvedCache[key]; exists {
			continue
		}
		if lt.targetObj == constants.ObjectRecordType {
			resolvedCache[key] = s.buildRecordTypeLookupRecord(ctx, lt.sourceObj, lt.targetID)
			continue
		}
		resolvedCache[key] = nil
		if _, exists := targetIDsByObject[lt.targetObj]; !exists {
			targetObjectOrder = append(targetObjectOrder, lt.targetObj)
		}
		targetIDsByObject[lt.targetObj] = append(targetIDsByObject[lt.targetObj], lt.targetID)
	}
	for _, objectName := range targetObjectOrder {
		ids := targetIDsByObject[objectName]
		recordsByID, err := s.records.RetrieveMany(lookupCtx, objectName, ids, user)
		if err == nil {
			for _, id := range ids {
				resolvedCache[resolveKey{objectName, id}] = recordsByID[id]
			}
			continue
		}
		for _, id := range ids {
			targetRec, retrieveErr := s.records.Retrieve(lookupCtx, objectName, id, user)
			if retrieveErr == nil && targetRec != nil {
				resolvedCache[resolveKey{objectName, id}] = targetRec
			}
		}
	}
	for _, lt := range lookups {
		key := resolveKey{lt.targetObj, lt.targetID}
		targetRec := resolvedCache[key]
		if targetRec == nil {
			continue
		}
		targetSchema, err := s.describe(ctx, lt.targetObj)
		if err != nil || targetSchema == nil {
			continue
		}
		// Retrieval and schema hydration for this target succeeded, so it is now
		// safe to publish the validated concrete type for a polymorphic lookup;
		// the frontend reads "<field>_type" to render a navigable link. Failed or
		// unsupported polymorphic lookups never reach here, so they stay plain
		// text (resolvedType is empty for single-target lookups).
		if lt.resolvedType != "" {
			uiRecord.Fields[lt.fieldName+lookupTypeSuffix] = uiAPIFieldValue{Value: lt.resolvedType}
		}
		resolvedName := resolveLookupDisplayName(targetSchema, models.SObject(targetRec))
		if lt.targetObj == constants.ObjectRecordType && resolvedName == "" {
			if name, ok := targetRec[constants.FieldName].(string); ok {
				resolvedName = name
			}
		}
		nestedRecord := uiAPIRecord{
			ApiName: lt.targetObj,
			Id:      lt.targetID,
			Fields: map[string]uiAPIFieldValue{
				constants.FieldName: {Value: resolvedName, DisplayValue: nil},
			},
		}
		var relDisplayValue *string
		if resolvedName != "" {
			relDisplayValue = &resolvedName
			if fieldVal, ok := uiRecord.Fields[lt.fieldName]; ok {
				fieldVal.DisplayValue = &resolvedName
				uiRecord.Fields[lt.fieldName] = fieldVal
			}
		}
		uiRecord.Fields[lt.relName] = uiAPIFieldValue{DisplayValue: relDisplayValue, Value: nestedRecord}
	}
	return nil
}

// matchDeclaredTarget returns the referenceTo entry (in its declared casing)
// that equals candidate case-insensitively, or "" when candidate is not a
// declared target of the polymorphic field. Restricting the resolved type to
// the field's allowlist keeps elevated hydration and frontend navigation on
// objects the field actually references, never a stale or unsupported type.
func matchDeclaredTarget(candidate string, allowed []string) string {
	candidate = strings.TrimSpace(candidate)
	if candidate == "" {
		return ""
	}
	for _, target := range allowed {
		if strings.EqualFold(strings.TrimSpace(target), candidate) {
			return target
		}
	}
	return ""
}

func (s *uiAPIService) buildRecordTypeLookupRecord(ctx context.Context, objectAPIName string, recordTypeID string) map[string]any {
	recordTypeName := s.recordTypeNameForObject(ctx, objectAPIName, recordTypeID)
	if recordTypeName == "" {
		return nil
	}
	return map[string]any{constants.FieldID: recordTypeID, constants.FieldName: recordTypeName}
}

func (s *uiAPIService) decorateRecordReadsInRecordUI(ctx context.Context, fileURLPrefix string, recordUI *uiAPIRecordUI, user *models.UserSession) {
	if s == nil || s.decorator == nil || recordUI == nil {
		return
	}
	req := recordReadDecorationRequest{
		FileURLPrefix:                fileURLPrefix,
		User:                         user,
		RecordAttachmentSource:       s.fileResolver,
		RecordAttachmentSourceLookup: s.fileLookup,
	}
	for recordKey, record := range recordUI.Records {
		if len(record.Fields) == 0 {
			continue
		}
		flat := make(contract.Record, len(record.Fields))
		for fieldName, fv := range record.Fields {
			flat[fieldName] = fv.Value
		}
		if err := s.decorator.DecorateRecordRead(ctx, record.ApiName, record.Id, flat, req); err != nil {
			log.Printf("record read decorator: serving UI record with raw values for %s/%s: %v", record.ApiName, record.Id, err)
			continue
		}
		for fieldName, newValue := range flat {
			fv, ok := record.Fields[fieldName]
			if !ok {
				continue
			}
			oldValue := fv.Value
			if reflect.DeepEqual(newValue, oldValue) {
				continue
			}
			fv.Value = newValue
			oldString, oldWasString := oldValue.(string)
			newString, newWasString := newValue.(string)
			if fv.DisplayValue != nil && oldWasString && newWasString && *fv.DisplayValue == oldString {
				fv.DisplayValue = &newString
			}
			record.Fields[fieldName] = fv
		}
		recordUI.Records[recordKey] = record
	}
}

func inferLookupRelationshipName(fieldDef *models.FieldMetadata) string {
	if fieldDef == nil {
		return ""
	}
	if fieldDef.RelationshipName != nil && *fieldDef.RelationshipName != "" {
		return *fieldDef.RelationshipName
	}
	apiName := fieldDef.APIName
	switch {
	case strings.HasSuffix(apiName, "__c") && len(apiName) > len("__c"):
		return strings.TrimSuffix(apiName, "__c") + "__r"
	case strings.HasSuffix(apiName, "Id") && len(apiName) > len("Id"):
		return strings.TrimSuffix(apiName, "Id")
	default:
		return ""
	}
}

func effectiveUIRecordFieldType(fieldDef *models.FieldMetadata) constants.SchemaFieldType {
	if fieldDef == nil {
		return ""
	}
	// Route through the dialect's single computed-type resolver so a rollup over
	// a percent/currency/number source is parsed and display-formatted as that
	// scalar here, exactly as the describe projection reports it.
	base := constants.NormalizeFieldType(string(fieldDef.Type))
	return constants.NormalizeFieldType(string(resolveComputedScalarType(base, *fieldDef)))
}

func recordCurrencyISOCode(rec models.SObject) string {
	for k, v := range rec {
		if strings.EqualFold(k, "CurrencyIsoCode") {
			if code := normalizeCurrencyISOCode(fmt.Sprint(v)); code != "" {
				return code
			}
			return "USD"
		}
	}
	return "USD"
}

func formatCurrencyDisplayValue(value float64, currencyISOCode string) string {
	return NewCurrencyDisplayFormatter().FormatCurrencyDisplayValue(value, currencyISOCode)
}

func toFloat64(v any) (float64, bool) {
	switch n := v.(type) {
	case float64:
		return n, true
	case float32:
		return float64(n), true
	case int:
		return float64(n), true
	case int64:
		return float64(n), true
	case int32:
		return float64(n), true
	case json.Number:
		f, err := n.Float64()
		return f, err == nil
	default:
		return 0, false
	}
}

func uiAPILayoutType(internalType string) string {
	if strings.EqualFold(strings.TrimSpace(internalType), "Compact") {
		return "Compact"
	}
	return "Full"
}

func recordDefaultsLayout(layout *uiAPILayout) *uiAPILayout {
	if layout == nil {
		return nil
	}
	copy := *layout
	copy.LayoutType = uiAPILayoutType(layout.LayoutType)
	return &copy
}

func relatedRecordFieldValue(targetObj string, targetID string, displayName string) uiAPIFieldValue {
	nestedRecord := uiAPIRecord{
		ApiName: targetObj,
		Id:      targetID,
		Fields: map[string]uiAPIFieldValue{
			constants.FieldName: {Value: displayName, DisplayValue: nil},
		},
	}
	var relDisplayValue *string
	if displayName != "" {
		relDisplayValue = &displayName
	}
	return uiAPIFieldValue{DisplayValue: relDisplayValue, Value: nestedRecord}
}

func isSystemAuditField(apiName string) bool {
	return apiName == constants.FieldID ||
		apiName == constants.FieldCreatedDate || apiName == constants.FieldCreatedByID ||
		apiName == constants.FieldLastModifiedDate || apiName == constants.FieldLastModifiedByID
}

func valueToDisplayString(v any) string {
	if v == nil {
		return ""
	}
	switch val := v.(type) {
	case string:
		return val
	case float64:
		return fmt.Sprintf("%g", val)
	case bool:
		if val {
			return "true"
		}
		return "false"
	default:
		return fmt.Sprintf("%v", val)
	}
}

func uiAPILayoutAssignmentResponseFromModel(assignment *models.LayoutAssignment) uiAPILayoutAssignmentResponse {
	if assignment == nil {
		return uiAPILayoutAssignmentResponse{}
	}
	return uiAPILayoutAssignmentResponse{
		ID:               assignment.ID,
		ObjectAPIName:    assignment.ObjectAPIName,
		LayoutID:         assignment.LayoutID,
		RoleID:           assignment.RoleID,
		ProfileID:        assignment.ProfileID,
		RecordTypeID:     assignment.RecordTypeID,
		Specificity:      assignment.Specificity,
		CreatedDate:      assignment.CreatedDate,
		LastModifiedDate: assignment.LastModifiedDate,
		CreatedByID:      assignment.CreatedByID,
	}
}

func uiAPILayoutSummaries(layouts []*models.PageLayout) []uiAPILayoutSummary {
	out := make([]uiAPILayoutSummary, 0, len(layouts))
	for _, layout := range layouts {
		if layout == nil {
			continue
		}
		out = append(out, uiAPILayoutSummary{
			ID:               layout.ID,
			LayoutName:       layout.LayoutName,
			ObjectAPIName:    layout.ObjectAPIName,
			Type:             layout.Type,
			IsDefault:        layout.IsDefault,
			CreatedDate:      layout.CreatedDate.Format("2006-01-02T15:04:05Z"),
			LastModifiedDate: layout.LastModifiedDate.Format("2006-01-02T15:04:05Z"),
		})
	}
	return out
}

func uiAPIApprovalLayoutSummaries(layouts []*models.ApprovalPageLayout) []uiAPIApprovalLayoutSummary {
	out := make([]uiAPIApprovalLayoutSummary, 0, len(layouts))
	for _, layout := range layouts {
		if layout == nil {
			continue
		}
		out = append(out, uiAPIApprovalLayoutSummary{
			ID:               layout.ID,
			LayoutName:       layout.LayoutName,
			ObjectAPIName:    layout.ObjectAPIName,
			Type:             layout.Type,
			IsDefault:        layout.IsDefault,
			CreatedDate:      layout.CreatedDate.Format("2006-01-02T15:04:05Z"),
			LastModifiedDate: layout.LastModifiedDate.Format("2006-01-02T15:04:05Z"),
		})
	}
	return out
}

func uiAPIServiceErrorResponse(req *dialect.Request, err error) *dialect.Response {
	if err == nil {
		return nil
	}
	if sharederrors.IsInternal(err) {
		log.Printf("ui api internal error: sanitized for client: %v", err)
		return uiAPIError(http.StatusInternalServerError, wire.SFCodeInternalError, uiAPIInternalErrorMessage)
	}
	if resp := validationErrorMaybe(err, wire.SFCodeInvalidRequest); resp != nil {
		return resp
	}
	var authoringErr *contract.MetadataAuthoringError
	if errors.As(err, &authoringErr) {
		if authoringErr.Kind == contract.MetadataAuthoringErrorConflict {
			message := authoringErr.Message
			if message == "" {
				message = authoringErr.Error()
			}
			return uiAPIError(http.StatusConflict, "DUPLICATE_VALUE", message)
		}
		return metadataAuthoringErrorResponseForVersion(err, req.Version)
	}
	if status, ok := httpStatusFromError(err); ok {
		switch status {
		case http.StatusBadRequest:
			return uiAPIError(http.StatusBadRequest, wire.SFCodeInvalidRequest, err.Error())
		case http.StatusUnauthorized:
			return uiAPIError(http.StatusUnauthorized, wire.SFCodeUnauthorized, err.Error())
		case http.StatusForbidden:
			return uiAPIError(http.StatusForbidden, wire.SFCodeForbidden, err.Error())
		case http.StatusNotFound:
			return uiAPIError(http.StatusNotFound, wire.SFCodeNotFound, err.Error())
		case http.StatusConflict:
			return uiAPIError(http.StatusConflict, "DUPLICATE_VALUE", err.Error())
		}
	}
	if sfErr, status, ok := wire.SFErrorFromDomainForVersion(err, apiVersionOrDefault(req.Version)); ok {
		return &dialect.Response{Status: status, Body: []wire.SFError{sfErr}}
	}
	log.Printf("ui api unclassified internal error: %v", err)
	return uiAPIError(http.StatusInternalServerError, wire.SFCodeInternalError, uiAPIInternalErrorMessage)
}

func uiAPIError(status int, code string, message string) *dialect.Response {
	return &dialect.Response{Status: status, Body: wire.SFErrorResponse(code, message)}
}

func generateUUID() string {
	var b [16]byte
	if _, err := rand.Read(b[:]); err != nil {
		panic(err)
	}
	b[6] = (b[6] & 0x0f) | 0x40
	b[8] = (b[8] & 0x3f) | 0x80
	return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
}

func uiAPIFilesBasePath(version string) string {
	return fmt.Sprintf("/services/data/%s/files", apiVersionOrDefault(version))
}

// Authored per-field behavior vocabulary. The platform stores the map without
// interpreting it (see models.PageSection.FieldBehaviors); this dialect owns
// what the keys and values mean and how they meet FLS.
//
// detail is honored, with a narrower vocabulary: shown and hidden only. It was
// deliberately absent until the Setup editor stopped authoring from the
// View-mode response, which would otherwise have treated a hidden item as a
// deleted field and written that back.
const (
	authoredBehaviorHidden   = "hidden"
	authoredBehaviorRequired = "required"

	uiAPILayoutModeCreate = "Create"
	uiAPILayoutModeEdit   = "Edit"
	// The mode a request gets when it asks for "Detail" or asks for nothing at
	// all: the record page and the record-ui composite both land here.
	uiAPILayoutModeView = "View"

	authoredModeCreate = "create"
	authoredModeEdit   = "edit"
	authoredModeDetail = "detail"
)

// isComponentSection reports whether this section's content is a component
// rather than fields. Such a section has no layout rows by nature, so it must
// not be mistaken for one whose fields all went absent.
func isComponentSection(sec models.PageSection) bool {
	return sec.Type != nil && strings.EqualFold(*sec.Type, "Component")
}

// authoredBehaviorModeKey maps the response mode onto the authored vocabulary,
// reporting false for a mode this projection does not honor.
//
// No mode the routes can produce reaches that false today: ?type= is validated
// to ""|Detail|Create|Edit, "" and Detail fold to View, and every internal
// caller passes View, Create or Edit. The second return is kept anyway because
// the lookup it guards is by string — dropping it would make an unmapped mode
// index the behavior map at the empty key, and an entry stored under "" would
// then match it. Nothing on the save path validates the vocabulary, so such an
// entry can exist. Unreachable through the handler is not the same as
// unnecessary, so it is tested directly on the resolver rather than left to a
// rendered-item test that cannot reach it.
func authoredBehaviorModeKey(mode string) (string, bool) {
	switch mode {
	case uiAPILayoutModeCreate:
		return authoredModeCreate, true
	case uiAPILayoutModeEdit:
		return authoredModeEdit, true
	case uiAPILayoutModeView:
		return authoredModeDetail, true
	default:
		return "", false
	}
}

// sectionsSupplyingCreateDemandedFields adds the fields a create cannot succeed
// without, when the layout does not carry them.
//
// The create form is built from a layout, but the save is not: it demands what
// the schema demands. A layout that omits one of those fields therefore renders
// a form that cannot succeed, and the error names a field the user was never
// shown. Two of thirty-five production layouts were in that state, and the
// likeliest way to get there involves no layout edit at all — flipping a field
// to required leaves every existing layout behind.
//
// Supplying the field here rather than refusing the layout at save is what
// makes the whole class disappear instead of moving. There are several ways a
// layout is written — the editor, the clone route, restore, pack install, and a
// schema change that writes no layout at all — and each would need its own
// guard. There is one way a create form is rendered. Guarding writes also
// cannot help the layouts that are already wrong, while this repairs the form
// they produce on the next request.
//
// Injected at the INPUT of the projection, not appended to its output, so the
// field goes through the same construction as every other one: field-level
// security still applies as the floor, labels and editability are resolved
// identically, and there is no second copy of that logic to drift.
//
// Salesforce reaches the same end by a different means: it refuses to let a
// universally required field leave a layout at all. The user-visible result
// matches — the create form always has the field — and this endpoint already
// serves a rendering rather than the stored layout, since it strips autonumber
// fields and drops hidden ones on the way out.
func sectionsSupplyingCreateDemandedFields(
	schema *models.ObjectMetadata,
	sections []models.PageSection,
	access *contract.EffectiveObjectAccess,
) []models.PageSection {
	if schema == nil {
		return sections
	}

	placed := map[string]bool{}
	for _, section := range sections {
		for _, fieldName := range section.Fields {
			placed[fieldName] = true
		}
	}

	var missing []string
	for i := range schema.Fields {
		field := schema.Fields[i]
		if !fieldRequiresUserInputOnCreate(schema, &field) || placed[field.APIName] {
			continue
		}
		// Only what this user could actually fill. Field-level security is the
		// floor for everything this projection emits, and supplying past it
		// would put a control on the form that cannot be typed into — the save
		// fails for them either way, and a disabled field they never asked for
		// explains none of it. Readable alone is not enough: the section loop
		// below admits a readable field and renders it non-editable, which for
		// a field nobody placed is noise rather than help.
		//
		// This is deliberately stricter than the create clamp, which keeps a
		// PLACED demanded field even when the caller cannot create it. The two
		// answer different questions — the clamp honors an arrangement the admin
		// made, this decides whether to invent one — and the difference is
		// invisible downstream, since the form drops non-editable items either
		// way.
		fieldAccess, ok := uiAPIFieldAccess(access, field.APIName)
		if !ok || !fieldAccess.Readable || !fieldAccess.Createable {
			continue
		}
		missing = append(missing, field.APIName)
	}
	if len(missing) == 0 {
		return sections
	}

	// Copied, never mutated in place: this layout may be shared with a cache or
	// another request, and the detail and edit forms must keep seeing exactly
	// what the admin authored. A fresh Fields slice too, rather than appending
	// to the stored one, which would write into its backing array whenever it
	// has spare capacity.
	supplied := make([]models.PageSection, len(sections))
	copy(supplied, sections)

	// A layout with no sections at all still renders a create form, and that
	// form still has to collect these. Bailing out here was the one place this
	// function abandoned its own invariant instead of satisfying it.
	if len(supplied) == 0 {
		return []models.PageSection{{
			ID:      "supplied_create_required",
			Label:   "Required",
			Columns: 2,
			Fields:  missing,
		}}
	}

	// The first section that already holds fields, not simply the first one.
	// Section zero can be a component section or one assigned to another tab —
	// a place the admin put a feed, not fields — and dropping a mandatory input
	// there puts it somewhere nobody expects to type.
	target := 0
	for i := range supplied {
		if len(supplied[i].Fields) > 0 {
			target = i
			break
		}
	}

	section := supplied[target]
	fields := make([]string, 0, len(section.Fields)+len(missing))
	fields = append(fields, section.Fields...)
	fields = append(fields, missing...)
	section.Fields = fields
	supplied[target] = section
	return supplied
}

// authoredBehaviorFor resolves the behavior this mode should apply to this
// field, after checking the stored value against the mode's own vocabulary.
//
// The vocabularies are not the same. create and edit accept shown, hidden and
// required; detail accepts shown and hidden only, because an authored required
// would have nothing behind it there.
//
// An authored required is a CLIENT-side obligation in every mode: the save
// path's check reads the field's own Required off the schema and knows nothing
// about layouts. The two form surfaces gate their own submit, so the obligation
// bites there. The record page has no such gate: its inline editor never
// refuses a save for being empty. Its one use of the required flag is
// dependent-picklist reconciliation, a different question that does not
// generalise. So the marker would be decoration.
//
// Note what this does NOT say. The record page is editable, and clearing a
// SCHEMA-required field is refused by the save path; that required flag is
// emitted for every mode and is untouched here. Only the layout's own extra
// obligation is dropped. An earlier version of this comment said the page was
// read-only, which is false and sent readers hunting for a defect that is not
// there.
//
// An out-of-vocabulary value resolves to shown, the same fall-through
// unrecognized values already take, so the platform's promise to preserve
// unknown keys is kept without the projection acting on them.
//
// One function for both the plain and the lookup-expansion arms: the same rule
// written twice is how this file's earlier defects started.
func authoredBehaviorFor(behaviors map[string]map[string]string, fieldName, mode string) string {
	modeKey, ok := authoredBehaviorModeKey(mode)
	if !ok {
		return ""
	}
	value := behaviors[fieldName][modeKey]
	if modeKey == authoredModeDetail && value == authoredBehaviorRequired {
		return ""
	}
	return value
}
