package querycodec

import (
	"fmt"
	"strconv"
	"strings"
)

// allowedFunctions is the closed function vocabulary that the current neutral
// Runtime query contract can carry in predicate operands. Selection and sort
// expressions have narrower shapes and are checked separately below; accepting
// a function in the lexer is not a promise that every query clause can execute
// it.
var allowedFunctions = map[string]bool{
	"COUNT":            true,
	"COUNT_DISTINCT":   true,
	"SUM":              true,
	"AVG":              true,
	"MIN":              true,
	"MAX":              true,
	"TOSTRING":         true,
	"FORMAT":           true,
	"TOLABEL":          true,
	"CONVERTCURRENCY":  true,
	"GROUPING":         true,
	"CALENDAR_MONTH":   true,
	"CALENDAR_YEAR":    true,
	"CALENDAR_QUARTER": true,
	"DAY_IN_MONTH":     true,
	"DAY_IN_WEEK":      true,
	"DAY_IN_YEAR":      true,
	"DAY_ONLY":         true,
	"HOUR_IN_DAY":      true,
	"FISCAL_MONTH":     true,
	"FISCAL_QUARTER":   true,
	"FISCAL_YEAR":      true,
	"WEEK_IN_MONTH":    true,
	"WEEK_IN_YEAR":     true,
	"DISTANCE":         true,
	"GEOLOCATION":      true,
}

// supportedSelectionFunctions is deliberately identical to the Runtime
// queryv1 aggregate-selection vocabulary. Other recognized functions remain
// usable in predicate operands, but the current coarse query capability has no
// function-expression selection arm. Keeping this distinction here prevents a
// query from parsing successfully only to become unrepresentable at the L1/L0
// boundary.
var supportedSelectionFunctions = map[string]bool{
	"COUNT": true,
	"SUM":   true,
	"AVG":   true,
	"MIN":   true,
	"MAX":   true,
}

func isAllowedFunction(name string) bool {
	return allowedFunctions[strings.ToUpper(name)]
}

func isSupportedSelectionFunction(name string) bool {
	return supportedSelectionFunctions[strings.ToUpper(name)]
}

// parseSelectionFunction consumes the deliberately narrow aggregate shape that
// the neutral Runtime selection carrier can represent: FUNC(Field.Path), plus
// COUNT(). Predicate functions use the typed expression parser below and are
// intentionally unaffected.
func (p *Parser) parseSelectionFunction(function string) (string, error) {
	p.nextToken() // consume "("
	if p.peekToken().Literal == ")" {
		if !strings.EqualFold(function, "COUNT") {
			return "", newValidationError("SOQL", fmt.Sprintf("%s requires a field argument in SELECT", function))
		}
		p.nextToken() // consume ")"
		return function + "()", nil
	}

	p.nextToken() // consume the first field segment
	field, err := p.parseDottedFieldName("SELECT aggregate")
	if err != nil {
		return "", err
	}
	if p.peekToken().Literal != ")" {
		return "", newValidationError("SOQL", fmt.Sprintf("%s requires exactly one field argument in SELECT", function))
	}
	p.nextToken() // consume ")"
	return function + "(" + field + ")", nil
}

func (p *Parser) parseDottedFieldName(context string) (string, error) {
	if p.curToken().Type != TokenIdentifier && p.curToken().Type != TokenKeyword {
		return "", newValidationError("SOQL", fmt.Sprintf("expected field name in %s, got %q (type %d)", context, p.curToken().Literal, p.curToken().Type))
	}
	field := p.curToken().Literal
	for p.peekToken().Literal == "." {
		p.nextToken() // consume "."
		p.nextToken() // consume next segment
		if p.curToken().Type != TokenIdentifier && p.curToken().Type != TokenKeyword {
			return "", newValidationError("SOQL", fmt.Sprintf("expected field segment after '.' in %s, got %q (type %d)", context, p.curToken().Literal, p.curToken().Type))
		}
		field += "." + p.curToken().Literal
	}
	return field, nil
}

// maxCallDepth is the maximum nesting depth for function calls in WHERE
// clause expressions, preventing stack overflow from malicious input.
const maxCallDepth = 10

// Precedence levels
const (
	_ int = iota
	LOWEST
	OR
	AND
	EQUALS      // =, !=, <>, LIKE, IN, NOT IN
	LESSGREATER // >, <, >=, <=
	PREFIX      // NOT, -
	CALL
)

var precedences = map[string]int{
	"OR":       OR,
	"AND":      AND,
	"=":        EQUALS,
	"!=":       EQUALS,
	"<>":       EQUALS,
	"LIKE":     EQUALS,
	"NOT LIKE": EQUALS,
	"IN":       EQUALS,
	"NOT IN":   EQUALS,
	"<":        LESSGREATER,
	">":        LESSGREATER,
	"<=":       LESSGREATER,
	">=":       LESSGREATER,
}

type (
	prefixParseFn func() (Expr, error)
	infixParseFn  func(Expr, string) (Expr, error)
)

type Parser struct {
	tokens []Token
	pos    int

	callDepth       int  // current function-call nesting depth in WHERE expressions
	semijoinDepth   int  // current `IN (SELECT...)` nesting depth; first cut allows max 1
	inChildSubquery bool // true while parsing a parent-child SubQueryRequest field; semi-join is rejected in that context because only top-level WHERE carries semi-join placeholders
	inHavingClause  bool // true while parsing a HAVING expression; semi-join is rejected because placeholder substitution is only supported on top-level WHERE

	semiJoinClauses []SemiJoinClause // accumulated during WHERE parse, lifted onto QueryRequest at Parse() exit

	prefixParseFns map[TokenType]prefixParseFn
	infixParseFns  map[string]infixParseFn
}

const maxSemijoinDepth = 1

func NewParser(l *Lexer) *Parser {
	p := &Parser{
		prefixParseFns: make(map[TokenType]prefixParseFn),
		infixParseFns:  make(map[string]infixParseFn),
	}

	// Buffer all tokens
	for {
		tok := l.NextToken()
		p.tokens = append(p.tokens, tok)
		if tok.Type == TokenEOF {
			break
		}
	}

	// Pre-process NOT IN and NOT LIKE
	for i := 0; i < len(p.tokens)-1; i++ {
		lit := strings.ToUpper(p.tokens[i].Literal)
		if lit == "NOT" {
			nextLit := strings.ToUpper(p.tokens[i+1].Literal)
			if nextLit == "IN" || nextLit == "LIKE" {
				p.tokens[i].Literal = lit + " " + nextLit
				p.tokens[i].Type = TokenOperator
				p.tokens = append(p.tokens[:i+1], p.tokens[i+2:]...)
			}
		}
	}

	p.registerPrefix(TokenIdentifier, p.parseIdentifier)
	p.registerPrefix(TokenString, p.parseStringLiteral)
	p.registerPrefix(TokenNumber, p.parseNumberLiteral)
	p.registerPrefix(TokenDateTime, p.parseDateTimeLiteral)
	p.registerPrefix(TokenRelativeDate, func() (Expr, error) {
		value := p.curToken().Literal
		if _, err := relativeDateDays(value); err != nil {
			return nil, err
		}
		return &RelativeDateLiteral{Value: value}, nil
	})
	p.registerPrefix(TokenError, p.parseErrorToken)
	p.registerPrefix(TokenKeyword, p.parseKeywordPrefix) // TRUE, FALSE, NULL, NOT
	p.registerPrefix(TokenOperator, p.parsePrefixExpression)
	p.registerPrefix(TokenPunctuation, p.parsePunctuationPrefix)

	p.registerInfixOp("=", p.parseInfixExpression)
	p.registerInfixOp("!=", p.parseInfixExpression)
	p.registerInfixOp("<>", p.parseInfixExpression)
	p.registerInfixOp("<", p.parseInfixExpression)
	p.registerInfixOp(">", p.parseInfixExpression)
	p.registerInfixOp("<=", p.parseInfixExpression)
	p.registerInfixOp(">=", p.parseInfixExpression)
	p.registerInfixOp("LIKE", p.parseInfixExpression)
	p.registerInfixOp("NOT LIKE", p.parseInfixExpression)
	p.registerInfixOp("IN", p.parseInfixExpression)
	p.registerInfixOp("NOT IN", p.parseInfixExpression)
	p.registerInfixOp("AND", p.parseInfixExpression)
	p.registerInfixOp("OR", p.parseInfixExpression)

	return p
}

func (p *Parser) registerPrefix(tokenType TokenType, fn prefixParseFn) {
	p.prefixParseFns[tokenType] = fn
}

func (p *Parser) registerInfixOp(op string, fn infixParseFn) {
	p.infixParseFns[strings.ToUpper(op)] = fn
}

func (p *Parser) curToken() Token {
	if p.pos >= len(p.tokens) {
		return p.tokens[len(p.tokens)-1]
	}
	return p.tokens[p.pos]
}

func (p *Parser) peekToken() Token {
	if p.pos+1 >= len(p.tokens) {
		return p.tokens[len(p.tokens)-1]
	}
	return p.tokens[p.pos+1]
}

func (p *Parser) nextToken() {
	if p.pos < len(p.tokens) {
		p.pos++
	}
}

func (p *Parser) curPrecedence() int {
	op := strings.ToUpper(p.curToken().Literal)
	if pr, ok := precedences[op]; ok {
		return pr
	}
	return LOWEST
}

func (p *Parser) peekPrecedence() int {
	op := strings.ToUpper(p.peekToken().Literal)
	if pr, ok := precedences[op]; ok {
		return pr
	}
	return LOWEST
}

func (p *Parser) expectPeek(t TokenType, literalUpper string) bool {
	if p.peekToken().Type == t && (literalUpper == "" || strings.ToUpper(p.peekToken().Literal) == literalUpper) {
		p.nextToken()
		return true
	}
	return false
}

func (p *Parser) Parse() (*QueryRequest, error) {
	req := &QueryRequest{}

	if strings.ToUpper(p.curToken().Literal) != "SELECT" {
		return nil, newValidationError("SOQL", "expected SELECT")
	}

	fields, subQueries, err := p.parseSelectFields()
	if err != nil {
		return nil, err
	}
	req.SelectFields = fields
	req.SubQueries = subQueries

	if strings.ToUpper(p.peekToken().Literal) != "FROM" {
		return nil, newValidationError("SOQL", "expected FROM")
	}
	p.nextToken() // consume FROM

	p.nextToken()
	if p.curToken().Type != TokenIdentifier && p.curToken().Type != TokenKeyword {
		return nil, newValidationError("SOQL", "expected Object Name after FROM")
	}
	req.ObjectAPIName = p.curToken().Literal
	if strings.ToUpper(req.ObjectAPIName) == "GROUP" {
		req.ObjectAPIName = "Group" // Titlecase edge case
	}

	// Parse optional clauses
	for p.peekToken().Type != TokenEOF {
		nextLit := strings.ToUpper(p.peekToken().Literal)
		switch nextLit {
		case "WHERE":
			p.nextToken() // consume WHERE
			p.nextToken() // step into expr
			expr, err := p.parseExpression(LOWEST)
			if err != nil {
				return nil, err
			}
			if expr != nil {
				req.Filter, err = predicateFromExpr(expr)
				if err != nil {
					return nil, err
				}
			}
		case "GROUP":
			p.nextToken()
			if !p.expectPeek(TokenKeyword, "BY") {
				return nil, newValidationError("SOQL", "expected BY after GROUP")
			}
			p.nextToken()
			groupBy, err := p.parseDottedFieldName("GROUP BY")
			if err != nil {
				return nil, err
			}
			req.GroupBy = groupBy
		case "HAVING":
			p.nextToken() // HAVING
			p.nextToken()
			p.inHavingClause = true
			expr, err := p.parseExpression(LOWEST)
			p.inHavingClause = false
			if err != nil {
				return nil, err
			}
			if expr != nil {
				req.HavingPredicate, err = predicateFromExpr(expr)
				if err != nil {
					return nil, err
				}
			}
		case "ORDER":
			p.nextToken()
			if !p.expectPeek(TokenKeyword, "BY") {
				return nil, newValidationError("SOQL", "expected BY after ORDER")
			}
			p.nextToken()
			for {
				if p.curToken().Type == TokenEOF {
					return nil, newValidationError("SOQL", "expected Field Name after ORDER BY")
				}
				field := p.curToken().Literal
				// The current neutral sort contract owns field paths only. Do not
				// accept a product expression that cannot cross that boundary.
				if p.peekToken().Literal == "(" {
					return nil, newValidationError("SOQL", fmt.Sprintf("function expressions are not supported in ORDER BY: %s", field))
				} else {
					var err error
					field, err = p.parseDottedFieldName("ORDER BY")
					if err != nil {
						return nil, err
					}
				}
				dir := "ASC" // default
				if strings.ToUpper(p.peekToken().Literal) == "ASC" || strings.ToUpper(p.peekToken().Literal) == "DESC" {
					p.nextToken()
					dir = strings.ToUpper(p.curToken().Literal)
				}
				if req.SortField == "" {
					req.SortField = field
					req.SortDirection = dir
				}
				req.OrderBy = append(req.OrderBy, SortCriterion{Field: field, Direction: dir})

				if p.peekToken().Literal == "," {
					p.nextToken() // consume comma
					p.nextToken() // consume next field
				} else {
					break
				}
			}
		case "LIMIT":
			p.nextToken()
			if !p.expectPeek(TokenNumber, "") {
				return nil, newValidationError("SOQL", fmt.Sprintf("expected Number after LIMIT, got %s", p.curToken().Literal))
			}
			limit, err := strconv.Atoi(p.curToken().Literal)
			if err != nil {
				return nil, newValidationError("SOQL", fmt.Sprintf("invalid LIMIT: %s", p.curToken().Literal))
			}
			req.Limit = limit
		case "OFFSET":
			p.nextToken()
			if !p.expectPeek(TokenNumber, "") {
				return nil, newValidationError("SOQL", fmt.Sprintf("expected Number after OFFSET, got %s", p.curToken().Literal))
			}
			offset, err := strconv.Atoi(p.curToken().Literal)
			if err != nil {
				return nil, newValidationError("SOQL", fmt.Sprintf("invalid OFFSET: %s", p.curToken().Literal))
			}
			req.Offset = offset
		default:
			// Unexpected
			return req, newValidationError("SOQL", fmt.Sprintf("unexpected token in query: %s", nextLit))
		}
	}

	if len(p.semiJoinClauses) > 0 {
		req.SemiJoins = p.semiJoinClauses
	}

	return req, nil
}

func (p *Parser) parseSelectFields() ([]string, []SubQueryRequest, error) {
	var fields []string
	var subQueries []SubQueryRequest
	for {
		p.nextToken() // Consume field
		if p.curToken().Type == TokenEOF {
			return nil, nil, newValidationError("SOQL", "unexpected EOF in field list")
		}

		lit := strings.ToUpper(p.curToken().Literal)
		if lit == "SELECT" || lit == "FROM" {
			return nil, nil, newValidationError("SOQL", fmt.Sprintf("unexpected keyword in select fields: %s", p.curToken().Literal))
		}

		// Check for subquery: current token is "(" and next is "SELECT"
		if p.curToken().Literal == "(" && strings.ToUpper(p.peekToken().Literal) == "SELECT" {
			sq, err := p.parseSubQuery()
			if err != nil {
				return nil, nil, err
			}
			subQueries = append(subQueries, sq)

			if p.peekToken().Literal == "," {
				p.nextToken() // consume comma
			} else {
				break
			}
			continue
		}

		field, err := p.parseDottedFieldName("SELECT")
		if err != nil {
			return nil, nil, err
		}

		// Check for Function Call syntax: Name(...)
		if p.peekToken().Literal == "(" {
			if !isSupportedSelectionFunction(field) {
				return nil, nil, newValidationError("SOQL", fmt.Sprintf("unrecognized function in SELECT: %s", field))
			}
			field, err = p.parseSelectionFunction(field)
			if err != nil {
				return nil, nil, err
			}
		}

		// Check if there's an alias (next token is identifier and not FROM)
		if p.peekToken().Type == TokenIdentifier && strings.ToUpper(p.peekToken().Literal) != "FROM" && strings.ToUpper(p.peekToken().Literal) != "AS" {
			p.nextToken() // consume alias
			field += " " + p.curToken().Literal
		} else if strings.ToUpper(p.peekToken().Literal) == "AS" {
			p.nextToken() // consume AS
			p.nextToken() // consume alias
			field += " " + p.curToken().Literal
		}

		fields = append(fields, field)

		if p.peekToken().Literal == "," {
			p.nextToken() // consume comma
		} else {
			break
		}
	}
	return fields, subQueries, nil
}

// parseSubQuery parses a subquery like (SELECT Id, Name FROM Contacts WHERE Active = true ORDER BY Name LIMIT 10)
// Current position: on "(", peek is "SELECT"
func (p *Parser) parseSubQuery() (SubQueryRequest, error) {
	sq := SubQueryRequest{}

	// Mark the parser context so semi-join inside this child subquery's
	// WHERE is rejected fail-closed (semi-join placeholder substitution is only
	// supported on the top-level WHERE predicate).
	prev := p.inChildSubquery
	p.inChildSubquery = true
	defer func() { p.inChildSubquery = prev }()

	// Consume "(" — we're already on it
	// Move to SELECT
	p.nextToken()
	// Now on SELECT, parse inner select fields (flat only, no nested subqueries)
	var innerFields []string
	for {
		p.nextToken() // consume field token
		if p.curToken().Type == TokenEOF {
			return sq, newValidationError("SOQL", "unexpected EOF in subquery field list")
		}
		upperLit := strings.ToUpper(p.curToken().Literal)
		if upperLit == "FROM" {
			break
		}

		field, err := p.parseDottedFieldName("subquery SELECT")
		if err != nil {
			return sq, err
		}

		// Check for Function Call syntax: Name(...)
		if p.peekToken().Literal == "(" {
			return sq, newValidationError("SOQL", fmt.Sprintf("function expressions are not supported in subquery SELECT: %s", field))
		}

		// Check if there's an alias (next token is identifier and not FROM)
		if p.peekToken().Type == TokenIdentifier && strings.ToUpper(p.peekToken().Literal) != "FROM" && strings.ToUpper(p.peekToken().Literal) != "AS" {
			p.nextToken() // consume alias
			field += " " + p.curToken().Literal
		} else if strings.ToUpper(p.peekToken().Literal) == "AS" {
			p.nextToken() // consume AS
			p.nextToken() // consume alias
			field += " " + p.curToken().Literal
		}

		innerFields = append(innerFields, field)

		if p.peekToken().Literal == "," {
			p.nextToken() // consume comma
		} else if strings.ToUpper(p.peekToken().Literal) == "FROM" {
			// next iteration will consume FROM and break
		}
	}
	sq.SelectFields = innerFields

	// Current token is FROM, consume relationship name
	p.nextToken()
	if p.curToken().Type != TokenIdentifier && p.curToken().Type != TokenKeyword {
		return sq, newValidationError("SOQL", fmt.Sprintf("expected relationship name after FROM in subquery, got %s", p.curToken().Literal))
	}
	sq.RelationshipName = p.curToken().Literal

	// Parse optional WHERE, ORDER BY, LIMIT (stop at ")")
	for p.peekToken().Literal != ")" && p.peekToken().Type != TokenEOF {
		nextLit := strings.ToUpper(p.peekToken().Literal)
		switch nextLit {
		case "WHERE":
			p.nextToken() // consume WHERE
			p.nextToken() // step into expr
			expr, err := p.parseExpression(LOWEST)
			if err != nil {
				return sq, err
			}
			if expr != nil {
				sq.Filter, err = predicateFromExpr(expr)
				if err != nil {
					return sq, err
				}
			}
		case "ORDER":
			p.nextToken() // consume ORDER
			if !p.expectPeek(TokenKeyword, "BY") {
				return sq, newValidationError("SOQL", "expected BY after ORDER in subquery")
			}
			p.nextToken() // consume first sort field
			for {
				if p.curToken().Type == TokenEOF {
					return sq, newValidationError("SOQL", "expected field after ORDER BY in subquery")
				}
				field := p.curToken().Literal
				// Child-query sorts have the same field-path-only neutral shape.
				if p.peekToken().Literal == "(" {
					return sq, newValidationError("SOQL", fmt.Sprintf("function expressions are not supported in subquery ORDER BY: %s", field))
				} else {
					var err error
					field, err = p.parseDottedFieldName("subquery ORDER BY")
					if err != nil {
						return sq, err
					}
				}
				dir := "ASC"
				if strings.ToUpper(p.peekToken().Literal) == "ASC" || strings.ToUpper(p.peekToken().Literal) == "DESC" {
					p.nextToken()
					dir = strings.ToUpper(p.curToken().Literal)
				}
				sq.OrderBy = append(sq.OrderBy, SortCriterion{Field: field, Direction: dir})

				if p.peekToken().Literal == "," {
					p.nextToken() // consume comma
					p.nextToken() // consume next field
				} else {
					break
				}
			}
		case "LIMIT":
			p.nextToken() // consume LIMIT
			if !p.expectPeek(TokenNumber, "") {
				return sq, newValidationError("SOQL", fmt.Sprintf("expected number after LIMIT in subquery, got %s", p.curToken().Literal))
			}
			limit, err := strconv.Atoi(p.curToken().Literal)
			if err != nil {
				return sq, newValidationError("SOQL", fmt.Sprintf("invalid LIMIT in subquery: %s", p.curToken().Literal))
			}
			sq.Limit = limit
		default:
			return sq, newValidationError("SOQL", fmt.Sprintf("unexpected token in subquery: %s", p.peekToken().Literal))
		}
	}

	// Consume closing ")"
	if p.peekToken().Literal != ")" {
		return sq, newValidationError("SOQL", "expected ')' at end of subquery")
	}
	p.nextToken() // consume )

	return sq, nil
}

// Pratt Parser Methods

func (p *Parser) parseExpression(precedence int) (Expr, error) {
	prefix := p.prefixParseFns[p.curToken().Type]
	if prefix == nil {
		return nil, newValidationError("SOQL", fmt.Sprintf("no prefix parse function for %d (%s)", p.curToken().Type, p.curToken().Literal))
	}
	leftExp, err := prefix()
	if err != nil {
		return nil, err
	}

	for p.peekToken().Type != TokenEOF && precedence < p.peekPrecedence() {
		op := strings.ToUpper(p.peekToken().Literal)
		infix := p.infixParseFns[op]
		if infix == nil {
			return leftExp, nil
		}
		p.nextToken()
		leftExp, err = infix(leftExp, op)
		if err != nil {
			return nil, err
		}
	}

	return leftExp, nil
}

func (p *Parser) parseIdentifier() (Expr, error) {
	val := p.curToken().Literal
	for p.peekToken().Literal == "." {
		p.nextToken()
		val += "."
		p.nextToken()
		val += p.curToken().Literal
	}
	// It could be a function call
	if p.peekToken().Literal == "(" {
		p.nextToken() // consume (
		return p.parseCallExpression(val)
	}
	return &Identifier{Value: val}, nil
}

func (p *Parser) parseCallExpression(function string) (Expr, error) {
	p.callDepth++
	defer func() { p.callDepth-- }()
	if p.callDepth > maxCallDepth {
		return nil, newValidationError("SOQL", "maximum function call nesting depth exceeded")
	}
	expr := &CallExpr{Function: function, Args: []Expr{}}
	if p.peekToken().Literal == ")" {
		p.nextToken() // consume )
		return expr, nil
	}
	p.nextToken() // move to first arg
	args, err := p.parseExpressionList()
	if err != nil {
		return nil, err
	}
	expr.Args = args
	return expr, nil
}

func (p *Parser) parseStringLiteral() (Expr, error) {
	return &StringLiteral{Value: p.curToken().Literal}, nil
}

func (p *Parser) parseNumberLiteral() (Expr, error) {
	return &NumberLiteral{Value: p.curToken().Literal}, nil
}

func (p *Parser) parseDateTimeLiteral() (Expr, error) {
	return &DateTimeLiteral{Value: p.curToken().Literal}, nil
}

func (p *Parser) parseErrorToken() (Expr, error) {
	return nil, newValidationError("SOQL", fmt.Sprintf("invalid date/datetime literal: %s", p.curToken().Literal))
}

func (p *Parser) parseKeywordPrefix() (Expr, error) {
	lit := strings.ToUpper(p.curToken().Literal)
	if lit == "TRUE" {
		return &BooleanLiteral{Value: true}, nil
	}
	if lit == "FALSE" {
		return &BooleanLiteral{Value: false}, nil
	}
	if lit == "NULL" {
		return &NullLiteral{}, nil
	}
	if lit == "NOT" {
		return p.parsePrefixExpression()
	}
	return nil, newValidationError("SOQL", fmt.Sprintf("unexpected keyword in expression: %s", lit))
}

func (p *Parser) parsePrefixExpression() (Expr, error) {
	expr := &UnaryExpr{Operator: p.curToken().Literal}
	p.nextToken()
	right, err := p.parseExpression(PREFIX)
	if err != nil {
		return nil, err
	}
	expr.Right = right
	return expr, nil
}

func (p *Parser) parsePunctuationPrefix() (Expr, error) {
	if p.curToken().Literal == "(" {
		p.nextToken()
		expr, err := p.parseExpression(LOWEST)
		if err != nil {
			return nil, err
		}
		if p.peekToken().Literal != ")" {
			return nil, newValidationError("SOQL", "expected ')'")
		}
		p.nextToken() // consume )
		return &ParenExpr{Expr: expr}, nil
	}
	return nil, newValidationError("SOQL", fmt.Sprintf("unexpected punctuation: %s", p.curToken().Literal))
}

func (p *Parser) parseInfixExpression(left Expr, op string) (Expr, error) {
	expr := &BinaryExpr{
		Left:     left,
		Operator: op,
	}
	precedence := precedences[op]

	// Special case for IN and NOT IN -> requires ListLiteral or semi-join (SELECT ...)
	if op == "IN" || op == "NOT IN" {
		if p.peekToken().Literal != "(" {
			return nil, newValidationError("SOQL", fmt.Sprintf("expected '(' after %s", op))
		}
		p.nextToken() // consume (
		p.nextToken() // move to first element

		// Semi-join: `field IN (SELECT field FROM Object [WHERE ...])`.
		// Gate on Type == TokenKeyword so a literal IN list whose first element
		// is the string 'SELECT' (e.g. `WHERE Name IN ('SELECT', 'Other')`)
		// goes to parseExpressionList, not parseSemiJoinSubquery. The lexer
		// strips quotes from string literals, so checking Literal alone matches
		// the literal value 'SELECT' and breaks valid SOQL — see
		// TestCodexR5Probe_StringLiteralSelectInINList.
		if p.curToken().Type == TokenKeyword && strings.ToUpper(p.curToken().Literal) == "SELECT" {
			placeholder, err := p.parseSemiJoinSubquery(left, op == "NOT IN")
			if err != nil {
				return nil, err
			}
			expr.Right = placeholder
			return expr, nil
		}

		if p.curToken().Literal == ")" {
			expr.Right = &ListLiteral{Elements: []Expr{}}
		} else {
			elements, err := p.parseExpressionList()
			if err != nil {
				return nil, err
			}
			expr.Right = &ListLiteral{Elements: elements}
		}
		return expr, nil
	}

	p.nextToken()
	right, err := p.parseExpression(precedence)
	if err != nil {
		return nil, err
	}
	expr.Right = right
	return expr, nil
}

// parseSemiJoinSubquery parses `(SELECT field FROM Object [WHERE ...] [ORDER BY ...] [LIMIT N])`
// when current token is SELECT (caller already consumed `(` and stepped onto SELECT).
// Registers the inner query against the parser's semiJoinClauses, returns a
// SemiJoinPlaceholder whose index matches the registration. Consumes through and
// including the closing `)`. The outer BinaryExpr's Right is set to the placeholder
// by the caller.
func (p *Parser) parseSemiJoinSubquery(left Expr, negate bool) (Expr, error) {
	ident, ok := left.(*Identifier)
	if !ok {
		return nil, newValidationError("SOQL", "semi-join LHS must be a field identifier")
	}

	// Resolver substitutes placeholders only for top-level WHERE, not
	// parent-child subquery WHERE or HAVING.
	// Allowing a semi-join in those contexts would leak the placeholder
	// past the resolver and produce wrong rows silently.
	if p.inChildSubquery {
		return nil, newValidationError("SOQL", "semi-join (IN (SELECT ...)) is not supported inside parent-child subqueries")
	}
	if p.inHavingClause {
		return nil, newValidationError("SOQL", "semi-join (IN (SELECT ...)) is not supported in HAVING clauses")
	}

	if p.semijoinDepth >= maxSemijoinDepth {
		return nil, newValidationError("SOQL", "nested semi-join subqueries are not supported")
	}
	p.semijoinDepth++
	defer func() { p.semijoinDepth-- }()

	inner := &QueryRequest{}

	// Inner SELECT field list (single field per first-cut)
	for {
		p.nextToken() // consume field token (first iteration steps off SELECT)
		if p.curToken().Type == TokenEOF {
			return nil, newValidationError("SOQL", "unexpected EOF in semi-join SELECT")
		}
		if strings.ToUpper(p.curToken().Literal) == "FROM" {
			break
		}
		if p.curToken().Literal == "," {
			return nil, newValidationError("SOQL", "semi-join subquery must select exactly one field")
		}

		field, err := p.parseDottedFieldName("semi-join SELECT")
		if err != nil {
			return nil, err
		}
		inner.SelectFields = append(inner.SelectFields, field)
		if len(inner.SelectFields) > 1 {
			return nil, newValidationError("SOQL", "semi-join subquery must select exactly one field")
		}
	}
	if len(inner.SelectFields) != 1 {
		return nil, newValidationError("SOQL", "semi-join subquery must select exactly one field")
	}

	// FROM Object
	p.nextToken()
	if p.curToken().Type != TokenIdentifier && p.curToken().Type != TokenKeyword {
		return nil, newValidationError("SOQL", fmt.Sprintf("expected object name after FROM in semi-join, got %s", p.curToken().Literal))
	}
	inner.ObjectAPIName = p.curToken().Literal
	if strings.ToUpper(inner.ObjectAPIName) == "GROUP" {
		inner.ObjectAPIName = "Group"
	}

	// Optional WHERE / ORDER BY / LIMIT — terminator is `)`
	for p.peekToken().Literal != ")" && p.peekToken().Type != TokenEOF {
		nextLit := strings.ToUpper(p.peekToken().Literal)
		switch nextLit {
		case "WHERE":
			p.nextToken() // consume WHERE
			p.nextToken() // step into expr
			expr, err := p.parseExpression(LOWEST)
			if err != nil {
				return nil, err
			}
			if expr != nil {
				inner.Filter, err = predicateFromExpr(expr)
				if err != nil {
					return nil, err
				}
			}
		case "ORDER":
			// ORDER BY in semi-join inner queries is intentionally rejected.
			// The persistence layer (query_repository.go:165-167) honors only
			// single-column SortField/SortDirection, not the OrderBy array
			// the parser would populate here. Combined with LIMIT, an
			// accepted-but-dropped ORDER BY would silently return wrong
			// outer rows (LIMIT applied to an unordered set). Fail-closed at
			// parse time keeps the inner-query contract narrow and explicit;
			// see ResolveSemiJoins's inner-contract assertion in
			// pkg/soql/resolver.go for the runtime backstop.
			// Full ORDER BY support tracked under issue #186.
			return nil, newValidationError("SOQL", "ORDER BY in semi-join inner queries is not supported")
		case "LIMIT":
			p.nextToken()
			if !p.expectPeek(TokenNumber, "") {
				return nil, newValidationError("SOQL", fmt.Sprintf("expected number after LIMIT in semi-join, got %s", p.curToken().Literal))
			}
			limit, err := strconv.Atoi(p.curToken().Literal)
			if err != nil {
				return nil, newValidationError("SOQL", fmt.Sprintf("invalid LIMIT in semi-join: %s", p.curToken().Literal))
			}
			inner.Limit = limit
		default:
			return nil, newValidationError("SOQL", fmt.Sprintf("unexpected token in semi-join: %s", p.peekToken().Literal))
		}
	}

	if p.peekToken().Literal != ")" {
		return nil, newValidationError("SOQL", "expected ')' at end of semi-join subquery")
	}
	p.nextToken() // consume )

	idx := len(p.semiJoinClauses)
	p.semiJoinClauses = append(p.semiJoinClauses, SemiJoinClause{
		OuterField: ident.Value,
		Negate:     negate,
		InnerQuery: inner,
		Index:      idx,
	})

	return &SemiJoinPlaceholder{Index: idx}, nil
}

func (p *Parser) parseExpressionList() ([]Expr, error) {
	var list []Expr

	exp, err := p.parseExpression(LOWEST)
	if err != nil {
		return nil, err
	}
	list = append(list, exp)

	for p.peekToken().Literal == "," {
		p.nextToken() // consume ,
		p.nextToken() // consume next expr
		exp, err := p.parseExpression(LOWEST)
		if err != nil {
			return nil, err
		}
		list = append(list, exp)
	}

	if p.peekToken().Literal != ")" {
		return nil, newValidationError("SOQL", "expected ')' at end of list")
	}
	p.nextToken() // consume )
	return list, nil
}
