//
//  Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

package infinity

import (
	"context"
	"fmt"
	"ragflow/internal/engine/types"
	"ragflow/internal/utility"
	"strings"
	"unicode/utf8"

	infinity "github.com/infiniflow/infinity-go-sdk"
)

const (
	PAGERANK_FLD = "pagerank_fea"
	TAG_FLD      = "tag_feas"
)

type SortType int

const (
	SortAsc  SortType = 0
	SortDesc SortType = 1
)

type OrderByExpr struct {
	Fields []OrderByField
}

type OrderByField struct {
	Field string
	Type  SortType
}

// fieldKeyword checks if field is a keyword field
func fieldKeyword(fieldName string) bool {
	// Treat "*_kwd" tag-like columns as keyword lists except knowledge_graph_kwd
	if fieldName == "source_id" {
		return true
	}
	if strings.HasSuffix(fieldName, "_kwd") &&
		fieldName != "knowledge_graph_kwd" &&
		fieldName != "docnm_kwd" &&
		fieldName != "important_kwd" &&
		fieldName != "question_kwd" {
		return true
	}
	return false
}

// equivalentConditionToStr converts condition dict to filter string
func equivalentConditionToStr(condition map[string]interface{}, tableColumns map[string]struct {
	Type    string
	Default interface{}
}) string {
	if len(condition) == 0 {
		return ""
	}

	var conditions []string

	for k, v := range condition {
		if !strings.HasPrefix(k, "_") {
			continue
		}
		if v == nil || v == "" {
			continue
		}

		// Handle keyword fields with filter_fulltext
		if fieldKeyword(k) {
			if listVal, isList := v.([]interface{}); isList {
				var orConds []string
				for _, item := range listVal {
					if strItem, ok := item.(string); ok {
						strItem = strings.ReplaceAll(strItem, "'", "''")
						orConds = append(orConds, fmt.Sprintf("filter_fulltext('%s', '%s')", convertMatchingField(k), strItem))
					}
				}
				if len(orConds) > 0 {
					conditions = append(conditions, "("+strings.Join(orConds, " OR ")+")")
				}
			} else if strVal, ok := v.(string); ok {
				strVal = strings.ReplaceAll(strVal, "'", "''")
				conditions = append(conditions, fmt.Sprintf("filter_fulltext('%s', '%s')", convertMatchingField(k), strVal))
			}
		} else if listVal, isList := v.([]interface{}); isList {
			// Handle IN conditions
			var inVals []string
			for _, item := range listVal {
				if strItem, ok := item.(string); ok {
					strItem = strings.ReplaceAll(strItem, "'", "''")
					inVals = append(inVals, fmt.Sprintf("'%s'", strItem))
				} else {
					inVals = append(inVals, fmt.Sprintf("%v", item))
				}
			}
			if len(inVals) > 0 {
				conditions = append(conditions, fmt.Sprintf("%s IN (%s)", k, strings.Join(inVals, ", ")))
			}
		} else if k == "must_not" {
			// Handle must_not conditions
			if mustNotMap, ok := v.(map[string]interface{}); ok {
				if existsVal, ok := mustNotMap["exists"]; ok {
					if existsField, ok := existsVal.(string); ok {
						col, colOk := tableColumns[existsField]
						if colOk && strings.Contains(strings.ToLower(col.Type), "char") {
							conditions = append(conditions, fmt.Sprintf(" %s!='' ", existsField))
						} else {
							conditions = append(conditions, fmt.Sprintf("%s!=null", existsField))
						}
					}
				}
			}
		} else if strVal, ok := v.(string); ok {
			strVal = strings.ReplaceAll(strVal, "'", "''")
			conditions = append(conditions, fmt.Sprintf("%s='%s'", k, strVal))
		} else if k == "exists" {
			if existsField, ok := v.(string); ok {
				col, colOk := tableColumns[existsField]
				if colOk && strings.Contains(strings.ToLower(col.Type), "char") {
					conditions = append(conditions, fmt.Sprintf(" %s!='' ", existsField))
				} else {
					conditions = append(conditions, fmt.Sprintf("%s!=null", existsField))
				}
			}
		} else {
			conditions = append(conditions, fmt.Sprintf("%s=%v", k, v))
		}
	}

	if len(conditions) == 0 {
		return ""
	}
	return strings.Join(conditions, " AND ")
}

// SearchRequest Infinity search request (legacy, kept for backward compatibility)
type SearchRequest struct {
	TableName   string
	ColumnNames []string
	MatchText   *MatchTextExpr
	MatchDense  *MatchDenseExpr
	Fusion      *FusionExpr
	Offset      int
	Limit       int
	Filter      map[string]interface{}
	OrderBy     *OrderByExpr
}

// SearchResponse Infinity search response
type SearchResponse struct {
	Rows  []map[string]interface{}
	Total int64
}

// MatchTextExpr text match expression
type MatchTextExpr struct {
	Fields       []string
	MatchingText string
	TopN         int
	ExtraOptions map[string]interface{}
}

// MatchDenseExpr vector match expression
type MatchDenseExpr struct {
	VectorColumnName  string
	EmbeddingData     []float64
	EmbeddingDataType string
	DistanceType      string
	TopN              int
	ExtraOptions      map[string]interface{}
}

// FusionExpr fusion expression
type FusionExpr struct {
	Method       string
	TopN         int
	Weights      []float64
	FusionParams map[string]interface{}
}

// Search executes search (supports unified engine.SearchRequest only)
func (e *infinityEngine) Search(ctx context.Context, req interface{}) (interface{}, error) {
	switch searchReq := req.(type) {
	case *types.SearchRequest:
		return e.searchUnified(ctx, searchReq)
	default:
		return nil, fmt.Errorf("invalid search request type: %T", req)
	}
}

// convertSelectFields converts field names to Infinity format
func convertSelectFields(output []string) []string {
	fieldMapping := map[string]string{
		"docnm_kwd":           "docnm",
		"title_tks":           "docnm",
		"title_sm_tks":        "docnm",
		"important_kwd":       "important_keywords",
		"important_tks":       "important_keywords",
		"question_kwd":        "questions",
		"question_tks":        "questions",
		"content_with_weight": "content",
		"content_ltks":        "content",
		"content_sm_ltks":     "content",
		"authors_tks":         "authors",
		"authors_sm_tks":      "authors",
	}

	needEmptyCount := false
	for i, field := range output {
		if field == "important_kwd" {
			needEmptyCount = true
		}
		if newField, ok := fieldMapping[field]; ok {
			output[i] = newField
		}
	}

	// Remove duplicates
	seen := make(map[string]bool)
	result := []string{}
	for _, f := range output {
		if f != "" && !seen[f] {
			seen[f] = true
			result = append(result, f)
		}
	}

	// Add id and empty count if needed
	hasID := false
	for _, f := range result {
		if f == "id" {
			hasID = true
			break
		}
	}
	if !hasID {
		result = append([]string{"id"}, result...)
	}

	if needEmptyCount {
		result = append(result, "important_kwd_empty_count")
	}

	return result
}

// isChinese checks if a string contains Chinese characters
func isChinese(s string) bool {
	for _, r := range s {
		if '\u4e00' <= r && r <= '\u9fff' {
			return true
		}
	}
	return false
}

// hasSubTokens checks if the text has sub-tokens after fine-grained tokenization
// - Returns False if len < 3
// - Returns False if text is only ASCII alphanumeric
// - Returns True otherwise (meaning there are sub-tokens)
func hasSubTokens(s string) bool {
	if utf8.RuneCountInString(s) < 3 {
		return false
	}
	isASCIIOnly := true
	for _, r := range s {
		if r > 127 {
			isASCIIOnly = false
			break
		}
	}
	if isASCIIOnly {
		// Check if it's only alphanumeric and allowed special chars
		for _, r := range s {
			if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '.' || r == '+' || r == '#' || r == '_' || r == '*' || r == '-') {
				isASCIIOnly = false
				break
			}
		}
		if isASCIIOnly {
			return false
		}
	}
	// Has sub-tokens if it's Chinese and length >= 3
	return isChinese(s)
}

// formatQuestion formats the question
// - If len < 3: returns ((query)^1.0)
// - If has sub-tokens: adds fuzzy search ((query OR "query" OR ("query"~2)^0.5)^1.0)
// - Otherwise: returns ((query)^1.0)
func formatQuestion(question string) string {
	// Trim whitespace
	question = strings.TrimSpace(question)
	fmt.Printf("[DEBUG formatQuestion] input: %q, len: %d, hasSubTokens: %v\n", question, len(question), hasSubTokens(question))

	// If no sub-tokens, use simple format
	if !hasSubTokens(question) {
		result := fmt.Sprintf("((%s)^1.0)", question)
		fmt.Printf("[DEBUG formatQuestion] simple: %s\n", result)
		return result
	}

	result := fmt.Sprintf("((%s OR \"%s\" OR (\"%s\"~2)^0.5)^1.0)", question, question, question)
	fmt.Printf("[DEBUG formatQuestion] fuzzy: %s\n", result)
	return result
}

// convertMatchingField converts field names for matching
func convertMatchingField(fieldWeightStr string) string {
	// Split on ^ to get field name
	parts := strings.Split(fieldWeightStr, "^")
	field := parts[0]

	// Field name conversion
	fieldMapping := map[string]string{
		"docnm_kwd":           "docnm@ft_docnm_rag_coarse",
		"title_tks":           "docnm@ft_docnm_rag_coarse",
		"title_sm_tks":        "docnm@ft_docnm_rag_fine",
		"important_kwd":       "important_keywords@ft_important_keywords_rag_coarse",
		"important_tks":       "important_keywords@ft_important_keywords_rag_fine",
		"question_kwd":        "questions@ft_questions_rag_coarse",
		"question_tks":        "questions@ft_questions_rag_fine",
		"content_with_weight": "content@ft_content_rag_coarse",
		"content_ltks":        "content@ft_content_rag_coarse",
		"content_sm_ltks":     "content@ft_content_rag_fine",
		"authors_tks":         "authors@ft_authors_rag_coarse",
		"authors_sm_tks":      "authors@ft_authors_rag_fine",
	}

	if newField, ok := fieldMapping[field]; ok {
		parts[0] = newField
	}

	return strings.Join(parts, "^")
}

// searchUnified handles the unified engine.SearchRequest
func (e *infinityEngine) searchUnified(ctx context.Context, req *types.SearchRequest) (*types.SearchResponse, error) {
	if len(req.IndexNames) == 0 {
		return nil, fmt.Errorf("index names cannot be empty")
	}

	// Get retrieval parameters with defaults
	topK := req.TopK
	if topK <= 0 {
		topK = 1024
	}

	pageSize := req.Size
	if pageSize <= 0 {
		pageSize = 30
	}

	offset := (req.Page - 1) * pageSize
	if offset < 0 {
		offset = 0
	}

	// Get database
	db, err := e.client.conn.GetDatabase(e.client.dbName)
	if err != nil {
		return nil, fmt.Errorf("failed to get database: %w", err)
	}

	// Determine if this is a metadata table
	isMetadataTable := false
	for _, idx := range req.IndexNames {
		if strings.HasPrefix(idx, "ragflow_doc_meta_") {
			isMetadataTable = true
			break
		}
	}

	// Build output columns
	// For metadata tables, only use: id, kb_id, meta_fields
	// For chunk tables, use all the standard fields
	var outputColumns []string
	if isMetadataTable {
		outputColumns = []string{"id", "kb_id", "meta_fields"}
	} else {
		outputColumns = []string{
			"id",
			"doc_id",
			"kb_id",
			"content",
			"content_ltks",
			"content_with_weight",
			"title_tks",
			"docnm_kwd",
			"img_id",
			"available_int",
			"important_kwd",
			"position_int",
			"page_num_int",
			"doc_type_kwd",
			"mom_id",
			"question_tks",
		}
	}
	outputColumns = convertSelectFields(outputColumns)

	// Determine if text or vector search
	hasTextMatch := req.Question != ""
	hasVectorMatch := !req.KeywordOnly && len(req.Vector) > 0

	// Determine score column
	scoreColumn := ""
	if hasTextMatch {
		scoreColumn = "SCORE"
	} else if hasVectorMatch {
		scoreColumn = "SIMILARITY"
	}

	// Add score column if needed
	if hasTextMatch || hasVectorMatch {
		if hasTextMatch {
			outputColumns = append(outputColumns, "score()")
		} else if hasVectorMatch {
			outputColumns = append(outputColumns, "similarity()")
		}
		// Add pagerank field
		outputColumns = append(outputColumns, PAGERANK_FLD)
	}

	// Remove duplicates
	outputColumns = convertSelectFields(outputColumns)

	// Build filter string
	var filterParts []string

	// For metadata tables, add kb_id filter if provided
	if isMetadataTable && len(req.KbIDs) > 0 && req.KbIDs[0] != "" {
		kbIDs := req.KbIDs
		if len(kbIDs) == 1 {
			filterParts = append(filterParts, fmt.Sprintf("kb_id = '%s'", kbIDs[0]))
		} else {
			kbIDStr := strings.Join(kbIDs, "', '")
			filterParts = append(filterParts, fmt.Sprintf("kb_id IN ('%s')", kbIDStr))
		}
	}

	// DocIDs filters by doc_id (document ID) to find all chunks belonging to a document
	// This is used by ChunkService.List() to list all chunks for a document
	if len(req.DocIDs) > 0 {
		if len(req.DocIDs) == 1 {
			filterParts = append(filterParts, fmt.Sprintf("doc_id = '%s'", req.DocIDs[0]))
		} else {
			docIDs := strings.Join(req.DocIDs, "', '")
			filterParts = append(filterParts, fmt.Sprintf("doc_id IN ('%s')", docIDs))
		}
	}

	// Only add available_int filter when there's text/vector match or AvailableInt is explicitly set
	// This matches Python's behavior where chunk_list doesn't filter by available_int
	if !isMetadataTable && (hasTextMatch || hasVectorMatch || req.AvailableInt != nil) {
		if req.AvailableInt != nil {
			filterParts = append(filterParts, fmt.Sprintf("available_int=%d", *req.AvailableInt))
		} else {
			filterParts = append(filterParts, "available_int=1")
		}
	}

	filterStr := strings.Join(filterParts, " AND ")

	// Build order_by
	var orderBy *OrderByExpr
	if req.OrderBy != "" {
		orderBy = &OrderByExpr{Fields: []OrderByField{}}
		// Parse order_by field and direction
		fields := strings.Split(req.OrderBy, ",")
		for _, field := range fields {
			field = strings.TrimSpace(field)
			if strings.HasSuffix(field, " desc") || strings.HasSuffix(field, " DESC") {
				fieldName := strings.TrimSuffix(field, " desc")
				fieldName = strings.TrimSuffix(fieldName, " DESC")
				orderBy.Fields = append(orderBy.Fields, OrderByField{Field: fieldName, Type: SortDesc})
			} else {
				orderBy.Fields = append(orderBy.Fields, OrderByField{Field: field, Type: SortAsc})
			}
		}
	}

	// rank_feature support
	var rankFeature map[string]float64
	if req.RankFeature != nil {
		rankFeature = req.RankFeature
	}

	// Results from all tables
	var allResults []map[string]interface{}
	totalHits := int64(0)

	// Search across all tables
	for _, indexName := range req.IndexNames {
		// Determine table names to search
		var tableNames []string
		if strings.HasPrefix(indexName, "ragflow_doc_meta_") {
			tableNames = []string{indexName}
		} else {
			// For each KB ID, create a table name
			kbIDs := req.KbIDs
			if len(kbIDs) == 0 {
				// If no KB IDs, use the index name directly
				kbIDs = []string{""}
			}
			for _, kbID := range kbIDs {
				if kbID == "" {
					tableNames = append(tableNames, indexName)
				} else {
					tableNames = append(tableNames, fmt.Sprintf("%s_%s", indexName, kbID))
				}
			}
		}

		// Search each table
		// 1. First try with min_match=0.3 (30%)
		// 2. If no results and has doc_id filter: search without match
		// 3. If no results and no doc_id filter: retry with min_match=0.1 (10%) and lower similarity
		minMatch := 0.3
		hasDocIDFilter := len(req.DocIDs) > 0

		for _, tableName := range tableNames {
			fmt.Printf("[DEBUG] Searching table: %s\n", tableName)
			// Try to get table
			_, err := db.GetTable(tableName)
			if err != nil {
				// Table doesn't exist, skip
				continue
			}

			// Build query for this table
			result, err := e.executeTableSearch(db, tableName, outputColumns, req.Question, req.Vector, filterStr, topK, pageSize, offset, orderBy, rankFeature, req.SimilarityThreshold, minMatch)
			if err != nil {
				// Skip this table on error
				continue
			}

			allResults = append(allResults, result.Chunks...)
			totalHits += result.Total
		}

		// If no results, try fallback strategies
		if totalHits == 0 && (hasTextMatch || hasVectorMatch) {
			fmt.Printf("[DEBUG] No results, trying fallback strategies\n")
			allResults = nil
			totalHits = 0

			if hasDocIDFilter {
				// If has doc_id filter, search without match
				fmt.Printf("[DEBUG] Retry with no match (has doc_id filter)\n")
				for _, tableName := range tableNames {
					_, err := db.GetTable(tableName)
					if err != nil {
						continue
					}
					// Search without match - pass empty question
					result, err := e.executeTableSearch(db, tableName, outputColumns, "", req.Vector, filterStr, topK, pageSize, offset, orderBy, rankFeature, req.SimilarityThreshold, 0.0)
					if err != nil {
						continue
					}
					allResults = append(allResults, result.Chunks...)
					totalHits += result.Total
				}
			} else {
				// Retry with lower min_match and similarity
				fmt.Printf("[DEBUG] Retry with min_match=0.1, similarity=0.17\n")
				lowerThreshold := 0.17
				for _, tableName := range tableNames {
					_, err := db.GetTable(tableName)
					if err != nil {
						continue
					}
					result, err := e.executeTableSearch(db, tableName, outputColumns, req.Question, req.Vector, filterStr, topK, pageSize, offset, orderBy, rankFeature, lowerThreshold, 0.1)
					if err != nil {
						continue
					}
					allResults = append(allResults, result.Chunks...)
					totalHits += result.Total
				}
			}
		}
	}

	if hasTextMatch || hasVectorMatch {
		allResults = calculateScores(allResults, scoreColumn, PAGERANK_FLD)
	}

	if hasTextMatch || hasVectorMatch {
		allResults = sortByScore(allResults, len(allResults))
	}

	// Apply threshold filter to combined results
	fmt.Printf("[DEBUG] Threshold check: SimilarityThreshold=%f, hasVectorMatch=%v, hasTextMatch=%v\n", req.SimilarityThreshold, hasVectorMatch, hasTextMatch)
	if req.SimilarityThreshold > 0 && hasVectorMatch {
		var filteredResults []map[string]interface{}
		for _, chunk := range allResults {
			score := getScore(chunk)
			chunkID := ""
			if id, ok := chunk["id"]; ok {
				chunkID = fmt.Sprintf("%v", id)
			}
			fmt.Printf("[DEBUG] Threshold filter: id=%s, score=%f, threshold=%f, pass=%v\n", chunkID, score, req.SimilarityThreshold, score >= req.SimilarityThreshold)
			if score >= req.SimilarityThreshold {
				filteredResults = append(filteredResults, chunk)
			}
		}
		fmt.Printf("[DEBUG] After threshold filter (combined): %d -> %d chunks\n", len(allResults), len(filteredResults))
		allResults = filteredResults
	}

	// Limit to pageSize
	if len(allResults) > pageSize {
		allResults = allResults[:pageSize]
	}

	return &types.SearchResponse{
		Chunks: allResults,
		Total:  totalHits,
	}, nil
}

// calculateScores calculates _score = score_column + pagerank
func calculateScores(chunks []map[string]interface{}, scoreColumn, pagerankField string) []map[string]interface{} {
	fmt.Printf("[DEBUG] calculateScores: scoreColumn=%s, pagerankField=%s\n", scoreColumn, pagerankField)
	for i := range chunks {
		score := 0.0
		if scoreVal, ok := chunks[i][scoreColumn]; ok {
			if f, ok := utility.ToFloat64(scoreVal); ok {
				score += f
				fmt.Printf("[DEBUG]   chunk[%d]: %s=%f\n", i, scoreColumn, f)
			}
		}
		if pagerankVal, ok := chunks[i][pagerankField]; ok {
			if f, ok := utility.ToFloat64(pagerankVal); ok {
				score += f
			}
		}
		chunks[i]["_score"] = score
		fmt.Printf("[DEBUG]   chunk[%d]: _score=%f\n", i, score)
	}
	return chunks
}

// sortByScore sorts by _score descending and limits
func sortByScore(chunks []map[string]interface{}, limit int) []map[string]interface{} {
	if len(chunks) == 0 {
		return chunks
	}

	// Sort by _score descending
	for i := 0; i < len(chunks)-1; i++ {
		for j := i + 1; j < len(chunks); j++ {
			scoreI := getScore(chunks[i])
			scoreJ := getScore(chunks[j])
			if scoreI < scoreJ {
				chunks[i], chunks[j] = chunks[j], chunks[i]
			}
		}
	}

	// Limit
	if len(chunks) > limit && limit > 0 {
		chunks = chunks[:limit]
	}

	return chunks
}

func getScore(chunk map[string]interface{}) float64 {
	// Check _score first
	if score, ok := chunk["_score"].(float64); ok {
		return score
	}
	if score, ok := chunk["_score"].(int); ok {
		return float64(score)
	}
	if score, ok := chunk["_score"].(int64); ok {
		return float64(score)
	}
	// Fallback to SCORE (for fusion) or SIMILARITY (for vector-only)
	if score, ok := chunk["SCORE"].(float64); ok {
		return score
	}
	if score, ok := chunk["SIMILARITY"].(float64); ok {
		return score
	}
	return 0.0
}

// executeTableSearch executes search on a single table
func (e *infinityEngine) executeTableSearch(db *infinity.Database, tableName string, outputColumns []string, question string, vector []float64, filterStr string, topK, pageSize, offset int, orderBy *OrderByExpr, rankFeature map[string]float64, similarityThreshold float64, minMatch float64) (*types.SearchResponse, error) {
	// Debug logging
	fmt.Printf("[DEBUG] executeTableSearch: question=%s, topK=%d, pageSize=%d, similarityThreshold=%f, filterStr=%s\n", question, topK, pageSize, similarityThreshold, filterStr)

	// Get table
	table, err := db.GetTable(tableName)
	if err != nil {
		return nil, err
	}

	// Build query using Table's chainable methods
	hasTextMatch := question != ""
	hasVectorMatch := len(vector) > 0

	table = table.Output(outputColumns)

	// Define text fields
	textFields := []string{
		"title_tks^10",
		"title_sm_tks^5",
		"important_kwd^30",
		"important_tks^20",
		"question_tks^20",
		"content_ltks^2",
		"content_sm_ltks",
	}

	// Convert field names for Infinity
	var convertedFields []string
	for _, f := range textFields {
		cf := convertMatchingField(f)
		convertedFields = append(convertedFields, cf)
	}
	fields := strings.Join(convertedFields, ",")

	// Format question
	formattedQuestion := formatQuestion(question)

	// Compute full filter with filter_fulltext for MatchDense extra_options
	var fullFilterWithFulltext string
	if filterStr != "" && fields != "" {
		fullFilterWithFulltext = fmt.Sprintf("(%s) AND FILTER_FULLTEXT('%s', '%s')", filterStr, fields, formattedQuestion)
	}

	// Add text match if question is provided
	if hasTextMatch {
		extraOptions := map[string]string{
			"topn":                 fmt.Sprintf("%d", topK),
			"minimum_should_match": fmt.Sprintf("%d%%", int(minMatch*100)),
		}

		// Add rank_features support
		if rankFeature != nil {
			var rankFeaturesList []string
			for featureName, weight := range rankFeature {
				rankFeaturesList = append(rankFeaturesList, fmt.Sprintf("%s^%s^%f", TAG_FLD, featureName, weight))
			}
			if len(rankFeaturesList) > 0 {
				extraOptions["rank_features"] = strings.Join(rankFeaturesList, ",")
			}
		}

		table = table.MatchText(fields, formattedQuestion, topK, extraOptions)
		fmt.Printf("[DEBUG] MatchTextExpr: fields=%s, matching_text=%s, topn=%d, extra_options=%v\n", fields, formattedQuestion, topK, extraOptions)
	}

	// Add vector match if provided
	if hasVectorMatch {
		vectorSize := len(vector)
		fieldName := fmt.Sprintf("q_%d_vec", vectorSize)
		threshold := similarityThreshold
		if threshold <= 0 {
			threshold = 0.1 // default
		}
		extraOptions := map[string]string{
			// Add threshold
			"threshold": fmt.Sprintf("%f", threshold),
		}

		// Add filter with filter_fulltext, add to MatchDense extra_options
		// This is the full filter that includes both available_int=1 AND filter_fulltext
		if fullFilterWithFulltext != "" {
			extraOptions["filter"] = fullFilterWithFulltext
			fmt.Printf("[DEBUG] filterStr=%s, fullFilterWithFulltext=%s\n", filterStr, fullFilterWithFulltext)
		}

		fmt.Printf("[DEBUG] MatchDenseExpr: field=%s, topn=%d, extra_options=%v\n", fieldName, topK, extraOptions)

		table = table.MatchDense(fieldName, vector, "float", "cosine", topK, extraOptions)
	}

	// Add fusion (for text+vector combination)
	if hasTextMatch && hasVectorMatch {
		fusionParams := map[string]interface{}{
			"normalize": "atan",
			"weights":   "0.05,0.95",
		}
		fmt.Printf("[DEBUG] FusionExpr: method=weighted_sum, topn=%d, fusion_params=%v\n", topK, fusionParams)
		fmt.Printf("[DEBUG] Before Fusion - table has MatchText=%v, MatchDense=%v\n", hasTextMatch, hasVectorMatch)
		table = table.Fusion("weighted_sum", topK, fusionParams)
	}

	// Add order_by if provided
	if orderBy != nil && len(orderBy.Fields) > 0 {
		var sortFields [][2]interface{}
		for _, field := range orderBy.Fields {
			sortType := infinity.SortTypeAsc
			if field.Type == SortDesc {
				sortType = infinity.SortTypeDesc
			}
			sortFields = append(sortFields, [2]interface{}{field.Field, sortType})
		}
		table = table.Sort(sortFields)
	}

	// Add filter when there's no text/vector match (like metadata queries)
	if !hasTextMatch && !hasVectorMatch && filterStr != "" {
		fmt.Printf("[DEBUG] Adding filter for no-match query: %s\n", filterStr)
		table = table.Filter(filterStr)
	}

	// Set limit and offset
	// Use topK to get more results from Infinity, then filter/sort in Go
	table = table.Limit(topK)
	if offset > 0 {
		table = table.Offset(offset)
	}

	// Execute query - get the raw query and execute via SDK
	result, err := e.executeQuery(table)
	if err != nil {
		return nil, err
	}

	// Debug logging - show returned chunks
	scoreColumn := "SIMILARITY"
	if hasTextMatch {
		scoreColumn = "SCORE"
	}
	fmt.Printf("[DEBUG] executeTableSearch returned %d chunks\n", len(result.Chunks))

	result.Chunks = calculateScores(result.Chunks, scoreColumn, PAGERANK_FLD)

	// Debug after calculateScores
	for i, chunk := range result.Chunks {
		chunkID := ""
		if id, ok := chunk["id"]; ok {
			chunkID = fmt.Sprintf("%v", id)
		}
		score := getScore(chunk)
		fmt.Printf("[DEBUG]   chunk[%d]: id=%s, _score=%f\n", i, chunkID, score)
	}

	// Sort by score
	result.Chunks = sortByScore(result.Chunks, len(result.Chunks))

	if len(result.Chunks) > pageSize {
		result.Chunks = result.Chunks[:pageSize]
	}
	result.Total = int64(len(result.Chunks))

	return result, nil
}

// executeQuery executes the query and returns results
func (e *infinityEngine) executeQuery(table *infinity.Table) (*types.SearchResponse, error) {
	// Use ToResult() to execute query
	result, err := table.ToResult()
	if err != nil {
		return nil, fmt.Errorf("Infinity query failed: %w", err)
	}

	// Debug: print raw result info
	// fmt.Printf("[DEBUG] Infinity raw result: %+v\n", result)

	// Convert result to SearchResponse format
	// The SDK returns QueryResult with Data as map[string][]interface{}
	qr, ok := result.(*infinity.QueryResult)
	if !ok {
		return &types.SearchResponse{
			Chunks: []map[string]interface{}{},
			Total:  0,
		}, nil
	}

	// Convert to chunks format
	chunks := make([]map[string]interface{}, 0)
	for colName, colData := range qr.Data {
		for i, val := range colData {
			// Ensure we have a row for this index
			for len(chunks) <= i {
				chunks = append(chunks, make(map[string]interface{}))
			}
			chunks[i][colName] = val
		}
	}

	// Post-process: convert nil/empty values to empty slices for array-like fields
	arrayFields := map[string]bool{
		"doc_type_kwd":    true,
		"important_kwd":   true,
		"important_tks":   true,
		"question_tks":    true,
		"authors_tks":     true,
		"authors_sm_tks":  true,
		"title_tks":       true,
		"title_sm_tks":    true,
		"content_ltks":    true,
		"content_sm_ltks": true,
	}
	for i := range chunks {
		for colName := range arrayFields {
			if val, ok := chunks[i][colName]; !ok || val == nil || val == "" {
				chunks[i][colName] = []interface{}{}
			}
		}
		// Convert position_int from hex string to array format
		if posVal, ok := chunks[i]["position_int"].(string); ok {
			chunks[i]["position_int"] = utility.ConvertHexToPositionIntArray(posVal)
		} else {
			chunks[i]["position_int"] = []interface{}{}
		}
		// Convert page_num_int and top_int from hex string to array
		for _, colName := range []string{"page_num_int", "top_int"} {
			if val, ok := chunks[i][colName].(string); ok {
				chunks[i][colName] = utility.ConvertHexToIntArray(val)
			}
		}
	}

	return &types.SearchResponse{
		Chunks: chunks,
		Total:  int64(len(chunks)),
	}, nil
}

// contains checks if slice contains string
func contains(slice []string, item string) bool {
	for _, s := range slice {
		if s == item {
			return true
		}
	}
	return false
}
