package dialectsfcrm

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

	soql "github.com/nexuscrm/dialect-sf-crm-soql"
	"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/fieldprovenance"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
	"github.com/nexuscrm/shared/pkg/searchmetadata"
)

const (
	defaultSOSLLimit = constants.DefaultSOSLLimit
	maxSOSLLimit     = constants.MaxSOSLLimit

	defaultLayoutLimitRows = 25
	maxSearchColumns       = 6

	minSuggestionTermLength = 2
	defaultSuggestionLimit  = 5
	maxSuggestionLimit      = 25
)

type searchHandler struct {
	query       contract.Query
	metadata    contract.Metadata
	policy      contract.Policy
	searchScope contract.SearchScope
}

type searchObjectConfig struct {
	Name     string
	Fields   []string
	Limit    int
	Offset   int
	OrderBy  string
	OrderDir string
	Filter   queryspec.Predicate
	Lookup   *searchLookupContext
}

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

type searchColumn 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 []searchColumn `json:"searchColumns"`
}

type searchScopeOrderItemV63 struct {
	Type      string `json:"type"`
	URL       string `json:"url"`
	Name      string `json:"name"`
	Label     string `json:"label"`
	KeyPrefix string `json:"keyPrefix"`
}

type executableSearchObject struct {
	config       searchObjectConfig
	schema       *models.ObjectMetadata
	searchFields []string
	filter       queryspec.Predicate
}

func SearchNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	return searchHandlerFromDeps(deps).executeSOSL(ctx, req)
}

func ParameterizedSearchNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	return searchHandlerFromDeps(deps).executeParameterized(ctx, req)
}

func ParameterizedSearchPostNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	return searchHandlerFromDeps(deps).executeParameterizedPost(ctx, req)
}

func SearchLayoutNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	return searchHandlerFromDeps(deps).executeSearchLayout(ctx, req)
}

func SearchSuggestionsNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	return searchHandlerFromDeps(deps).executeSuggestions(ctx, req)
}

func SearchScopeOrderNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	return searchHandlerFromDeps(deps).executeScopeOrder(ctx, req)
}

func MetadataSearchScopeOrderNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	return searchHandlerFromDeps(deps).executeMetadataScopeOrder(ctx, req)
}

func searchHandlerFromDeps(deps dialect.Deps) searchHandler {
	return searchHandler{
		query:       deps.Contracts.Query,
		metadata:    deps.Contracts.Metadata,
		policy:      deps.Contracts.Policy,
		searchScope: deps.Contracts.SearchScope,
	}
}

func (h searchHandler) executeSOSL(ctx context.Context, req *dialect.Request) (*dialect.Response, error) {
	raw := queryValue(req, "q")
	if raw == "" {
		return searchErrorResponse(http.StatusBadRequest, wire.SFCodeMalformedSearch, "q parameter is required"), nil
	}

	searchTerm, scope, objectConfigs, err := parseSOSL(raw)
	if err != nil {
		return searchErrorResponse(http.StatusBadRequest, wire.SFCodeMalformedSearch, err.Error()), nil
	}
	if len(objectConfigs) == 0 {
		for _, obj := range h.searchableObjectNames(ctx) {
			objectConfigs = append(objectConfigs, searchObjectConfig{Name: obj, Limit: defaultSOSLLimit})
		}
	}
	lookupContext, err := parseLookupContextFromSearchRequest(req)
	if err != nil {
		return searchErrorResponse(http.StatusBadRequest, wire.SFCodeInvalidRequest, err.Error()), nil
	}
	if lookupContext != nil {
		if len(objectConfigs) != 1 {
			return searchErrorResponse(http.StatusBadRequest, wire.SFCodeInvalidRequest, "lookup context requires a single RETURNING object"), nil
		}
		objectConfigs[0].Lookup = lookupContext
	}

	searchResults, err := h.executeSearchWithLimits(ctx, req, searchTerm, scope, objectConfigs, maxSOSLLimit, req.User)
	if err != nil {
		return h.searchWriteError(req, err, wire.SFCodeInvalidRequest), nil
	}
	return &dialect.Response{Status: http.StatusOK, Body: map[string]any{"searchRecords": searchResults}}, nil
}

func (h searchHandler) executeMetadataScopeOrder(ctx context.Context, req *dialect.Request) (*dialect.Response, error) {
	if h.searchScope == nil || req == nil || req.User == nil {
		return &dialect.Response{Status: http.StatusOK, Body: []string{}}, nil
	}
	return &dialect.Response{Status: http.StatusOK, Body: h.searchScope.CuratedObjectNames(ctx, req.User)}, nil
}

func (h searchHandler) executeParameterized(ctx context.Context, req *dialect.Request) (*dialect.Response, error) {
	term := queryValue(req, "q")
	objectName := queryValue(req, "sobject")
	fieldsParam := queryValue(req, "fields")
	limitParam := queryValue(req, "limit")
	whereParam := queryValue(req, "where")

	lookupContext, err := parseLookupContextFromSearchRequest(req)
	if err != nil {
		return searchErrorResponse(http.StatusBadRequest, wire.SFCodeInvalidRequest, err.Error()), nil
	}
	if term == "" {
		return searchErrorResponse(http.StatusBadRequest, wire.SFCodeMissingQuery, "Query parameter 'q' is required"), nil
	}
	if containsSOSLClauses(term) {
		return searchErrorResponse(http.StatusBadRequest, wire.SFCodeInvalidRequest, "Parameterized search q must not include SOSL clauses"), nil
	}
	if fieldsParam != "" && objectName == "" {
		return searchErrorResponse(http.StatusBadRequest, wire.SFCodeInvalidRequest, "Global fields parameter requires sobject"), nil
	}
	if lookupContext != nil && objectName == "" {
		return searchErrorResponse(http.StatusBadRequest, wire.SFCodeInvalidRequest, "lookup context requires sobject"), nil
	}
	whereFilter, err := soql.ParsePredicateFragment(whereParam)
	if err != nil {
		return searchErrorResponse(http.StatusBadRequest, wire.SFCodeInvalidRequest, err.Error()), nil
	}

	fields := parseSearchFields(fieldsParam)
	limit := defaultSOSLLimit
	if limitParam != "" {
		if parsed, err := fmt.Sscanf(limitParam, "%d", &limit); err != nil || parsed != 1 {
			limit = defaultSOSLLimit
		}
		if limit <= 0 {
			limit = defaultSOSLLimit
		} else if limit > maxSOSLLimit {
			limit = maxSOSLLimit
		}
	}

	var objectConfigs []searchObjectConfig
	if objectName != "" {
		objectFields := fields
		if scopedFields := parseSearchFields(queryValue(req, objectName+".fields")); len(scopedFields) > 0 {
			objectFields = scopedFields
		}
		objectLimit := limit
		if scopedLimit := parseSearchLimit(queryValue(req, objectName+".limit"), limit); scopedLimit > 0 {
			objectLimit = scopedLimit
		}
		objectConfigs = []searchObjectConfig{{Name: objectName, Fields: objectFields, Limit: objectLimit, Filter: whereFilter, Lookup: lookupContext}}
	} else {
		for _, obj := range h.searchableObjectNames(ctx) {
			objectConfigs = append(objectConfigs, searchObjectConfig{Name: obj, Fields: fields, Limit: limit, Filter: whereFilter})
		}
	}

	searchResults, err := h.executeSearchWithLimits(ctx, req, term, "ALL", objectConfigs, maxSOSLLimit, req.User)
	if err != nil {
		return h.searchWriteError(req, err, wire.SFCodeInvalidRequest), nil
	}
	return &dialect.Response{Status: http.StatusOK, Body: map[string]any{"searchRecords": searchResults}}, nil
}

func (h searchHandler) executeParameterizedPost(ctx context.Context, req *dialect.Request) (*dialect.Response, error) {
	var body struct {
		Q            string   `json:"q"`
		Fields       []string `json:"fields"`
		OverallLimit int      `json:"overallLimit"`
		Sobjects     []struct {
			Name          string               `json:"name"`
			Fields        []string             `json:"fields"`
			Limit         int                  `json:"limit"`
			LookupContext *searchLookupContext `json:"lookupContext,omitempty"`
		} `json:"sobjects"`
	}
	if err := json.Unmarshal(req.Body, &body); err != nil {
		return searchErrorResponse(http.StatusBadRequest, wire.SFCodeInvalidRequest, "Invalid JSON body"), nil
	}
	if body.Q == "" {
		return searchErrorResponse(http.StatusBadRequest, wire.SFCodeMissingQuery, "Field 'q' is required"), nil
	}
	if containsSOSLClauses(body.Q) {
		return searchErrorResponse(http.StatusBadRequest, wire.SFCodeInvalidRequest, "Parameterized search q must not include SOSL clauses"), nil
	}
	if len(body.Fields) > 0 && len(body.Sobjects) == 0 {
		return searchErrorResponse(http.StatusBadRequest, wire.SFCodeInvalidRequest, "Global fields array requires sobjects"), nil
	}

	overallLimit := body.OverallLimit
	if overallLimit <= 0 || overallLimit > maxSOSLLimit {
		overallLimit = maxSOSLLimit
	}

	objectConfigs := make([]searchObjectConfig, 0)
	multiObjectRequest := len(body.Sobjects) > 1
	if len(body.Sobjects) > 0 {
		for _, obj := range body.Sobjects {
			if obj.Name == "" {
				continue
			}
			if err := h.validateScopedLookupContext(ctx, obj.Name, obj.LookupContext); err != nil {
				if multiObjectRequest {
					continue
				}
				return searchErrorResponse(http.StatusBadRequest, wire.SFCodeInvalidRequest, err.Error()), nil
			}
			limit := obj.Limit
			if limit <= 0 {
				limit = defaultSOSLLimit
			}
			objectConfigs = append(objectConfigs, searchObjectConfig{
				Name:   obj.Name,
				Fields: obj.Fields,
				Limit:  limit,
				Lookup: obj.LookupContext,
			})
		}
	} else {
		for _, obj := range h.searchableObjectNames(ctx) {
			objectConfigs = append(objectConfigs, searchObjectConfig{
				Name:   obj,
				Fields: nil,
				Limit:  defaultSOSLLimit,
			})
		}
	}

	searchResults, err := h.executeSearchWithLimits(ctx, req, body.Q, "ALL", objectConfigs, overallLimit, req.User)
	if err != nil {
		return h.searchWriteError(req, err, wire.SFCodeInvalidRequest), nil
	}
	return &dialect.Response{Status: http.StatusOK, Body: map[string]any{"searchRecords": searchResults}}, nil
}

func (h searchHandler) executeSearchLayout(ctx context.Context, req *dialect.Request) (*dialect.Response, error) {
	if h.metadata == nil {
		return searchErrorResponse(http.StatusInternalServerError, wire.SFCodeInternalError, "Metadata service unavailable"), nil
	}
	objectsParam := queryValue(req, "q")
	if objectsParam == "" {
		return searchErrorResponse(http.StatusBadRequest, wire.SFCodeMissingQuery, "Query parameter 'q' is required"), nil
	}

	objects := strings.Split(objectsParam, ",")
	result := make([]searchLayoutResult, 0, len(objects))
	for _, objName := range objects {
		objName = strings.TrimSpace(objName)
		if objName == "" {
			continue
		}
		schema, err := h.metadata.Describe(ctx, objName)
		if err != nil {
			if sharederrors.IsNotFound(err) {
				return searchErrorResponse(http.StatusNotFound, wire.SFCodeNotFound, "Object not found: "+objName), nil
			}
			return h.searchWriteError(req, err, wire.SFCodeInternalError), nil
		}
		if schema == nil {
			return searchErrorResponse(http.StatusNotFound, wire.SFCodeNotFound, "Object not found: "+objName), nil
		}
		result = append(result, searchLayoutResult{
			Label:         "Search Results",
			LimitRows:     defaultLayoutLimitRows,
			ObjectType:    schema.APIName,
			SearchColumns: buildSearchColumns(schema),
		})
	}
	return &dialect.Response{Status: http.StatusOK, Body: result}, nil
}

func (h searchHandler) executeSuggestions(ctx context.Context, req *dialect.Request) (*dialect.Response, error) {
	term := strings.TrimSpace(queryValue(req, "q"))
	if len(term) < minSuggestionTermLength {
		return &dialect.Response{Status: http.StatusOK, Body: map[string]any{
			"autoSuggestResults": []map[string]any{},
			"hasMoreResults":     false,
		}}, nil
	}

	limit := defaultSuggestionLimit
	if raw := strings.TrimSpace(queryValue(req, "limit")); raw != "" {
		if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 {
			limit = parsed
		}
	}
	if limit > maxSuggestionLimit {
		limit = maxSuggestionLimit
	}

	includeEmail := false
	if sf := queryValue(req, "searchFields"); sf != "" {
		for _, tok := range strings.Split(sf, ",") {
			if strings.EqualFold(strings.TrimSpace(tok), "EMAIL") {
				includeEmail = true
				break
			}
		}
	}

	objects := parseSearchFields(queryValue(req, "sobject"))
	if len(objects) == 0 {
		objects = h.searchableObjectNames(ctx)
	}

	results := make([]map[string]any, 0, limit)
	hasMore := false
	executableObjects := h.buildExecutableSuggestionObjects(ctx, objects, includeEmail, req.User)
	multiObjectSearch := len(executableObjects) > 1
	skippedSearchFieldErrors := 0
	var representativeSearchFieldErr error
	version := apiVersionOrDefault(req.Version)

	for _, executable := range executableObjects {
		if h.query == nil {
			return searchErrorResponse(http.StatusInternalServerError, wire.SFCodeInternalError, "An internal server error occurred. Contact your system administrator."), nil
		}
		overfetch := limit
		if overfetch > 0 {
			overfetch = limit + 1
		}
		records, err := h.query.SearchWithSpec(ctx, queryspec.SearchSpec{
			ObjectAPIName:  executable.config.Name,
			Term:           term,
			SearchFields:   executable.searchFields,
			ReturnFields:   executable.searchFields,
			Limit:          overfetch,
			Prefix:         true,
			ForceBaseTable: true,
		}, req.User)
		if err != nil {
			if shouldSkipPerObjectSearchError(err, multiObjectSearch) {
				skippedSearchFieldErrors++
				if representativeSearchFieldErr == nil {
					representativeSearchFieldErr = err
				}
				continue
			}
			return h.searchWriteError(req, err, wire.SFCodeInvalidRequest), nil
		}

		if len(records) > limit {
			hasMore = true
			records = records[:limit]
		}

		termLower := strings.ToLower(term)
		for _, record := range records {
			wireRecord := models.SObject(record)
			normalizeNameField(wireRecord, executable.schema)
			suggestion := map[string]any{
				"attributes": map[string]any{
					"type": executable.config.Name,
					"url":  fmt.Sprintf("%s/%s/%v", versionedSObjectsBasePath(version), executable.config.Name, wireRecord[constants.FieldID]),
				},
				constants.FieldID:   wireRecord[constants.FieldID],
				constants.FieldName: wireRecord[constants.FieldName],
			}
			if includeEmail {
				for _, field := range executable.schema.Fields {
					if strings.EqualFold(string(field.Type), string(constants.FieldTypeEmail)) {
						if val, ok := wireRecord[field.APIName]; ok && val != nil {
							if s, isStr := val.(string); isStr && s != "" && strings.HasPrefix(strings.ToLower(s), termLower) {
								suggestion[field.APIName] = val
								break
							}
						}
					}
				}
			}
			results = append(results, suggestion)
		}
	}
	if len(results) == 0 && skippedSearchFieldErrors > 0 && representativeSearchFieldErr != nil {
		return h.searchWriteError(req, representativeSearchFieldErr, wire.SFCodeInvalidRequest), nil
	}

	return &dialect.Response{Status: http.StatusOK, Body: map[string]any{
		"autoSuggestResults": results,
		"hasMoreResults":     hasMore,
	}}, nil
}

func (h searchHandler) executeScopeOrder(ctx context.Context, req *dialect.Request) (*dialect.Response, error) {
	if h.searchScope == nil {
		return searchErrorResponse(http.StatusInternalServerError, wire.SFCodeInternalError, "An internal server error occurred. Contact your system administrator."), nil
	}
	return &dialect.Response{
		Status: http.StatusOK,
		Body:   searchScopeItemsToV63(h.searchScope.ScopeOrder(ctx, req.User), req.Version),
	}, nil
}

func searchScopeItemsToV63(items []contract.SearchScopeItem, version string) []searchScopeOrderItemV63 {
	if len(items) == 0 {
		return []searchScopeOrderItemV63{}
	}
	version = apiVersionOrDefault(version)
	out := make([]searchScopeOrderItemV63, 0, len(items))
	for _, item := range items {
		out = append(out, searchScopeOrderItemV63{
			Type:      item.ObjectAPIName,
			URL:       fmt.Sprintf("/services/data/%s/sobjects/%s/describe", version, item.ObjectAPIName),
			Name:      item.Name,
			Label:     item.Label,
			KeyPrefix: item.KeyPrefix,
		})
	}
	return out
}

func (h searchHandler) executeSearchWithLimits(ctx context.Context, req *dialect.Request, term string, scope string, objectConfigs []searchObjectConfig, overallLimit int, user *models.UserSession) ([]models.SObject, error) {
	flatResults := make([]models.SObject, 0)
	totalCount := 0
	executableObjects, err := h.buildExecutableSearchObjects(ctx, scope, objectConfigs, user)
	if err != nil {
		return nil, err
	}
	multiObjectSearch := len(executableObjects) > 1
	skippedSearchFieldErrors := 0
	var representativeSearchFieldErr error
	version := apiVersionOrDefault(req.Version)

	for _, executable := range executableObjects {
		if totalCount >= overallLimit {
			break
		}
		config := executable.config
		schema := executable.schema
		returnFields := config.Fields
		if h.query == nil {
			return nil, newInternalConditionError("Query service unavailable")
		}
		searchRecords, err := h.query.SearchWithSpec(ctx, queryspec.SearchSpec{
			ObjectAPIName: config.Name,
			Term:          term,
			SearchFields:  executable.searchFields,
			ReturnFields:  returnFields,
			Limit:         config.Limit,
			Offset:        config.Offset,
			SortField:     config.OrderBy,
			SortDirection: config.OrderDir,
			Filter:        executable.filter,
			// Route substring scans through the base-table fast path. The
			// repository demotes to the formula view when the request needs
			// view-only search/return/filter/RLS columns.
			ForceBaseTable: true,
		}, user)
		if err != nil {
			if isSearchFieldValidationError(err) {
				if multiObjectSearch {
					skippedSearchFieldErrors++
					if representativeSearchFieldErr == nil {
						representativeSearchFieldErr = err
					}
					continue
				}
				return nil, newInternalConditionError("search execution error").WithCause(err)
			}
			return nil, err
		}

		objectLimit := config.Limit
		if objectLimit <= 0 {
			objectLimit = defaultSOSLLimit
		}
		for i, searchRecord := range searchRecords {
			if i >= objectLimit || totalCount >= overallLimit {
				break
			}
			record := soslRecordToV63SObject(searchRecord, schema, config.Name, version)
			flatResults = append(flatResults, record)
			totalCount++
		}
	}
	if len(flatResults) == 0 && skippedSearchFieldErrors > 0 && representativeSearchFieldErr != nil {
		return nil, newInternalConditionError("search execution error").WithCause(representativeSearchFieldErr)
	}
	return flatResults, nil
}

func (h searchHandler) buildExecutableSuggestionObjects(ctx context.Context, objects []string, includeEmail bool, user *models.UserSession) []executableSearchObject {
	executableObjects := make([]executableSearchObject, 0, len(objects))
	for _, objectName := range objects {
		schema := h.describe(ctx, objectName)
		if schema == nil || !schema.Searchable {
			continue
		}
		if !h.canReadObject(ctx, objectName, user) {
			continue
		}

		searchFields := suggestionSearchFields(schema)
		if len(searchFields) == 0 {
			continue
		}
		if includeEmail {
			for _, field := range schema.Fields {
				if strings.EqualFold(string(field.Type), string(constants.FieldTypeEmail)) {
					searchFields = append(searchFields, field.APIName)
				}
			}
		}

		executableObjects = append(executableObjects, executableSearchObject{
			config:       searchObjectConfig{Name: objectName},
			schema:       schema,
			searchFields: searchFields,
		})
	}
	return executableObjects
}

func (h searchHandler) buildExecutableSearchObjects(ctx context.Context, scope string, objectConfigs []searchObjectConfig, user *models.UserSession) ([]executableSearchObject, error) {
	executableObjects := make([]executableSearchObject, 0, len(objectConfigs))
	for _, config := range objectConfigs {
		schema := h.describe(ctx, config.Name)
		if schema == nil || !schema.Searchable {
			continue
		}
		if !h.canReadObject(ctx, config.Name, user) {
			continue
		}
		searchFields, err := resolveSOSLSearchFields(schema, scope)
		if err != nil {
			return nil, err
		}
		if len(searchFields) == 0 {
			continue
		}

		filter := config.Filter
		scopedFilter, hasScopedLookup, scopeErr := h.deriveScopedLookupFilter(ctx, config.Name, config.Lookup, user)
		if scopeErr != nil {
			return nil, scopeErr
		}
		if hasScopedLookup {
			if scopedFilter.Kind == queryspec.PredicateNone {
				continue
			}
			filter = queryspec.And(filter, scopedFilter)
		}

		executableObjects = append(executableObjects, executableSearchObject{
			config:       config,
			schema:       schema,
			searchFields: searchFields,
			filter:       filter,
		})
	}
	return executableObjects, nil
}

func (h searchHandler) searchableObjectNames(ctx context.Context) []string {
	return searchmetadata.SearchableObjectNames(ctx, h.metadata)
}

func (h searchHandler) describe(ctx context.Context, objectName string) *models.ObjectMetadata {
	if h.metadata == nil {
		return nil
	}
	schema, err := h.metadata.Describe(ctx, objectName)
	if err != nil {
		return nil
	}
	return schema
}

func (h searchHandler) canReadObject(ctx context.Context, objectName string, user *models.UserSession) bool {
	if h.policy == nil {
		return false
	}
	access, err := h.policy.Access(ctx, user, objectName, contract.AccessRequest{})
	if err != nil || access == nil {
		return false
	}
	return access.Object.Read
}

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

func (h searchHandler) searchWriteError(req *dialect.Request, err error, fallbackCode string) *dialect.Response {
	if err == nil {
		return nil
	}
	if sharederrors.IsInternal(err) {
		return searchErrorResponse(http.StatusInternalServerError, wire.SFCodeInternalError, "An internal server error occurred. Contact your system administrator.")
	}
	var statusErr httpStatusError
	if errors.As(err, &statusErr) {
		switch statusErr.HTTPStatus() {
		case http.StatusForbidden:
			return searchErrorResponse(http.StatusForbidden, wire.SFCodeForbidden, err.Error())
		case http.StatusNotFound:
			return searchErrorResponse(http.StatusNotFound, wire.SFCodeNotFound, err.Error())
		}
	}
	if sfErr, status, ok := wire.SFErrorFromDomainForVersion(err, apiVersionOrDefault(req.Version)); ok {
		return &dialect.Response{Status: status, Body: []wire.SFError{sfErr}}
	}
	if field, message, ok := sharederrors.ValidationDetails(err); ok {
		status, body := wire.ValidationErrorEnvelope(field, message, fallbackCode)
		return &dialect.Response{Status: status, Body: body}
	}
	if fallbackCode == wire.SFCodeInternalError {
		return searchErrorResponse(http.StatusInternalServerError, fallbackCode, "Metadata service unavailable")
	}
	return searchErrorResponse(http.StatusBadRequest, fallbackCode, err.Error())
}

func parseLookupContextFromSearchRequest(req *dialect.Request) (*searchLookupContext, error) {
	sourceObject := strings.TrimSpace(queryValue(req, "lookupSourceObject"))
	sourceField := strings.TrimSpace(queryValue(req, "lookupSourceField"))
	rawContext := strings.TrimSpace(queryValue(req, "lookupContext"))
	if sourceObject == "" && sourceField == "" && rawContext == "" {
		return nil, nil
	}
	if sourceObject == "" || sourceField == "" || rawContext == "" {
		return nil, fmt.Errorf("lookup context requires lookupSourceObject, lookupSourceField, and lookupContext")
	}
	recordContext := map[string]any{}
	if err := json.Unmarshal([]byte(rawContext), &recordContext); err != nil {
		return nil, fmt.Errorf("invalid lookupContext JSON")
	}
	return &searchLookupContext{
		SourceObject:  sourceObject,
		SourceField:   sourceField,
		RecordContext: recordContext,
	}, nil
}

func parseSOSL(query string) (string, string, []searchObjectConfig, error) {
	query = strings.TrimSpace(query)
	upperQuery := strings.ToUpper(query)
	if !strings.HasPrefix(upperQuery, "FIND") {
		return "", "", nil, NewValidationError("query", "SOSL query must start with FIND")
	}
	start := strings.Index(query, "{")
	end := strings.Index(query, "}")
	if start == -1 || end == -1 || end <= start {
		return "", "", nil, NewValidationError("query", "Search term must be enclosed in braces: FIND {term}")
	}

	searchTerm := strings.TrimSpace(query[start+1 : end])
	if searchTerm == "" {
		return "", "", nil, NewValidationError("query", "Search term cannot be empty")
	}
	searchTerm = strings.ReplaceAll(searchTerm, "'", "")
	searchTerm = strings.ReplaceAll(searchTerm, "\"", "")
	searchTerm = strings.ReplaceAll(searchTerm, ";", "")
	searchTerm = strings.ReplaceAll(searchTerm, "\\", "")

	remaining := query[end+1:]
	scope := "ALL"
	upperRemaining := strings.ToUpper(remaining)
	inIdx := strings.Index(upperRemaining, " IN ")
	returningIdx := strings.Index(upperRemaining, " RETURNING ")
	if inIdx != -1 && (returningIdx == -1 || inIdx < returningIdx) {
		scopePart := upperRemaining[inIdx+4:]
		fieldsIdx := strings.Index(scopePart, " FIELDS")
		if fieldsIdx != -1 {
			extractedScope := strings.TrimSpace(scopePart[:fieldsIdx])
			if extractedScope == "ALL" || extractedScope == "NAME" || extractedScope == "EMAIL" || extractedScope == "PHONE" {
				scope = extractedScope
			}
			clauseLen := 4 + fieldsIdx + 7
			remaining = remaining[inIdx+clauseLen:]
		}
	}

	configs := []searchObjectConfig{}
	returningIdx = strings.Index(strings.ToUpper(remaining), "RETURNING")
	if returningIdx != -1 {
		objectsPart := strings.TrimSpace(remaining[returningIdx+9:])
		currentObj := ""
		inParens := false
		fieldBuffer := ""
		objBuffer := ""
		for i := 0; i < len(objectsPart); i++ {
			c := objectsPart[i]
			switch {
			case c == '(':
				inParens = true
				currentObj = strings.TrimSpace(objBuffer)
				objBuffer = ""
				fieldBuffer = ""
			case c == ')':
				inParens = false
				if currentObj != "" {
					fields, limit, offset, orderBy, orderDir, filter, err := parseFieldListWithOptions(fieldBuffer)
					if err != nil {
						return "", "", nil, err
					}
					configs = append(configs, searchObjectConfig{
						Name:     currentObj,
						Fields:   fields,
						Limit:    limit,
						Offset:   offset,
						OrderBy:  orderBy,
						OrderDir: orderDir,
						Filter:   filter,
					})
				}
				currentObj = ""
				fieldBuffer = ""
			case c == ',' && !inParens:
				if objBuffer != "" {
					objName := strings.TrimSpace(objBuffer)
					if objName != "" {
						configs = append(configs, searchObjectConfig{Name: objName, Limit: defaultSOSLLimit})
					}
				}
				objBuffer = ""
			case inParens:
				fieldBuffer += string(c)
			default:
				objBuffer += string(c)
			}
		}
		if objBuffer != "" {
			objName := strings.TrimSpace(objBuffer)
			if objName != "" {
				configs = append(configs, searchObjectConfig{Name: objName, Limit: defaultSOSLLimit})
			}
		}
	}
	return searchTerm, scope, configs, nil
}

func parseFieldListWithOptions(content string) ([]string, int, int, string, string, queryspec.Predicate, error) {
	content = strings.TrimSpace(content)
	limit := defaultSOSLLimit
	offset := 0
	orderBy := ""
	orderDir := ""
	filter := queryspec.Predicate{}
	upperContent := strings.ToUpper(content)

	offsetRegex := regexp.MustCompile(`(?i)\s+OFFSET\s+(\d+)$`)
	if match := offsetRegex.FindStringSubmatch(content); match != nil {
		if val, err := strconv.Atoi(match[1]); err == nil {
			offset = val
		}
		loc := offsetRegex.FindStringIndex(content)
		content = content[:loc[0]]
		upperContent = upperContent[:loc[0]]
	}

	limitRegex := regexp.MustCompile(`(?i)\s+LIMIT\s+(\d+)$`)
	if match := limitRegex.FindStringSubmatch(content); match != nil {
		if val, err := strconv.Atoi(match[1]); err == nil {
			limit = val
			if limit <= 0 {
				limit = defaultSOSLLimit
			}
		}
		loc := limitRegex.FindStringIndex(content)
		content = content[:loc[0]]
		upperContent = upperContent[:loc[0]]
	}

	orderByRegex := regexp.MustCompile(`(?i)\s+ORDER\s+BY\s+([a-zA-Z0-9_\.]+)(\s+(ASC|DESC))?$`)
	if match := orderByRegex.FindStringSubmatch(content); match != nil {
		orderBy = match[1]
		if len(match) > 3 && match[3] != "" {
			orderDir = match[3]
		}
		loc := orderByRegex.FindStringIndex(content)
		content = content[:loc[0]]
		upperContent = upperContent[:loc[0]]
	}

	whereIdx := strings.LastIndex(upperContent, " WHERE ")
	if whereIdx != -1 {
		var err error
		filter, err = soql.ParsePredicateFragment(content[whereIdx+7:])
		if err != nil {
			return nil, 0, 0, "", "", queryspec.Predicate{}, err
		}
		content = content[:whereIdx]
	}

	fields := []string{}
	if content != "" {
		for _, f := range strings.Split(content, ",") {
			field := strings.TrimSpace(f)
			if field != "" {
				fields = append(fields, field)
			}
		}
	}
	return fields, limit, offset, orderBy, orderDir, filter, nil
}

func containsSOSLClauses(term string) bool {
	trimmed := strings.TrimSpace(strings.ToUpper(term))
	return strings.HasPrefix(trimmed, "FIND ")
}

func parseSearchFields(raw string) []string {
	if raw == "" {
		return nil
	}
	fields := make([]string, 0)
	for _, f := range strings.Split(raw, ",") {
		field := strings.TrimSpace(f)
		if field != "" {
			fields = append(fields, field)
		}
	}
	return fields
}

func parseSearchLimit(raw string, fallback int) int {
	if raw == "" {
		return fallback
	}
	limit := fallback
	if parsed, err := fmt.Sscanf(raw, "%d", &limit); err != nil || parsed != 1 {
		return fallback
	}
	if limit <= 0 {
		return fallback
	}
	if limit > maxSOSLLimit {
		return maxSOSLLimit
	}
	return limit
}

func soslRecordToV63SObject(record contract.Record, schema *models.ObjectMetadata, objectName string, version string) models.SObject {
	wireRecord := projectRecordToV63(record, v63RecordProjectionFull)
	normalizeNameField(wireRecord, schema)
	recordID := wireRecord[constants.FieldID]
	wireRecord["attributes"] = map[string]any{
		"type": objectName,
		"url":  fmt.Sprintf("%s/%s/%v", versionedSObjectsBasePath(version), objectName, recordID),
	}
	return wireRecord
}

func isSearchFieldValidationError(err error) bool {
	if err == nil || sharederrors.IsInternal(err) {
		return false
	}
	field, ok := sharederrors.ValidationField(err)
	return ok && strings.EqualFold(field, "searchField")
}

func shouldSkipPerObjectSearchError(err error, multiObjectSearch bool) bool {
	return multiObjectSearch && isSearchFieldValidationError(err)
}

func suggestionSearchFields(schema *models.ObjectMetadata) []string {
	if schema == nil {
		return nil
	}
	for _, field := range schema.Fields {
		if field.IsNameField {
			return []string{field.APIName}
		}
	}
	for _, field := range schema.Fields {
		if strings.EqualFold(field.APIName, constants.FieldName) {
			return []string{field.APIName}
		}
	}
	return nil
}

func resolveSOSLSearchFields(schema *models.ObjectMetadata, scope string) ([]string, error) {
	resolution, err := searchmetadata.ResolveSOSLSearchFields(schema, scope)
	if err != nil {
		return nil, err
	}
	return resolution.Fields, nil
}

func buildSOSLSearchFields(schema *models.ObjectMetadata, scope string) []string {
	fields, err := resolveSOSLSearchFields(schema, scope)
	if err != nil {
		return nil
	}
	return fields
}

func addCompoundMembers(schema *models.ObjectMetadata, apiName string, add func(string)) {
	for _, member := range compoundMemberNames(schema, apiName) {
		add(member)
	}
}

func normalizeNameField(record models.SObject, schema *models.ObjectMetadata) {
	if value, ok := compoundFieldValue(schema, constants.FieldName, record); ok {
		record[constants.FieldName] = value
		return
	}
	for _, f := range schema.Fields {
		if f.IsNameField {
			if val, ok := record[f.APIName]; ok {
				record["Name"] = val
				return
			}
		}
	}
	for _, f := range schema.Fields {
		if strings.EqualFold(f.APIName, "Name") {
			if val, ok := record[f.APIName]; ok {
				record["Name"] = val
				return
			}
		}
	}
	if id, ok := record["Id"]; ok {
		record["Name"] = id
	}
}

func buildSearchColumns(schema *models.ObjectMetadata) []searchColumn {
	columns := make([]searchColumn, 0)
	priorityFields := []string{"Phone", "Email"}

	var nameField *models.FieldMetadata
	for i := range schema.Fields {
		if schema.Fields[i].APIName == "Name" {
			nameField = &schema.Fields[i]
			break
		}
	}
	if nameField == nil {
		for i := range schema.Fields {
			if schema.Fields[i].IsNameField {
				nameField = &schema.Fields[i]
				break
			}
		}
	}
	if nameField != nil {
		columns = append(columns, searchColumn{
			Field:  schema.APIName + "." + nameField.APIName,
			Format: nil,
			Label:  nameField.Label,
			Name:   nameField.APIName,
		})
	}
	for _, fieldName := range priorityFields {
		for i := range schema.Fields {
			f := &schema.Fields[i]
			if strings.EqualFold(f.APIName, fieldName) && !f.IsNameField {
				columns = append(columns, searchColumn{
					Field:  schema.APIName + "." + f.APIName,
					Format: nil,
					Label:  f.Label,
					Name:   f.APIName,
				})
				break
			}
		}
	}
	for i := range schema.Fields {
		f := &schema.Fields[i]
		if f.APIName == "OwnerId" && len(f.ReferenceTo) > 0 && f.ReferenceTo[0] == constants.TableUser {
			columns = append(columns, searchColumn{
				Field:  "User.Alias",
				Format: nil,
				Label:  schema.Label + " Owner Alias",
				Name:   "Owner.Alias",
			})
			break
		}
	}
	for i := range schema.Fields {
		if len(columns) >= maxSearchColumns {
			break
		}
		f := &schema.Fields[i]
		alreadyAdded := false
		for _, col := range columns {
			if col.Name == f.APIName {
				alreadyAdded = true
				break
			}
		}
		if alreadyAdded {
			continue
		}
		if (f.Type == "string" || f.Type == "VARCHAR(255)") && !f.IsSystem && f.APIName != "Id" {
			columns = append(columns, searchColumn{
				Field:  schema.APIName + "." + f.APIName,
				Format: nil,
				Label:  f.Label,
				Name:   f.APIName,
			})
		}
	}
	return columns
}

func (h searchHandler) deriveScopedLookupFilter(ctx context.Context, targetObject string, lookup *searchLookupContext, user *models.UserSession) (queryspec.Predicate, bool, error) {
	if lookup == nil {
		return queryspec.Predicate{}, false, nil
	}
	sourceObject := strings.TrimSpace(lookup.SourceObject)
	sourceField := strings.TrimSpace(lookup.SourceField)
	if sourceObject == "" || sourceField == "" {
		return queryspec.Predicate{}, false, fmt.Errorf("lookupContext requires sourceObject and sourceField")
	}
	sourceSchema := h.describe(ctx, sourceObject)
	if sourceSchema == nil {
		return queryspec.Predicate{}, false, fmt.Errorf("lookupContext source object %q not found", sourceObject)
	}
	sourceMeta := lookupFieldByAPIName(sourceSchema, sourceField)
	if sourceMeta == nil {
		return queryspec.Predicate{}, false, fmt.Errorf("lookupContext source field %q not found on %s", sourceField, sourceObject)
	}
	if sourceMeta.Validator == nil || !strings.EqualFold(strings.TrimSpace(*sourceMeta.Validator), runtimeScopedLookupValidator) {
		return queryspec.Predicate{}, false, nil
	}
	if len(sourceMeta.ReferenceTo) > 0 && !stringSliceContainsFold(sourceMeta.ReferenceTo, targetObject) {
		return queryspec.Predicate{}, true, fmt.Errorf("lookup scope target object %q is not declared on %s.%s", targetObject, sourceObject, sourceField)
	}
	runtimeLookup := buildRuntimeLookupInfo(*sourceMeta)
	if runtimeLookup == nil {
		return queryspec.Predicate{}, true, fmt.Errorf("lookup scope config on %s.%s is invalid", sourceObject, sourceField)
	}
	targetSchema := h.describe(ctx, targetObject)
	if targetSchema == nil {
		return queryspec.Predicate{}, false, fmt.Errorf("lookup target object %q not found", targetObject)
	}

	clauses := make([]queryspec.Predicate, 0, len(runtimeLookup.FilterMappings))
	for _, mapping := range runtimeLookup.FilterMappings {
		if lookupFieldByAPIName(targetSchema, mapping.TargetField) == nil {
			return queryspec.Predicate{}, true, fmt.Errorf("lookup scope target field %q not found on %s", mapping.TargetField, targetObject)
		}
		value, resolved, resolveErr := h.resolveScopedLookupValue(ctx, sourceSchema, lookup.RecordContext, mapping, user)
		if resolveErr != nil {
			return queryspec.Predicate{}, true, resolveErr
		}
		if !resolved {
			return queryspec.Predicate{}, true, nil
		}
		clauses = append(clauses, queryspec.FieldPredicate(mapping.TargetField, "=", value))
	}
	if len(clauses) == 0 {
		return queryspec.Predicate{}, true, nil
	}
	return queryspec.And(clauses...), true, nil
}

func (h searchHandler) validateScopedLookupContext(ctx context.Context, targetObject string, lookup *searchLookupContext) error {
	if lookup == nil {
		return nil
	}
	sourceObject := strings.TrimSpace(lookup.SourceObject)
	sourceField := strings.TrimSpace(lookup.SourceField)
	if sourceObject == "" || sourceField == "" {
		return fmt.Errorf("lookupContext requires sourceObject and sourceField")
	}
	sourceSchema := h.describe(ctx, sourceObject)
	if sourceSchema == nil {
		return fmt.Errorf("lookupContext source object %q not found", sourceObject)
	}
	sourceMeta := lookupFieldByAPIName(sourceSchema, sourceField)
	if sourceMeta == nil {
		return fmt.Errorf("lookupContext source field %q not found on %s", sourceField, sourceObject)
	}
	if sourceMeta.Validator == nil || !strings.EqualFold(strings.TrimSpace(*sourceMeta.Validator), runtimeScopedLookupValidator) {
		return fmt.Errorf("lookupContext source field %q is not declared as runtime_scoped_lookup", sourceField)
	}
	if len(sourceMeta.ReferenceTo) > 0 && !stringSliceContainsFold(sourceMeta.ReferenceTo, targetObject) {
		return fmt.Errorf("lookup scope target object %q is not declared on %s.%s", targetObject, sourceObject, sourceField)
	}
	if buildRuntimeLookupInfo(*sourceMeta) == nil {
		return fmt.Errorf("lookup scope config on %s.%s is invalid", sourceObject, sourceField)
	}
	return nil
}

func (h searchHandler) resolveScopedLookupValue(ctx context.Context, sourceSchema *models.ObjectMetadata, recordContext map[string]any, mapping runtimeLookupFilterMapping, user *models.UserSession) (string, bool, error) {
	recordField := lookupFieldByAPIName(sourceSchema, mapping.RecordField)
	if recordField == nil {
		return "", false, fmt.Errorf("lookup scope record field %q not found on %s", mapping.RecordField, sourceSchema.APIName)
	}
	raw := lookupRecordContextValue(recordContext, mapping.RecordField)
	if strings.TrimSpace(raw) == "" {
		return "", false, nil
	}
	normalizedType := constants.NormalizeFieldType(string(recordField.Type))
	if (normalizedType == constants.FieldTypeReference || normalizedType == constants.FieldTypeMasterDetail) &&
		len(recordField.ReferenceTo) == 1 &&
		!strings.EqualFold(mapping.RecordField, mapping.SourceField) {
		parentObject := strings.TrimSpace(recordField.ReferenceTo[0])
		if parentObject == "" {
			return "", false, nil
		}
		parentSchema := h.describe(ctx, parentObject)
		if parentSchema == nil {
			return "", false, fmt.Errorf("lookup scope parent object %q not found", parentObject)
		}
		if lookupFieldByAPIName(parentSchema, mapping.SourceField) == nil {
			return "", false, fmt.Errorf("lookup scope source field %q not found on %s", mapping.SourceField, parentObject)
		}
		if h.query == nil {
			return "", false, newInternalConditionError("Query service unavailable")
		}
		result, err := h.query.Query(ctx, queryspec.QuerySpec{
			ObjectAPIName: parentObject,
			SelectFields:  []string{mapping.SourceField},
			Filter: queryspec.CriteriaPredicate([]queryspec.QueryCriterion{
				{Field: constants.FieldID, Op: "=", Val: raw},
			}),
			Limit: 1,
		}, user)
		if err != nil {
			return "", false, err
		}
		if result == nil || len(result.Records) == 0 {
			return "", false, fmt.Errorf("lookup scope could not resolve %s.%s from %s", parentObject, mapping.SourceField, mapping.RecordField)
		}
		resolved := lookupRecordContextValue(result.Records[0], mapping.SourceField)
		if strings.TrimSpace(resolved) == "" {
			return "", false, fmt.Errorf("lookup scope resolved blank %s.%s from %s", parentObject, mapping.SourceField, mapping.RecordField)
		}
		return resolved, true, nil
	}
	if !strings.EqualFold(mapping.RecordField, mapping.SourceField) {
		return "", false, fmt.Errorf("lookup scope direct mapping requires sourceField %q to match recordField %q", mapping.SourceField, mapping.RecordField)
	}
	return raw, true, nil
}

func lookupFieldByAPIName(schema *models.ObjectMetadata, apiName string) *models.FieldMetadata {
	return models.FindField(schema, apiName)
}

func lookupRecordContextValue(record map[string]any, apiName string) string {
	if len(record) == 0 || strings.TrimSpace(apiName) == "" {
		return ""
	}
	for key, value := range record {
		if !strings.EqualFold(key, apiName) {
			continue
		}
		if value == nil {
			return ""
		}
		switch typed := value.(type) {
		case string:
			return typed
		default:
			return fmt.Sprint(typed)
		}
	}
	return ""
}

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

func compoundMemberNames(schema *models.ObjectMetadata, parentAPIName string) []string {
	if schema == nil || strings.TrimSpace(parentAPIName) == "" {
		return nil
	}
	if field := models.FindField(schema, parentAPIName); field != nil && field.IsCompound && len(field.ComposedFrom) > 0 {
		return canonicalSchemaFieldNames(schema, field.ComposedFrom)
	}
	isCompound, members := fieldprovenance.ClassifyCompound(schema.APIName, parentAPIName)
	if !isCompound || len(members) == 0 {
		return nil
	}
	return canonicalSchemaFieldNames(schema, members)
}

func canonicalSchemaFieldNames(schema *models.ObjectMetadata, names []string) []string {
	out := make([]string, 0, len(names))
	for _, name := range names {
		if field := models.FindField(schema, name); field != nil {
			out = append(out, field.APIName)
		}
	}
	return out
}

func compoundFieldValue(schema *models.ObjectMetadata, parentAPIName string, data map[string]any) (string, bool) {
	if len(data) == 0 {
		return "", false
	}
	members := compoundMemberNames(schema, parentAPIName)
	if len(members) == 0 {
		return "", false
	}
	parts := make([]string, 0, len(members))
	for _, member := range members {
		raw, present := lookupRecordContextValueAny(data, member)
		if !present || strings.TrimSpace(raw) == "" {
			continue
		}
		parts = append(parts, raw)
	}
	if len(parts) == 0 {
		return "", false
	}
	return strings.Join(parts, " "), true
}

func lookupRecordContextValueAny(record map[string]any, apiName string) (string, bool) {
	for key, value := range record {
		if !strings.EqualFold(key, apiName) {
			continue
		}
		if value == nil {
			return "", true
		}
		return fmt.Sprint(value), true
	}
	return "", false
}

type internalConditionError struct {
	msg   string
	cause error
}

func newInternalConditionError(msg string) internalConditionError {
	return internalConditionError{msg: msg}
}

func (e internalConditionError) Error() string           { return e.msg }
func (e internalConditionError) InternalCondition() bool { return true }
func (e internalConditionError) Unwrap() error           { return e.cause }

func (e internalConditionError) WithCause(cause error) error {
	e.cause = cause
	return e
}
