package querybuilder

import (
	"context"
	"fmt"
	"log/slog"
	"slices"
	"strconv"
	"strings"

	"github.com/SigNoz/signoz/pkg/errors"
	grammar "github.com/SigNoz/signoz/pkg/parser/filterquery/grammar"
	qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
	"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
	"github.com/antlr4-go/antlr/v4"

	sqlbuilder "github.com/huandu/go-sqlbuilder"
)

var searchTroubleshootingGuideURL = "https://signoz.io/docs/userguide/search-troubleshooting/"

const stringMatchingOperatorDocURL = "https://signoz.io/docs/userguide/operators-reference/#string-matching-operators"

// filterExpressionVisitor implements the FilterQueryVisitor interface
// to convert the parsed filter expressions into ClickHouse WHERE clause.
type filterExpressionVisitor struct {
	context            context.Context
	logger             *slog.Logger
	fieldMapper        qbtypes.FieldMapper
	conditionBuilder   qbtypes.ConditionBuilder
	warnings           []string
	mainWarnURL        string
	fieldKeys          map[string][]*telemetrytypes.TelemetryFieldKey
	errors             []string
	mainErrorURL       string
	builder            *sqlbuilder.SelectBuilder
	fullTextColumn     *telemetrytypes.TelemetryFieldKey
	jsonKeyToKey       qbtypes.JsonKeyToFieldFunc
	bodyJSONEnabled    bool
	skipResourceFilter bool
	skipFullTextFilter bool
	skipFunctionCalls  bool
	ignoreNotFoundKeys bool
	variables          map[string]qbtypes.VariableItem

	keysWithWarnings map[string]bool
	startNs          uint64
	endNs            uint64
}

type FilterExprVisitorOpts struct {
	Context            context.Context
	Logger             *slog.Logger
	FieldMapper        qbtypes.FieldMapper
	ConditionBuilder   qbtypes.ConditionBuilder
	FieldKeys          map[string][]*telemetrytypes.TelemetryFieldKey
	Builder            *sqlbuilder.SelectBuilder
	FullTextColumn     *telemetrytypes.TelemetryFieldKey
	JsonKeyToKey       qbtypes.JsonKeyToFieldFunc
	BodyJSONEnabled    bool
	SkipResourceFilter bool
	SkipFullTextFilter bool
	SkipFunctionCalls  bool
	IgnoreNotFoundKeys bool
	Variables          map[string]qbtypes.VariableItem
	StartNs            uint64
	EndNs              uint64
}

// newFilterExpressionVisitor creates a new filterExpressionVisitor.
func newFilterExpressionVisitor(opts FilterExprVisitorOpts) *filterExpressionVisitor {
	return &filterExpressionVisitor{
		context:            opts.Context,
		logger:             opts.Logger,
		fieldMapper:        opts.FieldMapper,
		conditionBuilder:   opts.ConditionBuilder,
		fieldKeys:          opts.FieldKeys,
		builder:            opts.Builder,
		fullTextColumn:     opts.FullTextColumn,
		jsonKeyToKey:       opts.JsonKeyToKey,
		bodyJSONEnabled:    opts.BodyJSONEnabled,
		skipResourceFilter: opts.SkipResourceFilter,
		skipFullTextFilter: opts.SkipFullTextFilter,
		skipFunctionCalls:  opts.SkipFunctionCalls,
		ignoreNotFoundKeys: opts.IgnoreNotFoundKeys,
		variables:          opts.Variables,
		keysWithWarnings:   make(map[string]bool),
		startNs:            opts.StartNs,
		endNs:              opts.EndNs,
	}
}

type PreparedWhereClause struct {
	WhereClause    *sqlbuilder.WhereClause
	Warnings       []string
	WarningsDocURL string
}

// PrepareWhereClause generates a ClickHouse compatible WHERE clause from the filter query.
func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (*PreparedWhereClause, error) {

	// Setup the ANTLR parsing pipeline
	input := antlr.NewInputStream(query)
	lexer := grammar.NewFilterQueryLexer(input)

	if opts.Builder == nil {
		sb := sqlbuilder.NewSelectBuilder()
		opts.Builder = sb
	}

	// Set up error handling
	lexerErrorListener := NewErrorListener()
	lexer.RemoveErrorListeners()
	lexer.AddErrorListener(lexerErrorListener)

	tokens := antlr.NewCommonTokenStream(lexer, 0)
	parserErrorListener := NewErrorListener()
	parser := grammar.NewFilterQueryParser(tokens)
	parser.RemoveErrorListeners()
	parser.AddErrorListener(parserErrorListener)

	// Parse the query
	tree := parser.Query()

	// override skipResourceFilter if the expression contains OR
	for _, tok := range tokens.GetAllTokens() {
		if tok.GetTokenType() == grammar.FilterQueryParserOR {
			opts.SkipResourceFilter = false
			break
		}
	}
	tokens.Reset()

	visitor := newFilterExpressionVisitor(opts)

	// Handle syntax errors
	if len(parserErrorListener.SyntaxErrors) > 0 {
		combinedErrors := errors.Newf(
			errors.TypeInvalidInput,
			errors.CodeInvalidInput,
			"Found %d syntax errors while parsing the search expression.",
			len(parserErrorListener.SyntaxErrors),
		)
		additionals := make([]string, len(parserErrorListener.SyntaxErrors))
		for _, err := range parserErrorListener.SyntaxErrors {
			if err.Error() != "" {
				additionals = append(additionals, err.Error())
			}
		}

		return nil, combinedErrors.WithAdditional(additionals...).WithUrl(searchTroubleshootingGuideURL)
	}

	// Visit the parse tree with our ClickHouse visitor
	cond := visitor.Visit(tree).(string)

	if len(visitor.errors) > 0 {
		// combine all errors into a single error
		combinedErrors := errors.Newf(
			errors.TypeInvalidInput,
			errors.CodeInvalidInput,
			"Found %d errors while parsing the search expression.",
			len(visitor.errors),
		)
		url := visitor.mainErrorURL
		if url == "" {
			url = searchTroubleshootingGuideURL
		}
		return nil, combinedErrors.WithAdditional(visitor.errors...).WithUrl(url)
	}

	// Return nil so callers can skip the
	// entire CTE/subquery rather than emitting WHERE clause that select all the rows
	if cond == "" || cond == SkipConditionLiteral {
		return nil, nil //nolint:nilnil
	}

	whereClause := sqlbuilder.NewWhereClause().AddWhereExpr(visitor.builder.Args, cond)

	return &PreparedWhereClause{WhereClause: whereClause, Warnings: visitor.warnings, WarningsDocURL: visitor.mainWarnURL}, nil
}

// Visit dispatches to the specific visit method based on node type.
func (v *filterExpressionVisitor) Visit(tree antlr.ParseTree) any {
	// Handle nil nodes to prevent panic
	if tree == nil {
		return SkipConditionLiteral
	}

	switch t := tree.(type) {
	case *grammar.QueryContext:
		return v.VisitQuery(t)
	case *grammar.ExpressionContext:
		return v.VisitExpression(t)
	case *grammar.OrExpressionContext:
		return v.VisitOrExpression(t)
	case *grammar.AndExpressionContext:
		return v.VisitAndExpression(t)
	case *grammar.UnaryExpressionContext:
		return v.VisitUnaryExpression(t)
	case *grammar.PrimaryContext:
		return v.VisitPrimary(t)
	case *grammar.ComparisonContext:
		return v.VisitComparison(t)
	case *grammar.InClauseContext:
		return v.VisitInClause(t)
	case *grammar.NotInClauseContext:
		return v.VisitNotInClause(t)
	case *grammar.ValueListContext:
		return v.VisitValueList(t)
	case *grammar.FullTextContext:
		return v.VisitFullText(t)
	case *grammar.FunctionCallContext:
		return v.VisitFunctionCall(t)
	case *grammar.FunctionParamListContext:
		return v.VisitFunctionParamList(t)
	case *grammar.FunctionParamContext:
		return v.VisitFunctionParam(t)
	case *grammar.ArrayContext:
		return v.VisitArray(t)
	case *grammar.ValueContext:
		return v.VisitValue(t)
	case *grammar.KeyContext:
		return v.VisitKey(t)
	default:
		return ErrorConditionLiteral
	}
}

func (v *filterExpressionVisitor) VisitQuery(ctx *grammar.QueryContext) any {

	return v.Visit(ctx.Expression())
}

// VisitExpression passes through to the orExpression.
func (v *filterExpressionVisitor) VisitExpression(ctx *grammar.ExpressionContext) any {
	return v.Visit(ctx.OrExpression())
}

// VisitOrExpression handles OR expressions.
func (v *filterExpressionVisitor) VisitOrExpression(ctx *grammar.OrExpressionContext) any {
	andExpressions := ctx.AllAndExpression()

	andExpressionConditions := make([]string, 0, len(andExpressions))
	for _, expr := range andExpressions {
		if condExpr, ok := v.Visit(expr).(string); ok && condExpr != "" {
			// In an OR, a single unevaluable branch makes the entire expression unevaluable,
			// so short-circuit immediately.
			if slices.Contains(SkippableConditionLiterals, condExpr) {
				return condExpr
			}
			andExpressionConditions = append(andExpressionConditions, condExpr)
		}
	}

	if len(andExpressionConditions) == 0 {
		return SkipConditionLiteral
	}

	if len(andExpressionConditions) == 1 {
		return andExpressionConditions[0]
	}

	return v.builder.Or(andExpressionConditions...)
}

// VisitAndExpression handles AND expressions.
func (v *filterExpressionVisitor) VisitAndExpression(ctx *grammar.AndExpressionContext) any {
	unaryExpressions := ctx.AllUnaryExpression()

	unaryExpressionConditions := make([]string, 0, len(unaryExpressions))
	for _, expr := range unaryExpressions {
		if condExpr, ok := v.Visit(expr).(string); ok && condExpr != "" {
			// filter out skippable no-op conditions (e.g. non-resource attributes in resource filter)
			// to avoid producing compound expressions like "(SkipConditionLiteral AND SkipConditionLiteral)"
			if slices.Contains(SkippableConditionLiterals, condExpr) {
				continue
			}
			unaryExpressionConditions = append(unaryExpressionConditions, condExpr)
		}
	}

	// If there are no conditions, return SkipConditionLiteral
	if len(unaryExpressionConditions) == 0 {
		return SkipConditionLiteral
	}

	if len(unaryExpressionConditions) == 1 {
		return unaryExpressionConditions[0]
	}

	return v.builder.And(unaryExpressionConditions...)
}

// VisitUnaryExpression handles NOT expressions.
func (v *filterExpressionVisitor) VisitUnaryExpression(ctx *grammar.UnaryExpressionContext) any {
	result := v.Visit(ctx.Primary()).(string)

	// Check if this is a NOT expression
	if ctx.NOT() != nil {
		// NOT (skippable) -> propagate the skippable literal unchanged
		if slices.Contains(SkippableConditionLiterals, result) {
			return result
		}
		return fmt.Sprintf("NOT (%s)", result)
	}

	return result
}

// VisitPrimary handles grouped expressions, comparisons, function calls, and full-text search.
func (v *filterExpressionVisitor) VisitPrimary(ctx *grammar.PrimaryContext) any {
	if ctx.OrExpression() != nil {
		// This is a parenthesized expression
		if condExpr, ok := v.Visit(ctx.OrExpression()).(string); ok {
			// Don't wrap skippable literals in parentheses — pass them through as-is
			if slices.Contains(SkippableConditionLiterals, condExpr) {
				return condExpr
			}
			return fmt.Sprintf("(%s)", condExpr)
		}
		return ErrorConditionLiteral
	} else if ctx.Comparison() != nil {
		return v.Visit(ctx.Comparison())
	} else if ctx.FunctionCall() != nil {
		return v.Visit(ctx.FunctionCall())
	} else if ctx.FullText() != nil {
		return v.Visit(ctx.FullText())
	}

	// Handle standalone key/value as a full text search term
	if ctx.GetChildCount() == 1 {
		if v.skipFullTextFilter {
			return SkipConditionLiteral
		}

		if v.fullTextColumn == nil {
			v.errors = append(v.errors, "full text search is not supported")
			return ErrorConditionLiteral
		}
		child := ctx.GetChild(0)
		var searchText string
		if keyCtx, ok := child.(*grammar.KeyContext); ok {
			// create a full text search condition on the body field
			searchText = keyCtx.GetText()
		} else if valCtx, ok := child.(*grammar.ValueContext); ok {
			if valCtx.QUOTED_TEXT() != nil {
				searchText = trimQuotes(valCtx.QUOTED_TEXT().GetText())
			} else if valCtx.NUMBER() != nil {
				searchText = valCtx.NUMBER().GetText()
			} else if valCtx.BOOL() != nil {
				searchText = valCtx.BOOL().GetText()
			} else if valCtx.KEY() != nil {
				searchText = valCtx.KEY().GetText()
			} else {
				v.errors = append(v.errors, fmt.Sprintf("unsupported value type: %s", valCtx.GetText()))
				return ErrorConditionLiteral
			}
		}
		cond, err := v.conditionBuilder.ConditionFor(context.Background(), v.startNs, v.endNs, v.fullTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(searchText), v.builder)
		if err != nil {
			v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
			return ErrorConditionLiteral
		}
		if v.bodyJSONEnabled && v.fullTextColumn.Name == "body" {
			v.warnings = append(v.warnings, BodyFullTextSearchDefaultWarning)
		}

		return cond
	}

	return ErrorConditionLiteral // Should not happen with valid input
}

// VisitComparison handles all comparison operators.
func (v *filterExpressionVisitor) VisitComparison(ctx *grammar.ComparisonContext) any {
	keys := v.Visit(ctx.Key()).([]*telemetrytypes.TelemetryFieldKey)

	// no keys resolved; VisitKey already recorded the error, skip this condition
	if len(keys) == 0 {
		return ErrorConditionLiteral
	}

	// this is used to skip the resource filtering on main table if
	// the query may use the resources table sub-query filter
	if v.skipResourceFilter {
		filteredKeys := []*telemetrytypes.TelemetryFieldKey{}
		for _, key := range keys {
			if key.FieldContext != telemetrytypes.FieldContextResource {
				filteredKeys = append(filteredKeys, key)
			}
		}
		keys = filteredKeys
		if len(keys) == 0 {
			return SkipConditionLiteral
		}
	}

	// Handle EXISTS specially
	if ctx.EXISTS() != nil {
		op := qbtypes.FilterOperatorExists
		if ctx.NOT() != nil {
			op = qbtypes.FilterOperatorNotExists
		}
		var conds []string
		for _, key := range keys {
			condition, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, key, op, nil, v.builder)
			if err != nil {
				v.errors = append(v.errors, fmt.Sprintf("failed to build condition: %s", err.Error()))
				return ErrorConditionLiteral
			}
			if slices.Contains(SkippableConditionLiterals, condition) {
				continue
			}
			conds = append(conds, condition)
		}
		if len(conds) == 0 {
			return SkipConditionLiteral
		}
		// if there is only one condition, return it directly, one less `()` wrapper
		if len(conds) == 1 {
			return conds[0]
		}
		if op.IsNegativeOperator() {
			return v.builder.And(conds...)
		}
		return v.builder.Or(conds...)
	}

	// Handle IN clause
	if ctx.InClause() != nil || ctx.NotInClause() != nil {

		var values []any
		var retValue any
		if ctx.InClause() != nil {
			retValue = v.Visit(ctx.InClause())
		} else if ctx.NotInClause() != nil {
			retValue = v.Visit(ctx.NotInClause())
		}
		switch ret := retValue.(type) {
		case []any:
			values = ret
		case any:
			values = []any{ret}
		}

		if len(values) == 1 {
			if var_, ok := values[0].(string); ok {
				// check if this is a variables
				var ok bool
				var varItem qbtypes.VariableItem
				varItem, ok = v.variables[var_]
				// if not present, try without `$` prefix
				if !ok && len(var_) > 0 {
					varItem, ok = v.variables[var_[1:]]
				}

				if ok {
					// we have a variable, now check for dynamic variable
					if varItem.Type == qbtypes.DynamicVariableType {
						// check if it is special value to skip entire filter, if so skip it
						if all_, ok := varItem.Value.(string); ok && all_ == "__all__" {
							return SkipConditionLiteral
						}
					}
					switch varValues := varItem.Value.(type) {
					case []any:
						if len(varValues) == 0 {
							v.errors = append(v.errors, fmt.Sprintf("malformed request payload: variable `%s` used in expression has an empty list value", strings.TrimPrefix(var_, "$")))
							return ErrorConditionLiteral
						}
						values = varValues
					case any:
						values = []any{varValues}
					}
				}
			}
		}

		op := qbtypes.FilterOperatorIn
		if ctx.NotInClause() != nil {
			op = qbtypes.FilterOperatorNotIn
		}
		var conds []string
		for _, key := range keys {
			condition, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, key, op, values, v.builder)
			if err != nil {
				return ErrorConditionLiteral
			}
			if slices.Contains(SkippableConditionLiterals, condition) {
				continue
			}
			conds = append(conds, condition)
		}
		if len(conds) == 0 {
			return SkipConditionLiteral
		}
		if len(conds) == 1 {
			return conds[0]
		}
		if op.IsNegativeOperator() {
			return v.builder.And(conds...)
		}
		return v.builder.Or(conds...)
	}

	// Handle BETWEEN
	if ctx.BETWEEN() != nil {
		op := qbtypes.FilterOperatorBetween
		if ctx.NOT() != nil {
			op = qbtypes.FilterOperatorNotBetween
		}

		values := ctx.AllValue()
		if len(values) != 2 {
			return SkipConditionLiteral
		}

		value1 := v.Visit(values[0])
		value2 := v.Visit(values[1])

		switch value1.(type) {
		case float64:
			if _, ok := value2.(float64); !ok {
				v.errors = append(v.errors, fmt.Sprintf("value type mismatch for key %s: expected number for both operands", keys[0].Name))
				return ErrorConditionLiteral
			}
		case string:
			if _, ok := value2.(string); !ok {
				v.errors = append(v.errors, fmt.Sprintf("value type mismatch for key %s: expected string for both operands", keys[0].Name))
				return ErrorConditionLiteral
			}
		default:
			v.errors = append(v.errors, fmt.Sprintf("value type mismatch for key %s: operands must be number or string", keys[0].Name))
			return ErrorConditionLiteral
		}

		var conds []string
		for _, key := range keys {
			condition, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, key, op, []any{value1, value2}, v.builder)
			if err != nil {
				return ErrorConditionLiteral
			}
			if slices.Contains(SkippableConditionLiterals, condition) {
				continue
			}
			conds = append(conds, condition)
		}
		if len(conds) == 0 {
			return SkipConditionLiteral
		}
		if len(conds) == 1 {
			return conds[0]
		}
		if op.IsNegativeOperator() {
			return v.builder.And(conds...)
		}
		return v.builder.Or(conds...)
	}

	// Get all values for operations that need them
	values := ctx.AllValue()
	if len(values) > 0 {
		value := v.Visit(values[0])

		if var_, ok := value.(string); ok {
			// check if this is a variables
			var ok bool
			var varItem qbtypes.VariableItem
			varItem, ok = v.variables[var_]
			// if not present, try without `$` prefix
			if !ok && len(var_) > 0 {
				varItem, ok = v.variables[var_[1:]]
			}

			if ok {
				switch varValues := varItem.Value.(type) {
				case []any:
					if len(varValues) == 0 {
						v.errors = append(v.errors, fmt.Sprintf("malformed request payload: variable `%s` used in expression has an empty list value", strings.TrimPrefix(var_, "$")))
						return ErrorConditionLiteral
					}
					value = varValues[0]
				case any:
					value = varValues
				}
			}
		}

		var op qbtypes.FilterOperator

		// Handle each type of comparison
		if ctx.EQUALS() != nil {
			op = qbtypes.FilterOperatorEqual
		} else if ctx.NOT_EQUALS() != nil || ctx.NEQ() != nil {
			op = qbtypes.FilterOperatorNotEqual
		} else if ctx.LT() != nil {
			op = qbtypes.FilterOperatorLessThan
		} else if ctx.LE() != nil {
			op = qbtypes.FilterOperatorLessThanOrEq
		} else if ctx.GT() != nil {
			op = qbtypes.FilterOperatorGreaterThan
		} else if ctx.GE() != nil {
			op = qbtypes.FilterOperatorGreaterThanOrEq
		} else if ctx.LIKE() != nil {
			op = qbtypes.FilterOperatorLike
			if ctx.NOT() != nil {
				op = qbtypes.FilterOperatorNotLike
			}
			v.warnIfLikeWithoutWildcards("LIKE", value)
		} else if ctx.ILIKE() != nil {
			op = qbtypes.FilterOperatorILike
			if ctx.NOT() != nil {
				op = qbtypes.FilterOperatorNotILike
			}
			v.warnIfLikeWithoutWildcards("ILIKE", value)
		} else if ctx.REGEXP() != nil {
			op = qbtypes.FilterOperatorRegexp
			if ctx.NOT() != nil {
				op = qbtypes.FilterOperatorNotRegexp
			}
		} else if ctx.CONTAINS() != nil {
			op = qbtypes.FilterOperatorContains
			if ctx.NOT() != nil {
				op = qbtypes.FilterOperatorNotContains
			}
		}

		var conds []string
		for _, key := range keys {
			condition, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, key, op, value, v.builder)
			if err != nil {
				v.errors = append(v.errors, fmt.Sprintf("failed to build condition: %s", err.Error()))
				return ErrorConditionLiteral
			}
			if slices.Contains(SkippableConditionLiterals, condition) {
				continue
			}
			conds = append(conds, condition)
		}
		if len(conds) == 0 {
			return SkipConditionLiteral
		}
		if len(conds) == 1 {
			return conds[0]
		}
		if op.IsNegativeOperator() {
			return v.builder.And(conds...)
		}
		return v.builder.Or(conds...)
	}

	return ErrorConditionLiteral // Should not happen with valid input
}

// warnIfLikeWithoutWildcards adds a guidance warning when LIKE/ILIKE is used without wildcards.
func (v *filterExpressionVisitor) warnIfLikeWithoutWildcards(op string, value any) {
	if hasLikeWildcards(value) {
		return
	}

	msg := op + " operator used without wildcards (% or _). Consider using = operator for exact matches or add wildcards for pattern matching."
	v.warnings = append(v.warnings, msg)
	if v.mainWarnURL == "" {
		v.mainWarnURL = stringMatchingOperatorDocURL
	}
}

// VisitInClause handles IN expressions.
func (v *filterExpressionVisitor) VisitInClause(ctx *grammar.InClauseContext) any {
	if ctx.ValueList() != nil {
		return v.Visit(ctx.ValueList())
	}
	return v.Visit(ctx.Value())
}

// VisitNotInClause handles NOT IN expressions.
func (v *filterExpressionVisitor) VisitNotInClause(ctx *grammar.NotInClauseContext) any {
	if ctx.ValueList() != nil {
		return v.Visit(ctx.ValueList())
	}
	return v.Visit(ctx.Value())
}

// VisitValueList handles comma-separated value lists.
func (v *filterExpressionVisitor) VisitValueList(ctx *grammar.ValueListContext) any {
	values := ctx.AllValue()

	parts := []any{}
	for _, val := range values {
		parts = append(parts, v.Visit(val))
	}

	return parts
}

// VisitFullText handles standalone quoted strings for full-text search.
func (v *filterExpressionVisitor) VisitFullText(ctx *grammar.FullTextContext) any {
	if v.skipFullTextFilter {
		// A skipped FT term must be treated as TrueConditionLiteral, not "".
		// Returning "" would silently drop this branch from an OR, incorrectly
		// excluding rows that could match the FT condition on the real table.
		return SkipConditionLiteral
	}

	var text string

	if ctx.QUOTED_TEXT() != nil {
		text = trimQuotes(ctx.QUOTED_TEXT().GetText())
	} else if ctx.FREETEXT() != nil {
		text = ctx.FREETEXT().GetText()
	}

	if v.fullTextColumn == nil {
		v.errors = append(v.errors, "full text search is not supported")
		return ErrorConditionLiteral
	}
	cond, err := v.conditionBuilder.ConditionFor(v.context, v.startNs, v.endNs, v.fullTextColumn, qbtypes.FilterOperatorRegexp, FormatFullTextSearch(text), v.builder)
	if err != nil {
		v.errors = append(v.errors, fmt.Sprintf("failed to build full text search condition: %s", err.Error()))
		return ErrorConditionLiteral
	}

	if v.bodyJSONEnabled && v.fullTextColumn.Name == "body" {
		v.warnings = append(v.warnings, BodyFullTextSearchDefaultWarning)
	}

	return cond
}

// VisitFunctionCall handles function calls like has(), hasAny(), etc.
func (v *filterExpressionVisitor) VisitFunctionCall(ctx *grammar.FunctionCallContext) any {
	if v.skipFunctionCalls {
		return SkipConditionLiteral
	}

	// Get function name based on which token is present
	var functionName string
	if ctx.HAS() != nil {
		functionName = "has"
	} else if ctx.HASANY() != nil {
		functionName = "hasAny"
	} else if ctx.HASALL() != nil {
		functionName = "hasAll"
	} else if ctx.HASTOKEN() != nil {
		functionName = "hasToken"
	} else {
		// Default fallback
		v.errors = append(v.errors, fmt.Sprintf("unknown function `%s`", ctx.GetText()))
		return ErrorConditionLiteral
	}
	params := v.Visit(ctx.FunctionParamList()).([]any)

	if len(params) < 2 {
		v.errors = append(v.errors, fmt.Sprintf("function `%s` expects key and value parameters", functionName))
		return ErrorConditionLiteral
	}

	keys, ok := params[0].([]*telemetrytypes.TelemetryFieldKey)
	if !ok {
		v.errors = append(v.errors, fmt.Sprintf("function `%s` expects key parameter to be a field key", functionName))
		return ErrorConditionLiteral
	}

	// TODO(Tushar): thread orgID here to evaluate correctly
	if v.bodyJSONEnabled && functionName != "hasToken" {
		filteredKeys := []*telemetrytypes.TelemetryFieldKey{}
		for _, key := range keys {
			if key.FieldDataType.IsArray() {
				filteredKeys = append(filteredKeys, key)
			}
		}
		if len(filteredKeys) == 0 {
			v.errors = append(v.errors, fmt.Sprintf("function `%s` expects key parameter to be an array field; no array fields found", functionName))
			return ErrorConditionLiteral
		}
		keys = filteredKeys
	}

	value := params[1:]
	var conds []string
	for _, key := range keys {
		var fieldName string

		if functionName == "hasToken" {

			if key.Name != "body" {
				if v.mainErrorURL == "" {
					v.mainErrorURL = "https://signoz.io/docs/userguide/functions-reference/#hastoken-function"
				}
				v.errors = append(v.errors, fmt.Sprintf("function `%s` only supports body field as first parameter", functionName))
			}

			// this will only work with string.
			if _, ok := value[0].(string); !ok {
				if v.mainErrorURL == "" {
					v.mainErrorURL = "https://signoz.io/docs/userguide/functions-reference/#hastoken-function"
				}
				v.errors = append(v.errors, fmt.Sprintf("function `%s` expects value parameter to be a string", functionName))
				return ErrorConditionLiteral
			}
			conds = append(conds, fmt.Sprintf("hasToken(LOWER(%s), LOWER(%s))", key.Name, v.builder.Var(value[0])))
		} else {
			// this is that all other functions only support array fields
			if key.FieldContext == telemetrytypes.FieldContextBody {
				var err error
				if v.bodyJSONEnabled {
					fieldName, err = v.fieldMapper.FieldFor(v.context, v.startNs, v.endNs, key)
					if err != nil {
						v.errors = append(v.errors, fmt.Sprintf("failed to get field name for key %s: %s", key.Name, err.Error()))
						return ErrorConditionLiteral
					}
				} else {
					fieldName, _ = v.jsonKeyToKey(v.context, key, qbtypes.FilterOperatorUnknown, value)
				}
			} else {
				// TODO(add docs for json body search)
				if v.mainErrorURL == "" {
					v.mainErrorURL = "https://signoz.io/docs/userguide/search-troubleshooting/#function-supports-only-body-json-search"
				}
				v.errors = append(v.errors, fmt.Sprintf("function `%s` supports only body JSON search", functionName))
				return ErrorConditionLiteral
			}

			var cond string
			// Map our functions to ClickHouse equivalents
			switch functionName {
			case "has":
				cond = fmt.Sprintf("has(%s, %s)", fieldName, v.builder.Var(value[0]))
			case "hasAny":
				cond = fmt.Sprintf("hasAny(%s, %s)", fieldName, v.builder.Var(value[0]))
			case "hasAll":
				cond = fmt.Sprintf("hasAll(%s, %s)", fieldName, v.builder.Var(value[0]))
			}
			conds = append(conds, cond)
		}
	}

	if len(conds) == 1 {
		return conds[0]
	}
	return v.builder.Or(conds...)
}

// VisitFunctionParamList handles the parameter list for function calls.
func (v *filterExpressionVisitor) VisitFunctionParamList(ctx *grammar.FunctionParamListContext) any {
	params := ctx.AllFunctionParam()
	parts := make([]any, len(params))

	for i, param := range params {
		parts[i] = v.Visit(param)
	}

	return parts
}

// VisitFunctionParam handles individual parameters in function calls.
func (v *filterExpressionVisitor) VisitFunctionParam(ctx *grammar.FunctionParamContext) any {
	if ctx.Key() != nil {
		return v.Visit(ctx.Key())
	} else if ctx.Value() != nil {
		return v.Visit(ctx.Value())
	} else if ctx.Array() != nil {
		return v.Visit(ctx.Array())
	}

	return ErrorConditionLiteral // Should not happen with valid input
}

// VisitArray handles array literals.
func (v *filterExpressionVisitor) VisitArray(ctx *grammar.ArrayContext) any {
	return v.Visit(ctx.ValueList())
}

// VisitValue handles literal values: strings, numbers, booleans.
func (v *filterExpressionVisitor) VisitValue(ctx *grammar.ValueContext) any {
	if ctx.QUOTED_TEXT() != nil {
		txt := ctx.QUOTED_TEXT().GetText()
		// trim quotes and return the value
		return trimQuotes(txt)
	} else if ctx.NUMBER() != nil {
		number, err := strconv.ParseFloat(ctx.NUMBER().GetText(), 64)
		if err != nil {
			v.errors = append(v.errors, fmt.Sprintf("failed to parse number %s", ctx.NUMBER().GetText()))
			return ErrorConditionLiteral
		}
		return number
	} else if ctx.BOOL() != nil {
		// Convert to ClickHouse boolean literal
		boolText := strings.ToLower(ctx.BOOL().GetText())
		return boolText == TrueConditionLiteral
	} else if ctx.KEY() != nil {
		// Why do we have a KEY context here?
		// When the user writes an expression like `service.name=redis`
		// The `redis` part is a VALUE context but parsed as a KEY token
		// so we return the text as is
		return ctx.KEY().GetText()
	}

	return ErrorConditionLiteral // Should not happen with valid input
}

// VisitKey handles field/column references.
func (v *filterExpressionVisitor) VisitKey(ctx *grammar.KeyContext) any {
	fieldKey := telemetrytypes.GetFieldKeyFromKeyText(ctx.GetText())
	keyName := fieldKey.Name

	// GetFieldKeyFromKeyText function extracts the context prefix (attribute., resource., body.) if present
	// extracts data type if present (e.g., :string, :int)
	// so we need to filter the available field keys based on the provided context and data type
	fieldKeysForName := []*telemetrytypes.TelemetryFieldKey{}

	// If user said key = 'xyz', we look up in the map for all possible field keys with name 'xyz'
	for _, item := range v.fieldKeys[keyName] {
		// Match items where:
		// 1) user didn't specify context OR context matches, AND
		// 2) user didn't specify data type OR data type matches.
		if (fieldKey.FieldContext == telemetrytypes.FieldContextUnspecified || fieldKey.FieldContext == item.FieldContext) &&
			(fieldKey.FieldDataType == telemetrytypes.FieldDataTypeUnspecified || fieldKey.FieldDataType == item.FieldDataType) {
			fieldKeysForName = append(fieldKeysForName, item)
		}
	}

	// Now consider that GetFieldKeyFromKeyText may have extracted the context which was actually part of the name
	// If user said attribute.key = 'xyz',
	// 1. either user meant key ( this is already handled above in fieldKeysForName )
	// 2. or user meant `attribute.key` we look up in the map for all possible field keys with name 'attribute.key'

	// Note:
	// If user only wants to search `attribute.key`, then they have to use `attribute.attribute.key`
	// If user only wants to search `key`, then they have to use `key`
	// If user wants to search both, they can use `attribute.key` and we will resolve the ambiguity
	if fieldKey.FieldContext != telemetrytypes.FieldContextUnspecified {
		contextPrefixedFieldName := fmt.Sprintf("%s.%s", fieldKey.FieldContext.StringValue(), fieldKey.Name)
		for _, item := range v.fieldKeys[contextPrefixedFieldName] {
			// Match items where: user didn't specify data type OR data type matches. Context filtering not needed here since context was used to construct the lookup key.
			if fieldKey.FieldDataType == telemetrytypes.FieldDataTypeUnspecified || item.FieldDataType == fieldKey.FieldDataType {
				fieldKeysForName = append(fieldKeysForName, item)
			}
		}
	}

	// for the body json search, we need to add search on the body field even
	// if there is a field with the same name as attribute/resource attribute
	// Since it will ORed with the fieldKeysForName, it will not result empty
	// when either of them have values
	// Note: Skip this logic if body json query is enabled so we can look up the key inside fields
	//
	// TODO(Piyush): After entire migration this is supposed to be removed.
	// TODO(Tushar): thread orgID here to evaluate correctly
	if fieldKey.FieldContext == telemetrytypes.FieldContextBody && !v.bodyJSONEnabled {
		fieldKeysForName = append(fieldKeysForName, &fieldKey)
	}

	if len(fieldKeysForName) == 0 {
		if fieldKey.FieldContext == telemetrytypes.FieldContextBody && keyName == "" {
			v.errors = append(v.errors, "missing key for body json search - expected key of the form `body.key` (ex: `body.status`)")
		} else if !v.ignoreNotFoundKeys {
			// TODO(srikanthccv): do we want to return an error here?
			// should we infer the type and auto-magically build a key for expression?
			v.errors = append(v.errors, fmt.Sprintf("key `%s` not found", fieldKey.Name))
			v.mainErrorURL = "https://signoz.io/docs/userguide/search-troubleshooting/#key-fieldname-not-found"
		}
	}

	if len(fieldKeysForName) > 1 {
		warnMsg := fmt.Sprintf(
			"Key `%s` is ambiguous, found %d different combinations of field context / data type: %v.",
			fieldKey.Name,
			len(fieldKeysForName),
			fieldKeysForName,
		)
		mixedFieldContext := map[string]bool{}
		for _, item := range fieldKeysForName {
			mixedFieldContext[item.FieldContext.StringValue()] = true
		}

		// when there is both resource and attribute context, default to resource only
		if mixedFieldContext[telemetrytypes.FieldContextResource.StringValue()] &&
			mixedFieldContext[telemetrytypes.FieldContextAttribute.StringValue()] {
			filteredKeys := []*telemetrytypes.TelemetryFieldKey{}
			for _, item := range fieldKeysForName {
				if item.FieldContext != telemetrytypes.FieldContextResource {
					continue
				}
				filteredKeys = append(filteredKeys, item)
			}
			fieldKeysForName = filteredKeys
			warnMsg += " " + "Using `resource` context by default. To query attributes explicitly, " +
				fmt.Sprintf("use the fully qualified name (e.g., 'attribute.%s')", fieldKey.Name)
		}

		if !v.keysWithWarnings[keyName] {
			v.mainWarnURL = "https://signoz.io/docs/userguide/field-context-data-types/"
			// this is warning state, we must have a unambiguous key
			v.warnings = append(v.warnings, warnMsg)
		}
		v.keysWithWarnings[keyName] = true
		v.logger.Warn("ambiguous key", slog.String("field_key_name", fieldKey.Name)) //nolint:sloglint
	}

	return fieldKeysForName
}

// hasLikeWildcards checks if a value contains LIKE wildcards (% or _).
func hasLikeWildcards(value any) bool {
	str, ok := value.(string)
	if !ok {
		return false
	}
	return strings.Contains(str, "%") || strings.Contains(str, "_")
}

func trimQuotes(txt string) string {
	if len(txt) >= 2 {
		if (txt[0] == '"' && txt[len(txt)-1] == '"') ||
			(txt[0] == '\'' && txt[len(txt)-1] == '\'') {
			txt = txt[1 : len(txt)-1]
		}
	}

	// unescape so clickhouse-go can escape it
	// https://github.com/ClickHouse/clickhouse-go/blob/6c5ddb38dd2edc841a3b927711b841014759bede/bind.go#L278
	txt = strings.ReplaceAll(txt, `\\`, `\`)
	txt = strings.ReplaceAll(txt, `\'`, `'`)
	return txt
}
