package sfcrmappservice

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"regexp"
	"sort"
	"strconv"
	"strings"

	crmwire "github.com/pingcap-inc/nexuscrm-app-packs/sfcrm/wire"
	catalogsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/catalog/consumerv1"
	commonsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	permissionsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/effectivepermissionsv1"
	querysemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/queryv1"
	recordsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/recordsv1"
	searchsemantic "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/searchv1"
)

const (
	SearchCapabilityID        = "io.nexuscrm.sfcrm.search.v1"
	defaultSearchLimit        = 200
	defaultSuggestionLimit    = 5
	maxSuggestionLimit        = 25
	defaultSearchLayoutRows   = 25
	maxSearchLayoutColumns    = 6
	minimumSearchTermLength   = 2
	runtimeScopedLookupRule   = "runtime_scoped_lookup"
	runtimeScopedPicklistRule = "runtime_scoped_picklist"
)

var searchObjectIdentifierPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)

type RuntimeSearchFunc func(
	context.Context,
	string,
	searchsemantic.Request,
) (searchsemantic.Response, error)

type RuntimeGetAppMetadataViewV3Func func(
	context.Context,
	catalogsemantic.GetRequest,
) (catalogsemantic.AppMetadataViewV3, error)

type RuntimeSearchClient struct {
	search               RuntimeSearchFunc
	query                RuntimeQueryFunc
	appMetadata          RuntimeGetAppMetadataViewV3Func
	reconcileAppMetadata RuntimeReconcileAppMetadataV3ReceiptsFunc
}

type searchRoute uint8

const (
	searchRouteSOSL searchRoute = iota + 1
	searchRouteParameterized
	searchRouteLayout
	searchRouteSuggestions
)

type searchObjectSpec struct {
	APIName                string            `json:"apiName"`
	KeyPrefix              string            `json:"keyPrefix"`
	Label                  string            `json:"label"`
	PluralLabel            string            `json:"pluralLabel,omitempty"`
	DisplayNameField       string            `json:"displayNameField,omitempty"`
	PathField              string            `json:"pathField,omitempty"`
	ListFields             []string          `json:"listFields,omitempty"`
	TableType              *string           `json:"tableType,omitempty"`
	Searchable             *bool             `json:"searchable,omitempty"`
	SharingModel           string            `json:"sharingModel,omitempty"`
	SharingParentObject    string            `json:"sharingParentObject,omitempty"`
	SharingParentField     string            `json:"sharingParentField,omitempty"`
	EnableHierarchySharing bool              `json:"enableHierarchySharing,omitempty"`
	SearchAllDefaultFields []string          `json:"searchAllDefaultFields,omitempty"`
	CreateRequiredFields   *[]string         `json:"createRequiredFields,omitempty"`
	Fields                 []searchFieldSpec `json:"fields"`
	PresentationFactsV5    bool              `json:"-"`
}

type effectiveObjectSearchDocument struct {
	Objects []searchObjectSpec `json:"objects"`
}

const (
	// Format 2 remains readable during the producer-first/consumer-first rollout
	// boundary, but it cannot carry the object pathField fact.
	effectiveObjectSearchComponentFormatV2 uint32 = 2
	// Format 3 adds the sparse object pathField fact used by the CRM Path UI.
	effectiveObjectSearchComponentFormatV3 uint32 = 3
	// Format 4 adds ordered listFields, sparse formula bodies, and typed
	// picklist value-domain sources derived by Catalog.
	effectiveObjectSearchComponentFormatV4 uint32 = 4
	// Format 5 completes retained object/field presentation facts required by
	// describe and UI API.
	effectiveObjectSearchComponentFormatV5 uint32 = 5
	// Format 6 adds the producer-derived static create-input requirement set.
	// Historical formats remain readable but cannot project a Create layout.
	effectiveObjectSearchComponentFormatV6 uint32 = 6
)

type searchFieldSpec struct {
	APIName               string                   `json:"apiName"`
	Label                 string                   `json:"label"`
	Type                  string                   `json:"type"`
	ReturnType            string                   `json:"returnType,omitempty"`
	Precision             *int                     `json:"precision,omitempty"`
	Scale                 *int                     `json:"scale,omitempty"`
	Required              bool                     `json:"required,omitempty"`
	Unique                bool                     `json:"unique,omitempty"`
	DefaultedOnCreate     bool                     `json:"defaultedOnCreate,omitempty"`
	Createable            bool                     `json:"createable"`
	Updateable            bool                     `json:"updateable"`
	DefaultValue          *string                  `json:"defaultValue,omitempty"`
	SiblingMaxParentField *string                  `json:"siblingMaxParentField,omitempty"`
	IsNameField           bool                     `json:"isNameField,omitempty"`
	IsSystem              *bool                    `json:"isSystem,omitempty"`
	CompoundFieldName     string                   `json:"compoundFieldName,omitempty"`
	CompoundComponentRole string                   `json:"compoundComponentRole,omitempty"`
	ReferenceTo           []string                 `json:"referenceTo,omitempty"`
	IsPolymorphic         bool                     `json:"isPolymorphic,omitempty"`
	RelationshipName      string                   `json:"relationshipName,omitempty"`
	ChildRelationshipName string                   `json:"childRelationshipName,omitempty"`
	DeleteRule            string                   `json:"deleteRule,omitempty"`
	IsMasterDetail        bool                     `json:"isMasterDetail,omitempty"`
	InlineHelpText        *string                  `json:"inlineHelpText,omitempty"`
	MinValue              *float64                 `json:"minValue,omitempty"`
	MaxValue              *float64                 `json:"maxValue,omitempty"`
	MinLength             *int                     `json:"minLength,omitempty"`
	MaxLength             *int                     `json:"maxLength,omitempty"`
	Options               []searchFieldOption      `json:"options,omitempty"`
	ControllingField      string                   `json:"controllingField,omitempty"`
	PicklistDependency    map[string][]string      `json:"picklistDependency,omitempty"`
	Validator             string                   `json:"validator,omitempty"`
	ValidatorConfig       string                   `json:"validatorConfig,omitempty"`
	Formula               *string                  `json:"formula,omitempty"`
	PicklistValueSources  []searchFieldValueSource `json:"picklistValueSources,omitempty"`
}

type searchFieldValueSource struct {
	Object string `json:"object"`
	Field  string `json:"field"`
}

type searchFieldOption struct {
	Value string   `json:"value"`
	Label string   `json:"label"`
	Tags  []string `json:"tags,omitempty"`
}

type searchLookupContext struct {
	SourceObject  string         `json:"sourceObject"`
	SourceField   string         `json:"sourceField"`
	RecordContext map[string]any `json:"recordContext"`
}

type searchLookupFilterMapping struct {
	RecordField string `json:"recordField"`
	SourceField string `json:"sourceField"`
	TargetField string `json:"targetField"`
}

type searchLookupConfig struct {
	FilterMappings []searchLookupFilterMapping `json:"filterMappings"`
}

type searchLayoutColumn struct {
	Field  string `json:"field"`
	Format any    `json:"format"`
	Label  string `json:"label"`
	Name   string `json:"name"`
}

type searchLayoutResult struct {
	Label         string               `json:"label"`
	LimitRows     int                  `json:"limitRows"`
	ObjectType    string               `json:"objectType"`
	SearchColumns []searchLayoutColumn `json:"searchColumns"`
}

type searchObjectRequest struct {
	Name          string               `json:"name"`
	Fields        []string             `json:"fields"`
	Limit         int                  `json:"limit,omitempty"`
	LookupContext *searchLookupContext `json:"lookupContext,omitempty"`
	Implicit      bool                 `json:"-"`
}

type parameterizedSearchRequest struct {
	Q            string                `json:"q"`
	OverallLimit int                   `json:"overallLimit"`
	Sobjects     []searchObjectRequest `json:"sobjects"`
}

type soslSearchRequest struct {
	Term    string
	Scope   string
	Objects []searchObjectRequest
	Limit   int
}

func NewRuntimeSearchClient(
	search RuntimeSearchFunc,
	query RuntimeQueryFunc,
	appMetadata RuntimeGetAppMetadataViewV3Func,
) (*RuntimeSearchClient, error) {
	if search == nil || query == nil || appMetadata == nil {
		return nil, errors.New(
			"sfcrm AppService: Runtime Search, Query and AppMetadata ports are required",
		)
	}
	return &RuntimeSearchClient{
		search: search, query: query, appMetadata: appMetadata,
	}, nil
}

func matchSearchRoute(path string) (searchRoute, string) {
	parts := strings.Split(strings.Trim(path, "/"), "/")
	if len(parts) < 4 || parts[0] != "services" || parts[1] != "data" {
		return 0, ""
	}
	if _, supported := recordViewVersions[parts[2]]; !supported {
		return 0, ""
	}
	switch {
	case len(parts) == 4 && parts[3] == "search":
		return searchRouteSOSL, parts[2]
	case len(parts) == 4 && parts[3] == "parameterizedSearch":
		return searchRouteParameterized, parts[2]
	case len(parts) == 5 && parts[3] == "search" && parts[4] == "layout":
		return searchRouteLayout, parts[2]
	case len(parts) == 5 && parts[3] == "search" && parts[4] == "suggestions":
		return searchRouteSuggestions, parts[2]
	default:
		return 0, ""
	}
}

func (service *Service) serveSearch(
	writer http.ResponseWriter,
	request *http.Request,
) bool {
	route, version := matchSearchRoute(request.URL.EscapedPath())
	if route == 0 || service.search == nil {
		return false
	}
	allowedMethod := request.Method == http.MethodGet ||
		(route == searchRouteParameterized && request.Method == http.MethodPost)
	if !allowedMethod {
		allow := http.MethodGet
		if route == searchRouteParameterized {
			allow = http.MethodGet + ", " + http.MethodPost
		}
		writer.Header().Set("Allow", allow)
		writeCRMError(
			writer,
			http.StatusMethodNotAllowed,
			crmwire.SFCodeInvalidRequest,
			"Method not allowed",
		)
		return true
	}

	service.requests.Add(1)
	if service.draining.Load() {
		service.drainRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(
			writer,
			http.StatusServiceUnavailable,
			crmwire.SFCodeUnknownException,
			"Service is draining",
		)
		return true
	}
	bearer, ok := endUserBearer(request.Header.Values("Authorization"))
	if !ok {
		service.localRejected.Add(1)
		service.failures.Add(1)
		writeCRMError(
			writer,
			http.StatusUnauthorized,
			crmwire.SFCodeInvalidSessionID,
			"Session expired or invalid",
		)
		return true
	}

	service.inFlight.Add(1)
	defer service.inFlight.Add(-1)
	ctx, cancel := context.WithTimeout(request.Context(), service.requestTimeout)
	defer cancel()

	var (
		response any
		err      error
	)
	switch route {
	case searchRouteSOSL:
		response, err = service.search.executeSOSL(
			ctx, bearer, version, request.URL.Query(),
		)
	case searchRouteParameterized:
		if request.Method == http.MethodGet {
			response, err = service.search.executeParameterizedGET(
				ctx, bearer, version, request.URL.Query(),
			)
		} else {
			request.Body = http.MaxBytesReader(
				writer, request.Body, service.maxRequestBytes,
			)
			response, err = service.search.executeParameterized(
				ctx, bearer, version, request.Body,
			)
		}
	case searchRouteLayout:
		response, err = service.search.executeLayout(
			ctx, request.URL.Query(),
		)
	case searchRouteSuggestions:
		response, err = service.search.executeSuggestions(
			ctx, bearer, version, request.URL.Query(),
		)
	}
	if err != nil {
		service.failures.Add(1)
		writeSearchFailure(writer, err)
		return true
	}
	service.successes.Add(1)
	writeJSON(writer, http.StatusOK, response)
	return true
}

func (client *RuntimeSearchClient) executeParameterized(
	ctx context.Context,
	bearer string,
	version string,
	body io.Reader,
) (map[string]any, error) {
	var request parameterizedSearchRequest
	decoder := json.NewDecoder(body)
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&request); err != nil || requireJSONEOF(decoder) != nil {
		return nil, invalidSearch("Invalid JSON body")
	}
	request.Q = strings.TrimSpace(request.Q)
	if len(request.Q) < minimumSearchTermLength {
		return nil, invalidSearch("Field 'q' must contain at least 2 characters")
	}
	if strings.HasPrefix(strings.ToUpper(request.Q), "FIND ") {
		return nil, invalidSearch("Parameterized search q must not include SOSL clauses")
	}
	limit := request.OverallLimit
	if limit <= 0 {
		limit = defaultSearchLimit
	}
	if limit > searchsemantic.MaxRecords {
		return nil, invalidSearch("overallLimit exceeds the supported maximum")
	}
	view, specs, err := client.readObjectSpecs(ctx)
	if err != nil {
		return nil, err
	}
	if len(request.Sobjects) == 0 {
		request.Sobjects = implicitSearchObjects(specs, limit)
	}
	records, err := client.executeObjects(
		ctx, bearer, version, view, specs, request.Q, "ALL",
		request.Sobjects, limit,
	)
	if err != nil {
		return nil, err
	}
	return map[string]any{"searchRecords": records}, nil
}

func (client *RuntimeSearchClient) executeParameterizedGET(
	ctx context.Context,
	bearer string,
	version string,
	values map[string][]string,
) (map[string]any, error) {
	term, ok := oneSearchQueryValue(values, "q")
	if !ok || len(term) < minimumSearchTermLength {
		return nil, invalidSearch("Query parameter 'q' must contain at least 2 characters")
	}
	if strings.HasPrefix(strings.ToUpper(term), "FIND ") {
		return nil, invalidSearch("Parameterized search q must not include SOSL clauses")
	}
	objectName, hasObject := oneSearchQueryValue(values, "sobject")
	rawFields, hasFields := oneSearchQueryValue(values, "fields")
	if hasFields && !hasObject {
		return nil, invalidSearch("Global fields parameter requires sobject")
	}
	fields, err := splitSearchIdentifiers(rawFields)
	if err != nil {
		return nil, err
	}
	limit := defaultSearchLimit
	if rawLimit, present := oneSearchQueryValue(values, "limit"); present {
		parsed, parseErr := strconv.Atoi(rawLimit)
		if parseErr == nil && parsed > 0 {
			limit = parsed
		}
	}
	if limit > searchsemantic.MaxRecords {
		limit = searchsemantic.MaxRecords
	}
	lookup, err := parseSearchLookupContext(values)
	if err != nil {
		return nil, err
	}
	if lookup != nil && !hasObject {
		return nil, invalidSearch("lookup context requires sobject")
	}
	if where, present := oneSearchQueryValue(values, "where"); present &&
		where != "" {
		return nil, invalidSearch("Parameterized where is unavailable")
	}
	view, specs, err := client.readObjectSpecs(ctx)
	if err != nil {
		return nil, err
	}
	objects := make([]searchObjectRequest, 0, len(specs))
	if hasObject {
		objects = append(objects, searchObjectRequest{
			Name: objectName, Fields: fields, Limit: limit, LookupContext: lookup,
		})
	} else {
		objects = implicitSearchObjects(specs, limit)
	}
	records, err := client.executeObjects(
		ctx, bearer, version, view, specs, term, "ALL", objects, limit,
	)
	if err != nil {
		return nil, err
	}
	return map[string]any{"searchRecords": records}, nil
}

func (client *RuntimeSearchClient) executeSOSL(
	ctx context.Context,
	bearer string,
	version string,
	values map[string][]string,
) (map[string]any, error) {
	raw, ok := oneSearchQueryValue(values, "q")
	if !ok || raw == "" {
		return nil, malformedSearch("q parameter is required")
	}
	parsed, err := parseCurrentSOSL(raw)
	if err != nil {
		return nil, err
	}
	lookup, err := parseSearchLookupContext(values)
	if err != nil {
		return nil, err
	}
	view, specs, err := client.readObjectSpecs(ctx)
	if err != nil {
		return nil, err
	}
	overallLimit := parsed.Limit
	if overallLimit == 0 {
		overallLimit = defaultSearchLimit
	}
	if len(parsed.Objects) == 0 {
		parsed.Objects = implicitSearchObjects(specs, overallLimit)
	}
	if lookup != nil {
		if len(parsed.Objects) != 1 {
			return nil, invalidSearch(
				"lookup context requires exactly one RETURNING object",
			)
		}
		parsed.Objects[0].LookupContext = lookup
	}
	records, err := client.executeObjects(
		ctx, bearer, version, view, specs, parsed.Term, parsed.Scope,
		parsed.Objects, overallLimit,
	)
	if err != nil {
		return nil, err
	}
	return map[string]any{"searchRecords": records}, nil
}

func (client *RuntimeSearchClient) executeLayout(
	ctx context.Context,
	values map[string][]string,
) ([]searchLayoutResult, error) {
	rawObjects, ok := oneSearchQueryValue(values, "q")
	if !ok || rawObjects == "" {
		return nil, &searchFailure{
			status:  http.StatusBadRequest,
			code:    crmwire.SFCodeMissingQuery,
			message: "Query parameter 'q' is required",
		}
	}
	objectNames, err := splitSearchIdentifiers(rawObjects)
	if err != nil || len(objectNames) == 0 {
		return nil, invalidSearch("q contains an invalid object name")
	}
	_, specs, err := client.readObjectSpecs(ctx)
	if err != nil {
		return nil, err
	}
	result := make([]searchLayoutResult, 0, len(objectNames))
	for _, objectName := range objectNames {
		spec, found := specs[strings.ToLower(objectName)]
		if !found || !strings.EqualFold(spec.APIName, objectName) {
			return nil, &searchFailure{
				status:  http.StatusNotFound,
				code:    crmwire.SFCodeNotFound,
				message: "Object not found: " + objectName,
			}
		}
		result = append(result, searchLayoutResult{
			Label:         "Search Results",
			LimitRows:     defaultSearchLayoutRows,
			ObjectType:    spec.APIName,
			SearchColumns: buildSearchLayoutColumns(spec),
		})
	}
	return result, nil
}

func (client *RuntimeSearchClient) executeSuggestions(
	ctx context.Context,
	bearer string,
	version string,
	values map[string][]string,
) (map[string]any, error) {
	term, ok := oneSearchQueryValue(values, "q")
	if !ok {
		return nil, invalidSearch("q parameter is required")
	}
	term = strings.TrimSpace(term)
	if len(term) < minimumSearchTermLength {
		return map[string]any{
			"autoSuggestResults": []map[string]any{},
			"hasMoreResults":     false,
		}, nil
	}
	limit := defaultSuggestionLimit
	if raw, present := oneSearchQueryValue(values, "limit"); present && raw != "" {
		parsed, parseErr := strconv.Atoi(raw)
		if parseErr != nil || parsed <= 0 {
			return nil, invalidSearch("limit must be a positive integer")
		}
		limit = parsed
	}
	if limit > maxSuggestionLimit {
		limit = maxSuggestionLimit
	}
	objects, ok := oneSearchQueryValue(values, "sobject")
	if !ok {
		return nil, invalidSearch("sobject parameter is required")
	}
	objectNames, err := splitSearchIdentifiers(objects)
	if err != nil || len(objectNames) == 0 ||
		len(objectNames) > searchsemantic.MaxObjects {
		return nil, invalidSearch("sobject contains an invalid object name")
	}
	includeEmail := false
	if raw, present := oneSearchQueryValue(values, "searchFields"); present {
		for _, value := range strings.Split(raw, ",") {
			if strings.EqualFold(strings.TrimSpace(value), "EMAIL") {
				includeEmail = true
			}
		}
	}
	view, specs, err := client.readObjectSpecs(ctx)
	if err != nil {
		return nil, err
	}
	results := make([]map[string]any, 0, limit)
	for _, objectName := range objectNames {
		if len(results) > limit {
			break
		}
		spec, found := specs[strings.ToLower(objectName)]
		if !found || !objectSearchable(spec) {
			return nil, invalidSearch("Search object is unavailable")
		}
		fields := suggestionFields(spec, includeEmail)
		if len(fields) == 0 {
			return nil, invalidSearch("Search object has no suggestion field")
		}
		remaining := limit + 1 - len(results)
		records, queryErr := client.queryPrefix(
			ctx, bearer, view, spec, term, fields, remaining,
		)
		if queryErr != nil {
			return nil, queryErr
		}
		for _, record := range records {
			result, projectErr := suggestionProjection(
				version, spec, record, includeEmail, term,
			)
			if projectErr != nil {
				return nil, projectErr
			}
			results = append(results, result)
			if len(results) > limit {
				break
			}
		}
	}
	hasMore := len(results) > limit
	if hasMore {
		results = results[:limit]
	}
	return map[string]any{
		"autoSuggestResults": results,
		"hasMoreResults":     hasMore,
	}, nil
}

func (client *RuntimeSearchClient) readObjectSpecs(
	ctx context.Context,
) (
	catalogsemantic.AppMetadataViewV3,
	map[string]searchObjectSpec,
	error,
) {
	if client == nil {
		return catalogsemantic.AppMetadataViewV3{}, nil, unavailableSearch()
	}
	return readObjectSpecs(ctx, client.appMetadata)
}

// Metadata-view validation is shared by query, search and other consumers.
// Return the neutral dependency failure; each API owns its HTTP projection.
func unavailableObjectMetadata() error {
	return &commonsemantic.Failure{Category: commonsemantic.FailureUnavailable}
}

func readObjectSpecs(
	ctx context.Context,
	appMetadata RuntimeGetAppMetadataViewV3Func,
) (
	catalogsemantic.AppMetadataViewV3,
	map[string]searchObjectSpec,
	error,
) {
	if appMetadata == nil {
		return catalogsemantic.AppMetadataViewV3{}, nil, unavailableObjectMetadata()
	}
	view, err := appMetadata(ctx, catalogsemantic.GetRequest{})
	if err != nil {
		return catalogsemantic.AppMetadataViewV3{}, nil, err
	}
	if err := catalogsemantic.ValidateAppMetadataViewV3(view); err != nil ||
		view.AppID != AppID {
		return catalogsemantic.AppMetadataViewV3{}, nil, unavailableObjectMetadata()
	}
	specs, err := searchObjectSpecsFromView(view)
	if err != nil {
		return catalogsemantic.AppMetadataViewV3{}, nil, err
	}
	return view, specs, nil
}

func searchObjectSpecsFromView(
	view catalogsemantic.AppMetadataViewV3,
) (map[string]searchObjectSpec, error) {
	var component *catalogsemantic.EffectiveMetadataComponent
	for index := range view.EffectiveComponents {
		candidate := &view.EffectiveComponents[index]
		if candidate.ComponentKind !=
			catalogsemantic.EffectiveObjectSearchComponentKind ||
			candidate.BusinessKey !=
				catalogsemantic.EffectiveObjectSearchComponentBusinessKey {
			continue
		}
		if component != nil {
			return nil, unavailableObjectMetadata()
		}
		component = candidate
	}
	if component == nil ||
		(component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV2 &&
			component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV3 &&
			component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV4 &&
			component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV5 &&
			component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV6) ||
		len(component.CanonicalBehavior) == 0 {
		return nil, unavailableObjectMetadata()
	}
	var document effectiveObjectSearchDocument
	decoder := json.NewDecoder(bytes.NewReader(component.CanonicalBehavior))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&document); err != nil ||
		len(document.Objects) == 0 {
		return nil, unavailableObjectMetadata()
	}
	var extra any
	if err := decoder.Decode(&extra); err != io.EOF {
		return nil, unavailableObjectMetadata()
	}
	var rawDocument struct {
		Objects []map[string]json.RawMessage `json:"objects"`
	}
	if err := json.Unmarshal(component.CanonicalBehavior, &rawDocument); err != nil ||
		len(rawDocument.Objects) != len(document.Objects) {
		return nil, unavailableObjectMetadata()
	}
	var rawFieldDocument struct {
		Objects []struct {
			Fields []map[string]json.RawMessage `json:"fields"`
		} `json:"objects"`
	}
	if err := json.Unmarshal(component.CanonicalBehavior, &rawFieldDocument); err != nil ||
		len(rawFieldDocument.Objects) != len(document.Objects) {
		return nil, unavailableObjectMetadata()
	}
	result := make(map[string]searchObjectSpec, len(document.Objects))
	for index, spec := range document.Objects {
		if !validSearchIdentifier(spec.APIName) ||
			strings.TrimSpace(spec.Label) == "" ||
			spec.Label != strings.TrimSpace(spec.Label) ||
			(spec.TableType != nil && strings.TrimSpace(*spec.TableType) == "") ||
			(spec.DisplayNameField != "" && !validSearchDisplayNameFieldSyntax(spec.DisplayNameField)) ||
			(spec.SharingParentObject != "" && !validSearchIdentifier(spec.SharingParentObject)) ||
			(spec.SharingParentField != "" && !validSearchIdentifier(spec.SharingParentField)) {
			return nil, unavailableObjectMetadata()
		}
		key := strings.ToLower(spec.APIName)
		if _, duplicate := result[key]; duplicate {
			return nil, unavailableObjectMetadata()
		}
		if len(rawFieldDocument.Objects[index].Fields) != len(spec.Fields) {
			return nil, unavailableObjectMetadata()
		}
		fieldNames := make(map[string]searchFieldSpec, len(spec.Fields))
		for fieldIndex, field := range spec.Fields {
			if !validSearchIdentifier(field.APIName) ||
				strings.TrimSpace(field.Type) == "" ||
				(field.CompoundFieldName != "" &&
					!validSearchIdentifier(field.CompoundFieldName)) ||
				(field.CompoundComponentRole != "" &&
					field.CompoundComponentRole != strings.TrimSpace(field.CompoundComponentRole)) ||
				(field.CompoundComponentRole != "" && field.CompoundFieldName == "") ||
				(field.RelationshipName != "" && !validSearchIdentifier(field.RelationshipName)) ||
				(field.Precision == nil) != (field.Scale == nil) ||
				(field.Precision != nil &&
					(*field.Precision < 0 || *field.Scale < 0 ||
						*field.Scale > *field.Precision)) ||
				(field.MinLength != nil && *field.MinLength < 0) ||
				(field.MaxLength != nil && *field.MaxLength < 0) ||
				(field.MinLength != nil && field.MaxLength != nil &&
					*field.MinLength > *field.MaxLength) ||
				(field.MinValue != nil && field.MaxValue != nil &&
					*field.MinValue > *field.MaxValue) {
				return nil, unavailableObjectMetadata()
			}
			formulaPresent := false
			picklistSourcesPresent := false
			presentationFactsPresent := false
			var rawFormula, rawPicklistSources json.RawMessage
			for rawKey, rawValue := range rawFieldDocument.Objects[index].Fields[fieldIndex] {
				switch {
				case strings.EqualFold(rawKey, "formula"):
					formulaPresent = true
					rawFormula = rawValue
				case strings.EqualFold(rawKey, "picklistValueSources"):
					picklistSourcesPresent = true
					rawPicklistSources = rawValue
				case strings.EqualFold(rawKey, "compoundComponentRole"),
					strings.EqualFold(rawKey, "isPolymorphic"),
					strings.EqualFold(rawKey, "relationshipName"),
					strings.EqualFold(rawKey, "inlineHelpText"),
					strings.EqualFold(rawKey, "minValue"),
					strings.EqualFold(rawKey, "maxValue"),
					strings.EqualFold(rawKey, "minLength"),
					strings.EqualFold(rawKey, "maxLength"):
					presentationFactsPresent = true
				}
			}
			if presentationFactsPresent &&
				component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV5 &&
				component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV6 {
				return nil, unavailableObjectMetadata()
			}
			if (formulaPresent || picklistSourcesPresent) &&
				component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV4 &&
				component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV5 &&
				component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV6 {
				return nil, unavailableObjectMetadata()
			}
			if formulaPresent &&
				(bytes.Equal(bytes.TrimSpace(rawFormula), []byte("null")) ||
					field.Formula == nil ||
					strings.TrimSpace(*field.Formula) == "") {
				return nil, unavailableObjectMetadata()
			}
			if picklistSourcesPresent &&
				(bytes.Equal(bytes.TrimSpace(rawPicklistSources), []byte("null")) ||
					!formulaPresent || len(field.PicklistValueSources) == 0) {
				return nil, unavailableObjectMetadata()
			}
			fieldKey := strings.ToLower(field.APIName)
			if _, duplicate := fieldNames[fieldKey]; duplicate {
				return nil, unavailableObjectMetadata()
			}
			fieldNames[fieldKey] = field
		}
		if spec.DisplayNameField != "" {
			if _, valid := resolveSearchDisplayNameField(spec); !valid {
				return nil, unavailableObjectMetadata()
			}
		}
		pathFieldPresent := false
		listFieldsPresent := false
		createRequiredFieldsPresent := false
		presentationFactsPresent := false
		var rawListFields json.RawMessage
		for key, rawValue := range rawDocument.Objects[index] {
			switch {
			case strings.EqualFold(key, "pathField"):
				pathFieldPresent = true
			case strings.EqualFold(key, "createRequiredFields"):
				if key != "createRequiredFields" {
					return nil, unavailableObjectMetadata()
				}
				createRequiredFieldsPresent = true
			case strings.EqualFold(key, "listFields"):
				listFieldsPresent = true
				rawListFields = rawValue
			case strings.EqualFold(key, "displayNameField"),
				strings.EqualFold(key, "sharingModel"),
				strings.EqualFold(key, "sharingParentObject"),
				strings.EqualFold(key, "sharingParentField"),
				strings.EqualFold(key, "enableHierarchySharing"):
				presentationFactsPresent = true
			}
		}
		if presentationFactsPresent &&
			component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV5 &&
			component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV6 {
			return nil, unavailableObjectMetadata()
		}
		if pathFieldPresent {
			if component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV3 &&
				component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV4 &&
				component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV5 &&
				component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV6 {
				return nil, unavailableObjectMetadata()
			}
			pathField, exists := fieldNames[strings.ToLower(spec.PathField)]
			if !validSearchIdentifier(spec.PathField) ||
				spec.PathField != strings.TrimSpace(spec.PathField) ||
				!exists || pathField.APIName != spec.PathField ||
				describeFieldType(pathField) != "picklist" {
				return nil, unavailableObjectMetadata()
			}
		}
		if listFieldsPresent {
			if (component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV4 &&
				component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV5 &&
				component.ComponentFormatVersion != effectiveObjectSearchComponentFormatV6) ||
				bytes.Equal(bytes.TrimSpace(rawListFields), []byte("null")) {
				return nil, unavailableObjectMetadata()
			}
			seenListFields := make(map[string]struct{}, len(spec.ListFields))
			for _, fieldName := range spec.ListFields {
				fieldKey := strings.ToLower(fieldName)
				field, exists := fieldNames[fieldKey]
				if !validSearchIdentifier(fieldName) ||
					fieldName != strings.TrimSpace(fieldName) ||
					!exists || field.APIName != fieldName {
					return nil, unavailableObjectMetadata()
				}
				if _, duplicate := seenListFields[fieldKey]; duplicate {
					return nil, unavailableObjectMetadata()
				}
				seenListFields[fieldKey] = struct{}{}
			}
		}
		if component.ComponentFormatVersion == effectiveObjectSearchComponentFormatV6 {
			if !createRequiredFieldsPresent || spec.CreateRequiredFields == nil || *spec.CreateRequiredFields == nil {
				return nil, unavailableObjectMetadata()
			}
			previous := ""
			for _, fieldName := range *spec.CreateRequiredFields {
				field, exists := fieldNames[strings.ToLower(fieldName)]
				if !exists || field.APIName != fieldName || fieldName <= previous {
					return nil, unavailableObjectMetadata()
				}
				previous = fieldName
			}
		} else if createRequiredFieldsPresent {
			return nil, unavailableObjectMetadata()
		}
		searchFields := make(map[string]struct{}, len(spec.SearchAllDefaultFields))
		for _, field := range spec.SearchAllDefaultFields {
			key := strings.ToLower(field)
			if !validSearchIdentifier(field) {
				return nil, unavailableObjectMetadata()
			}
			if _, duplicate := searchFields[key]; duplicate {
				return nil, unavailableObjectMetadata()
			}
			if !strings.EqualFold(field, "Id") {
				if _, exists := fieldNames[key]; !exists {
					return nil, unavailableObjectMetadata()
				}
			}
			searchFields[key] = struct{}{}
		}
		spec.PresentationFactsV5 =
			component.ComponentFormatVersion == effectiveObjectSearchComponentFormatV5 ||
				component.ComponentFormatVersion == effectiveObjectSearchComponentFormatV6
		result[key] = spec
	}
	for _, spec := range result {
		if spec.DisplayNameField != "" {
			resolution, valid := resolveSearchDisplayNameField(spec)
			if !valid {
				return nil, unavailableObjectMetadata()
			}
			if resolution.explicitChild {
				target, found := result[strings.ToLower(resolution.targetObject)]
				if !found {
					return nil, unavailableObjectMetadata()
				}
				child, found := layoutFieldSpec(target, resolution.childField)
				if !found || child.APIName != resolution.childField {
					return nil, unavailableObjectMetadata()
				}
			}
		}
		for _, field := range spec.Fields {
			if len(field.PicklistValueSources) > 1 {
				return nil, unavailableObjectMetadata()
			}
			for _, source := range field.PicklistValueSources {
				if !validSearchIdentifier(source.Object) ||
					!validSearchIdentifier(source.Field) ||
					source.Object != strings.TrimSpace(source.Object) ||
					source.Field != strings.TrimSpace(source.Field) {
					return nil, unavailableObjectMetadata()
				}
				sourceObject, found := result[strings.ToLower(source.Object)]
				if !found || sourceObject.APIName != source.Object {
					return nil, unavailableObjectMetadata()
				}
				var sourceField *searchFieldSpec
				for sourceIndex := range sourceObject.Fields {
					candidate := &sourceObject.Fields[sourceIndex]
					if strings.EqualFold(candidate.APIName, source.Field) {
						if sourceField != nil {
							return nil, unavailableObjectMetadata()
						}
						sourceField = candidate
					}
				}
				if sourceField == nil || sourceField.APIName != source.Field ||
					!strings.EqualFold(strings.TrimSpace(sourceField.Type), "picklist") {
					return nil, unavailableObjectMetadata()
				}
			}
		}
	}
	return result, nil
}

func (client *RuntimeSearchClient) executeObjects(
	ctx context.Context,
	bearer string,
	version string,
	view catalogsemantic.AppMetadataViewV3,
	specs map[string]searchObjectSpec,
	term string,
	scope string,
	objects []searchObjectRequest,
	overallLimit int,
) ([]map[string]any, error) {
	if overallLimit <= 0 || overallLimit > searchsemantic.MaxRecords {
		return nil, invalidSearch("Search limit is invalid")
	}
	if len(objects) == 0 || len(objects) > searchsemantic.MaxObjects {
		return nil, invalidSearch("Search object count is invalid")
	}
	type preparedObjectSearch struct {
		spec         searchObjectSpec
		searchFields []string
		selected     []string
		limit        int
		lookup       *searchLookupContext
	}
	prepared := make([]preparedObjectSearch, 0, len(objects))
	seenObjects := make(map[string]struct{}, len(objects))
	for _, object := range objects {
		if !validSearchIdentifier(object.Name) {
			return nil, invalidSearch("Search object is invalid")
		}
		key := strings.ToLower(object.Name)
		if _, duplicate := seenObjects[key]; duplicate {
			return nil, invalidSearch("Search object is duplicated")
		}
		seenObjects[key] = struct{}{}
		spec, found := specs[key]
		if !found || !objectSearchable(spec) {
			return nil, invalidSearch("Search object is unavailable")
		}
		requestedLimit := object.Limit
		if requestedLimit <= 0 {
			requestedLimit = overallLimit
		}
		if requestedLimit > overallLimit {
			requestedLimit = overallLimit
		}
		selected, err := selectedSearchFields(spec, object.Fields)
		if err != nil {
			return nil, err
		}
		searchFields := scopedSearchFields(spec, scope)
		if len(searchFields) == 0 {
			if object.Implicit {
				continue
			}
			return nil, invalidSearch("Search object has no fields for the requested scope")
		}
		prepared = append(prepared, preparedObjectSearch{
			spec: spec, searchFields: searchFields, selected: selected,
			limit: requestedLimit, lookup: object.LookupContext,
		})
	}
	if len(prepared) == 0 {
		return []map[string]any{}, nil
	}

	// Scoped lookup search includes a source-record value resolution and stays
	// on its dedicated Query plan. The common global/multi-object path crosses
	// the Runtime boundary once with object-scoped plans.
	for _, object := range prepared {
		if object.lookup != nil {
			results := make([]map[string]any, 0)
			for _, scoped := range prepared {
				if len(results) >= overallLimit {
					break
				}
				remaining := overallLimit - len(results)
				limit := scoped.limit
				if limit > remaining {
					limit = remaining
				}
				var records []map[string]any
				var searchErr error
				if scoped.lookup != nil {
					records, searchErr = client.queryScopedLookup(
						ctx, bearer, version, view, specs, scoped.spec, term,
						scoped.searchFields, scoped.selected, limit, *scoped.lookup,
					)
				} else {
					records, searchErr = client.searchObject(
						ctx, bearer, version, view, scoped.spec, term,
						scoped.searchFields, scoped.selected, limit,
					)
				}
				if searchErr != nil {
					return nil, searchErr
				}
				results = append(results, records...)
			}
			return results, nil
		}
	}

	revision := view.Receipts.CatalogRevision
	request := searchsemantic.Request{
		Term: term, Limit: uint32(overallLimit), MinCatalogRevision: &revision,
		ObjectPlans: make([]searchsemantic.ObjectPlan, 0, len(prepared)),
	}
	objectSpecs := make(map[string]searchObjectSpec, len(prepared))
	for _, object := range prepared {
		request.ObjectPlans = append(request.ObjectPlans, searchsemantic.ObjectPlan{
			ObjectID:       object.spec.APIName,
			SearchFields:   append([]string(nil), object.searchFields...),
			SelectedFields: append([]string(nil), object.selected...),
			Limit:          uint32(object.limit),
		})
		objectSpecs[strings.ToLower(object.spec.APIName)] = object.spec
	}
	if err := searchsemantic.ValidateRequest(ctx, request); err != nil {
		return nil, err
	}
	response, err := client.search(ctx, bearer, request)
	if err != nil {
		return nil, err
	}
	if err := searchsemantic.ValidateResponse(request, response); err != nil {
		return nil, err
	}
	if err := validateSearchMetadataAuthority(
		ctx, client.reconcileAppMetadata, view, response.Receipts,
	); err != nil {
		return nil, err
	}
	results := make([]map[string]any, 0, len(response.Records))
	for _, record := range response.Records {
		spec, found := objectSpecs[strings.ToLower(record.ObjectID)]
		if !found {
			return nil, unavailableSearch()
		}
		projected, projectErr := searchRecordProjection(version, spec, record)
		if projectErr != nil {
			return nil, projectErr
		}
		results = append(results, projected)
	}
	return results, nil
}

func (client *RuntimeSearchClient) searchObject(
	ctx context.Context,
	bearer string,
	version string,
	view catalogsemantic.AppMetadataViewV3,
	spec searchObjectSpec,
	term string,
	searchFields []string,
	selectedFields []string,
	limit int,
) ([]map[string]any, error) {
	revision := view.Receipts.CatalogRevision
	request := searchsemantic.Request{
		ObjectIDs: []string{spec.APIName}, Term: term,
		SearchFields: searchFields, SelectedFields: selectedFields,
		Limit: uint32(limit), MinCatalogRevision: &revision,
	}
	if err := searchsemantic.ValidateRequest(ctx, request); err != nil {
		return nil, err
	}
	response, err := client.search(ctx, bearer, request)
	if err != nil {
		return nil, err
	}
	if err := searchsemantic.ValidateResponse(request, response); err != nil {
		return nil, err
	}
	if err := validateSearchMetadataAuthority(
		ctx, client.reconcileAppMetadata, view, response.Receipts,
	); err != nil {
		return nil, err
	}
	results := make([]map[string]any, 0, len(response.Records))
	for _, record := range response.Records {
		projected, err := searchRecordProjection(version, spec, record)
		if err != nil {
			return nil, err
		}
		results = append(results, projected)
	}
	return results, nil
}

func (client *RuntimeSearchClient) queryPrefix(
	ctx context.Context,
	bearer string,
	view catalogsemantic.AppMetadataViewV3,
	spec searchObjectSpec,
	term string,
	fields []string,
	limit int,
) ([]querysemantic.Record, error) {
	predicates := make([]querysemantic.Predicate, 0, len(fields))
	for _, field := range fields {
		predicates = append(predicates, stringComparison(
			field, querysemantic.ComparisonStartsWith, term,
		))
	}
	selected := append([]string{"Id"}, fields...)
	request := querysemantic.Request{
		ObjectID: spec.APIName, Selections: fieldSelections(selected),
		Filter: combinePredicates(querysemantic.PredicateOr, predicates),
		Limit:  uint32(limit), SkipTotalSize: true,
		MinCatalogRevision: pointerTo(view.Receipts.CatalogRevision),
	}
	if err := querysemantic.ValidateRequest(ctx, request); err != nil {
		return nil, err
	}
	response, err := client.query(ctx, bearer, request)
	if err != nil {
		return nil, err
	}
	if err := querysemantic.ValidateResponse(response); err != nil {
		return nil, err
	}
	if err := validateSearchMetadataAuthority(
		ctx, client.reconcileAppMetadata, view, response.Receipts,
	); err != nil {
		return nil, err
	}
	return response.Records, nil
}

func (client *RuntimeSearchClient) queryScopedLookup(
	ctx context.Context,
	bearer string,
	version string,
	view catalogsemantic.AppMetadataViewV3,
	specs map[string]searchObjectSpec,
	target searchObjectSpec,
	term string,
	searchFields []string,
	selectedFields []string,
	limit int,
	lookup searchLookupContext,
) ([]map[string]any, error) {
	source, found := specs[strings.ToLower(lookup.SourceObject)]
	if !found || !strings.EqualFold(source.APIName, lookup.SourceObject) {
		return nil, unavailableSearch()
	}
	sourceField, found := findSearchField(source, lookup.SourceField)
	if !found || !strings.EqualFold(sourceField.Validator, runtimeScopedLookupRule) {
		// A caller-provided scope can never silently degrade to an unfiltered
		// lookup when the immutable metadata view does not prove that scope.
		return nil, unavailableSearch()
	}
	if len(sourceField.ReferenceTo) > 0 &&
		!containsFold(sourceField.ReferenceTo, target.APIName) {
		return nil, invalidSearch("lookup target is not declared by source metadata")
	}
	config, err := parseSearchLookupConfig(sourceField.ValidatorConfig)
	if err != nil {
		return nil, err
	}
	filters := make([]querysemantic.Predicate, 0, len(config.FilterMappings)+1)
	searchPredicates := make([]querysemantic.Predicate, 0, len(searchFields))
	for _, field := range searchFields {
		searchPredicates = append(searchPredicates, stringComparison(
			field, querysemantic.ComparisonContains, term,
		))
	}
	filters = append(filters, combinePredicates(
		querysemantic.PredicateOr, searchPredicates,
	))
	for _, mapping := range config.FilterMappings {
		targetField, exists := findSearchField(target, mapping.TargetField)
		if !exists {
			return nil, unavailableSearch()
		}
		_ = targetField
		value, resolved, resolveErr := client.resolveLookupValue(
			ctx, bearer, view, specs, source, lookup.RecordContext, mapping,
		)
		if resolveErr != nil {
			return nil, resolveErr
		}
		if !resolved {
			return []map[string]any{}, nil
		}
		filter, filterErr := scalarComparison(
			mapping.TargetField, querysemantic.ComparisonEqual, value,
		)
		if filterErr != nil {
			return nil, unavailableSearch()
		}
		filters = append(filters, filter)
	}
	selections := append([]string{"Id"}, selectedFields...)
	request := querysemantic.Request{
		ObjectID: target.APIName, Selections: fieldSelections(selections),
		Filter: combinePredicates(querysemantic.PredicateAnd, filters),
		Limit:  uint32(limit), SkipTotalSize: true,
		MinCatalogRevision: pointerTo(view.Receipts.CatalogRevision),
	}
	if err := querysemantic.ValidateRequest(ctx, request); err != nil {
		return nil, err
	}
	response, err := client.query(ctx, bearer, request)
	if err != nil {
		return nil, err
	}
	if err := querysemantic.ValidateResponse(response); err != nil {
		return nil, err
	}
	if err := validateSearchMetadataAuthority(
		ctx, client.reconcileAppMetadata, view, response.Receipts,
	); err != nil {
		return nil, err
	}
	results := make([]map[string]any, 0, len(response.Records))
	for _, record := range response.Records {
		projected, err := querySearchRecordProjection(version, target, record)
		if err != nil {
			return nil, err
		}
		results = append(results, projected)
	}
	return results, nil
}

func (client *RuntimeSearchClient) resolveLookupValue(
	ctx context.Context,
	bearer string,
	view catalogsemantic.AppMetadataViewV3,
	specs map[string]searchObjectSpec,
	source searchObjectSpec,
	recordContext map[string]any,
	mapping searchLookupFilterMapping,
) (any, bool, error) {
	raw, found := recordContext[mapping.RecordField]
	if !found || raw == nil {
		return nil, false, nil
	}
	recordField, found := findSearchField(source, mapping.RecordField)
	if !found || len(recordField.ReferenceTo) != 1 {
		return nil, false, unavailableSearch()
	}
	parent, found := specs[strings.ToLower(recordField.ReferenceTo[0])]
	if !found {
		return nil, false, unavailableSearch()
	}
	if _, found := findSearchField(parent, mapping.SourceField); !found &&
		!strings.EqualFold(mapping.SourceField, "Id") {
		return nil, false, unavailableSearch()
	}
	recordID, ok := raw.(string)
	if !ok || strings.TrimSpace(recordID) == "" || recordID != strings.TrimSpace(recordID) {
		return nil, false, invalidSearch("lookup record context is invalid")
	}
	request := querysemantic.Request{
		ObjectID:   parent.APIName,
		Selections: fieldSelections([]string{mapping.SourceField}),
		Filter: stringComparison(
			"Id", querysemantic.ComparisonEqual, recordID,
		),
		Limit: 1, SkipTotalSize: true,
		MinCatalogRevision: pointerTo(view.Receipts.CatalogRevision),
	}
	if err := querysemantic.ValidateRequest(ctx, request); err != nil {
		return nil, false, err
	}
	response, err := client.query(ctx, bearer, request)
	if err != nil {
		return nil, false, err
	}
	if err := querysemantic.ValidateResponse(response); err != nil {
		return nil, false, err
	}
	if err := validateSearchMetadataAuthority(
		ctx, client.reconcileAppMetadata, view, response.Receipts,
	); err != nil {
		return nil, false, err
	}
	if len(response.Records) == 0 {
		return nil, false, nil
	}
	projected, err := queryRecordFromSemantic(response.Records[0])
	if err != nil {
		return nil, false, err
	}
	value, found := projected.Fields[mapping.SourceField]
	if !found || value == nil {
		return nil, false, nil
	}
	return value, true, nil
}

func validateSearchMetadataAuthority(
	ctx context.Context,
	reconcile RuntimeReconcileAppMetadataV3ReceiptsFunc,
	view catalogsemantic.AppMetadataViewV3,
	receipts commonsemantic.Receipts,
) error {
	if view.AppID != AppID {
		return unavailableObjectMetadata()
	}
	if view.Receipts.CatalogRevision != receipts.CatalogRevision {
		return &commonsemantic.Failure{
			Category:         commonsemantic.FailureStaleState,
			StaleAxis:        commonsemantic.StaleAxisCatalog,
			RequiredRevision: view.Receipts.CatalogRevision,
			ServedRevision:   receipts.CatalogRevision,
		}
	}
	if view.Receipts.SecurityEpoch != receipts.SecurityEpoch {
		reconciled, err := reconcileAppMetadataSecurityEpoch(
			ctx, reconcile, view.Receipts, receipts,
		)
		if err != nil {
			return err
		}
		if reconciled {
			return nil
		}
		return &commonsemantic.Failure{
			Category:         commonsemantic.FailureStaleState,
			StaleAxis:        commonsemantic.StaleAxisSecurity,
			RequiredRevision: view.Receipts.SecurityEpoch,
			ServedRevision:   receipts.SecurityEpoch,
		}
	}
	if view.Receipts.SchemaGeneration != receipts.SchemaGeneration ||
		view.Receipts.EngineGeneration != receipts.EngineGeneration {
		return unavailableObjectMetadata()
	}
	return nil
}

func searchRecordProjection(
	version string,
	spec searchObjectSpec,
	record searchsemantic.Record,
) (map[string]any, error) {
	result := map[string]any{
		"Id": record.RecordID,
		"attributes": map[string]any{
			"type": spec.APIName,
			"url":  searchRecordURL(version, spec.APIName, record.RecordID),
		},
	}
	for _, field := range record.Fields {
		value, err := searchFieldValue(field)
		if err != nil {
			return nil, err
		}
		result[field.Name] = value
	}
	normalizeSearchName(result, spec)
	return result, nil
}

func querySearchRecordProjection(
	version string,
	spec searchObjectSpec,
	record querysemantic.Record,
) (map[string]any, error) {
	projected, err := queryRecordFromSemantic(record)
	if err != nil {
		return nil, err
	}
	recordID, ok := projected.Fields["Id"].(string)
	if !ok || recordID == "" {
		return nil, unavailableSearch()
	}
	result := projected.Fields
	result["attributes"] = map[string]any{
		"type": spec.APIName,
		"url":  searchRecordURL(version, spec.APIName, recordID),
	}
	normalizeSearchName(result, spec)
	return result, nil
}

func suggestionProjection(
	version string,
	spec searchObjectSpec,
	record querysemantic.Record,
	includeEmail bool,
	term string,
) (map[string]any, error) {
	result, err := querySearchRecordProjection(version, spec, record)
	if err != nil {
		return nil, err
	}
	suggestion := map[string]any{
		"Id": result["Id"], "Name": result["Name"],
		"attributes": result["attributes"],
	}
	if includeEmail {
		normalizedTerm := strings.ToLower(strings.TrimSpace(term))
		for _, field := range spec.Fields {
			if !strings.EqualFold(field.Type, "email") {
				continue
			}
			value, found := result[field.APIName]
			email, isString := value.(string)
			if found && isString && email != "" &&
				strings.HasPrefix(strings.ToLower(email), normalizedTerm) {
				suggestion[field.APIName] = email
				break
			}
		}
	}
	return suggestion, nil
}

func searchFieldValue(field recordsemantic.Field) (any, error) {
	if field.Null {
		return nil, nil
	}
	switch field.Kind {
	case recordsemantic.ValueKindString:
		return dereference(field.String)
	case recordsemantic.ValueKindBoolean:
		return dereference(field.Boolean)
	case recordsemantic.ValueKindInteger:
		return dereference(field.Integer)
	case recordsemantic.ValueKindDecimal:
		if field.Decimal == nil {
			break
		}
		return exactJSONDecimal(decimalText(
			field.Decimal.Coefficient, field.Decimal.Scale,
		)), nil
	case recordsemantic.ValueKindDate:
		return dereference(field.Date)
	case recordsemantic.ValueKindDateTime:
		return dereference(field.DateTime)
	case recordsemantic.ValueKindReference:
		return dereference(field.Reference)
	}
	return nil, unavailableSearch()
}

func selectedSearchFields(
	spec searchObjectSpec,
	requested []string,
) ([]string, error) {
	result := make([]string, 0, len(requested))
	seen := make(map[string]struct{}, len(requested))
	add := func(field string) {
		key := strings.ToLower(field)
		if _, duplicate := seen[key]; duplicate {
			return
		}
		seen[key] = struct{}{}
		result = append(result, field)
	}
	for _, raw := range requested {
		field := strings.TrimSpace(raw)
		if strings.EqualFold(field, "Id") {
			continue
		}
		if strings.EqualFold(field, "Name") {
			if members := compoundSearchFields(spec, "Name"); len(members) > 0 {
				for _, member := range members {
					add(member)
				}
				continue
			}
		}
		meta, found := findSearchField(spec, field)
		if !found {
			return nil, invalidSearch("Search return field is unavailable")
		}
		add(meta.APIName)
	}
	if len(result) == 0 {
		if names := nameSearchFields(spec); len(names) > 0 {
			result = append(result, names...)
		} else if len(spec.Fields) > 0 {
			result = append(result, spec.Fields[0].APIName)
		}
	}
	if len(result) == 0 || len(result) > searchsemantic.MaxSelectedFields {
		return nil, invalidSearch("Search return fields are unavailable")
	}
	return result, nil
}

func scopedSearchFields(spec searchObjectSpec, scope string) []string {
	var result []string
	switch strings.ToUpper(strings.TrimSpace(scope)) {
	case "NAME":
		result = nameSearchFields(spec)
	case "EMAIL":
		result = fieldsByType(spec, "email")
	case "PHONE":
		result = fieldsByType(spec, "phone")
	case "ALL":
		declared := spec.SearchAllDefaultFields
		if len(declared) == 0 {
			declared = append(declared, nameSearchFields(spec)...)
			declared = append(declared, fieldsByType(spec, "email")...)
			declared = append(declared, fieldsByType(spec, "phone")...)
		}
		seen := make(map[string]struct{}, len(declared))
		for _, field := range declared {
			if strings.EqualFold(field, "Id") {
				continue
			}
			if strings.EqualFold(field, "Name") {
				if members := compoundSearchFields(spec, "Name"); len(members) > 0 {
					for _, member := range members {
						key := strings.ToLower(member)
						if _, duplicate := seen[key]; duplicate {
							continue
						}
						seen[key] = struct{}{}
						result = append(result, member)
					}
					continue
				}
			}
			key := strings.ToLower(field)
			if _, duplicate := seen[key]; duplicate {
				continue
			}
			seen[key] = struct{}{}
			result = append(result, field)
		}
	}
	if len(result) > searchsemantic.MaxSelectedFields {
		return nil
	}
	return result
}

func suggestionFields(spec searchObjectSpec, includeEmail bool) []string {
	result := append([]string(nil), nameSearchFields(spec)...)
	if includeEmail {
		result = append(result, fieldsByType(spec, "email")...)
	}
	return result
}

func buildSearchLayoutColumns(spec searchObjectSpec) []searchLayoutColumn {
	columns := make([]searchLayoutColumn, 0, maxSearchLayoutColumns)
	add := func(field searchFieldSpec, wireField, wireName string) {
		if len(columns) >= maxSearchLayoutColumns {
			return
		}
		for _, column := range columns {
			if strings.EqualFold(column.Name, wireName) {
				return
			}
		}
		columns = append(columns, searchLayoutColumn{
			Field: wireField, Format: nil, Label: field.Label, Name: wireName,
		})
	}

	if name := nameSearchField(spec); name != "" {
		if field, found := findSearchField(spec, name); found {
			add(field, spec.APIName+"."+field.APIName, field.APIName)
		}
	}
	for _, priority := range []string{"Phone", "Email"} {
		if field, found := findSearchField(spec, priority); found &&
			!field.IsNameField {
			add(field, spec.APIName+"."+field.APIName, field.APIName)
		}
	}
	if owner, found := findSearchField(spec, "OwnerId"); found &&
		containsFold(owner.ReferenceTo, "User") {
		owner.Label = spec.Label + " Owner Alias"
		add(owner, "User.Alias", "Owner.Alias")
	}
	for _, field := range spec.Fields {
		if !strings.EqualFold(field.Type, "string") ||
			strings.EqualFold(field.APIName, "Id") {
			continue
		}
		add(field, spec.APIName+"."+field.APIName, field.APIName)
	}
	return columns
}

func objectSearchable(spec searchObjectSpec) bool {
	return spec.Searchable == nil || *spec.Searchable
}

func nameSearchField(spec searchObjectSpec) string {
	for _, field := range spec.Fields {
		if field.IsNameField {
			return field.APIName
		}
	}
	for _, field := range spec.Fields {
		if strings.EqualFold(field.APIName, "Name") {
			return field.APIName
		}
	}
	return ""
}

func nameSearchFields(spec searchObjectSpec) []string {
	if members := compoundSearchFields(spec, "Name"); len(members) > 0 {
		return members
	}
	if name := nameSearchField(spec); name != "" {
		return []string{name}
	}
	return nil
}

func compoundSearchFields(spec searchObjectSpec, parent string) []string {
	result := make([]string, 0)
	for _, field := range spec.Fields {
		if strings.EqualFold(field.CompoundFieldName, parent) {
			result = append(result, field.APIName)
		}
	}
	return result
}

func fieldsByType(spec searchObjectSpec, fieldType string) []string {
	result := []string{}
	for _, field := range spec.Fields {
		if strings.EqualFold(field.Type, fieldType) {
			result = append(result, field.APIName)
		}
	}
	return result
}

func findSearchField(
	spec searchObjectSpec,
	name string,
) (searchFieldSpec, bool) {
	for _, field := range spec.Fields {
		if strings.EqualFold(field.APIName, name) {
			return field, true
		}
	}
	return searchFieldSpec{}, false
}

func normalizeSearchName(record map[string]any, spec searchObjectSpec) {
	if _, present := record["Name"]; present {
		return
	}
	if members := compoundSearchFields(spec, "Name"); len(members) > 0 {
		parts := make([]string, 0, len(members))
		for _, member := range members {
			value, present := record[member]
			if !present || value == nil || strings.TrimSpace(fmt.Sprint(value)) == "" {
				continue
			}
			parts = append(parts, fmt.Sprint(value))
		}
		if len(parts) > 0 {
			record["Name"] = strings.Join(parts, " ")
			return
		}
	}
	if name := nameSearchField(spec); name != "" {
		if value, present := record[name]; present {
			record["Name"] = value
			return
		}
	}
	record["Name"] = record["Id"]
}

func parseCurrentSOSL(raw string) (soslSearchRequest, error) {
	value := strings.TrimSpace(raw)
	if len(value) < len("FIND {}") ||
		!strings.EqualFold(value[:4], "FIND") ||
		!isSearchSpace(value[4]) {
		return soslSearchRequest{}, malformedSearch("SOSL query must start with FIND")
	}
	open := strings.Index(value, "{")
	if open < 0 {
		return soslSearchRequest{}, malformedSearch("Search term must be enclosed in braces")
	}
	closeIndex := -1
	escaped := false
	for index := open + 1; index < len(value); index++ {
		switch {
		case escaped:
			escaped = false
		case value[index] == '\\':
			escaped = true
		case value[index] == '}':
			closeIndex = index
			index = len(value)
		}
	}
	if closeIndex <= open+1 {
		return soslSearchRequest{}, malformedSearch("Search term cannot be empty")
	}
	term, err := unescapeSOSLTerm(value[open+1 : closeIndex])
	if err != nil || len(strings.TrimSpace(term)) < minimumSearchTermLength {
		return soslSearchRequest{}, malformedSearch("Search term is invalid")
	}
	remaining := strings.TrimSpace(value[closeIndex+1:])
	remaining, overallLimit, err := parseSOSLTrailingLimit(remaining)
	if err != nil {
		return soslSearchRequest{}, err
	}
	scope := "ALL"
	var objectClause string
	if remaining != "" {
		upper := strings.ToUpper(remaining)
		switch {
		case upper == "RETURNING":
			return soslSearchRequest{}, malformedSearch(
				"RETURNING object is required",
			)
		case strings.HasPrefix(upper, "RETURNING "):
			objectClause = strings.TrimSpace(remaining[len("RETURNING "):])
		default:
			index := strings.Index(upper, " RETURNING ")
			scopeText := remaining
			if index >= 0 {
				scopeText = strings.TrimSpace(remaining[:index])
				objectClause = strings.TrimSpace(
					remaining[index+len(" RETURNING "):],
				)
			}
			scopeClause := strings.Fields(scopeText)
			if len(scopeClause) != 3 ||
				!strings.EqualFold(scopeClause[0], "IN") ||
				!strings.EqualFold(scopeClause[2], "FIELDS") {
				return soslSearchRequest{}, malformedSearch(
					"Search scope is invalid",
				)
			}
			scope = strings.ToUpper(scopeClause[1])
			switch scope {
			case "ALL", "NAME", "EMAIL", "PHONE":
			default:
				return soslSearchRequest{}, malformedSearch(
					"Search scope is invalid",
				)
			}
		}
	}
	objects, err := parseSOSLReturningObjects(objectClause)
	if err != nil {
		return soslSearchRequest{}, err
	}
	return soslSearchRequest{
		Term: term, Scope: scope, Objects: objects, Limit: overallLimit,
	}, nil
}

func parseSOSLTrailingLimit(value string) (string, int, error) {
	depth := 0
	limitIndex := -1
	upper := strings.ToUpper(value)
	for index := 0; index < len(value); index++ {
		switch value[index] {
		case '(':
			depth++
		case ')':
			depth--
		case 'L', 'l':
			if depth == 0 && strings.HasPrefix(upper[index:], "LIMIT") &&
				(index == 0 || isSearchSpace(value[index-1])) &&
				index+len("LIMIT") < len(value) &&
				isSearchSpace(value[index+len("LIMIT")]) {
				if limitIndex >= 0 {
					return "", 0, malformedSearch("LIMIT is duplicated")
				}
				limitIndex = index
			}
		}
	}
	if limitIndex < 0 {
		return value, 0, nil
	}
	rawLimit := strings.TrimSpace(value[limitIndex+len("LIMIT"):])
	parsed, err := strconv.Atoi(rawLimit)
	if err != nil || parsed <= 0 || parsed > searchsemantic.MaxRecords {
		return "", 0, invalidSearch("LIMIT is invalid")
	}
	return strings.TrimSpace(value[:limitIndex]), parsed, nil
}

func parseSOSLReturningObjects(
	value string,
) ([]searchObjectRequest, error) {
	if value == "" {
		return nil, nil
	}
	clauses := make([]string, 0, 1)
	start := 0
	depth := 0
	for index, character := range value {
		switch character {
		case '(':
			depth++
		case ')':
			depth--
			if depth < 0 {
				return nil, malformedSearch("RETURNING object is invalid")
			}
		case ',':
			if depth == 0 {
				clauses = append(clauses, strings.TrimSpace(value[start:index]))
				start = index + 1
			}
		}
	}
	if depth != 0 {
		return nil, malformedSearch("RETURNING object is invalid")
	}
	clauses = append(clauses, strings.TrimSpace(value[start:]))
	objects := make([]searchObjectRequest, 0, len(clauses))
	for _, clause := range clauses {
		openFields := strings.Index(clause, "(")
		if openFields <= 0 || !strings.HasSuffix(clause, ")") {
			return nil, malformedSearch("RETURNING object is invalid")
		}
		objectName := strings.TrimSpace(clause[:openFields])
		if !validSearchIdentifier(objectName) {
			return nil, malformedSearch("RETURNING object is invalid")
		}
		fields, limit, err := parseSOSLReturningFields(
			strings.TrimSpace(clause[openFields+1 : len(clause)-1]),
		)
		if err != nil || len(fields) == 0 {
			return nil, malformedSearch("RETURNING fields are invalid")
		}
		objects = append(objects, searchObjectRequest{
			Name: objectName, Fields: fields, Limit: limit,
		})
	}
	return objects, nil
}

// parseSOSLReturningFields owns the bounded subset used by the launch
// journeys: RETURNING Object(Field,Field LIMIT n). The limit is part of the
// object clause, not a field identifier, and is still capped by the neutral
// Runtime search contract before any call crosses the L1/L0 boundary.
func parseSOSLReturningFields(value string) ([]string, int, error) {
	fieldsText := strings.TrimSpace(value)
	limit := 0
	upper := strings.ToUpper(fieldsText)
	if index := strings.LastIndex(upper, " LIMIT "); index >= 0 {
		rawLimit := strings.TrimSpace(fieldsText[index+len(" LIMIT "):])
		parsed, err := strconv.Atoi(rawLimit)
		if err != nil || parsed <= 0 || parsed > searchsemantic.MaxRecords {
			return nil, 0, invalidSearch("RETURNING LIMIT is invalid")
		}
		fieldsText = strings.TrimSpace(fieldsText[:index])
		limit = parsed
	}
	fields, err := splitSearchIdentifiers(fieldsText)
	return fields, limit, err
}

func implicitSearchObjects(
	specs map[string]searchObjectSpec,
	limit int,
) []searchObjectRequest {
	names := make([]string, 0, len(specs))
	for _, spec := range specs {
		if objectSearchable(spec) {
			names = append(names, spec.APIName)
		}
	}
	sort.Strings(names)
	objects := make([]searchObjectRequest, 0, len(names))
	for _, name := range names {
		objects = append(objects, searchObjectRequest{
			Name: name, Limit: limit, Implicit: true,
		})
	}
	return objects
}

func isSearchSpace(value byte) bool {
	switch value {
	case ' ', '\t', '\n', '\r':
		return true
	default:
		return false
	}
}

func unescapeSOSLTerm(value string) (string, error) {
	var result strings.Builder
	escaped := false
	for index := 0; index < len(value); index++ {
		if escaped {
			result.WriteByte(value[index])
			escaped = false
			continue
		}
		if value[index] == '\\' {
			escaped = true
			continue
		}
		result.WriteByte(value[index])
	}
	if escaped {
		return "", malformedSearch("Search term has an incomplete escape")
	}
	return strings.TrimSpace(result.String()), nil
}

func parseSearchLookupContext(
	values map[string][]string,
) (*searchLookupContext, error) {
	sourceObject, hasObject := oneSearchQueryValue(values, "lookupSourceObject")
	sourceField, hasField := oneSearchQueryValue(values, "lookupSourceField")
	rawContext, hasContext := oneSearchQueryValue(values, "lookupContext")
	if !hasObject && !hasField && !hasContext {
		return nil, nil
	}
	if !hasObject || !hasField || !hasContext ||
		!validSearchIdentifier(sourceObject) ||
		!validSearchIdentifier(sourceField) {
		return nil, invalidSearch(
			"lookup context requires lookupSourceObject, lookupSourceField, and lookupContext",
		)
	}
	contextValues := map[string]any{}
	decoder := json.NewDecoder(strings.NewReader(rawContext))
	decoder.UseNumber()
	if err := decoder.Decode(&contextValues); err != nil ||
		contextValues == nil || requireJSONEOF(decoder) != nil {
		return nil, invalidSearch("lookupContext JSON is invalid")
	}
	return &searchLookupContext{
		SourceObject: sourceObject, SourceField: sourceField,
		RecordContext: contextValues,
	}, nil
}

func parseSearchLookupConfig(raw string) (searchLookupConfig, error) {
	var config searchLookupConfig
	decoder := json.NewDecoder(strings.NewReader(raw))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&config); err != nil ||
		requireJSONEOF(decoder) != nil ||
		len(config.FilterMappings) == 0 {
		return searchLookupConfig{}, unavailableSearch()
	}
	seenTargets := make(map[string]struct{}, len(config.FilterMappings))
	for _, mapping := range config.FilterMappings {
		if !validSearchIdentifier(mapping.RecordField) ||
			!validSearchIdentifier(mapping.SourceField) ||
			!validSearchIdentifier(mapping.TargetField) {
			return searchLookupConfig{}, unavailableSearch()
		}
		key := strings.ToLower(mapping.TargetField)
		if _, duplicate := seenTargets[key]; duplicate {
			return searchLookupConfig{}, unavailableSearch()
		}
		seenTargets[key] = struct{}{}
	}
	return config, nil
}

func oneSearchQueryValue(
	values map[string][]string,
	key string,
) (string, bool) {
	items, present := values[key]
	if !present || len(items) != 1 {
		return "", false
	}
	return strings.TrimSpace(items[0]), true
}

func splitSearchIdentifiers(value string) ([]string, error) {
	if strings.TrimSpace(value) == "" {
		return nil, nil
	}
	result := []string{}
	seen := map[string]struct{}{}
	for _, raw := range strings.Split(value, ",") {
		field := strings.TrimSpace(raw)
		if !validSearchIdentifier(field) {
			return nil, invalidSearch("Search identifier is invalid")
		}
		key := strings.ToLower(field)
		if _, duplicate := seen[key]; duplicate {
			return nil, invalidSearch("Search identifier is duplicated")
		}
		seen[key] = struct{}{}
		result = append(result, field)
	}
	return result, nil
}

func validSearchIdentifier(value string) bool {
	return value == strings.TrimSpace(value) &&
		searchObjectIdentifierPattern.MatchString(value)
}

type searchDisplayNameFieldResolution struct {
	rootField     searchFieldSpec
	targetObject  string
	childField    string
	explicitChild bool
}

func validSearchDisplayNameFieldSyntax(value string) bool {
	if value == "" || value != strings.TrimSpace(value) || strings.Count(value, ".") > 1 {
		return false
	}
	for _, part := range strings.Split(value, ".") {
		if !validSearchIdentifier(part) {
			return false
		}
	}
	return true
}

func resolveSearchDisplayNameField(
	spec searchObjectSpec,
) (searchDisplayNameFieldResolution, bool) {
	value := spec.DisplayNameField
	if !validSearchDisplayNameFieldSyntax(value) {
		return searchDisplayNameFieldResolution{}, false
	}
	parts := strings.SplitN(value, ".", 2)
	root := parts[0]
	child := ""
	explicitChild := len(parts) == 2
	if explicitChild {
		child = parts[1]
	}
	resolve := func(field searchFieldSpec, matchedAPIName bool) (searchDisplayNameFieldResolution, bool) {
		if !explicitChild && matchedAPIName && describeFieldType(field) != "reference" {
			return searchDisplayNameFieldResolution{rootField: field}, true
		}
		if describeFieldType(field) != "reference" || len(field.ReferenceTo) != 1 {
			return searchDisplayNameFieldResolution{}, false
		}
		if !explicitChild {
			child = "Name"
		}
		return searchDisplayNameFieldResolution{
			rootField: field, targetObject: field.ReferenceTo[0], childField: child,
			explicitChild: explicitChild,
		}, true
	}
	for _, field := range spec.Fields {
		if field.APIName == root {
			return resolve(field, true)
		}
	}
	for _, field := range spec.Fields {
		if field.RelationshipName == root {
			return resolve(field, false)
		}
	}
	return searchDisplayNameFieldResolution{}, false
}

func readableSearchDisplayNameField(
	spec searchObjectSpec,
	permissions permissionsemantic.Response,
) bool {
	resolution, valid := resolveSearchDisplayNameField(spec)
	if !valid {
		return false
	}
	rootPermissions := fieldPermissionsForDescribe(permissions, spec.APIName)
	rootPermission, readable := rootPermissions[strings.ToLower(resolution.rootField.APIName)]
	if !readable || !rootPermission.CanRead {
		return false
	}
	if resolution.targetObject == "" {
		return true
	}
	if _, readable := objectPermissionForDescribe(permissions, resolution.targetObject); !readable {
		return false
	}
	targetPermissions := fieldPermissionsForDescribe(permissions, resolution.targetObject)
	childPermission, readable := targetPermissions[strings.ToLower(resolution.childField)]
	return readable && childPermission.CanRead
}

func fieldSelections(fields []string) []querysemantic.Selection {
	result := make([]querysemantic.Selection, 0, len(fields))
	seen := make(map[string]struct{}, len(fields))
	for _, field := range fields {
		key := strings.ToLower(field)
		if _, duplicate := seen[key]; duplicate {
			continue
		}
		seen[key] = struct{}{}
		result = append(result, querysemantic.Selection{
			Kind: querysemantic.SelectionField, Field: field,
		})
	}
	return result
}

func stringComparison(
	field string,
	operator querysemantic.ComparisonOperator,
	value string,
) querysemantic.Predicate {
	return querysemantic.Predicate{
		Kind: querysemantic.PredicateComparison, Operator: operator,
		Left: querysemantic.Operand{
			Kind: querysemantic.OperandField, Field: field,
		},
		Right: querysemantic.Operand{
			Kind:    querysemantic.OperandLiteral,
			Literal: &querysemantic.Scalar{String: &value},
		},
	}
}

func scalarComparison(
	field string,
	operator querysemantic.ComparisonOperator,
	value any,
) (querysemantic.Predicate, error) {
	scalar, err := queryScalarToSemantic(value)
	if err != nil {
		return querysemantic.Predicate{}, err
	}
	return querysemantic.Predicate{
		Kind: querysemantic.PredicateComparison, Operator: operator,
		Left: querysemantic.Operand{
			Kind: querysemantic.OperandField, Field: field,
		},
		Right: querysemantic.Operand{
			Kind: querysemantic.OperandLiteral, Literal: &scalar,
		},
	}, nil
}

func combinePredicates(
	kind querysemantic.PredicateKind,
	children []querysemantic.Predicate,
) querysemantic.Predicate {
	if len(children) == 1 {
		return children[0]
	}
	return querysemantic.Predicate{Kind: kind, Children: children}
}

func containsFold(values []string, target string) bool {
	for _, value := range values {
		if strings.EqualFold(value, target) {
			return true
		}
	}
	return false
}

func searchRecordURL(version, objectID, recordID string) string {
	return fmt.Sprintf(
		"/services/data/%s/sobjects/%s/%s",
		version, objectID, recordID,
	)
}

func pointerTo[T any](value T) *T { return &value }

type searchFailure struct {
	status  int
	code    string
	message string
}

func (failure *searchFailure) Error() string { return failure.message }

func invalidSearch(message string) error {
	return &searchFailure{
		status: http.StatusBadRequest, code: crmwire.SFCodeInvalidRequest,
		message: message,
	}
}

func malformedSearch(message string) error {
	return &searchFailure{
		status: http.StatusBadRequest, code: crmwire.SFCodeMalformedSearch,
		message: message,
	}
}

func unavailableSearch() error {
	return &searchFailure{
		status: http.StatusServiceUnavailable, code: crmwire.SFCodeUnknownException,
		message: "Service temporarily unavailable",
	}
}

func writeSearchFailure(writer http.ResponseWriter, err error) {
	var failure *searchFailure
	if errors.As(err, &failure) {
		writeCRMError(
			writer, failure.status, failure.code, failure.message,
		)
		return
	}
	writeRuntimeFailure(writer, err)
}
