package soql

import (
	"context"
	"fmt"
	"regexp"
	"strings"

	"github.com/nexuscrm/shared/pkg/models"
	"github.com/nexuscrm/shared/pkg/queryspec"
)

// MaxSemiJoinResultIDs caps how many IDs a single semi-join inner query may
// return before the outer query is rejected. Mirrors Salesforce's documented
// semi-join cap; protects the rewritten IN-list from unbounded growth.
const MaxSemiJoinResultIDs = 1000

// SemiJoinResolver executes a parsed semi-join inner QueryRequest and returns
// the projected IDs (extracted from the inner.SelectFields[0] column). The
// caller adapts whatever record-execution stack it has into this interface.
type SemiJoinResolver interface {
	ResolveSemiJoinIDs(ctx context.Context, inner *models.QueryRequest) ([]string, error)
}

// assertInnerQueryContract enforces that a semi-join inner query only uses the
// narrow QueryRequest features the resolver is known to dispatch correctly
// through the downstream QueryService → repository pipeline.
//
// History: every silent-data-loss bug in this PR's review history (R1..R3)
// was an instance of "parser populates inner.X, downstream pipeline drops
// or mishandles X, resolver returns wrong outer rows." Examples:
//
//   - inner.Limit=0 → repo applies DefaultSOSLLimit=500 (silent truncation)
//   - inner.OrderBy → repo honors only single-column SortField (silent drop)
//   - inner.SortField → not previously populated, but the same risk class
//   - inner.GroupBy / Having / SubQueries / Criteria / Offset / MRUUserID
//     → no integration audit; safer to fail-closed than to assume support
//
// Each of those was caught reactively after a review round flagged it.
// Codifying the contract here means any FUTURE parser change that fills a
// non-supported field will trip this assertion at runtime, turning what
// would otherwise be silent wrong data into a loud InternalError. To
// extend support for any of these fields, audit the downstream pipeline
// integration and remove the assertion for that specific field with a
// dedicated test for the integration path.
func assertInnerQueryContract(inner *models.QueryRequest) error {
	if inner == nil {
		return fmt.Errorf("inner QueryRequest is nil")
	}
	if len(inner.SelectFields) != 1 {
		return fmt.Errorf("inner.SelectFields must have exactly one element, got %d", len(inner.SelectFields))
	}
	if inner.SortField != "" || inner.SortDirection != "" || len(inner.OrderBy) > 0 {
		return fmt.Errorf("inner ORDER BY / SortField is not supported by the resolver pipeline")
	}
	if inner.GroupBy != "" || inner.Having != "" {
		return fmt.Errorf("inner GROUP BY / HAVING is not supported by the resolver pipeline")
	}
	if len(inner.SubQueries) > 0 {
		return fmt.Errorf("inner parent-child subqueries are not supported by the resolver pipeline")
	}
	if len(inner.Criteria) > 0 {
		return fmt.Errorf("inner structured Criteria is not supported by the resolver pipeline")
	}
	if inner.Offset > 0 {
		return fmt.Errorf("inner OFFSET is not supported by the resolver pipeline")
	}
	if inner.MRUUserID != "" {
		return fmt.Errorf("inner MRU sort is not supported by the resolver pipeline")
	}
	if len(inner.SemiJoins) > 0 {
		return fmt.Errorf("inner nested semi-joins are not supported (depth guard should have caught this earlier)")
	}
	return nil
}

// ResolveSemiJoins executes every semi-join inner query attached to req and
// rewrites req.Filter by substituting the placeholders with the literal IDs (or
// with the appropriate boolean constant when the inner returns 0). FilterExpr is
// still updated when a legacy compatibility caller supplied one, but the parser
// no longer treats it as the authoritative predicate fact.
func ResolveSemiJoins(ctx context.Context, req *models.QueryRequest, resolver SemiJoinResolver) error {
	if req == nil {
		return nil
	}

	if len(req.SemiJoins) > 0 {
		if resolver == nil {
			return newInternalError("semi-join resolver is required when QueryRequest carries SemiJoins", nil)
		}
		for i, clause := range req.SemiJoins {
			if clause.Index != i {
				return newInternalError(fmt.Sprintf("semi-join clause index drift: clause[%d].Index=%d", i, clause.Index), nil)
			}
			// Inner-query contract: the resolver only knows how to dispatch a
			// narrow subset of QueryRequest fields through QueryService.Query
			// reliably (ObjectAPIName, Filter/FilterExpr, exactly one SelectField,
			// Limit, ScanAll). Any other field set by a future parser change
			// (OrderBy / SortField / GroupBy / Having / SubQueries / Criteria
			// / Offset / MRUUserID) interacts with the downstream pipeline in
			// ways the resolver hasn't validated and historically the failures
			// have been silent (e.g. OrderBy is honored as a single SortField
			// only, so an OrderBy array is silently dropped). Fail-closed
			// here so any such drift surfaces as an explicit error instead of
			// returning wrong rows.
			if err := assertInnerQueryContract(clause.InnerQuery); err != nil {
				return newInternalError(fmt.Sprintf("semi-join clause[%d] (OuterField=%q): %v", i, clause.OuterField, err), nil)
			}
			if err := assertSemiJoinPlaceholderContract(req, clause); err != nil {
				return newInternalError(fmt.Sprintf("semi-join clause[%d] (OuterField=%q): %v", i, clause.OuterField, err), nil)
			}

			// Salesforce v63 parity: queryAll's include-deleted semantics extend
			// to subqueries. Without this propagation the inner query silently
			// excludes soft-deleted rows, producing wrong outer results on
			// /queryAll endpoints.
			clause.InnerQuery.ScanAll = req.ScanAll

			// Override the persistence-layer default of 500 (DefaultSOSLLimit)
			// when the SOQL author didn't set one. We ask for one more than the
			// cap so the post-fetch check below detects overflow rather than
			// the persistence layer truncating silently.
			if clause.InnerQuery.Limit == 0 {
				clause.InnerQuery.Limit = MaxSemiJoinResultIDs + 1
			}

			ids, err := resolver.ResolveSemiJoinIDs(ctx, clause.InnerQuery)
			if err != nil {
				// %w preserves the error chain so the handler's RespondError
				// can classify a PermissionError / NotFoundError / InternalError
				// from the inner query into 403 / 404 / 500 instead of the
				// resolver collapsing every inner failure to a 400 MALFORMED_QUERY
				// (which both leaks internal detail and breaks SDK retry logic
				// keyed on HTTP status — see docs/design/salesforce-v63-api-contract.md).
				return fmt.Errorf("semi-join subquery on %q failed: %w", clause.OuterField, err)
			}
			if len(ids) > MaxSemiJoinResultIDs {
				return newValidationError("SOQL", fmt.Sprintf("semi-join subquery on %q returned %d IDs, exceeding limit %d", clause.OuterField, len(ids), MaxSemiJoinResultIDs))
			}

			req.FilterExpr = substituteSemiJoinPlaceholder(req.FilterExpr, clause, ids)
			if req.Filter.Kind != queryspec.PredicateNone {
				req.Filter = substituteSemiJoinPredicate(req.Filter, clause, ids)
			}
			if err := assertSemiJoinPlaceholderResolved(req, clause); err != nil {
				return newInternalError(fmt.Sprintf("semi-join clause[%d] (OuterField=%q): %v", i, clause.OuterField, err), nil)
			}
		}
		req.SemiJoins = nil
	}

	// Defense-in-depth for legacy raw expression surfaces. Structured predicate
	// placeholder validation happens against the active SemiJoinClause above, so
	// ordinary user literals that resemble the internal token remain literals.
	if strings.Contains(req.FilterExpr, SemiJoinPlaceholderPrefix) {
		return newInternalError("unresolved semi-join placeholder remains in Filter predicate", nil)
	}
	if strings.Contains(req.Having, SemiJoinPlaceholderPrefix) {
		return newInternalError("unresolved semi-join placeholder remains in Having predicate", nil)
	}
	for i, sq := range req.SubQueries {
		if strings.Contains(sq.FilterExpr, SemiJoinPlaceholderPrefix) {
			return newInternalError(fmt.Sprintf("unresolved semi-join placeholder remains in SubQueries[%d].Filter (%q)", i, sq.RelationshipName), nil)
		}
	}

	return nil
}

// substituteSemiJoinPlaceholder replaces the parser-emitted placeholder
// expression in filterExpr. Two cases:
//
//   - inner returned at least one ID: the bracketed placeholder list
//     `['__nx_semijoin_N__']` is replaced with `['id1','id2',...]`, leaving
//     the surrounding `<field> [not] in` operator intact.
//   - inner returned zero IDs: the entire binary subexpression
//     `<field> [not] in ['__nx_semijoin_N__']` is replaced with the boolean
//     constant `false` (IN) or `true` (NOT IN) — semi-join semantics.
//
// Unknown placeholder shapes are left intact so a downstream parse error makes
// the bug visible rather than silently misexecuting.
func substituteSemiJoinPlaceholder(filterExpr string, clause models.SemiJoinClause, ids []string) string {
	bracketed := SemiJoinPlaceholderBracketed(clause.Index)
	if !strings.Contains(filterExpr, bracketed) {
		return filterExpr
	}

	if len(ids) == 0 {
		replacement := "false"
		if clause.Negate {
			replacement = "true"
		}
		if start, end, ok := rawSemiJoinPlaceholderFragment(filterExpr, clause); ok {
			return filterExpr[:start] + replacement + filterExpr[end:]
		}
		return filterExpr
	}

	var sb strings.Builder
	sb.WriteString("[")
	for i, id := range ids {
		if i > 0 {
			sb.WriteString(",")
		}
		sb.WriteString("'")
		sb.WriteString(EscapeStringValue(id))
		sb.WriteString("'")
	}
	sb.WriteString("]")
	return strings.Replace(filterExpr, bracketed, sb.String(), 1)
}

func assertSemiJoinPlaceholderContract(req *models.QueryRequest, clause models.SemiJoinClause) error {
	found := false
	if req.Filter.Kind != queryspec.PredicateNone {
		if predicateHasSemiJoinPlaceholder(req.Filter, clause) {
			found = true
		} else {
			return fmt.Errorf("structured placeholder for index %d does not match the parsed outer field/operator", clause.Index)
		}
	}

	if req.FilterExpr != "" {
		bracketed := SemiJoinPlaceholderBracketed(clause.Index)
		if strings.Contains(req.FilterExpr, bracketed) {
			if _, _, ok := rawSemiJoinPlaceholderFragment(req.FilterExpr, clause); !ok {
				return fmt.Errorf("raw placeholder for index %d does not match the parsed outer field/operator", clause.Index)
			}
			found = true
		}
	}

	if found {
		return nil
	}
	return fmt.Errorf("placeholder for index %d is missing from the Filter predicate", clause.Index)
}

func assertSemiJoinPlaceholderResolved(req *models.QueryRequest, clause models.SemiJoinClause) error {
	if req.Filter.Kind != queryspec.PredicateNone && predicateHasSemiJoinPlaceholder(req.Filter, clause) {
		return fmt.Errorf("structured placeholder for index %d remained after resolution", clause.Index)
	}
	if req.FilterExpr != "" && strings.Contains(req.FilterExpr, SemiJoinPlaceholderBracketed(clause.Index)) {
		return fmt.Errorf("raw placeholder for index %d remained after resolution", clause.Index)
	}
	return nil
}

func rawSemiJoinPlaceholderFragment(filterExpr string, clause models.SemiJoinClause) (int, int, bool) {
	bracketed := SemiJoinPlaceholderBracketed(clause.Index)
	placeholderEnd := strings.Index(filterExpr, bracketed)
	if placeholderEnd < 0 {
		return 0, 0, false
	}
	placeholderEnd += len(bracketed)

	operatorPattern := `in`
	if clause.Negate {
		operatorPattern = `not\s+in`
	}
	pattern := regexp.MustCompile(`(?i)(^|[^A-Za-z0-9_.])(` +
		regexp.QuoteMeta(clause.OuterField) + `\s+` +
		operatorPattern + `\s+` +
		regexp.QuoteMeta(bracketed) + `)`)
	matches := pattern.FindStringSubmatchIndex(filterExpr)
	if len(matches) < 6 || matches[5] != placeholderEnd {
		return 0, 0, false
	}
	return matches[4], matches[5], true
}

func predicateHasSemiJoinPlaceholder(predicate queryspec.Predicate, clause models.SemiJoinClause) bool {
	switch predicate.Kind {
	case queryspec.PredicateComparison:
		return semiJoinPredicateMatches(predicate, clause)
	case queryspec.PredicateAnd, queryspec.PredicateOr, queryspec.PredicateNot:
		for _, child := range predicate.Children {
			if predicateHasSemiJoinPlaceholder(child, clause) {
				return true
			}
		}
	}
	return false
}

func substituteSemiJoinPredicate(predicate queryspec.Predicate, clause models.SemiJoinClause, ids []string) queryspec.Predicate {
	switch predicate.Kind {
	case queryspec.PredicateAnd, queryspec.PredicateOr:
		children := make([]queryspec.Predicate, len(predicate.Children))
		for i, child := range predicate.Children {
			children[i] = substituteSemiJoinPredicate(child, clause, ids)
		}
		predicate.Children = children
		return predicate
	case queryspec.PredicateNot:
		if len(predicate.Children) == 0 {
			return predicate
		}
		children := make([]queryspec.Predicate, len(predicate.Children))
		for i, child := range predicate.Children {
			children[i] = substituteSemiJoinPredicate(child, clause, ids)
		}
		predicate.Children = children
		return predicate
	case queryspec.PredicateComparison:
		if !semiJoinPredicateMatches(predicate, clause) {
			return predicate
		}
		if len(ids) == 0 {
			if clause.Negate {
				return queryspec.ExpressionPredicate(queryspec.LiteralOperand(true))
			}
			return queryspec.ExpressionPredicate(queryspec.LiteralOperand(false))
		}
		elements := make([]queryspec.Operand, 0, len(ids))
		for _, id := range ids {
			elements = append(elements, queryspec.LiteralOperand(id))
		}
		predicate.Right = queryspec.ListOperand(elements...)
		return predicate
	default:
		return predicate
	}
}

func semiJoinPredicateMatches(predicate queryspec.Predicate, clause models.SemiJoinClause) bool {
	if predicate.Left.Kind != queryspec.OperandField || !strings.EqualFold(predicate.Left.Field, clause.OuterField) {
		return false
	}
	if clause.Negate {
		if !strings.EqualFold(predicate.Op, "not in") {
			return false
		}
	} else if !strings.EqualFold(predicate.Op, "in") {
		return false
	}
	if predicate.Right.Kind != queryspec.OperandList || len(predicate.Right.Elements) != 1 {
		return false
	}
	token, ok := predicate.Right.Elements[0].Value.(string)
	return ok && token == SemiJoinPlaceholderToken(clause.Index)
}
