package analytics

import (
	"context"
	"fmt"
	"strings"
	"unicode"

	"github.com/expr-lang/expr/ast"
	pkgErrors "github.com/nexuscrm/kernel/pkg/errors"
	"github.com/nexuscrm/kernel/pkg/expression"
	"github.com/nexuscrm/kernel/pkg/formula"
	domainrls "github.com/nexuscrm/kernel/pkg/rls"
	"github.com/nexuscrm/kernel/pkg/schemautil"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/formulastorage"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

func (qs *QueryService) validateQueryReadClauses(ctx context.Context, objectName string, schema *models.ObjectMetadata, req models.QueryRequest, currentUser *models.UserSession) ([]models.QueryParentFilterPath, error) {
	parentFilterPaths, err := qs.validateQueryFormulaReadClause(ctx, objectName, schema, req.FilterExpr, "FilterExpr", req.ScanAll, currentUser)
	if err != nil {
		return nil, err
	}
	for _, criterion := range req.Criteria {
		plan, ok, err := qs.validateQueryStructuredReadClause(ctx, objectName, schema, criterion.Field, "Criteria", req.ScanAll, currentUser)
		if err != nil {
			return nil, err
		}
		if ok {
			parentFilterPaths = appendQueryParentFilterPlan(parentFilterPaths, plan)
		}
	}
	if req.SortField != "" && req.SortField != "xMruAccessedAt" {
		plan, ok, err := qs.validateQueryStructuredReadClause(ctx, objectName, schema, req.SortField, "SortField", req.ScanAll, currentUser)
		if err != nil {
			return nil, err
		}
		if ok {
			parentFilterPaths = appendQueryParentFilterPlan(parentFilterPaths, plan)
		}
	}
	for _, sort := range req.OrderBy {
		plan, ok, err := qs.validateQueryStructuredReadClause(ctx, objectName, schema, sort.Field, "OrderBy", req.ScanAll, currentUser)
		if err != nil {
			return nil, err
		}
		if ok {
			parentFilterPaths = appendQueryParentFilterPlan(parentFilterPaths, plan)
		}
	}
	return parentFilterPaths, nil
}

func (qs *QueryService) validateQueryStructuredReadClause(ctx context.Context, objectName string, schema *models.ObjectMetadata, rawField string, clause string, scanAll bool, currentUser *models.UserSession) (models.QueryParentFilterPath, bool, error) {
	if !isStructuredQueryParentField(schema, rawField) {
		return models.QueryParentFilterPath{}, false, qs.validateClauseFieldReadable(ctx, objectName, schema, rawField, clause, currentUser)
	}
	path, err := qs.resolveQueryParentFilterPath(ctx, objectName, schema, rawField, clause)
	if err != nil {
		return models.QueryParentFilterPath{}, true, err
	}
	plan, err := qs.buildQueryParentFilterPlan(ctx, objectName, schema, path, clause, scanAll, currentUser)
	if err != nil {
		return models.QueryParentFilterPath{}, true, err
	}
	return plan, true, nil
}

func isStructuredQueryParentField(schema *models.ObjectMetadata, rawField string) bool {
	field := strings.Trim(strings.TrimSpace(rawField), "`")
	if field == "" || !strings.Contains(field, ".") {
		return false
	}
	parts := strings.Split(field, ".")
	if len(parts) == 2 && schema != nil && strings.EqualFold(strings.Trim(parts[0], "`"), schema.APIName) {
		return false
	}
	return true
}

func appendQueryParentFilterPlan(paths []models.QueryParentFilterPath, plan models.QueryParentFilterPath) []models.QueryParentFilterPath {
	key := normalizeQueryParentFilterPath(plan.RawPath)
	if key == "" {
		return paths
	}
	for _, existing := range paths {
		if normalizeQueryParentFilterPath(existing.RawPath) == key {
			return paths
		}
	}
	return append(paths, plan)
}

func (qs *QueryService) validateQueryFormulaReadClause(ctx context.Context, objectName string, schema *models.ObjectMetadata, expr string, clause string, scanAll bool, currentUser *models.UserSession) ([]models.QueryParentFilterPath, error) {
	if strings.TrimSpace(expr) == "" {
		return nil, nil
	}
	const parentMarker = "__ncrm_parent_filter_path_0__"

	var validationErr error
	parentPathsByKey := make(map[string]*queryParentFilterPath)
	parentOccurrences := 0
	functionSeen := false

	_, _, err := formula.ToSQLWithOptions(expr, formula.SQLOptions{
		AllowMemberPaths: true,
		Mapper: func(identifier string) string {
			if validationErr != nil {
				return identifier
			}
			trimmed := strings.Trim(strings.TrimSpace(identifier), "`")
			if strings.Contains(trimmed, ".") {
				path, err := qs.resolveQueryParentFilterPath(ctx, objectName, schema, identifier, clause)
				if err != nil {
					validationErr = err
					return identifier
				}
				parentOccurrences++
				key := normalizeQueryParentFilterPath(path.rawPath)
				if _, ok := parentPathsByKey[key]; !ok {
					parentPathsByKey[key] = path
				}
				return parentMarker
			}
			validationErr = qs.validateClauseFieldReadable(ctx, objectName, schema, identifier, clause, currentUser)
			return identifier
		},
		FunctionLowerer: func(ctx expression.SQLFunctionCallContext) (expression.SQLFragment, bool, error) {
			// Structured LIKE predicates use this internal expression bridge.
			// The AST validator below still requires a direct parent path and
			// a literal pattern; this does not enable arbitrary parent calls.
			if strings.EqualFold(ctx.Name, queryspec.InternalLikeExpressionFunction) {
				return expression.SQLFragment{}, false, nil
			}
			if isQueryParentFilterUnaryBlankFunction(ctx.Name, ctx.Args) {
				return expression.SQLFragment{}, false, nil
			}
			if isQueryParentFilterDirectBlankCoalescingFunction(ctx.Name, ctx.Args) {
				fragment, err := queryParentFilterBlankCoalescingSQL(ctx)
				return fragment, true, err
			}
			if isQueryParentFilterBlankCoalescingFunction(ctx.Name) && queryParentFilterArgsContainParentPath(ctx.Args) {
				return expression.SQLFragment{}, true, unsupportedQueryParentFilterShapeError(clause)
			}
			functionSeen = true
			return expression.SQLFragment{}, false, nil
		},
	})
	if err != nil {
		return nil, err
	}
	if validationErr != nil {
		return nil, validationErr
	}
	if parentOccurrences == 0 {
		return nil, nil
	}
	if functionSeen {
		return nil, unsupportedQueryParentFilterShapeError(clause)
	}
	parentPaths, err := validateQueryParentFilterExpressionShape(expr, parentPathsByKey, clause)
	if err != nil {
		return nil, err
	}
	if len(parentPaths) == 0 {
		return nil, unsupportedQueryParentFilterShapeError(clause)
	}
	plans := make([]models.QueryParentFilterPath, 0, len(parentPaths))
	for _, parentPath := range parentPaths {
		plan, err := qs.buildQueryParentFilterPlan(ctx, objectName, schema, parentPath, clause, scanAll, currentUser)
		if err != nil {
			return nil, err
		}
		plans = append(plans, plan)
	}
	return plans, nil
}

func (qs *QueryService) buildQueryParentFilterPlan(ctx context.Context, rootObject string, rootSchema *models.ObjectMetadata, path *queryParentFilterPath, clause string, scanAll bool, currentUser *models.UserSession) (models.QueryParentFilterPath, error) {
	if path == nil || path.refField == nil || path.targetSchema == nil || path.targetFieldMeta == nil {
		return models.QueryParentFilterPath{}, pkgErrors.NewValidationError(clause, "relationship field filters require resolved metadata")
	}
	if len(path.hops) > 1 {
		return qs.buildQueryParentFilterMultiHopPlan(ctx, path, clause, scanAll, currentUser)
	}
	if err := validateQueryParentFilterTargetReadSemantics(path.targetSchema, path.targetObject, clause); err != nil {
		return models.QueryParentFilterPath{}, err
	}
	if err := validateQueryParentFilterPhysicalField(path.refField, clause, "root relationship field", path.refField.APIName); err != nil {
		return models.QueryParentFilterPath{}, err
	}
	if err := qs.checkObjectReadAccess(ctx, path.targetObject, currentUser); err != nil {
		return models.QueryParentFilterPath{}, err
	}
	targetVisibility, err := qs.queryParentFilterTargetVisibility(ctx, path.targetSchema, path.targetObject, clause, scanAll, currentUser)
	if err != nil {
		return models.QueryParentFilterPath{}, err
	}
	if err := qs.validateClauseFieldReadable(ctx, rootObject, rootSchema, path.refField.APIName, clause, currentUser); err != nil {
		return models.QueryParentFilterPath{}, err
	}
	if err := qs.validateClauseFieldReadable(ctx, path.targetObject, path.targetSchema, path.targetFieldMeta.APIName, clause, currentUser); err != nil {
		return models.QueryParentFilterPath{}, err
	}
	targetFieldPhysicalName, targetCompoundMemberPhysicalNames, err := qs.queryParentFilterTargetFieldColumns(ctx, path.targetObject, path.targetSchema, path.targetFieldMeta, clause, currentUser)
	if err != nil {
		return models.QueryParentFilterPath{}, err
	}

	targetTable, err := path.targetSchema.ResolveReadSourceTable()
	if err != nil {
		return models.QueryParentFilterPath{}, pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require a physical parent target: %v", err))
	}
	idField := models.FindField(path.targetSchema, constants.FieldID)
	if idField == nil {
		return models.QueryParentFilterPath{}, pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require a physical parent target ID field: %s", path.targetObject))
	}
	if err := validateQueryParentFilterPhysicalField(idField, clause, "parent target ID field", path.targetObject); err != nil {
		return models.QueryParentFilterPath{}, err
	}

	plan := models.QueryParentFilterPath{
		RawPath:                                               path.rawPath,
		RelationshipName:                                      path.relationName,
		RootForeignKeyField:                                   path.refField.APIName,
		RootForeignKeyPhysicalName:                            queryParentFilterColumnName(path.refField),
		TargetObjectAPIName:                                   path.targetObject,
		TargetTable:                                           targetTable,
		TargetIDPhysicalName:                                  queryParentFilterColumnName(idField),
		TargetFieldAPIName:                                    path.targetFieldMeta.APIName,
		TargetFieldPhysicalName:                               targetFieldPhysicalName,
		TargetFieldCompoundMemberPhysicalNames:                targetCompoundMemberPhysicalNames,
		TargetVisibilityDenyAll:                               targetVisibility.denyAll,
		TargetVisibilityAllowedIDPhysicalName:                 targetVisibility.allowedIDPhysicalName,
		TargetVisibilityAllowedIDs:                            targetVisibility.allowedIDs,
		TargetVisibilityOwnerPhysicalName:                     targetVisibility.ownerPhysicalName,
		TargetVisibilityOwnerUserIDs:                          targetVisibility.ownerUserIDs,
		TargetVisibilityGroupOwnerIDs:                         targetVisibility.groupOwnerIDs,
		TargetVisibilityManualShareUserID:                     targetVisibility.manualShareUserID,
		TargetVisibilityTeamMemberUserID:                      targetVisibility.teamMemberUserID,
		TargetVisibilitySharingRuleOwnerUserIDs:               targetVisibility.sharingRuleOwnerUserIDs,
		TargetVisibilitySharingRuleFromGroupIDs:               targetVisibility.sharingRuleFromGroupIDs,
		TargetVisibilityOwnerVisibilityOwnerPhysicalName:      targetVisibility.ownerVisibilityOwnerPhysicalName,
		TargetVisibilityOwnerVisibilityVisibilityPhysicalName: targetVisibility.ownerVisibilityVisibilityPhysicalName,
		TargetVisibilityOwnerVisibilityUserID:                 targetVisibility.ownerVisibilityUserID,
		TargetVisibilityOwnerVisibilityOwnerValues:            targetVisibility.ownerVisibilityOwnerValues,
		TargetVisibilityOwnerVisibilityPublicValues:           targetVisibility.ownerVisibilityPublicValues,
		TargetVisibilityCriteriaExpressions:                   targetVisibility.criteriaExpressions,
		TargetVisibilityCriteriaFields:                        targetVisibility.criteriaFields,
		TargetVisibilityTargetRecordFilters:                   targetVisibility.targetRecordFilters,
	}
	if isDeletedField := models.FindField(path.targetSchema, constants.FieldIsDeleted); isDeletedField != nil {
		if err := validateQueryParentFilterPhysicalField(isDeletedField, clause, "parent is-deleted field", path.targetObject); err != nil {
			return models.QueryParentFilterPath{}, err
		}
		plan.TargetHasIsDeleted = true
		plan.TargetIsDeletedPhysicalName = queryParentFilterColumnName(isDeletedField)
	}
	return plan, nil
}

func (qs *QueryService) buildQueryParentFilterMultiHopPlan(ctx context.Context, path *queryParentFilterPath, clause string, scanAll bool, currentUser *models.UserSession) (models.QueryParentFilterPath, error) {
	hops := make([]models.QueryParentFilterHop, 0, len(path.hops))
	for _, hop := range path.hops {
		plan, err := qs.buildQueryParentFilterHopPlan(ctx, hop, clause, scanAll, currentUser)
		if err != nil {
			return models.QueryParentFilterPath{}, err
		}
		hops = append(hops, plan)
	}
	if len(hops) == 0 {
		return models.QueryParentFilterPath{}, pkgErrors.NewValidationError(clause, "relationship field filters require resolved relationship metadata")
	}
	if err := qs.validateClauseFieldReadable(ctx, path.targetObject, path.targetSchema, path.targetFieldMeta.APIName, clause, currentUser); err != nil {
		return models.QueryParentFilterPath{}, err
	}
	targetFieldPhysicalName, targetCompoundMemberPhysicalNames, err := qs.queryParentFilterTargetFieldColumns(ctx, path.targetObject, path.targetSchema, path.targetFieldMeta, clause, currentUser)
	if err != nil {
		return models.QueryParentFilterPath{}, err
	}

	firstHop := hops[0]
	lastHop := hops[len(hops)-1]
	return models.QueryParentFilterPath{
		RawPath:                                               path.rawPath,
		RelationshipHops:                                      hops,
		RelationshipName:                                      firstHop.RelationshipName,
		RootForeignKeyField:                                   firstHop.ForeignKeyField,
		RootForeignKeyPhysicalName:                            firstHop.ForeignKeyPhysicalName,
		TargetObjectAPIName:                                   lastHop.TargetObjectAPIName,
		TargetTable:                                           lastHop.TargetTable,
		TargetIDPhysicalName:                                  lastHop.TargetIDPhysicalName,
		TargetFieldAPIName:                                    path.targetFieldMeta.APIName,
		TargetFieldPhysicalName:                               targetFieldPhysicalName,
		TargetFieldCompoundMemberPhysicalNames:                targetCompoundMemberPhysicalNames,
		TargetHasIsDeleted:                                    lastHop.TargetHasIsDeleted,
		TargetIsDeletedPhysicalName:                           lastHop.TargetIsDeletedPhysicalName,
		TargetVisibilityDenyAll:                               lastHop.TargetVisibilityDenyAll,
		TargetVisibilityAllowedIDPhysicalName:                 lastHop.TargetVisibilityAllowedIDPhysicalName,
		TargetVisibilityAllowedIDs:                            lastHop.TargetVisibilityAllowedIDs,
		TargetVisibilityOwnerPhysicalName:                     lastHop.TargetVisibilityOwnerPhysicalName,
		TargetVisibilityOwnerUserIDs:                          lastHop.TargetVisibilityOwnerUserIDs,
		TargetVisibilityGroupOwnerIDs:                         lastHop.TargetVisibilityGroupOwnerIDs,
		TargetVisibilityManualShareUserID:                     lastHop.TargetVisibilityManualShareUserID,
		TargetVisibilityTeamMemberUserID:                      lastHop.TargetVisibilityTeamMemberUserID,
		TargetVisibilitySharingRuleOwnerUserIDs:               lastHop.TargetVisibilitySharingRuleOwnerUserIDs,
		TargetVisibilitySharingRuleFromGroupIDs:               lastHop.TargetVisibilitySharingRuleFromGroupIDs,
		TargetVisibilityOwnerVisibilityOwnerPhysicalName:      lastHop.TargetVisibilityOwnerVisibilityOwnerPhysicalName,
		TargetVisibilityOwnerVisibilityVisibilityPhysicalName: lastHop.TargetVisibilityOwnerVisibilityVisibilityPhysicalName,
		TargetVisibilityOwnerVisibilityUserID:                 lastHop.TargetVisibilityOwnerVisibilityUserID,
		TargetVisibilityOwnerVisibilityOwnerValues:            lastHop.TargetVisibilityOwnerVisibilityOwnerValues,
		TargetVisibilityOwnerVisibilityPublicValues:           lastHop.TargetVisibilityOwnerVisibilityPublicValues,
		TargetVisibilityCriteriaExpressions:                   lastHop.TargetVisibilityCriteriaExpressions,
		TargetVisibilityCriteriaFields:                        lastHop.TargetVisibilityCriteriaFields,
		TargetVisibilityTargetRecordFilters:                   lastHop.TargetVisibilityTargetRecordFilters,
	}, nil
}

func (qs *QueryService) buildQueryParentFilterHopPlan(ctx context.Context, hop queryParentFilterPathHop, clause string, scanAll bool, currentUser *models.UserSession) (models.QueryParentFilterHop, error) {
	if hop.refField == nil || hop.sourceSchema == nil || hop.targetSchema == nil {
		return models.QueryParentFilterHop{}, pkgErrors.NewValidationError(clause, "relationship field filters require resolved relationship metadata")
	}
	if err := validateQueryParentFilterTargetReadSemantics(hop.targetSchema, hop.targetObject, clause); err != nil {
		return models.QueryParentFilterHop{}, err
	}
	if err := validateQueryParentFilterPhysicalField(hop.refField, clause, "relationship field", hop.refField.APIName); err != nil {
		return models.QueryParentFilterHop{}, err
	}
	if err := qs.checkObjectReadAccess(ctx, hop.targetObject, currentUser); err != nil {
		return models.QueryParentFilterHop{}, err
	}
	if err := qs.validateClauseFieldReadable(ctx, hop.sourceObject, hop.sourceSchema, hop.refField.APIName, clause, currentUser); err != nil {
		return models.QueryParentFilterHop{}, err
	}
	targetVisibility, err := qs.queryParentFilterTargetVisibility(ctx, hop.targetSchema, hop.targetObject, clause, scanAll, currentUser)
	if err != nil {
		return models.QueryParentFilterHop{}, err
	}
	targetTable, err := hop.targetSchema.ResolveReadSourceTable()
	if err != nil {
		return models.QueryParentFilterHop{}, pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require a physical parent target: %v", err))
	}
	idField := models.FindField(hop.targetSchema, constants.FieldID)
	if idField == nil {
		return models.QueryParentFilterHop{}, pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require a physical parent target ID field: %s", hop.targetObject))
	}
	if err := validateQueryParentFilterPhysicalField(idField, clause, "parent target ID field", hop.targetObject); err != nil {
		return models.QueryParentFilterHop{}, err
	}

	plan := models.QueryParentFilterHop{
		RelationshipName:                                      hop.relationName,
		SourceObjectAPIName:                                   hop.sourceObject,
		ForeignKeyField:                                       hop.refField.APIName,
		ForeignKeyPhysicalName:                                queryParentFilterColumnName(hop.refField),
		TargetObjectAPIName:                                   hop.targetObject,
		TargetTable:                                           targetTable,
		TargetIDPhysicalName:                                  queryParentFilterColumnName(idField),
		TargetVisibilityDenyAll:                               targetVisibility.denyAll,
		TargetVisibilityAllowedIDPhysicalName:                 targetVisibility.allowedIDPhysicalName,
		TargetVisibilityAllowedIDs:                            targetVisibility.allowedIDs,
		TargetVisibilityOwnerPhysicalName:                     targetVisibility.ownerPhysicalName,
		TargetVisibilityOwnerUserIDs:                          targetVisibility.ownerUserIDs,
		TargetVisibilityGroupOwnerIDs:                         targetVisibility.groupOwnerIDs,
		TargetVisibilityManualShareUserID:                     targetVisibility.manualShareUserID,
		TargetVisibilityTeamMemberUserID:                      targetVisibility.teamMemberUserID,
		TargetVisibilitySharingRuleOwnerUserIDs:               targetVisibility.sharingRuleOwnerUserIDs,
		TargetVisibilitySharingRuleFromGroupIDs:               targetVisibility.sharingRuleFromGroupIDs,
		TargetVisibilityOwnerVisibilityOwnerPhysicalName:      targetVisibility.ownerVisibilityOwnerPhysicalName,
		TargetVisibilityOwnerVisibilityVisibilityPhysicalName: targetVisibility.ownerVisibilityVisibilityPhysicalName,
		TargetVisibilityOwnerVisibilityUserID:                 targetVisibility.ownerVisibilityUserID,
		TargetVisibilityOwnerVisibilityOwnerValues:            targetVisibility.ownerVisibilityOwnerValues,
		TargetVisibilityOwnerVisibilityPublicValues:           targetVisibility.ownerVisibilityPublicValues,
		TargetVisibilityCriteriaExpressions:                   targetVisibility.criteriaExpressions,
		TargetVisibilityCriteriaFields:                        targetVisibility.criteriaFields,
		TargetVisibilityTargetRecordFilters:                   targetVisibility.targetRecordFilters,
	}
	if isDeletedField := models.FindField(hop.targetSchema, constants.FieldIsDeleted); isDeletedField != nil {
		if err := validateQueryParentFilterPhysicalField(isDeletedField, clause, "parent is-deleted field", hop.targetObject); err != nil {
			return models.QueryParentFilterHop{}, err
		}
		plan.TargetHasIsDeleted = true
		plan.TargetIsDeletedPhysicalName = queryParentFilterColumnName(isDeletedField)
	}
	return plan, nil
}

func (qs *QueryService) queryParentFilterTargetFieldColumns(ctx context.Context, objectName string, schema *models.ObjectMetadata, field *models.FieldMetadata, clause string, currentUser *models.UserSession) (string, []string, error) {
	if qs.isQueryParentFilterPhysicalTargetField(ctx, objectName, field) {
		return queryParentFilterColumnName(field), nil, nil
	}
	if !isQueryParentFilterCompoundTargetField(field) {
		label := ""
		if field != nil {
			label = field.APIName
		}
		return "", nil, pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require a physical parent target field: %s", label))
	}
	members := schemautil.CompoundMemberNames(schema, field.APIName)
	if len(members) == 0 {
		return "", nil, pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require physical compound members: %s", field.APIName))
	}
	physicalNames := make([]string, 0, len(members))
	for _, memberName := range members {
		member := models.FindField(schema, memberName)
		if member != nil {
			if err := qs.validateClauseFieldReadable(ctx, objectName, schema, member.APIName, clause, currentUser); err != nil {
				return "", nil, err
			}
		}
		if err := validateQueryParentFilterPhysicalField(member, clause, "parent target compound member field", memberName); err != nil {
			return "", nil, err
		}
		physicalNames = append(physicalNames, queryParentFilterColumnName(member))
	}
	return "", physicalNames, nil
}

func validateQueryParentFilterTargetReadSemantics(schema *models.ObjectMetadata, targetObject string, clause string) error {
	if schema == nil {
		return pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require a supported parent target: %s", targetObject))
	}
	if policy := schema.RowReadPolicy(); policy != nil &&
		!isQueryParentFilterOwnerVisibilityRowReadPolicy(policy) &&
		!isQueryParentFilterTargetRecordReadRowReadPolicy(policy) {
		return pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require a supported parent target; %s has a row-read policy", targetObject))
	}
	return nil
}

func isQueryParentFilterOwnerVisibilityRowReadPolicy(policy *models.RowReadPolicy) bool {
	return policy != nil && strings.EqualFold(strings.TrimSpace(policy.Kind), "ownerVisibility")
}

func isQueryParentFilterTargetRecordReadRowReadPolicy(policy *models.RowReadPolicy) bool {
	return policy != nil && strings.EqualFold(strings.TrimSpace(policy.Kind), "targetRecordRead")
}

type queryParentFilterVisibilityPlan struct {
	denyAll                               bool
	allowedIDPhysicalName                 string
	allowedIDs                            []string
	ownerPhysicalName                     string
	ownerUserIDs                          []string
	groupOwnerIDs                         []string
	manualShareUserID                     string
	teamMemberUserID                      string
	sharingRuleOwnerUserIDs               []string
	sharingRuleFromGroupIDs               []string
	ownerVisibilityOwnerPhysicalName      string
	ownerVisibilityVisibilityPhysicalName string
	ownerVisibilityUserID                 string
	ownerVisibilityOwnerValues            []string
	ownerVisibilityPublicValues           []string
	criteriaExpressions                   []string
	criteriaFields                        []models.QueryParentFilterCriteriaField
	targetRecordFilters                   []models.QueryParentFilterTargetRecordVisibility
}

const queryParentFilterTargetRecordVisibilityMaxDepth = 4

func (qs *QueryService) queryParentFilterTargetVisibility(ctx context.Context, schema *models.ObjectMetadata, targetObject string, clause string, scanAll bool, currentUser *models.UserSession) (queryParentFilterVisibilityPlan, error) {
	targetRLS := qs.readRLSResult(ctx, schema, targetObject, currentUser)
	if targetRLS == nil && schema != nil && isQueryParentFilterTargetRecordReadRowReadPolicy(schema.RowReadPolicy()) {
		targetRLS = qs.queryParentFilterBypassTargetRecordRLS(ctx, schema, targetObject)
	}
	return queryParentFilterVisibilityPlanFromRLS(schema, targetRLS, clause, targetObject, scanAll, queryParentFilterTargetRecordVisibilityMaxDepth, make(map[string]bool))
}

func queryParentFilterVisibilityPlanFromRLS(schema *models.ObjectMetadata, targetRLS *domainrls.Result, clause string, targetObject string, scanAll bool, remainingTargetRecordDepth int, visited map[string]bool) (queryParentFilterVisibilityPlan, error) {
	if targetRLS == nil || targetRLS.Skip {
		return queryParentFilterVisibilityPlan{}, nil
	}
	if targetRLS.DenyAll {
		return queryParentFilterVisibilityPlan{denyAll: true}, nil
	}
	visitKey := strings.ToLower(strings.TrimSpace(targetObject))
	if visitKey != "" {
		if visited == nil {
			visited = make(map[string]bool)
		}
		if visited[visitKey] {
			return queryParentFilterVisibilityPlan{}, pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require target-record visibility cycle protection: %s", targetObject))
		}
		visited[visitKey] = true
		defer delete(visited, visitKey)
	}
	allowedIDsVisible := len(targetRLS.AllowedIDs) > 0
	ownerVisible := targetRLS.HasOwnerField && len(targetRLS.OwnerUserIDs) > 0
	groupOwnerVisible := len(targetRLS.GroupOwnerIDs) > 0
	manualShareVisible := targetRLS.IncludeManualShares && strings.TrimSpace(targetRLS.UserID) != ""
	teamMemberVisible := targetRLS.IncludeTeamMembers && strings.TrimSpace(targetRLS.UserID) != ""
	sharingRuleOwnerVisible := len(targetRLS.SharingRuleOwnerIDs) > 0
	sharingRuleFromGroupVisible := len(targetRLS.SharingRuleFromGroupIDs) > 0
	ownerVisibilityVisible := queryParentFilterOwnerVisibilityHasVisibleArm(targetRLS.OwnerVisibility)
	criteriaExpressions, criteriaFields, err := queryParentFilterCriteriaVisibilityPlan(schema, targetRLS, clause, targetObject)
	if err != nil {
		return queryParentFilterVisibilityPlan{}, err
	}
	criteriaVisible := len(criteriaExpressions) > 0
	targetRecordVisible := len(targetRLS.TargetRecordFilters) > 0
	if targetRecordVisible && remainingTargetRecordDepth <= 0 {
		return queryParentFilterVisibilityPlan{}, pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require bounded target-record visibility: %s", targetObject))
	}
	if !queryParentFilterTargetRLSSupported(targetRLS, allowedIDsVisible, ownerVisible, groupOwnerVisible, manualShareVisible, teamMemberVisible, sharingRuleOwnerVisible, sharingRuleFromGroupVisible, ownerVisibilityVisible, criteriaVisible, targetRecordVisible) {
		return queryParentFilterVisibilityPlan{}, pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require supported parent target visibility: %s", targetObject))
	}
	if !queryParentFilterTargetRLSHasVisibleArm(allowedIDsVisible, ownerVisible, groupOwnerVisible, manualShareVisible, teamMemberVisible, sharingRuleOwnerVisible, sharingRuleFromGroupVisible, ownerVisibilityVisible, criteriaVisible, targetRecordVisible) {
		return queryParentFilterVisibilityPlan{denyAll: true}, nil
	}
	allowedIDPhysicalName := ""
	allowedIDs := []string(nil)
	if allowedIDsVisible {
		physicalName, err := queryParentFilterAllowedIDPhysicalName(schema, targetRLS.AllowedIDsColumn, clause, targetObject)
		if err != nil {
			return queryParentFilterVisibilityPlan{}, err
		}
		allowedIDPhysicalName = physicalName
		allowedIDs = append([]string(nil), targetRLS.AllowedIDs...)
	}
	ownerPhysicalName := ""
	ownerUserIDs := []string(nil)
	if ownerVisible || groupOwnerVisible || sharingRuleOwnerVisible || sharingRuleFromGroupVisible {
		ownerField := models.FindField(schema, constants.FieldOwnerID)
		if err := validateQueryParentFilterPhysicalField(ownerField, clause, "parent target owner field", targetObject); err != nil {
			return queryParentFilterVisibilityPlan{}, err
		}
		ownerPhysicalName = queryParentFilterColumnName(ownerField)
	}
	if ownerVisible {
		ownerUserIDs = append([]string(nil), targetRLS.OwnerUserIDs...)
	}
	groupOwnerIDs := []string(nil)
	if groupOwnerVisible {
		groupOwnerIDs = append([]string(nil), targetRLS.GroupOwnerIDs...)
	}
	manualShareUserID := ""
	if manualShareVisible {
		manualShareUserID = strings.TrimSpace(targetRLS.UserID)
	}
	teamMemberUserID := ""
	if teamMemberVisible {
		teamMemberUserID = strings.TrimSpace(targetRLS.UserID)
	}
	sharingRuleOwnerUserIDs := []string(nil)
	if sharingRuleOwnerVisible {
		sharingRuleOwnerUserIDs = append([]string(nil), targetRLS.SharingRuleOwnerIDs...)
	}
	sharingRuleFromGroupIDs := []string(nil)
	if sharingRuleFromGroupVisible {
		sharingRuleFromGroupIDs = append([]string(nil), targetRLS.SharingRuleFromGroupIDs...)
	}
	ownerVisibilityOwnerPhysicalName := ""
	ownerVisibilityVisibilityPhysicalName := ""
	ownerVisibilityUserID := ""
	ownerVisibilityOwnerValues := []string(nil)
	ownerVisibilityPublicValues := []string(nil)
	if ownerVisibilityVisible {
		var err error
		ownerVisibilityOwnerPhysicalName, ownerVisibilityVisibilityPhysicalName, ownerVisibilityUserID, ownerVisibilityOwnerValues, ownerVisibilityPublicValues, err = queryParentFilterOwnerVisibilityPlan(schema, targetRLS.OwnerVisibility, clause, targetObject)
		if err != nil {
			return queryParentFilterVisibilityPlan{}, err
		}
	}
	targetRecordFilters := []models.QueryParentFilterTargetRecordVisibility(nil)
	if targetRecordVisible {
		var err error
		targetRecordFilters, err = queryParentFilterTargetRecordVisibilityPlans(schema, targetRLS.TargetRecordFilters, clause, targetObject, scanAll, remainingTargetRecordDepth-1, visited)
		if err != nil {
			return queryParentFilterVisibilityPlan{}, err
		}
	}
	return queryParentFilterVisibilityPlan{
		allowedIDPhysicalName:                 allowedIDPhysicalName,
		allowedIDs:                            allowedIDs,
		ownerPhysicalName:                     ownerPhysicalName,
		ownerUserIDs:                          ownerUserIDs,
		groupOwnerIDs:                         groupOwnerIDs,
		manualShareUserID:                     manualShareUserID,
		teamMemberUserID:                      teamMemberUserID,
		sharingRuleOwnerUserIDs:               sharingRuleOwnerUserIDs,
		sharingRuleFromGroupIDs:               sharingRuleFromGroupIDs,
		ownerVisibilityOwnerPhysicalName:      ownerVisibilityOwnerPhysicalName,
		ownerVisibilityVisibilityPhysicalName: ownerVisibilityVisibilityPhysicalName,
		ownerVisibilityUserID:                 ownerVisibilityUserID,
		ownerVisibilityOwnerValues:            ownerVisibilityOwnerValues,
		ownerVisibilityPublicValues:           ownerVisibilityPublicValues,
		criteriaExpressions:                   criteriaExpressions,
		criteriaFields:                        criteriaFields,
		targetRecordFilters:                   targetRecordFilters,
	}, nil
}

func (qs *QueryService) queryParentFilterBypassTargetRecordRLS(ctx context.Context, schema *models.ObjectMetadata, targetObject string) *domainrls.Result {
	policy := schema.RowReadPolicy()
	if !isQueryParentFilterTargetRecordReadRowReadPolicy(policy) {
		return nil
	}
	associations, valid := policy.TargetRecordReadAssociations()
	if !valid || qs.metadata == nil {
		return &domainrls.Result{ObjectAPIName: targetObject, DenyAll: true}
	}
	if len(policy.TargetAssociations) == 0 {
		association := associations[0]
		filters := []domainrls.TargetRecordFilter(nil)
		for _, targetSchema := range qs.metadata.GetSchemas(ctx) {
			if !targetRecordReadAllowsSchemaField(schema, association.TargetRecordField, targetSchema.APIName) ||
				!qs.queryParentFilterBypassTargetRecordSchemaAllowed(ctx, schema, targetSchema) {
				continue
			}
			targetObjectAPIName := strings.TrimSpace(targetSchema.APIName)
			filters = append(filters, domainrls.TargetRecordFilter{
				TargetObjectAPIName: targetObjectAPIName,
				TargetObjectField:   strings.TrimSpace(association.TargetObjectField),
				TargetRecordField:   strings.TrimSpace(association.TargetRecordField),
				TargetRLSResult:     &domainrls.Result{Skip: true, ObjectAPIName: targetObjectAPIName},
				TargetSchema:        targetSchema,
			})
		}
		if len(filters) == 0 {
			return &domainrls.Result{ObjectAPIName: targetObject, DenyAll: true}
		}
		return &domainrls.Result{ObjectAPIName: targetObject, TargetRecordFilters: filters}
	}

	filters := []domainrls.TargetRecordFilter(nil)
	for _, association := range associations {
		targetObjectField := association.TargetObjectField
		targetRecordField := association.TargetRecordField
		filter := domainrls.TargetRecordFilter{
			TargetObjectField: targetObjectField,
			TargetRecordField: targetRecordField,
			Alternatives:      make([]domainrls.TargetRecordFilterAlternative, 0),
		}
		for _, targetSchema := range qs.metadata.GetSchemas(ctx) {
			if !targetRecordReadAllowsSchemaField(schema, targetRecordField, targetSchema.APIName) ||
				!qs.queryParentFilterBypassTargetRecordSchemaAllowed(ctx, schema, targetSchema) {
				continue
			}
			targetObjectAPIName := strings.TrimSpace(targetSchema.APIName)
			filter.Alternatives = append(filter.Alternatives, domainrls.TargetRecordFilterAlternative{
				TargetObjectAPIName: targetObjectAPIName,
				TargetRLSResult:     &domainrls.Result{Skip: true, ObjectAPIName: targetObjectAPIName},
				TargetSchema:        targetSchema,
			})
		}
		if len(filter.Alternatives) == 1 {
			alternative := filter.Alternatives[0]
			filter.TargetObjectAPIName = alternative.TargetObjectAPIName
			filter.TargetRLSResult = alternative.TargetRLSResult
			filter.TargetSchema = alternative.TargetSchema
		}
		filters = append(filters, filter)
	}
	if len(filters) == 0 {
		return &domainrls.Result{ObjectAPIName: targetObject, DenyAll: true}
	}
	return &domainrls.Result{ObjectAPIName: targetObject, TargetRecordFilters: filters}
}

func targetRecordReadAllowsSchemaField(sourceSchema *models.ObjectMetadata, targetRecordField, targetObjectAPIName string) bool {
	field := models.FindField(sourceSchema, targetRecordField)
	if field == nil || len(field.ReferenceTo) == 0 {
		return true
	}
	for _, allowed := range field.ReferenceTo {
		if strings.EqualFold(strings.TrimSpace(allowed), strings.TrimSpace(targetObjectAPIName)) {
			return true
		}
	}
	return false
}

func (qs *QueryService) queryParentFilterBypassTargetRecordSchemaAllowed(ctx context.Context, sourceSchema *models.ObjectMetadata, targetSchema *models.ObjectMetadata) bool {
	if sourceSchema == nil || targetSchema == nil || strings.TrimSpace(targetSchema.APIName) == "" {
		return false
	}
	if strings.EqualFold(targetSchema.APIName, sourceSchema.APIName) {
		return false
	}
	if qs.permissions != nil && qs.permissions.IsBlockedObject(ctx, targetSchema.APIName, constants.PermRead) {
		return false
	}
	return queryParentFilterBypassTargetRecordSchemaUnrestricted(targetSchema)
}

func queryParentFilterBypassTargetRecordSchemaUnrestricted(schema *models.ObjectMetadata) bool {
	if schema == nil || schema.UsesDerivedAccessPolicy() || schema.RowReadPolicy() != nil {
		return false
	}
	// L0-D D1: classify via the app-blind translation point instead of branching
	// on constants.SharingModel* directly. Only fully-public (read or read+write)
	// models qualify for the parent-filter bypass.
	p := models.RowVisibilityFor(schema)
	return p.Kind == models.RowVisibilityPublicRead || p.Kind == models.RowVisibilityPublicReadWrite
}

func queryParentFilterTargetRecordVisibilityPlans(schema *models.ObjectMetadata, filters []domainrls.TargetRecordFilter, clause string, targetObject string, includeDeleted bool, remainingTargetRecordDepth int, visited map[string]bool) ([]models.QueryParentFilterTargetRecordVisibility, error) {
	plans := make([]models.QueryParentFilterTargetRecordVisibility, 0, len(filters))
	for _, filter := range filters {
		objectField := models.FindField(schema, strings.TrimSpace(filter.TargetObjectField))
		if err := validateQueryParentFilterPhysicalField(objectField, clause, "parent target-record object field", targetObject); err != nil {
			return nil, err
		}
		recordField := models.FindField(schema, strings.TrimSpace(filter.TargetRecordField))
		if err := validateQueryParentFilterPhysicalField(recordField, clause, "parent target-record record field", targetObject); err != nil {
			return nil, err
		}
		plan := models.QueryParentFilterTargetRecordVisibility{
			ObjectFieldPhysicalName: queryParentFilterColumnName(objectField),
			RecordFieldPhysicalName: queryParentFilterColumnName(recordField),
		}
		if filter.Alternatives != nil {
			plan.Alternatives = make([]models.QueryParentFilterTargetRecordVisibility, 0, len(filter.Alternatives))
		}
		for _, alternative := range filter.TargetRecordAlternatives() {
			alternativePlan, err := queryParentFilterTargetRecordVisibilityAlternativePlan(alternative, plan, clause, targetObject, includeDeleted, remainingTargetRecordDepth, visited)
			if err != nil {
				return nil, err
			}
			plan.Alternatives = append(plan.Alternatives, alternativePlan)
		}
		if filter.Alternatives == nil && len(plan.Alternatives) == 1 {
			plan = plan.Alternatives[0]
		}
		plans = append(plans, plan)
	}
	return plans, nil
}

func queryParentFilterTargetRecordVisibilityAlternativePlan(alternative domainrls.TargetRecordFilterAlternative, association models.QueryParentFilterTargetRecordVisibility, clause string, targetObject string, includeDeleted bool, remainingTargetRecordDepth int, visited map[string]bool) (models.QueryParentFilterTargetRecordVisibility, error) {
	if alternative.TargetRLSResult == nil {
		return models.QueryParentFilterTargetRecordVisibility{}, pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require target-record visibility: %s", targetObject))
	}
	targetSchema := alternative.TargetSchema
	if targetSchema == nil {
		return models.QueryParentFilterTargetRecordVisibility{}, pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require target-record metadata: %s", targetObject))
	}
	targetTable, err := targetSchema.ResolveReadSourceTable()
	if err != nil {
		return models.QueryParentFilterTargetRecordVisibility{}, pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require a physical target-record table: %v", err))
	}
	targetIDField := models.FindField(targetSchema, constants.FieldID)
	if err := validateQueryParentFilterPhysicalField(targetIDField, clause, "target-record ID field", strings.TrimSpace(alternative.TargetObjectAPIName)); err != nil {
		return models.QueryParentFilterTargetRecordVisibility{}, err
	}
	plan := models.QueryParentFilterTargetRecordVisibility{
		ReferencedObjectAPIName:  strings.TrimSpace(alternative.TargetObjectAPIName),
		ObjectFieldPhysicalName:  association.ObjectFieldPhysicalName,
		RecordFieldPhysicalName:  association.RecordFieldPhysicalName,
		ReferencedTable:          targetTable,
		ReferencedIDPhysicalName: queryParentFilterColumnName(targetIDField),
		ReferencedIncludeDeleted: includeDeleted,
	}
	if plan.ReferencedObjectAPIName == "" {
		return models.QueryParentFilterTargetRecordVisibility{}, pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require a target-record object: %s", targetObject))
	}
	if isDeletedField := models.FindField(targetSchema, constants.FieldIsDeleted); isDeletedField != nil {
		if err := validateQueryParentFilterPhysicalField(isDeletedField, clause, "target-record is-deleted field", plan.ReferencedObjectAPIName); err != nil {
			return models.QueryParentFilterTargetRecordVisibility{}, err
		}
		plan.ReferencedHasIsDeleted = true
		plan.ReferencedIsDeletedPhysicalName = queryParentFilterColumnName(isDeletedField)
	}
	visibility, err := queryParentFilterVisibilityPlanFromRLS(targetSchema, alternative.TargetRLSResult, clause, plan.ReferencedObjectAPIName, includeDeleted, remainingTargetRecordDepth, visited)
	if err != nil {
		return models.QueryParentFilterTargetRecordVisibility{}, err
	}
	plan.ReferencedVisibilityDenyAll = visibility.denyAll
	plan.ReferencedVisibilityAllowedIDPhysicalName = visibility.allowedIDPhysicalName
	plan.ReferencedVisibilityAllowedIDs = visibility.allowedIDs
	plan.ReferencedVisibilityOwnerPhysicalName = visibility.ownerPhysicalName
	plan.ReferencedVisibilityOwnerUserIDs = visibility.ownerUserIDs
	plan.ReferencedVisibilityGroupOwnerIDs = visibility.groupOwnerIDs
	plan.ReferencedVisibilityManualShareUserID = visibility.manualShareUserID
	plan.ReferencedVisibilityTeamMemberUserID = visibility.teamMemberUserID
	plan.ReferencedVisibilitySharingRuleOwnerUserIDs = visibility.sharingRuleOwnerUserIDs
	plan.ReferencedVisibilitySharingRuleFromGroupIDs = visibility.sharingRuleFromGroupIDs
	plan.ReferencedVisibilityOwnerVisibilityOwnerPhysicalName = visibility.ownerVisibilityOwnerPhysicalName
	plan.ReferencedVisibilityOwnerVisibilityVisibilityPhysicalName = visibility.ownerVisibilityVisibilityPhysicalName
	plan.ReferencedVisibilityOwnerVisibilityUserID = visibility.ownerVisibilityUserID
	plan.ReferencedVisibilityOwnerVisibilityOwnerValues = visibility.ownerVisibilityOwnerValues
	plan.ReferencedVisibilityOwnerVisibilityPublicValues = visibility.ownerVisibilityPublicValues
	plan.ReferencedVisibilityCriteriaExpressions = visibility.criteriaExpressions
	plan.ReferencedVisibilityCriteriaFields = visibility.criteriaFields
	plan.ReferencedVisibilityTargetRecordFilters = visibility.targetRecordFilters
	return plan, nil
}

func queryParentFilterOwnerVisibilityHasVisibleArm(filter *domainrls.OwnerVisibilityFilter) bool {
	if filter == nil {
		return false
	}
	return len(queryParentFilterCompactStrings(filter.PublicVisibleValues)) > 0 ||
		len(queryParentFilterCompactStrings(filter.OwnerVisibleValues)) > 0
}

func queryParentFilterOwnerVisibilityPlan(schema *models.ObjectMetadata, filter *domainrls.OwnerVisibilityFilter, clause string, targetObject string) (string, string, string, []string, []string, error) {
	publicValues := queryParentFilterCompactStrings(filter.PublicVisibleValues)
	ownerValues := queryParentFilterCompactStrings(filter.OwnerVisibleValues)
	visibilityField := models.FindField(schema, strings.TrimSpace(filter.VisibilityField))
	if err := validateQueryParentFilterPhysicalField(visibilityField, clause, "parent target owner-visibility field", targetObject); err != nil {
		return "", "", "", nil, nil, err
	}
	visibilityPhysicalName := queryParentFilterColumnName(visibilityField)

	ownerPhysicalName := ""
	userID := ""
	if len(ownerValues) > 0 {
		ownerField := models.FindField(schema, strings.TrimSpace(filter.OwnerField))
		if err := validateQueryParentFilterPhysicalField(ownerField, clause, "parent target owner-visibility owner field", targetObject); err != nil {
			return "", "", "", nil, nil, err
		}
		userID = strings.TrimSpace(filter.UserID)
		if userID == "" {
			return "", "", "", nil, nil, pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require a supported parent target owner-visibility user: %s", targetObject))
		}
		ownerPhysicalName = queryParentFilterColumnName(ownerField)
	}
	return ownerPhysicalName, visibilityPhysicalName, userID, ownerValues, publicValues, nil
}

func queryParentFilterCompactStrings(values []string) []string {
	out := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value != "" {
			out = append(out, value)
		}
	}
	return out
}

func queryParentFilterAllowedIDPhysicalName(schema *models.ObjectMetadata, allowedIDsColumn string, clause string, targetObject string) (string, error) {
	column := strings.TrimSpace(allowedIDsColumn)
	if column == "" {
		column = constants.FieldID
	}
	field := models.FindField(schema, column)
	if err := validateQueryParentFilterPhysicalField(field, clause, "parent target allowed-id field", targetObject); err != nil {
		return "", err
	}
	return queryParentFilterColumnName(field), nil
}

func queryParentFilterCriteriaVisibilityPlan(schema *models.ObjectMetadata, result *domainrls.Result, clause string, targetObject string) ([]string, []models.QueryParentFilterCriteriaField, error) {
	if result == nil || len(result.CriteriaExpressions) == 0 {
		if result != nil && len(result.CriteriaSQLFragments) > 0 {
			return nil, nil, pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require criteria source expressions for parent target visibility: %s", targetObject))
		}
		return nil, nil, nil
	}
	if len(result.CriteriaSQLFragments) > 0 && len(result.CriteriaSQLFragments) != len(result.CriteriaExpressions) {
		return nil, nil, pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require matched criteria source expressions for parent target visibility: %s", targetObject))
	}
	expressions := make([]string, 0, len(result.CriteriaExpressions))
	fieldsByName := make(map[string]models.QueryParentFilterCriteriaField)
	for _, expr := range result.CriteriaExpressions {
		expr = strings.TrimSpace(expr)
		if expr == "" {
			continue
		}
		if err := queryParentFilterValidateCriteriaExpression(schema, expr, fieldsByName, clause, targetObject); err != nil {
			return nil, nil, err
		}
		expressions = append(expressions, expr)
	}
	if len(expressions) == 0 {
		return nil, nil, nil
	}
	fields := make([]models.QueryParentFilterCriteriaField, 0, len(fieldsByName))
	for _, field := range fieldsByName {
		fields = append(fields, field)
	}
	return expressions, fields, nil
}

func queryParentFilterValidateCriteriaExpression(schema *models.ObjectMetadata, expr string, fieldsByName map[string]models.QueryParentFilterCriteriaField, clause string, targetObject string) error {
	var invalid string
	_, _, err := formula.ToSQLWithMapper(expr, func(identifier string) string {
		trimmed := strings.Trim(strings.TrimSpace(identifier), "`")
		if strings.Contains(trimmed, ".") {
			invalid = trimmed
			return trimmed
		}
		field := models.FindField(schema, trimmed)
		if err := validateQueryParentFilterPhysicalField(field, clause, "parent target criteria field", trimmed); err != nil {
			invalid = trimmed
			return trimmed
		}
		fieldsByName[strings.ToLower(field.APIName)] = models.QueryParentFilterCriteriaField{
			APIName:      field.APIName,
			PhysicalName: queryParentFilterColumnName(field),
		}
		return queryParentFilterColumnName(field)
	})
	if err != nil {
		return pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require supported parent target criteria visibility: %s", targetObject))
	}
	if invalid != "" {
		return pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require a physical parent target criteria field: %s", invalid))
	}
	return nil
}

func queryParentFilterTargetRLSSupported(result *domainrls.Result, allowedIDsVisible bool, ownerVisible bool, groupOwnerVisible bool, manualShareVisible bool, teamMemberVisible bool, sharingRuleOwnerVisible bool, sharingRuleFromGroupVisible bool, ownerVisibilityVisible bool, criteriaVisible bool, targetRecordVisible bool) bool {
	if result == nil || result.Skip {
		return true
	}
	if result.ParentFilter != nil ||
		(len(result.TargetRecordFilters) > 0 && !targetRecordVisible) {
		return false
	}
	if len(result.CriteriaSQLFragments) > 0 && len(result.CriteriaSQLFragments) != len(result.CriteriaExpressions) {
		return false
	}
	if len(result.CriteriaExpressions) > 0 && !criteriaVisible {
		return false
	}
	if result.IncludeManualShares && !manualShareVisible {
		return false
	}
	if result.IncludeTeamMembers && !teamMemberVisible {
		return false
	}
	if result.OwnerVisibility != nil && !ownerVisibilityVisible {
		return false
	}
	return true
}

func queryParentFilterTargetRLSHasVisibleArm(allowedIDsVisible bool, ownerVisible bool, groupOwnerVisible bool, manualShareVisible bool, teamMemberVisible bool, sharingRuleOwnerVisible bool, sharingRuleFromGroupVisible bool, ownerVisibilityVisible bool, criteriaVisible bool, targetRecordVisible bool) bool {
	return allowedIDsVisible || ownerVisible || groupOwnerVisible || manualShareVisible || teamMemberVisible || sharingRuleOwnerVisible || sharingRuleFromGroupVisible || ownerVisibilityVisible || criteriaVisible || targetRecordVisible
}

func validateQueryParentFilterPhysicalField(field *models.FieldMetadata, clause string, kind string, label string) error {
	if field == nil || field.IsVirtual || field.IsCompound || !formulastorage.FieldRequiresPhysicalColumn(field) {
		return pkgErrors.NewValidationError(clause, fmt.Sprintf("relationship field filters require a physical %s: %s", kind, label))
	}
	return nil
}

func queryParentFilterColumnName(field *models.FieldMetadata) string {
	if field == nil {
		return ""
	}
	column := analyticsPhysicalColumnName(field.APIName, field.PhysicalName)
	if strings.TrimSpace(column) == "" {
		return field.APIName
	}
	return column
}

func validateQueryParentFilterExpressionShape(expr string, resolvedPaths map[string]*queryParentFilterPath, clause string) ([]*queryParentFilterPath, error) {
	root, err := formula.ParseNormalizedExpressionAST(expr)
	if err != nil {
		return nil, err
	}
	validator := &queryParentFilterExpressionValidator{
		resolvedPaths: resolvedPaths,
		clause:        clause,
		seen:          make(map[string]struct{}),
	}
	if err := validator.validate(root); err != nil {
		return nil, err
	}
	return validator.paths, nil
}

type queryParentFilterExpressionValidator struct {
	resolvedPaths map[string]*queryParentFilterPath
	clause        string
	seen          map[string]struct{}
	paths         []*queryParentFilterPath
}

func (v *queryParentFilterExpressionValidator) validate(node ast.Node) error {
	if v == nil || node == nil || !v.containsParentPath(node) {
		return nil
	}
	if path, ok := v.directParentBlankPredicate(node); ok {
		v.addPath(path)
		return nil
	}
	if path, ok := v.directParentNegatedBlankPredicate(node); ok {
		v.addPath(path)
		return nil
	}
	if path, ok := v.directParentListPredicate(node); ok {
		v.addPath(path)
		return nil
	}
	if path, ok := v.directParentNegatedListPredicate(node); ok {
		v.addPath(path)
		return nil
	}
	if path, ok := v.directParentStringPredicate(node); ok {
		v.addPath(path)
		return nil
	}
	if path, ok := v.directParentNegatedStringPredicate(node); ok {
		v.addPath(path)
		return nil
	}
	if path, ok := v.directParentBlankCoalescingComparison(node); ok {
		v.addPath(path)
		return nil
	}
	binary, ok := node.(*ast.BinaryNode)
	if !ok {
		return unsupportedQueryParentFilterShapeError(v.clause)
	}
	op := strings.ToLower(strings.TrimSpace(binary.Operator))
	if op == "and" || op == "or" || op == "&&" || op == "||" {
		if err := v.validate(binary.Left); err != nil {
			return err
		}
		return v.validate(binary.Right)
	}
	if !isQueryParentFilterComparisonOperator(op) {
		return unsupportedQueryParentFilterShapeError(v.clause)
	}
	leftPath, leftIsParent := v.directParentPath(binary.Left)
	rightPath, rightIsParent := v.directParentPath(binary.Right)
	if leftIsParent == rightIsParent {
		return unsupportedQueryParentFilterShapeError(v.clause)
	}
	literalSide := binary.Left
	path := rightPath
	if leftIsParent {
		literalSide = binary.Right
		path = leftPath
	}
	if !isQueryParentFilterComparisonLiteral(literalSide) || v.containsParentPath(literalSide) {
		return unsupportedQueryParentFilterShapeError(v.clause)
	}
	v.addPath(path)
	return nil
}

func (v *queryParentFilterExpressionValidator) containsParentPath(node ast.Node) bool {
	if v == nil || node == nil {
		return false
	}
	if _, ok := v.directParentPath(node); ok {
		return true
	}
	switch n := node.(type) {
	case *ast.UnaryNode:
		return v.containsParentPath(n.Node)
	case *ast.BinaryNode:
		return v.containsParentPath(n.Left) || v.containsParentPath(n.Right)
	case *ast.CallNode:
		if v.containsParentPath(n.Callee) {
			return true
		}
		for i := range n.Arguments {
			if v.containsParentPath(n.Arguments[i]) {
				return true
			}
		}
	case *ast.BuiltinNode:
		for i := range n.Arguments {
			if v.containsParentPath(n.Arguments[i]) {
				return true
			}
		}
	case *ast.ConditionalNode:
		return v.containsParentPath(n.Cond) || v.containsParentPath(n.Exp1) || v.containsParentPath(n.Exp2)
	case *ast.ChainNode:
		return v.containsParentPath(n.Node)
	case *ast.PredicateNode:
		return v.containsParentPath(n.Node)
	case *ast.ArrayNode:
		for i := range n.Nodes {
			if v.containsParentPath(n.Nodes[i]) {
				return true
			}
		}
	case *ast.MapNode:
		for i := range n.Pairs {
			if v.containsParentPath(n.Pairs[i]) {
				return true
			}
		}
	case *ast.PairNode:
		return v.containsParentPath(n.Key) || v.containsParentPath(n.Value)
	}
	return false
}

func (v *queryParentFilterExpressionValidator) directParentPath(node ast.Node) (*queryParentFilterPath, bool) {
	path, ok := queryParentFilterASTPath(node)
	if !ok {
		return nil, false
	}
	resolved, ok := v.resolvedPaths[normalizeQueryParentFilterPath(path)]
	return resolved, ok
}

func (v *queryParentFilterExpressionValidator) directParentBlankPredicate(node ast.Node) (*queryParentFilterPath, bool) {
	name, args, ok := queryParentFilterCall(node)
	if !ok || !isQueryParentFilterBlankPredicateFunction(name) || len(args) != 1 {
		return nil, false
	}
	return v.directParentPath(args[0])
}

func (v *queryParentFilterExpressionValidator) directParentNegatedBlankPredicate(node ast.Node) (*queryParentFilterPath, bool) {
	inner, ok := queryParentFilterNegatedNode(node)
	if !ok {
		return nil, false
	}
	name, args, ok := queryParentFilterCall(inner)
	if !ok || !isQueryParentFilterBlankPredicateFunction(name) || len(args) != 1 {
		return nil, false
	}
	return v.directParentPath(args[0])
}

func (v *queryParentFilterExpressionValidator) directParentListPredicate(node ast.Node) (*queryParentFilterPath, bool) {
	binary, ok := node.(*ast.BinaryNode)
	if !ok || strings.ToLower(strings.TrimSpace(binary.Operator)) != "in" {
		return nil, false
	}
	path, ok := v.directParentPath(binary.Left)
	if !ok || v.containsParentPath(binary.Right) || !isQueryParentFilterLiteralList(binary.Right) {
		return nil, false
	}
	return path, true
}

func (v *queryParentFilterExpressionValidator) directParentNegatedListPredicate(node ast.Node) (*queryParentFilterPath, bool) {
	inner, ok := queryParentFilterNegatedNode(node)
	if !ok {
		return nil, false
	}
	return v.directParentListPredicate(inner)
}

func (v *queryParentFilterExpressionValidator) directParentStringPredicate(node ast.Node) (*queryParentFilterPath, bool) {
	if name, args, ok := queryParentFilterCall(node); ok && strings.EqualFold(name, queryspec.InternalLikeExpressionFunction) {
		if len(args) != 2 {
			return nil, false
		}
		path, ok := v.directParentPath(args[0])
		if !ok {
			return nil, false
		}
		if _, ok := args[1].(*ast.StringNode); !ok {
			return nil, false
		}
		return path, true
	}
	binary, ok := node.(*ast.BinaryNode)
	if !ok || !isQueryParentFilterStringPredicateOperator(binary.Operator) {
		return nil, false
	}
	path, ok := v.directParentPath(binary.Left)
	if !ok || v.containsParentPath(binary.Right) {
		return nil, false
	}
	if _, ok := binary.Right.(*ast.StringNode); !ok {
		return nil, false
	}
	return path, true
}

func (v *queryParentFilterExpressionValidator) directParentNegatedStringPredicate(node ast.Node) (*queryParentFilterPath, bool) {
	inner, ok := queryParentFilterNegatedNode(node)
	if !ok {
		return nil, false
	}
	return v.directParentStringPredicate(inner)
}

func (v *queryParentFilterExpressionValidator) directParentBlankCoalescingComparison(node ast.Node) (*queryParentFilterPath, bool) {
	binary, ok := node.(*ast.BinaryNode)
	if !ok || !isQueryParentFilterComparisonOperator(strings.ToLower(strings.TrimSpace(binary.Operator))) {
		return nil, false
	}
	leftPath, leftOK := v.directParentBlankCoalescingFunction(binary.Left)
	rightPath, rightOK := v.directParentBlankCoalescingFunction(binary.Right)
	if leftOK == rightOK {
		return nil, false
	}
	literalSide := binary.Left
	path := rightPath
	if leftOK {
		literalSide = binary.Right
		path = leftPath
	}
	if !isQueryParentFilterComparisonLiteral(literalSide) || isQueryParentFilterNilLiteral(literalSide) || v.containsParentPath(literalSide) {
		return nil, false
	}
	return path, true
}

func (v *queryParentFilterExpressionValidator) directParentBlankCoalescingFunction(node ast.Node) (*queryParentFilterPath, bool) {
	name, args, ok := queryParentFilterCall(node)
	if !ok || !isQueryParentFilterDirectBlankCoalescingFunction(name, args) {
		return nil, false
	}
	path, ok := v.directParentPath(args[0])
	return path, ok
}

func isQueryParentFilterStringPredicateOperator(op string) bool {
	switch strings.ToLower(strings.TrimSpace(op)) {
	case "contains", "startswith", "endswith":
		return true
	default:
		return false
	}
}

func isQueryParentFilterBlankCoalescingFunction(name string) bool {
	switch strings.ToUpper(strings.TrimSpace(name)) {
	case "BLANKVALUE", "NULLVALUE":
		return true
	default:
		return false
	}
}

func isQueryParentFilterDirectBlankCoalescingFunction(name string, args []ast.Node) bool {
	if !isQueryParentFilterBlankCoalescingFunction(name) || len(args) != 2 {
		return false
	}
	if _, ok := queryParentFilterASTPath(args[0]); !ok {
		return false
	}
	if queryParentFilterArgsContainParentPath(args[1:]) {
		return false
	}
	return isQueryParentFilterComparisonLiteral(args[1])
}

func queryParentFilterArgsContainParentPath(args []ast.Node) bool {
	for i := range args {
		if queryParentFilterNodeContainsParentPath(args[i]) {
			return true
		}
	}
	return false
}

func queryParentFilterNodeContainsParentPath(node ast.Node) bool {
	if node == nil {
		return false
	}
	if _, ok := queryParentFilterASTPath(node); ok {
		return true
	}
	switch n := node.(type) {
	case *ast.UnaryNode:
		return queryParentFilterNodeContainsParentPath(n.Node)
	case *ast.BinaryNode:
		return queryParentFilterNodeContainsParentPath(n.Left) || queryParentFilterNodeContainsParentPath(n.Right)
	case *ast.CallNode:
		if queryParentFilterNodeContainsParentPath(n.Callee) {
			return true
		}
		return queryParentFilterArgsContainParentPath(n.Arguments)
	case *ast.BuiltinNode:
		return queryParentFilterArgsContainParentPath(n.Arguments)
	case *ast.ConditionalNode:
		return queryParentFilterNodeContainsParentPath(n.Cond) ||
			queryParentFilterNodeContainsParentPath(n.Exp1) ||
			queryParentFilterNodeContainsParentPath(n.Exp2)
	case *ast.ChainNode:
		return queryParentFilterNodeContainsParentPath(n.Node)
	case *ast.PredicateNode:
		return queryParentFilterNodeContainsParentPath(n.Node)
	case *ast.ArrayNode:
		return queryParentFilterArgsContainParentPath(n.Nodes)
	case *ast.MapNode:
		for i := range n.Pairs {
			if queryParentFilterNodeContainsParentPath(n.Pairs[i]) {
				return true
			}
		}
	case *ast.PairNode:
		return queryParentFilterNodeContainsParentPath(n.Key) || queryParentFilterNodeContainsParentPath(n.Value)
	}
	return false
}

func queryParentFilterBlankCoalescingSQL(ctx expression.SQLFunctionCallContext) (expression.SQLFragment, error) {
	if len(ctx.Args) != 2 {
		return expression.SQLFragment{}, pkgErrors.NewValidationError("FilterExpr", fmt.Sprintf("%s requires 2 arguments", ctx.Name))
	}
	field, err := ctx.CompileArg(ctx.Args[0])
	if err != nil {
		return expression.SQLFragment{}, err
	}
	fallback, err := ctx.CompileArg(ctx.Args[1])
	if err != nil {
		return expression.SQLFragment{}, err
	}
	args := append([]interface{}(nil), field.Args...)
	args = append(args, fallback.Args...)
	return expression.SQLFragment{
		SQL:  fmt.Sprintf("COALESCE(NULLIF(CAST(%s AS CHAR), ''), %s)", field.SQL, fallback.SQL),
		Args: args,
	}, nil
}

func (v *queryParentFilterExpressionValidator) addPath(path *queryParentFilterPath) {
	if path == nil {
		return
	}
	key := normalizeQueryParentFilterPath(path.rawPath)
	if _, ok := v.seen[key]; ok {
		return
	}
	v.seen[key] = struct{}{}
	v.paths = append(v.paths, path)
}

func isQueryParentFilterComparisonOperator(op string) bool {
	switch op {
	case "==", "!=", ">", ">=", "<", "<=":
		return true
	default:
		return false
	}
}

func isQueryParentFilterComparisonLiteral(node ast.Node) bool {
	switch node.(type) {
	case *ast.StringNode, *ast.IntegerNode, *ast.FloatNode, *ast.BoolNode, *ast.NilNode:
		return true
	default:
		return false
	}
}

func isQueryParentFilterNilLiteral(node ast.Node) bool {
	switch n := node.(type) {
	case *ast.NilNode:
		return true
	case *ast.IdentifierNode:
		value := strings.ToLower(strings.TrimSpace(n.Value))
		return value == "nil" || value == "null"
	default:
		return false
	}
}

func isQueryParentFilterLiteralList(node ast.Node) bool {
	array, ok := node.(*ast.ArrayNode)
	if !ok || len(array.Nodes) == 0 {
		return false
	}
	for i := range array.Nodes {
		if !isQueryParentFilterListLiteral(array.Nodes[i]) {
			return false
		}
	}
	return true
}

func isQueryParentFilterListLiteral(node ast.Node) bool {
	switch node.(type) {
	case *ast.StringNode, *ast.IntegerNode, *ast.FloatNode, *ast.BoolNode:
		return true
	default:
		return false
	}
}

func isQueryParentFilterUnaryBlankFunction(name string, args []ast.Node) bool {
	normalized := strings.ToUpper(strings.TrimSpace(name))
	if normalized == "NOT" {
		return isQueryParentFilterDirectNamedBlankPredicate(args, "ISBLANK") ||
			isQueryParentFilterDirectNamedBlankPredicate(args, "ISNULL") ||
			isQueryParentFilterDirectListPredicate(args) ||
			isQueryParentFilterDirectStringPredicate(args)
	}
	if !isQueryParentFilterBlankPredicateFunction(normalized) || len(args) != 1 {
		return false
	}
	_, ok := queryParentFilterASTPath(args[0])
	return ok
}

func isQueryParentFilterDirectNamedBlankPredicate(args []ast.Node, name string) bool {
	if len(args) != 1 {
		return false
	}
	innerName, innerArgs, ok := queryParentFilterCall(args[0])
	if !ok || !strings.EqualFold(strings.TrimSpace(innerName), name) || len(innerArgs) != 1 {
		return false
	}
	_, ok = queryParentFilterASTPath(innerArgs[0])
	return ok
}

func isQueryParentFilterDirectListPredicate(args []ast.Node) bool {
	if len(args) != 1 {
		return false
	}
	binary, ok := args[0].(*ast.BinaryNode)
	if !ok || strings.ToLower(strings.TrimSpace(binary.Operator)) != "in" {
		return false
	}
	if _, ok := queryParentFilterASTPath(binary.Left); !ok {
		return false
	}
	return isQueryParentFilterLiteralList(binary.Right)
}

func isQueryParentFilterDirectStringPredicate(args []ast.Node) bool {
	if len(args) != 1 {
		return false
	}
	if name, callArgs, ok := queryParentFilterCall(args[0]); ok && strings.EqualFold(name, queryspec.InternalLikeExpressionFunction) {
		if len(callArgs) != 2 {
			return false
		}
		if _, ok := queryParentFilterASTPath(callArgs[0]); !ok {
			return false
		}
		_, ok := callArgs[1].(*ast.StringNode)
		return ok
	}
	binary, ok := args[0].(*ast.BinaryNode)
	if !ok || !isQueryParentFilterStringPredicateOperator(binary.Operator) {
		return false
	}
	if _, ok := queryParentFilterASTPath(binary.Left); !ok {
		return false
	}
	_, ok = binary.Right.(*ast.StringNode)
	return ok
}

func isQueryParentFilterBlankPredicateFunction(name string) bool {
	switch strings.ToUpper(strings.TrimSpace(name)) {
	case "ISBLANK", "ISNULL":
		return true
	default:
		return false
	}
}

func queryParentFilterNegatedNode(node ast.Node) (ast.Node, bool) {
	switch n := node.(type) {
	case *ast.UnaryNode:
		op := strings.ToLower(strings.TrimSpace(n.Operator))
		if op == "!" || op == "not" {
			return n.Node, true
		}
		return nil, false
	default:
		name, args, ok := queryParentFilterCall(node)
		if !ok || !strings.EqualFold(strings.TrimSpace(name), "NOT") || len(args) != 1 {
			return nil, false
		}
		return args[0], true
	}
}

func queryParentFilterASTPath(node ast.Node) (string, bool) {
	root, segments := queryParentFilterASTMemberChain(node)
	if root == "" || len(segments) == 0 {
		return "", false
	}
	return root + "." + strings.Join(segments, "."), true
}

func queryParentFilterCall(node ast.Node) (string, []ast.Node, bool) {
	switch n := node.(type) {
	case *ast.CallNode:
		name, ok := queryParentFilterCallName(n.Callee)
		return name, n.Arguments, ok
	case *ast.BuiltinNode:
		return n.Name, n.Arguments, true
	default:
		return "", nil, false
	}
}

func queryParentFilterCallName(node ast.Node) (string, bool) {
	if id, ok := node.(*ast.IdentifierNode); ok {
		return id.Value, true
	}
	return "", false
}

func queryParentFilterASTMemberChain(node ast.Node) (string, []string) {
	member, ok := node.(*ast.MemberNode)
	if !ok {
		if id, ok := node.(*ast.IdentifierNode); ok {
			return id.Value, nil
		}
		return "", nil
	}
	root, segments := queryParentFilterASTMemberChain(member.Node)
	switch prop := member.Property.(type) {
	case *ast.IdentifierNode:
		segments = append(segments, prop.Value)
	case *ast.StringNode:
		segments = append(segments, prop.Value)
	}
	return root, segments
}

func normalizeQueryParentFilterPath(path string) string {
	return strings.ToLower(strings.Trim(strings.TrimSpace(path), "`"))
}

func unsupportedQueryParentFilterShapeError(clause string) error {
	return pkgErrors.NewValidationError(clause, fmt.Sprintf("unsupported relationship field filter shape in %s; this phase accepts direct parent-field literal comparisons combined with AND/OR", clause))
}

func (qs *QueryService) validateAnalyticsReadClauses(ctx context.Context, objectName string, schema *models.ObjectMetadata, q models.AnalyticsQuery, currentUser *models.UserSession) ([]models.QueryParentFilterPath, error) {
	var parentFilterPaths []models.QueryParentFilterPath
	filterPaths, err := qs.validateQueryFormulaReadClause(ctx, objectName, schema, q.FilterExpr, "FilterExpr", q.ScanAll, currentUser)
	if err != nil {
		return nil, err
	}
	for _, plan := range filterPaths {
		parentFilterPaths = appendQueryParentFilterPlan(parentFilterPaths, plan)
	}
	for _, criterion := range q.Criteria {
		plan, ok, err := qs.validateQueryStructuredReadClause(ctx, objectName, schema, criterion.Field, "Criteria", q.ScanAll, currentUser)
		if err != nil {
			return nil, err
		}
		if ok {
			parentFilterPaths = appendQueryParentFilterPlan(parentFilterPaths, plan)
		}
	}
	if q.GroupBy != nil {
		plan, ok, err := qs.validateQueryStructuredReadClause(ctx, objectName, schema, *q.GroupBy, "GroupBy", q.ScanAll, currentUser)
		if err != nil {
			return nil, err
		}
		if ok {
			parentFilterPaths = appendQueryParentFilterPlan(parentFilterPaths, plan)
		}
	}
	for _, field := range fieldsFromHavingClause(q.Having) {
		plan, ok, err := qs.validateQueryStructuredReadClause(ctx, objectName, schema, field, "Having", q.ScanAll, currentUser)
		if err != nil {
			return nil, err
		}
		if ok {
			parentFilterPaths = appendQueryParentFilterPlan(parentFilterPaths, plan)
		}
	}
	return parentFilterPaths, nil
}

func (qs *QueryService) validateFormulaReadClause(ctx context.Context, objectName string, schema *models.ObjectMetadata, expr string, clause string, currentUser *models.UserSession) error {
	if strings.TrimSpace(expr) == "" {
		return nil
	}
	var denied error
	_, _, err := formula.ToSQLWithMapper(expr, func(identifier string) string {
		if denied != nil {
			return identifier
		}
		denied = qs.validateClauseFieldReadable(ctx, objectName, schema, identifier, clause, currentUser)
		return identifier
	})
	if err != nil {
		return err
	}
	return denied
}

func (qs *QueryService) validateClauseFieldReadable(ctx context.Context, objectName string, schema *models.ObjectMetadata, rawField string, clause string, currentUser *models.UserSession) error {
	fieldName := clauseFieldName(schema, rawField)
	if fieldName == "" {
		return nil
	}
	field := models.FindField(schema, fieldName)
	if field == nil {
		return nil
	}

	bypassFieldVisibility := scopedReadBypassesFieldVisibility(ctx, objectName)
	if bypassFieldVisibility {
		if qs.fieldReadBlockedWithSchema(ctx, objectName, schema, *field) {
			return pkgErrors.NewPermissionError("read field "+field.APIName+" in "+clause, objectName)
		}
		return nil
	}
	visible, err := qs.checkFieldVisibilityWithSchema(ctx, objectName, schema, field, currentUser)
	if err != nil {
		return err
	}
	if !visible {
		return pkgErrors.NewPermissionError("read field "+field.APIName+" in "+clause, objectName)
	}
	return nil
}

func clauseFieldName(schema *models.ObjectMetadata, rawField string) string {
	if schema == nil {
		return ""
	}
	field := strings.Trim(strings.TrimSpace(rawField), "`")
	if field == "" {
		return ""
	}
	if strings.Contains(field, ".") {
		parts := strings.Split(field, ".")
		last := strings.Trim(strings.TrimSpace(parts[len(parts)-1]), "`")
		if models.FindField(schema, last) != nil {
			return last
		}
		if len(parts) == 2 && strings.EqualFold(strings.Trim(parts[0], "`"), schema.APIName) {
			return strings.Trim(parts[1], "`")
		}
		return ""
	}
	return field
}

func fieldsFromHavingClause(having string) []string {
	stripped := stripStringLiterals(having)
	seen := map[string]struct{}{}
	var fields []string
	for i := 0; i < len(stripped); {
		r := rune(stripped[i])
		if !isIdentifierStart(r) {
			i++
			continue
		}
		start := i
		i++
		for i < len(stripped) {
			r = rune(stripped[i])
			if !isIdentifierPart(r) && r != '.' {
				break
			}
			i++
		}
		token := strings.TrimSpace(stripped[start:i])
		if token == "" || isClauseKeyword(token) || nextNonSpace(stripped, i) == '(' {
			continue
		}
		if _, ok := seen[strings.ToLower(token)]; ok {
			continue
		}
		seen[strings.ToLower(token)] = struct{}{}
		fields = append(fields, token)
	}
	return fields
}

func stripStringLiterals(s string) string {
	var b strings.Builder
	var quote rune
	escaped := false
	for _, r := range s {
		if quote != 0 {
			if escaped {
				escaped = false
				b.WriteRune(' ')
				continue
			}
			if r == '\\' {
				escaped = true
				b.WriteRune(' ')
				continue
			}
			if r == quote {
				quote = 0
			}
			b.WriteRune(' ')
			continue
		}
		if r == '\'' || r == '"' {
			quote = r
			b.WriteRune(' ')
			continue
		}
		b.WriteRune(r)
	}
	return b.String()
}

func isIdentifierStart(r rune) bool {
	return r == '_' || unicode.IsLetter(r)
}

func isIdentifierPart(r rune) bool {
	return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
}

func nextNonSpace(s string, start int) rune {
	for _, r := range s[start:] {
		if !unicode.IsSpace(r) {
			return r
		}
	}
	return 0
}

func isClauseKeyword(token string) bool {
	switch strings.ToUpper(token) {
	case "AND", "OR", "NOT", "NULL", "NIL", "TRUE", "FALSE", "IS", "IN", "LIKE", "COUNT", "SUM", "AVG", "MIN", "MAX", "COUNT_DISTINCT", "DISTINCT":
		return true
	}
	if strings.HasPrefix(strings.ToLower(token), "expr") {
		var n int
		_, err := fmt.Sscanf(strings.ToLower(token), "expr%d", &n)
		return err == nil
	}
	return false
}
