//
//  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 chunk

import (
	"bytes"
	"context"
	"encoding/base64"
	"fmt"
	"image"
	"image/color"
	"image/draw"
	"image/jpeg"
	"math"
	"ragflow/internal/common"
	"ragflow/internal/entity"
	"ragflow/internal/entity/models"
	"strings"
	"sync"
	"time"

	_ "image/gif"
	_ "image/png"

	"go.uber.org/zap"
	"gorm.io/gorm"

	"ragflow/internal/dao"
	"ragflow/internal/engine"
	"ragflow/internal/engine/types"
	"ragflow/internal/service"
	"ragflow/internal/service/document"
	"ragflow/internal/service/nlp"
	"ragflow/internal/storage"
	"ragflow/internal/tokenizer"
	"ragflow/internal/utility"
)

var chunkImageMergeLocks = struct {
	sync.Mutex
	locks map[string]*chunkImageMergeLock
}{locks: make(map[string]*chunkImageMergeLock)}

type chunkImageMergeLock struct {
	mu   sync.Mutex
	refs int
}

func searchConfigMap(value interface{}) (map[string]interface{}, bool) {
	switch typed := value.(type) {
	case entity.JSONMap:
		return map[string]interface{}(typed), true
	case map[string]interface{}:
		return typed, true
	default:
		return nil, false
	}
}

// ChunkService chunk service
type ChunkService struct {
	docEngine      engine.DocEngine
	embeddingCache *utility.EmbeddingLRU
	kbDAO          *dao.KnowledgebaseDAO
	userTenantDAO  *dao.UserTenantDAO
	documentDAO    *dao.DocumentDAO
	taskDAO        *dao.TaskDAO
	searchService  *service.SearchService

	accessibleFunc           func(string, string) bool
	getKnowledgebaseByIDFunc func(string) (*entity.Knowledgebase, error)
	getDocumentsByIDsFunc    func([]string) ([]*entity.Document, error)
	// startParseDocumentsFunc overrides the DSL start-parse flow. Production
	// uses service.DocumentService.StartParseDocuments; tests inject a fake
	// to avoid the MQ publisher.
	startParseDocumentsFunc func(ctx context.Context, doc *entity.Document, kb *entity.Knowledgebase, userID string, opts document.StartParseOptions) error
	// cancelIngestionTaskFunc overrides the document-parsing cancellation.
	// Production uses service.DocumentService.CancelDocParse; tests inject
	// a fake to avoid the MQ publisher.
	cancelIngestionTaskFunc func(ctx context.Context, doc *entity.Document) error
	getEmbeddingModelFunc   func(string, string) (*models.EmbeddingModel, error)
	incrementChunkStatsFunc func(string, string, int64, int64, float64) error
	decrementChunkStatsFunc func(string, string, int64, int64, float64) error
	storeChunkImageFunc     func(string, string, []byte) error
	tokenizeFunc            func(string) (string, error)
	fineGrainedTokenizeFunc func(string) (string, error)
	numTokensFunc           func(string) int
}

// NewChunkService creates chunk service
func NewChunkService() *ChunkService {
	return &ChunkService{
		docEngine:      engine.Get(),
		embeddingCache: utility.NewEmbeddingLRU(1000), // default capacity
		kbDAO:          dao.NewKnowledgebaseDAO(),
		userTenantDAO:  dao.NewUserTenantDAO(),
		documentDAO:    dao.NewDocumentDAO(),
		taskDAO:        dao.NewTaskDAO(),
		searchService:  service.NewSearchService(),
	}
}

// RetrievalTest performs retrieval test for a given question against specified knowledge bases.
//
// Flow:
//  1. Validate kbs permissions and embedding model
//  2. Apply metadata filter if specified (auto/semi_auto uses LLM, manual uses provided conditions)
//  3. Apply cross_languages transformation if requested (translate question)
//  4. Apply keyword extraction if requested (append keywords to question)
//  5. Get rank features via LabelQuestion() - tag-based weights or pagerank_fld fallback
//  6. Call RetrievalService.Retrieval() which:
//     - Computes query embedding
//     - Performs hybrid search (text + vector) with rank features
//     - Reranks results
//     - Builds doc_aggs by aggregating chunks per document
//  7. knowledge graph retrieval (not implemented)
//  8. Apply retrieval by children to group child chunks under parent chunks
func (s *ChunkService) RetrievalTest(ctx context.Context, req *service.RetrievalTestRequest, userID string) (*service.RetrievalTestResponse, error) {
	common.Info("RetrievalTest started", zap.String("userID", userID), zap.Any("kbID", req.Datasets), zap.String("question", req.Question))

	common.Debug(fmt.Sprintf("RetrievalTest request:\n"+
		"    kbID=%v\n"+
		"    question=%s\n"+
		"    page=%v, size=%v\n"+
		"    docIDs=%v\n"+
		"    useKG=%v, topK=%v\n"+
		"    crossLanguages=%v\n"+
		"    searchID=%v\n"+
		"    filter=%v\n"+
		"    tenantRerankID=%v\n"+
		"    rerankID=%v\n"+
		"    keyword=%v\n"+
		"    similarityThreshold=%v, vectorSimilarityWeight=%v",
		req.Datasets, req.Question,
		common.PtrString(req.Page), common.PtrString(req.Size), req.DocIDs,
		common.PtrString(req.UseKG), common.PtrString(req.TopK), req.CrossLanguages, common.PtrString(req.SearchID),
		req.Filter,
		common.PtrString(req.TenantRerankID), common.PtrString(req.RerankID),
		common.PtrString(req.Keyword),
		common.PtrString(req.SimilarityThreshold), common.PtrString(req.VectorSimilarityWeight)))

	if req.Question == "" {
		return nil, fmt.Errorf("question is required")
	}
	if len(req.Datasets) == 0 {
		return nil, fmt.Errorf("dataset_ids is required")
	}

	tenants, err := s.userTenantDAO.GetByUserID(ctx, dao.DB, userID)
	if err != nil {
		return nil, fmt.Errorf("failed to get user tenants: %w", err)
	}
	if len(tenants) == 0 {
		return nil, fmt.Errorf("user has no accessible tenants")
	}
	common.Debug("Retrieved user tenants from database", zap.String("userID", userID), zap.Int("tenantCount", len(tenants)))

	var tenantIDs []string
	var kbRecords []*entity.Knowledgebase
	for _, datasetID := range req.Datasets {
		found := false
		for _, tenant := range tenants {
			kb, err := s.kbDAO.GetByIDAndTenantID(ctx, dao.DB, datasetID, tenant.TenantID)
			if err == nil && kb != nil {
				common.Debug("Found knowledge base in database",
					zap.String("datasetID", datasetID),
					zap.String("tenantID", tenant.TenantID),
					zap.String("kbName", kb.Name),
					zap.String("embdID", kb.EmbdID))
				tenantIDs = append(tenantIDs, tenant.TenantID)
				kbRecords = append(kbRecords, kb)
				found = true
				break
			}
		}
		if !found {
			return nil, fmt.Errorf("only owner of dataset is authorized for this operation")
		}
	}

	// Check if all kbs have the same embedding model
	if len(kbRecords) > 1 {
		firstEmbeddingKey := knowledgebaseEmbeddingKey(kbRecords[0], tenantIDs[0])
		for i := 1; i < len(kbRecords); i++ {
			if knowledgebaseEmbeddingKey(kbRecords[i], tenantIDs[i]) != firstEmbeddingKey {
				return nil, fmt.Errorf("cannot retrieve across datasets with different embedding models")
			}
		}
	}

	// Determine meta_data_filter
	var chatID string
	var chatModelForFilter *models.ChatModel
	filter := req.Filter

	if req.SearchID != nil && *req.SearchID != "" {
		// If search_id is set, get meta_data_filter and chat_id from search_config
		searchDetail, err := s.searchService.GetDetail(ctx, *req.SearchID)
		if err != nil {
			common.Warn("Failed to get search detail for search_id, proceeding without it", zap.String("searchID", *req.SearchID), zap.Error(err))
		} else if searchConfig, ok := searchConfigMap(searchDetail["search_config"]); ok && searchConfig != nil {
			if searchMetaFilter, ok := searchConfigMap(searchConfig["meta_data_filter"]); ok {
				filter = searchMetaFilter
			}
			chatID, _ = searchConfig["chat_id"].(string)
		} else {
			common.Warn("No search_config found in search detail", zap.String("searchID", *req.SearchID))
		}
	}

	// If meta_data_filter method is auto/semi_auto, get chat model
	if filter != nil {
		method, _ := filter["method"].(string)
		if method == "auto" || method == "semi_auto" {
			modelProviderSvc := service.NewModelProviderService()
			if chatID != "" {
				// Use chat_id from search_config (it's actually the model name)
				driver, mdlName, apiConfig, _, getErr := modelProviderSvc.ResolveModelConfig(ctx, tenantIDs[0], entity.ModelTypeChat, chatID)
				if getErr != nil {
					common.Warn("Failed to get chat model from search_config chat_id, using tenant default", zap.String("chatID", chatID), zap.Error(getErr))
				} else {
					chatModelForFilter = models.NewChatModel(driver, &mdlName, apiConfig)
					common.Info("Fetched chat model (from search_config) for metadata filter",
						zap.String("chatID", chatID),
						zap.String("tenantID", tenantIDs[0]))
				}

			}

			// If no chatID from search_config, or chatModel not found, use tenant default
			if chatModelForFilter == nil {
				tenantSvc := service.NewTenantService()
				modelName, err := tenantSvc.GetDefaultModelName(ctx, tenantIDs[0], entity.ModelTypeChat)
				if err != nil || modelName == "" {
					common.Warn("Failed to get tenant default chat model name for meta_data_filter", zap.Error(err))
				} else {
					driver, mdlName, apiConfig, _, getErr := modelProviderSvc.ResolveModelConfig(ctx, tenantIDs[0], entity.ModelTypeChat, modelName)
					if getErr != nil {
						common.Warn("Failed to get chat model for meta_data_filter", zap.Error(getErr))
					} else {
						chatModelForFilter = models.NewChatModel(driver, &mdlName, apiConfig)
						common.Info("Fetched chat model (tenant default) for metadata filter",
							zap.String("tenantID", tenantIDs[0]),
							zap.String("modelName", modelName))
					}

				}
			}
		}
	}

	// Apply meta_data_filter to get filtered doc_ids (filter by metadata before retrieval)
	docIDs := make([]string, len(req.DocIDs))
	copy(docIDs, req.DocIDs)
	if filter != nil {
		// Get flattened metadata
		metadataSvc := service.NewMetadataService()
		flattedMeta, err := metadataSvc.GetFlattedMetaByKBs(ctx, []string(req.Datasets))
		if err != nil {
			common.Warn("Failed to get flatted metadata", zap.Error(err))
		} else {
			common.Info("metadata filter conditions", zap.Any("filter", filter))
			filteredDocIDs, _ := service.ApplyMetaDataFilter(ctx, filter, flattedMeta, req.Question, chatModelForFilter, req.DocIDs, []string(req.Datasets))
			docIDs = filteredDocIDs
			common.Info("ApplyMetaDataFilter result", zap.Strings("docIDs", docIDs))
		}
	}

	// Apply cross_languages and keyword extraction with tenant default chat model
	modifiedQuestion := req.Question
	var chatModel *models.ChatModel

	// Get chat model for cross_languages and keyword_extraction
	var llmModelName string
	if len(req.CrossLanguages) > 0 || (req.Keyword != nil && *req.Keyword) {
		tenantSvc := service.NewTenantService()
		modelProviderSvc := service.NewModelProviderService()
		var err error
		llmModelName, err = tenantSvc.GetDefaultModelName(ctx, tenantIDs[0], entity.ModelTypeChat)
		if err != nil || llmModelName == "" {
			common.Warn("Failed to get default chat model name for LLM transformations", zap.Error(err))
		} else {
			driver, mdlName, apiConfig, _, getErr := modelProviderSvc.ResolveModelConfig(ctx, tenantIDs[0], entity.ModelTypeChat, llmModelName)
			if getErr != nil {
				common.Warn("Failed to get chat model for LLM transformations", zap.Error(getErr))
			} else {
				chatModel = models.NewChatModel(driver, &mdlName, apiConfig)
				common.Info("Fetched chat model (tenant default) for cross_languages/keyword_extraction",
					zap.String("tenantID", tenantIDs[0]),
					zap.String("modelName", llmModelName))
			}
		}
	}

	// Apply cross_languages on the question (translate question)
	if len(req.CrossLanguages) > 0 {
		translated, err := service.CrossLanguages(ctx, tenantIDs[0], llmModelName, req.Question, req.CrossLanguages)
		if err != nil {
			common.Warn("Failed to translate question", zap.Error(err))
		} else {
			modifiedQuestion = translated
		}
	}

	// Apply keyword extraction on the question (append keywords to question)
	if chatModel != nil && req.Keyword != nil && *req.Keyword {
		extractedKeywords, err := service.KeywordExtraction(ctx, chatModel, modifiedQuestion, 3)
		if err != nil {
			common.Warn("Failed to extract keywords from question", zap.Error(err))
		} else if extractedKeywords != "" {
			modifiedQuestion = modifiedQuestion + " " + extractedKeywords
		}
	}

	if modifiedQuestion != req.Question {
		common.Info("Modified question after transformations",
			zap.String("originalQuestion", req.Question),
			zap.String("modifiedQuestion", modifiedQuestion),
			zap.Strings("crossLanguages", req.CrossLanguages),
			zap.Bool("keywordExtraction", req.Keyword != nil && *req.Keyword))
	}

	// Get tag-based rank features via LabelQuestion
	metadataSvc := service.NewMetadataService()
	labels := metadataSvc.LabelQuestion(ctx, modifiedQuestion, kbRecords)
	common.Debug("LabelQuestion result", zap.Any("labels", labels))

	// Determine embedding model.
	modelProviderSvc := service.NewModelProviderService()
	var embeddingModel *models.EmbeddingModel
	var embdID string
	if kbRecords[0].TenantEmbdID != nil && *kbRecords[0].TenantEmbdID != "" {
		driver, modelName, apiConfig, maxTokens, getErr := modelProviderSvc.GetModelConfigByID(ctx, tenantIDs[0], entity.ModelTypeEmbedding, *kbRecords[0].TenantEmbdID)
		if getErr != nil {
			return nil, fmt.Errorf("failed to get embedding model by tenant_embd_id: %w", getErr)
		}
		embeddingModel = models.NewEmbeddingModel(driver, &modelName, apiConfig, maxTokens)
	} else if kbRecords[0].EmbdID != "" {
		embdID = kbRecords[0].EmbdID
		driver, modelName, apiConfig, maxTokens, getErr := modelProviderSvc.ResolveModelConfig(ctx, tenantIDs[0], entity.ModelTypeEmbedding, embdID)
		if getErr != nil {
			_, embdID, err = dao.LookupTenantLLMByName(ctx, dao.DB, dao.NewTenantLLMDAO(), tenantIDs[0], kbRecords[0].EmbdID, entity.ModelTypeEmbedding)
			if err != nil {
				return nil, fmt.Errorf("failed to get embedding model by embd_id: %w", getErr)
			}
			driver, modelName, apiConfig, maxTokens, getErr = modelProviderSvc.ResolveModelConfig(ctx, tenantIDs[0], entity.ModelTypeEmbedding, embdID)
			if getErr != nil {
				return nil, fmt.Errorf("failed to get embedding model by embd_id: %w", getErr)
			}
		}
		embeddingModel = models.NewEmbeddingModel(driver, &modelName, apiConfig, maxTokens)
	} else {
		driver, modelName, apiConfig, maxTokens, getErr := modelProviderSvc.GetTenantDefaultModelByType(ctx, tenantIDs[0], entity.ModelTypeEmbedding)
		if getErr != nil {
			return nil, fmt.Errorf("failed to get tenant default embedding model: %w", getErr)
		}
		embeddingModel = models.NewEmbeddingModel(driver, &modelName, apiConfig, maxTokens)
		embdID = fmt.Sprintf("%s@default", modelName)
	}

	if embeddingModel == nil {
		return nil, fmt.Errorf("no embedding model found for tenant %s", tenantIDs[0])
	}

	common.Info("Fetched embedding model for retrieval",
		zap.String("tenantID", tenantIDs[0]),
		zap.String("embdID", embdID))

	// Get rerank model if RerankID is specified
	var rerankModel *models.RerankModel
	if req.TenantRerankID != nil && *req.TenantRerankID != "" {
		driver, mdlName, apiConfig, _, getErr := modelProviderSvc.GetModelConfigByID(ctx, tenantIDs[0], entity.ModelTypeRerank, *req.TenantRerankID)
		if getErr != nil {
			return nil, fmt.Errorf("failed to get rerank model by tenant_rerank_id: %w", getErr)
		}
		rerankModel = models.NewRerankModel(driver, &mdlName, apiConfig)
	} else if req.RerankID != nil && *req.RerankID != "" {
		rerankCompositeName := *req.RerankID
		driver, mdlName, apiConfig, _, getErr := modelProviderSvc.ResolveModelConfig(ctx, tenantIDs[0], entity.ModelTypeRerank, rerankCompositeName)
		if getErr != nil {
			rerankModel = nil
		} else {
			rerankModel = models.NewRerankModel(driver, &mdlName, apiConfig)
		}
	}

	retrievalReq := &nlp.RetrievalRequest{
		TenantIDs:              tenantIDs,
		Question:               modifiedQuestion,
		KbIDs:                  []string(req.Datasets),
		DocIDs:                 docIDs,
		Page:                   common.CoalesceInt(req.Page, 1),
		PageSize:               common.CoalesceInt(req.Size, 30),
		Top:                    req.TopK,
		SimilarityThreshold:    req.SimilarityThreshold,
		VectorSimilarityWeight: req.VectorSimilarityWeight,
		RerankModel:            rerankModel,
		RankFeature:            &labels,
		EmbeddingModel:         embeddingModel,
	}

	// Call RetrievalService to perform retrieval
	retrievalResult, err := nlp.NewRetrievalService(s.docEngine, s.documentDAO).Retrieval(ctx, retrievalReq)
	if err != nil {
		return nil, fmt.Errorf("retrieval search failed: %w", err)
	}

	filteredChunks := retrievalResult.Chunks

	// Handle knowledge graph retrieval
	// TODO: KG retrieval requires GraphRAG infrastructure which is not yet implemented in Go
	if req.UseKG != nil && *req.UseKG {
		common.Warn("use_kg is not yet implemented in Go - skipping KG retrieval")
	}

	// Apply retrieval_by_children - aggregate child chunks into parent chunks
	filteredChunks = nlp.RetrievalByChildren(filteredChunks, tenantIDs, s.docEngine, ctx)

	// Hydrate: ES returns zero vectors; replace with real vectors from FetchChunkVectors.
	// Infinity/OceanBase chunks already carry real vectors and are left unchanged.
	hydrateChunkVectors(ctx, s.docEngine, filteredChunks, req.Datasets, tenantIDs)

	common.Info("RetrievalTest completed", zap.String("userID", userID), zap.Any("kbID", req.Datasets), zap.String("question", req.Question), zap.Int64("chunkCount", int64(len(filteredChunks))))

	return &service.RetrievalTestResponse{
		Chunks:  filteredChunks,
		DocAggs: retrievalResult.DocAggs,
		Labels:  &labels,
		Total:   retrievalResult.Total,
	}, nil
}

func knowledgebaseEmbeddingKey(kb *entity.Knowledgebase, tenantID string) string {
	if kb.TenantEmbdID != nil && *kb.TenantEmbdID != "" {
		return fmt.Sprintf("tenant:%s", *kb.TenantEmbdID)
	}
	if kb.EmbdID == "" {
		return fmt.Sprintf("default:%s", tenantID)
	}
	return fmt.Sprintf("embd:%s", kb.EmbdID)
}

// hydrateChunkVectors replaces zero (placeholder) vectors in chunks with real
// vectors fetched from the engine.  Infinity and OceanBase already ship real
// vectors with chunks, so this is a no-op for those engines; for ES it queries
// the engine by chunk ID list.  No if/else on engine type — just replaces
// whatever is missing or zero.
func hydrateChunkVectors(ctx context.Context, engine engine.DocEngine, chunks []map[string]interface{}, kbIDs []string, tenantIDs []string) {
	if len(chunks) == 0 {
		return
	}

	// Collect chunk IDs whose vectors are missing or all-zero.
	var missingIDs []string
	missingIdx := make(map[string]int)
	for i, ck := range chunks {
		id, _ := ck["id"].(string)
		if id == "" {
			continue
		}
		v, _ := ck["vector"].([]float64)
		if len(v) == 0 || common.IsZeroVector(v) {
			missingIDs = append(missingIDs, id)
			missingIdx[id] = i
		}
	}
	if len(missingIDs) == 0 {
		return
	}

	dim := 0
	for _, ck := range chunks {
		if v, _ := ck["vector"].([]float64); len(v) > 0 {
			dim = len(v)
			break
		}
	}
	if dim == 0 {
		return
	}

	vectors := FetchChunkVectors(ctx, engine, missingIDs, tenantIDs, kbIDs, dim)
	for id, v := range vectors {
		if idx, ok := missingIdx[id]; ok && !common.IsZeroVector(v) {
			chunks[idx]["vector"] = v
		}
	}
}

// Get retrieves a chunk by ID
func (s *ChunkService) Get(ctx context.Context, req *service.GetChunkRequest, userID string) (*service.GetChunkResponse, error) {
	if s.docEngine == nil {
		return nil, fmt.Errorf("doc engine not initialized")
	}

	if req.ChunkID == "" {
		return nil, fmt.Errorf("chunk_id is required")
	}

	// Get user's tenants
	tenants, err := s.userTenantDAO.GetByUserID(ctx, dao.DB, userID)
	if err != nil {
		return nil, fmt.Errorf("failed to get user tenants: %w", err)
	}
	if len(tenants) == 0 {
		return nil, fmt.Errorf("user has no accessible tenants")
	}

	// Try each tenant to find the chunk
	var chunk map[string]interface{}
	for _, tenant := range tenants {
		// Get kbIDs for this tenant
		kbIDs, err := s.kbDAO.GetKBIDsByTenantID(ctx, dao.DB, tenant.TenantID)
		if err != nil {
			continue
		}

		indexName := fmt.Sprintf("ragflow_%s", tenant.TenantID)

		doc, err := s.docEngine.GetChunk(ctx, indexName, req.ChunkID, kbIDs)
		if err != nil {
			continue
		}

		if doc != nil {
			chunk, ok := doc.(map[string]interface{})
			if ok {
				result := make(map[string]interface{})
				skipFields := map[string]bool{
					"id": true, "authors": true, "_score": true, "SCORE": true,
				}
				for k, v := range chunk {
					if skipFields[k] || strings.HasSuffix(k, "_vec") || strings.Contains(k, "_sm_") || strings.HasSuffix(k, "_tks") || strings.HasSuffix(k, "_ltks") {
						continue
					}
					switch k {
					case "content":
						result["content_with_weight"] = v
					case "docnm":
						result["docnm_kwd"] = v
					case "important_keywords":
						utility.SetFieldArray(result, "important_kwd", v)
					case "questions":
						utility.SetFieldArray(result, "question_kwd", v)
					case "entities_kwd", "entity_kwd", "entity_type_kwd", "from_entity_kwd",
						"name_kwd", "raptor_kwd", "removed_kwd", "source_id", "tag_kwd",
						"to_entity_kwd", "toc_kwd", "authors_tks", "doc_type_kwd":
						if utility.IsEmpty(v) {
							result[k] = []interface{}{}
						} else {
							result[k] = v
						}
					case "tag_feas":
						if utility.IsEmpty(v) {
							result[k] = map[string]interface{}{}
						} else {
							result[k] = v
						}
					case "create_timestamp_flt", "rank_flt", "weight_flt":
						if floatVal, ok := utility.ToFloat64(v); ok {
							result[k] = utility.JSONFloat64(floatVal)
						}
					default:
						result[k] = v
					}
				}
				return &service.GetChunkResponse{Chunk: result}, nil
			}
		}
	}

	if chunk == nil {
		return nil, fmt.Errorf("chunk not found")
	}

	return &service.GetChunkResponse{Chunk: chunk}, nil
}

const (
	docStopParsingInvalidStateMessage   = "Can't stop parsing document that has not started or already completed"
	docStopParsingInvalidStateErrorCode = "DOC_STOP_PARSING_INVALID_STATE"
)

func (s *ChunkService) cancelAllTasksOfDoc(ctx context.Context, doc *entity.Document) error {
	cancel := s.cancelIngestionTaskFunc
	if cancel == nil {
		cancel = document.NewDocumentService().CancelDocParse
	}
	return cancel(ctx, doc)
}

func (s *ChunkService) StopParsing(ctx context.Context, userID, datasetID string, req service.StopParsingRequest) (*service.StopParsingResponse, common.ErrorCode, error) {
	if !s.kbDAO.Accessible(ctx, dao.DB, datasetID, userID) {
		return nil, common.CodeDataError, fmt.Errorf("you don't own the dataset %s", datasetID)
	}

	if len(req.DocumentIDs) == 0 {
		return nil, common.CodeDataError, fmt.Errorf("`document_ids` is required")
	}

	_, err := s.kbDAO.GetByID(ctx, dao.DB, datasetID)
	if err != nil {
		return nil, common.CodeDataError, fmt.Errorf("you don't own the dataset %s", datasetID)
	}

	docIDs, duplicateMessages := service.CheckDuplicateIDs(req.DocumentIDs, "document")
	successCount := 0

	for _, docID := range docIDs {
		doc, err := s.documentDAO.GetByDocumentIDAndDatasetID(ctx, dao.DB, docID, datasetID)
		if err != nil || doc == nil {
			return nil, common.CodeDataError, fmt.Errorf("you don't own the document %s", docID)
		}

		task, err := dao.NewIngestionTaskDAO().GetByDocumentID(ctx, dao.DB, docID)
		if err != nil {
			return nil, common.CodeServerError, fmt.Errorf("get ingestion task for %s: %w", docID, err)
		}
		if task == nil || task.Status == common.COMPLETED ||
			task.Status == common.STOPPED || task.Status == common.FAILED {
			return &service.StopParsingResponse{
				Data: map[string]interface{}{"error_code": docStopParsingInvalidStateErrorCode},
			}, common.CodeDataError, fmt.Errorf("%s", docStopParsingInvalidStateMessage)
		}

		if err = s.cancelAllTasksOfDoc(ctx, doc); err != nil {
			return nil, common.CodeServerError, err
		}
		// CancelDocParse (inside cancelAllTasksOfDoc) already issues
		// RequestStop (STOPPING) and updates doc.run=CANCEL. Defer
		// destruction (chunk deletion, counter reset) until the worker
		// detects STOPPING and reaches a terminal state.

		successCount++
	}

	if len(duplicateMessages) > 0 {
		if successCount > 0 {
			return &service.StopParsingResponse{
				Message: fmt.Sprintf("Partially stopped %d documents with %d errors", successCount, len(duplicateMessages)),
				Data: map[string]interface{}{
					"success_count": successCount,
					"errors":        duplicateMessages,
				},
			}, common.CodeSuccess, nil
		}
		return nil, common.CodeDataError, fmt.Errorf("%s", strings.Join(duplicateMessages, ";"))
	}

	return nil, common.CodeSuccess, nil
}

func checkDuplicateIDs(documentIDs []string, idTypes string) ([]string, []string) {
	idCount := make(map[string]int, len(documentIDs))
	duplicateMessages := make([]string, 0)
	uniqueDocIDs := make([]string, 0, len(documentIDs))

	for _, id := range documentIDs {
		idCount[id]++
	}
	for id, count := range idCount {
		if count > 1 {
			duplicateMessages = append(duplicateMessages, fmt.Sprintf("Duplicate %s ids: %s ", idTypes, id))
		}
		uniqueDocIDs = append(uniqueDocIDs, id)
	}
	return uniqueDocIDs, duplicateMessages
}

func (s *ChunkService) accessible(ctx context.Context, datasetID, userID string) bool {
	if s.accessibleFunc != nil {
		return s.accessibleFunc(datasetID, userID)
	}
	return s.kbDAO.Accessible(ctx, dao.DB, datasetID, userID)
}

func (s *ChunkService) getKnowledgebaseByID(ctx context.Context, datasetID string) (*entity.Knowledgebase, error) {
	if s.getKnowledgebaseByIDFunc != nil {
		return s.getKnowledgebaseByIDFunc(datasetID)
	}
	return s.kbDAO.GetByID(ctx, dao.DB, datasetID)
}

func (s *ChunkService) getDocumentsByIDs(ctx context.Context, docIDs []string) ([]*entity.Document, error) {
	if s.getDocumentsByIDsFunc != nil {
		return s.getDocumentsByIDsFunc(docIDs)
	}
	return s.documentDAO.GetByIDs(ctx, dao.DB, docIDs)
}

func (s *ChunkService) Parse(ctx context.Context, userID, datasetID string, req *service.ParseFileRequest) (map[string]interface{}, common.ErrorCode, error) {
	if !s.accessible(ctx, datasetID, userID) {
		return nil, common.CodeOperatingError, fmt.Errorf("you don't own the dataset %s", datasetID)
	}
	if req == nil || len(req.DocumentIDs) == 0 {
		return nil, common.CodeDataError, fmt.Errorf("`document_ids` is required")
	}

	kb, err := s.getKnowledgebaseByID(ctx, datasetID)
	if err != nil || kb == nil {
		return nil, common.CodeDataError, fmt.Errorf("dataset not found")
	}

	docIDs, duplicateMessages := checkDuplicateIDs(req.DocumentIDs, "document")
	notFound := make([]string, 0)

	docs, err := s.getDocumentsByIDs(ctx, docIDs)
	if err != nil {
		return nil, common.CodeServerError, err
	}
	docByID := make(map[string]*entity.Document, len(docs))
	for _, doc := range docs {
		docByID[doc.ID] = doc
	}
	for _, docID := range docIDs {
		doc := docByID[docID]
		if doc == nil || doc.KbID != datasetID {
			notFound = append(notFound, docID)
		}
	}
	if len(notFound) > 0 {
		return nil, common.CodeDataError, fmt.Errorf("documents not found: %v", notFound)
	}
	for _, docID := range docIDs {
		doc := docByID[docID]
		if doc.Run != nil && *doc.Run == string(entity.TaskStatusRunning) {
			return nil, common.CodeDataError, fmt.Errorf("can't parse document that is currently being processed")
		}
	}

	// Batch pre-check: refuse the whole request if any document's ingestion
	// task is non-terminal (RUNNING/STOPPING), so we never partially clean.
	if err = (document.NewDocumentService().AssertIngestionTasksTerminal(ctx, docIDs)); err != nil {
		return nil, common.CodeDataError, err
	}

	startParse := s.startParseDocumentsFunc
	if startParse == nil {
		docSvc := document.NewDocumentService()
		startParse = docSvc.StartParseDocuments
	}

	successCount := 0

	for _, docID := range docIDs {
		doc := docByID[docID]
		if err = startParse(ctx, doc, kb, userID, document.StartParseOptions{RerunWithDelete: true}); err != nil {
			return nil, common.CodeServerError, err
		}
		successCount++
	}

	if len(duplicateMessages) > 0 {
		if successCount > 0 {
			return map[string]interface{}{
				"success_count": successCount,
				"errors":        duplicateMessages,
			}, common.CodeSuccess, fmt.Errorf("Partially parsed %d documents with %d errors", successCount, len(duplicateMessages))
		}
		return nil, common.CodeDataError, fmt.Errorf("%s", strings.Join(duplicateMessages, ";"))
	}
	return nil, common.CodeSuccess, nil
}

// List retrieves chunks for a document
func (s *ChunkService) List(ctx context.Context, req *service.ListChunksRequest, userID string) (*service.ListChunksResponse, error) {
	if s.docEngine == nil {
		return nil, fmt.Errorf("doc engine not initialized")
	}

	if req.DocID == "" {
		return nil, fmt.Errorf("doc_id is required")
	}

	// Get user's tenants
	tenants, err := s.userTenantDAO.GetByUserID(ctx, dao.DB, userID)
	if err != nil {
		return nil, fmt.Errorf("failed to get user tenants: %w", err)
	}
	if len(tenants) == 0 {
		return nil, fmt.Errorf("user has no accessible tenants")
	}

	// Get document to find its tenant
	docDAO := dao.NewDocumentDAO()
	doc, err := docDAO.GetByID(ctx, dao.DB, req.DocID)
	if err != nil || doc == nil {
		return nil, fmt.Errorf("document not found")
	}
	if req.DatasetID != "" && doc.KbID != req.DatasetID {
		return nil, fmt.Errorf("document not found")
	}

	// Get knowledge base to find tenant
	kb, err := s.kbDAO.GetByID(ctx, dao.DB, doc.KbID)
	if err != nil || kb == nil {
		return nil, fmt.Errorf("knowledge base not found")
	}

	// Find which tenant this document belongs to
	var targetTenantID string
	for _, tenant := range tenants {
		if tenant.TenantID == kb.TenantID {
			targetTenantID = tenant.TenantID
			break
		}
	}
	if targetTenantID == "" {
		return nil, fmt.Errorf("user does not have access to this document")
	}

	// Get kbIDs for this tenant
	kbIDs, err := s.kbDAO.GetKBIDsByTenantID(ctx, dao.DB, targetTenantID)
	if err != nil {
		return nil, fmt.Errorf("failed to get kb ids: %w", err)
	}

	indexName := fmt.Sprintf("ragflow_%s", targetTenantID)

	page := common.CoalesceInt(req.Page, 1)
	size := common.CoalesceInt(req.Size, 30)
	keywords := strings.TrimSpace(req.Keywords)
	var orderBy *types.OrderByExpr
	matchExprs := make([]interface{}, 0, 1)
	if keywords == "" {
		orderBy = (&types.OrderByExpr{}).
			Asc("chunk_order_int").
			Asc("page_num_int").
			Asc("top_int").
			Desc("create_timestamp_flt")
	} else {
		matchExprs = append(matchExprs, &types.MatchTextExpr{
			MatchingText: keywords,
			TopN:         size,
		})
	}

	// Build search request - same as retrieval test but filtered by doc_id
	searchReq := &types.SearchRequest{
		IndexNames: []string{indexName},
		MatchExprs: matchExprs,
		KbIDs:      kbIDs,
		Offset:     (page - 1) * size,
		Limit:      size,
		OrderBy:    orderBy,
		SelectFields: []string{
			"id",
			"content_with_weight",
			"img_id",
			"position_int",
			"docnm",
			"important_keywords",
			"questions",
			"entities_kwd",
			"entity_kwd",
			"entity_type_kwd",
			"from_entity_kwd",
			"name_kwd",
			"raptor_kwd",
			"removed_kwd",
			"source_id",
			"tag_kwd",
			"to_entity_kwd",
			"toc_kwd",
			"doc_type_kwd",
			"available_int",
		},
		Filter: map[string]interface{}{
			"doc_id": req.DocID,
		},
	}

	// Add available_int filter if specified
	if req.AvailableInt != nil {
		searchReq.Filter["available_int"] = *req.AvailableInt
	}

	// Execute search through unified engine interface
	searchResp, err := s.docEngine.Search(ctx, searchReq)
	if err != nil {
		return nil, fmt.Errorf("search failed: %w", err)
	}

	chunks := make([]map[string]interface{}, 0, len(searchResp.Chunks))
	for _, chunk := range searchResp.Chunks {
		// Inline formatChunkForList
		result := make(map[string]interface{})
		skipFields := map[string]bool{
			"_id": true, "authors": true, "_score": true, "SCORE": true,
			"important_kwd_empty_count": true, "kb_id": true, "mom_id": true, "page_num_int": true,
		}
		for k, v := range chunk {
			if skipFields[k] || strings.HasSuffix(k, "_vec") || strings.Contains(k, "_sm_") || strings.HasSuffix(k, "_ltks") || strings.HasSuffix(k, "_tks") {
				continue
			}
			switch k {
			case "img_id":
				if strVal, ok := v.(string); ok {
					result["image_id"] = strVal
				} else {
					result["image_id"] = ""
				}
			case "position_int":
				result["positions"] = v
			case "id":
				result["chunk_id"] = v
			case "content_with_weight":
				result["content_with_weight"] = v
			case "content":
				if _, ok := result["content_with_weight"]; !ok {
					result["content_with_weight"] = v
				}
			case "docnm":
				result["docnm_kwd"] = v
			case "important_keywords":
				utility.SetFieldArray(result, "important_kwd", v)
			case "questions":
				utility.SetFieldArray(result, "question_kwd", v)
			case "entities_kwd", "entity_kwd", "entity_type_kwd", "from_entity_kwd",
				"name_kwd", "raptor_kwd", "removed_kwd",
				"source_id", "tag_kwd", "to_entity_kwd", "toc_kwd", "doc_type_kwd":
				if utility.IsEmpty(v) {
					result[k] = []interface{}{}
				} else {
					result[k] = v
				}
			default:
				// Handle _kwd fields that need "###" splitting
				if strings.HasSuffix(k, "_kwd") && k != "knowledge_graph_kwd" {
					if strVal, ok := v.(string); ok && strings.Contains(strVal, "###") {
						parts := strings.Split(strVal, "###")
						var filtered []interface{}
						for _, p := range parts {
							if p != "" {
								filtered = append(filtered, p)
							}
						}
						result[k] = filtered
					} else {
						result[k] = v
					}
				} else {
					result[k] = v
				}
			}
		}
		chunks = append(chunks, result)
	}

	// Build document info, mirroring Python's _map_doc key renames:
	// kb_id→dataset_id, parser_id→chunk_method, token_num→token_count,
	// chunk_num→chunk_count, run→text status.
	timeFormat := "2006-01-02T15:04:05"
	docInfo := map[string]interface{}{
		"id":               doc.ID,
		"thumbnail":        doc.Thumbnail,
		"dataset_id":       doc.KbID,
		"chunk_method":     doc.ParserID,
		"pipeline_id":      doc.PipelineID,
		"parser_config":    doc.ParserConfig,
		"source_type":      doc.SourceType,
		"type":             doc.Type,
		"created_by":       doc.CreatedBy,
		"name":             doc.Name,
		"location":         doc.Location,
		"size":             doc.Size,
		"token_count":      doc.TokenNum,
		"chunk_count":      doc.ChunkNum,
		"progress":         utility.JSONFloat64(doc.Progress),
		"progress_msg":     doc.ProgressMsg,
		"process_begin_at": utility.FormatTimeToString(doc.ProcessBeginAt, timeFormat),
		"process_duration": doc.ProcessDuration,
		"content_hash":     doc.ContentHash,
		"suffix":           doc.Suffix,
		"run":              chunkDocRunText(doc.Run),
		"status":           doc.Status,
		"create_time":      doc.CreateTime,
		"create_date":      utility.FormatTimeToString(doc.CreateDate, timeFormat),
		"update_time":      doc.UpdateTime,
		"update_date":      utility.FormatTimeToString(doc.UpdateDate, timeFormat),
	}

	return &service.ListChunksResponse{
		Total:  searchResp.Total,
		Chunks: chunks,
		Doc:    docInfo,
	}, nil
}

func (s *ChunkService) SwitchChunks(ctx context.Context, userID, datasetID, documentID string, availableInt int, chunkIDs []string) error {
	if s.docEngine == nil {
		return fmt.Errorf("doc engine not initialized")
	}

	if availableInt != 0 && availableInt != 1 {
		return fmt.Errorf("available_int should be 0 or 1")
	}

	if chunkIDs == nil || len(chunkIDs) == 0 {
		return fmt.Errorf("req is null")
	}

	// Get user's tenants
	tenants, err := s.userTenantDAO.GetByUserID(ctx, dao.DB, userID)
	if err != nil {
		return fmt.Errorf("failed to get user tenants: %w", err)
	}
	if len(tenants) == 0 {
		return fmt.Errorf("user has no accessible tenants")
	}

	// Find the tenant that owns this dataset
	var targetTenantID string
	for _, tenant := range tenants {
		kb, err := s.kbDAO.GetByIDAndTenantID(ctx, dao.DB, datasetID, tenant.TenantID)
		if err == nil && kb != nil {
			targetTenantID = tenant.TenantID
			break
		}
	}
	if targetTenantID == "" {
		return fmt.Errorf("user does not have access to this dataset")
	}

	docDAO := dao.NewDocumentDAO()
	doc, err := docDAO.GetByID(ctx, dao.DB, documentID)
	if err != nil || doc == nil {
		return fmt.Errorf("document not found")
	}
	if doc.KbID != datasetID {
		return fmt.Errorf("document does not belong to this dataset")
	}

	for _, cid := range chunkIDs {
		indexName := fmt.Sprintf("ragflow_%s", targetTenantID)

		if err = s.docEngine.UpdateChunks(ctx, map[string]interface{}{
			"id":     cid,
			"doc_id": documentID,
		}, map[string]interface{}{
			"id":            cid,
			"available_int": availableInt,
		}, indexName, datasetID); err != nil {
			return err
		}
	}

	return nil
}

func (s *ChunkService) UpdateChunk(ctx context.Context, req *service.UpdateChunkRequest, userID string) error {
	if s.docEngine == nil {
		return fmt.Errorf("doc engine not initialized")
	}

	if req.ChunkID == "" {
		return fmt.Errorf("chunk_id is required")
	}

	// Get user's tenants
	tenants, err := s.userTenantDAO.GetByUserID(ctx, dao.DB, userID)
	if err != nil {
		return fmt.Errorf("failed to get user tenants: %w", err)
	}
	if len(tenants) == 0 {
		return fmt.Errorf("user has no accessible tenants")
	}

	// Find the tenant that owns this dataset
	var targetTenantID string
	for _, tenant := range tenants {
		kb, err := s.kbDAO.GetByIDAndTenantID(ctx, dao.DB, req.DatasetID, tenant.TenantID)
		if err == nil && kb != nil {
			targetTenantID = tenant.TenantID
			break
		}
	}
	if targetTenantID == "" {
		return fmt.Errorf("user does not have access to this dataset")
	}

	// Verify document belongs to dataset
	docDAO := dao.NewDocumentDAO()
	doc, err := docDAO.GetByID(ctx, dao.DB, req.DocumentID)
	if err != nil || doc == nil {
		return fmt.Errorf("document not found")
	}
	if doc.KbID != req.DatasetID {
		return fmt.Errorf("document does not belong to this dataset")
	}

	// Fetch existing chunk first
	indexName := fmt.Sprintf("ragflow_%s", targetTenantID)
	existingChunk, err := s.docEngine.GetChunk(ctx, indexName, req.ChunkID, []string{req.DatasetID})
	if err != nil {
		return fmt.Errorf("failed to get existing chunk: %w", err)
	}

	existing, ok := existingChunk.(map[string]interface{})
	if !ok {
		return fmt.Errorf("invalid chunk format")
	}

	// Build update dict
	d := make(map[string]interface{})

	// Content - use new value or existing
	if req.Content != nil {
		d["content_with_weight"] = *req.Content
	} else {
		if v, ok := existing["content_with_weight"].(string); ok {
			d["content_with_weight"] = v
		} else if v, ok := existing["content"].(string); ok {
			d["content_with_weight"] = v
		} else {
			d["content_with_weight"] = ""
		}
	}

	// Tokenize content
	contentStr := d["content_with_weight"].(string)
	d["content_ltks"], _ = tokenizer.Tokenize(contentStr)
	d["content_sm_ltks"], _ = tokenizer.FineGrainedTokenize(d["content_ltks"].(string))

	// Important keywords - convert []string to []interface{} for transformChunkFields
	if req.ImportantKwd != nil {
		impKwd := make([]interface{}, len(req.ImportantKwd))
		for i, v := range req.ImportantKwd {
			impKwd[i] = v
		}
		d["important_kwd"] = impKwd
	}

	// Questions
	if req.Questions != nil {
		// Filter out empty questions and trim
		filteredQuestions := []string{}
		for _, q := range req.Questions {
			q = strings.TrimSpace(q)
			if q != "" {
				filteredQuestions = append(filteredQuestions, q)
			}
		}
		d["question_kwd"] = filteredQuestions
	}

	// Available
	if req.Available != nil {
		if *req.Available {
			d["available_int"] = 1
		} else {
			d["available_int"] = 0
		}
	}

	// Positions
	if req.Positions != nil {
		d["position_int"] = req.Positions
	}

	// Tag keywords
	if req.TagKwd != nil {
		d["tag_kwd"] = req.TagKwd
	}

	// Tag features
	if req.TagFeas != nil {
		tagFeas, err := validateTagFeatures(req.TagFeas)
		if err != nil {
			return updateChunkError{code: common.CodeArgumentError, message: "`tag_feas` " + err.Error()}
		}
		d["tag_feas"] = tagFeas
	}

	// Always include id
	d["id"] = req.ChunkID

	// Call update
	condition := map[string]interface{}{
		"id": req.ChunkID,
	}

	err = s.docEngine.UpdateChunks(ctx, condition, d, indexName, req.DatasetID)
	if err != nil {
		return fmt.Errorf("failed to update chunk: %w", err)
	}

	return nil
}
func (s *ChunkService) RemoveChunks(ctx context.Context, req *service.RemoveChunksRequest, userID string) (int64, error) {
	if s.docEngine == nil {
		return 0, fmt.Errorf("doc engine not initialized")
	}

	if req.DocID == "" {
		return 0, fmt.Errorf("doc_id is required")
	}

	// Get user's tenants
	tenants, err := s.userTenantDAO.GetByUserID(ctx, dao.DB, userID)
	if err != nil {
		return 0, fmt.Errorf("failed to get user tenants: %w", err)
	}
	if len(tenants) == 0 {
		return 0, fmt.Errorf("user has no accessible tenants")
	}

	// Verify document exists and belongs to a dataset (do this first to get doc.KbID)
	docDAO := dao.NewDocumentDAO()
	doc, err := docDAO.GetByID(ctx, dao.DB, req.DocID)
	if err != nil || doc == nil {
		return 0, fmt.Errorf("document not found")
	}

	// Find the tenant that owns this document
	var targetTenantID string
	for _, tenant := range tenants {
		kb, err := s.kbDAO.GetByIDAndTenantID(ctx, dao.DB, doc.KbID, tenant.TenantID)
		if err == nil && kb != nil {
			targetTenantID = tenant.TenantID
			break
		}
	}
	if targetTenantID == "" {
		return 0, fmt.Errorf("user does not have access to this document")
	}

	indexName := fmt.Sprintf("ragflow_%s", targetTenantID)

	// Build condition
	condition := make(map[string]interface{})
	switch {
	case len(req.ChunkIDs) > 0 && req.DeleteAll:
		return 0, fmt.Errorf("chunk_ids and delete_all are mutually exclusive")
	case len(req.ChunkIDs) > 0:
		// Delete specific chunks - convert []string to []interface{} for buildFilterFromCondition
		chunkIDsIf := make([]interface{}, len(req.ChunkIDs))
		for i, id := range req.ChunkIDs {
			chunkIDsIf[i] = id
		}
		condition["id"] = chunkIDsIf
		condition["doc_id"] = req.DocID
	case req.DeleteAll:
		// Delete all chunks for this document
		condition["doc_id"] = req.DocID
	default:
		return 0, fmt.Errorf("either chunk_ids or delete_all must be provided")
	}

	deletedCount, err := s.docEngine.DeleteChunks(ctx, condition, indexName, doc.KbID)
	if err != nil {
		return 0, fmt.Errorf("failed to delete chunks: %w", err)
	}

	if deletedCount > 0 {
		if err = s.decrementChunkStats(req.DocID, doc.KbID, 0, deletedCount, 0); err != nil {
			return deletedCount, fmt.Errorf("failed to update chunk stats: %w", err)
		}
	}

	return deletedCount, nil
}

func (s *ChunkService) AddChunk(ctx context.Context, req *service.AddChunkRequest, userID string) (*service.AddChunkResponse, error) {
	if s.docEngine == nil {
		return nil, addChunkError{code: common.CodeServerError, message: "doc engine not initialized"}
	}
	if req == nil {
		return nil, addChunkError{code: common.CodeDataError, message: "invalid request payload"}
	}
	if !s.accessible(ctx, req.DatasetID, userID) {
		return nil, addChunkError{code: common.CodeDataError, message: fmt.Sprintf("You don't own the dataset %s.", req.DatasetID)}
	}

	kb, err := s.getKnowledgebaseByID(ctx, req.DatasetID)
	if err != nil || kb == nil {
		return nil, addChunkError{code: common.CodeDataError, message: fmt.Sprintf("You don't own the dataset %s.", req.DatasetID)}
	}

	doc, err := s.documentDAO.GetByDocumentIDAndDatasetID(ctx, dao.DB, req.DocumentID, req.DatasetID)
	if err != nil || doc == nil {
		return nil, addChunkError{code: common.CodeDataError, message: fmt.Sprintf("you don't own the document %s", req.DocumentID)}
	}

	content := strings.TrimSpace(req.Content)
	if content == "" {
		return nil, addChunkError{code: common.CodeDataError, message: "`content` is required"}
	}

	var tagFeas map[string]float64
	if req.TagFeas != nil {
		tagFeas, err = validateTagFeatures(req.TagFeas)
		if err != nil {
			return nil, addChunkError{code: common.CodeDataError, message: "`tag_feas` " + err.Error()}
		}
	}

	chunkID := common.ChunkID(req.DocumentID, req.Content)
	indexName := fmt.Sprintf("ragflow_%s", kb.TenantID)
	contentLtks, err := s.tokenize(req.Content)
	if err != nil {
		return nil, addChunkError{code: common.CodeServerError, message: fmt.Sprintf("tokenize content: %v", err)}
	}
	contentSmLtks, err := s.fineGrainedTokenize(contentLtks)
	if err != nil {
		return nil, addChunkError{code: common.CodeServerError, message: fmt.Sprintf("tokenize content fine-grained: %v", err)}
	}
	importantTks, err := s.tokenize(strings.Join(req.ImportantKeywords, " "))
	if err != nil {
		return nil, addChunkError{code: common.CodeServerError, message: fmt.Sprintf("tokenize important keywords: %v", err)}
	}
	questionKwd := filterTrimmedStrings(req.Questions)
	questionTks, err := s.tokenize(strings.Join(req.Questions, "\n"))
	if err != nil {
		return nil, addChunkError{code: common.CodeServerError, message: fmt.Sprintf("tokenize questions: %v", err)}
	}

	now := time.Now()
	docName := ""
	if doc.Name != nil {
		docName = *doc.Name
	}
	importantKeywords := req.ImportantKeywords
	if importantKeywords == nil {
		importantKeywords = []string{}
	}

	chunkData := map[string]interface{}{
		"id":                   chunkID,
		"content_with_weight":  req.Content,
		"content_ltks":         contentLtks,
		"content_sm_ltks":      contentSmLtks,
		"important_kwd":        importantKeywords,
		"important_tks":        importantTks,
		"question_kwd":         questionKwd,
		"question_tks":         questionTks,
		"create_time":          now.Format("2006-01-02 15:04:05"),
		"create_timestamp_flt": float64(now.UnixNano()) / float64(time.Second),
		"kb_id":                req.DatasetID,
		"docnm_kwd":            docName,
		"doc_id":               req.DocumentID,
	}
	if req.TagKwd != nil {
		chunkData["tag_kwd"] = req.TagKwd
	}
	if tagFeas != nil {
		chunkData["tag_feas"] = tagFeas
	}

	if req.ImageBase64 != nil {
		imageBinary, err := decodeChunkImageBase64(*req.ImageBase64)
		if err != nil {
			return nil, addChunkError{code: common.CodeDataError, message: err.Error()}
		}
		if err := s.storeChunkImage(req.DatasetID, chunkID, imageBinary); err != nil {
			return nil, addChunkError{code: common.CodeDataError, message: "Failed to store chunk image"}
		}
		chunkData["img_id"] = fmt.Sprintf("%s-%s", req.DatasetID, chunkID)
		chunkData["doc_type_kwd"] = "image"
	}

	embeddingModel, err := s.getEmbeddingModel(ctx, kb.TenantID, kb.EmbdID)
	if err != nil {
		return nil, addChunkError{code: common.CodeServerError, message: fmt.Sprintf("get embedding model: %v", err)}
	}
	embeddingText := req.Content
	if len(questionKwd) > 0 {
		embeddingText = strings.Join(questionKwd, "\n")
	}
	embeddings, err := embeddingModel.ModelDriver.Embed(ctx, embeddingModel.ModelName, []string{docName, embeddingText}, embeddingModel.APIConfig, &models.EmbeddingConfig{Dimension: 0}, nil)
	if err != nil {
		return nil, addChunkError{code: common.CodeServerError, message: fmt.Sprintf("encode chunk embedding: %v", err)}
	}
	if len(embeddings) != 2 {
		return nil, addChunkError{code: common.CodeServerError, message: fmt.Sprintf("unexpected embedding count: %d", len(embeddings))}
	}
	mergedVec, err := mergeChunkEmbeddings(embeddings[0].Embedding, embeddings[1].Embedding)
	if err != nil {
		return nil, addChunkError{code: common.CodeServerError, message: err.Error()}
	}
	chunkData[fmt.Sprintf("q_%d_vec", len(mergedVec))] = mergedVec

	ctx, cancel := context.WithTimeout(context.Background(), 600*time.Second)
	defer cancel()
	if _, err := s.docEngine.InsertChunks(ctx, []map[string]interface{}{chunkData}, indexName, req.DatasetID); err != nil {
		return nil, addChunkError{code: common.CodeServerError, message: fmt.Sprintf("insert chunk: %v", err)}
	}

	tokenNum := int64(s.numTokens(req.Content))
	if err := s.incrementChunkStats(req.DocumentID, req.DatasetID, tokenNum, 1, 0); err != nil {
		return nil, addChunkError{code: common.CodeServerError, message: fmt.Sprintf("increment chunk stats: %v", err)}
	}

	renamedChunk := map[string]interface{}{
		"id":                 chunkID,
		"content":            req.Content,
		"document_id":        req.DocumentID,
		"document":           docName,
		"important_keywords": importantKeywords,
		"questions":          questionKwd,
		"dataset_id":         req.DatasetID,
		"create_timestamp":   chunkData["create_timestamp_flt"],
		"create_time":        chunkData["create_time"],
	}
	if req.TagKwd != nil {
		renamedChunk["tag_kwd"] = req.TagKwd
	}
	if imgID, ok := chunkData["img_id"]; ok {
		renamedChunk["image_id"] = imgID
	}

	return &service.AddChunkResponse{Chunk: renamedChunk}, nil
}

type addChunkError struct {
	code    common.ErrorCode
	message string
}

type updateChunkError struct {
	code    common.ErrorCode
	message string
}

func (e updateChunkError) Error() string {
	return e.message
}

func (e updateChunkError) Code() common.ErrorCode {
	return e.code
}

func (e addChunkError) Error() string {
	return e.message
}

func (e addChunkError) Code() common.ErrorCode {
	return e.code
}

func validateTagFeatures(raw interface{}) (map[string]float64, error) {
	parsed, ok := raw.(map[string]interface{})
	if !ok {
		return nil, fmt.Errorf("must be an object mapping string tags to finite numeric scores")
	}
	cleaned := make(map[string]float64, len(parsed))
	for key, value := range parsed {
		key = strings.TrimSpace(key)
		if key == "" {
			return nil, fmt.Errorf("keys must be non-empty strings")
		}
		switch typed := value.(type) {
		case float64:
			if math.IsNaN(typed) || math.IsInf(typed, 0) || typed <= 0 {
				return nil, fmt.Errorf("values must be finite numbers greater than 0")
			}
			cleaned[key] = typed
		case float32:
			if math.IsNaN(float64(typed)) || math.IsInf(float64(typed), 0) || typed <= 0 {
				return nil, fmt.Errorf("values must be finite numbers greater than 0")
			}
			cleaned[key] = float64(typed)
		case int:
			if typed <= 0 {
				return nil, fmt.Errorf("values must be finite numbers greater than 0")
			}
			cleaned[key] = float64(typed)
		case int8:
			if typed <= 0 {
				return nil, fmt.Errorf("values must be finite numbers greater than 0")
			}
			cleaned[key] = float64(typed)
		case int16:
			if typed <= 0 {
				return nil, fmt.Errorf("values must be finite numbers greater than 0")
			}
			cleaned[key] = float64(typed)
		case int32:
			if typed <= 0 {
				return nil, fmt.Errorf("values must be finite numbers greater than 0")
			}
			cleaned[key] = float64(typed)
		case int64:
			if typed <= 0 {
				return nil, fmt.Errorf("values must be finite numbers greater than 0")
			}
			cleaned[key] = float64(typed)
		default:
			return nil, fmt.Errorf("values must be finite numbers greater than 0")
		}
	}
	return cleaned, nil
}

func decodeChunkImageBase64(raw string) ([]byte, error) {
	if strings.TrimSpace(raw) == "" {
		return nil, fmt.Errorf("`image_base64` must be a non-empty string")
	}
	imageBinary, err := base64.StdEncoding.Strict().DecodeString(raw)
	if err != nil {
		return nil, fmt.Errorf("invalid `image_base64`")
	}
	if len(imageBinary) == 0 {
		return nil, fmt.Errorf("`image_base64` is empty")
	}
	return imageBinary, nil
}

func mergeChunkEmbeddings(a, b []float64) ([]float64, error) {
	if len(a) == 0 || len(b) == 0 || len(a) != len(b) {
		return nil, fmt.Errorf("unexpected embedding dimensions")
	}
	merged := make([]float64, len(a))
	for i := range a {
		merged[i] = 0.1*a[i] + 0.9*b[i]
	}
	return merged, nil
}

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

func (s *ChunkService) tokenize(text string) (string, error) {
	if s.tokenizeFunc != nil {
		return s.tokenizeFunc(text)
	}
	return tokenizer.Tokenize(text)
}

func (s *ChunkService) fineGrainedTokenize(text string) (string, error) {
	if s.fineGrainedTokenizeFunc != nil {
		return s.fineGrainedTokenizeFunc(text)
	}
	return tokenizer.FineGrainedTokenize(text)
}

func (s *ChunkService) numTokens(text string) int {
	if s.numTokensFunc != nil {
		return s.numTokensFunc(text)
	}
	return tokenizer.NumTokensFromString(text)
}

func (s *ChunkService) getEmbeddingModel(ctx context.Context, tenantID, embdID string) (*models.EmbeddingModel, error) {
	if s.getEmbeddingModelFunc != nil {
		return s.getEmbeddingModelFunc(tenantID, embdID)
	}
	return service.NewModelProviderService().GetEmbeddingModel(ctx, tenantID, embdID)
}

func (s *ChunkService) incrementChunkStats(docID, kbID string, tokenNum, chunkNum int64, duration float64) error {
	if s.incrementChunkStatsFunc != nil {
		return s.incrementChunkStatsFunc(docID, kbID, tokenNum, chunkNum, duration)
	}
	return dao.DB.Transaction(func(tx *gorm.DB) error {
		result := tx.Model(&entity.Document{}).
			Where("id = ? AND kb_id = ?", docID, kbID).
			Updates(map[string]interface{}{
				"token_num":        gorm.Expr("token_num + ?", tokenNum),
				"chunk_num":        gorm.Expr("chunk_num + ?", chunkNum),
				"process_duration": gorm.Expr("process_duration + ?", duration),
			})
		if result.Error != nil {
			return result.Error
		}
		if result.RowsAffected == 0 {
			return fmt.Errorf("document not found")
		}

		result = tx.Model(&entity.Knowledgebase{}).
			Where("id = ?", kbID).
			Updates(map[string]interface{}{
				"token_num": gorm.Expr("token_num + ?", tokenNum),
				"chunk_num": gorm.Expr("chunk_num + ?", chunkNum),
			})
		if result.Error != nil {
			return result.Error
		}
		if result.RowsAffected == 0 {
			return fmt.Errorf("knowledgebase not found")
		}
		return nil
	})
}

func (s *ChunkService) decrementChunkStats(docID, kbID string, tokenNum, chunkNum int64, duration float64) error {
	if s.decrementChunkStatsFunc != nil {
		return s.decrementChunkStatsFunc(docID, kbID, tokenNum, chunkNum, duration)
	}
	return dao.DB.Transaction(func(tx *gorm.DB) error {
		result := tx.Model(&entity.Document{}).
			Where("id = ? AND kb_id = ?", docID, kbID).
			Updates(map[string]interface{}{
				"token_num":        gorm.Expr("CASE WHEN token_num - ? >= 0 THEN token_num - ? ELSE 0 END", tokenNum, tokenNum),
				"chunk_num":        gorm.Expr("CASE WHEN chunk_num - ? >= 0 THEN chunk_num - ? ELSE 0 END", chunkNum, chunkNum),
				"process_duration": gorm.Expr("CASE WHEN process_duration + ? >= 0 THEN process_duration + ? ELSE 0 END", duration, duration),
			})
		if result.Error != nil {
			return result.Error
		}
		if result.RowsAffected == 0 {
			return fmt.Errorf("document not found")
		}

		result = tx.Model(&entity.Knowledgebase{}).
			Where("id = ?", kbID).
			Updates(map[string]interface{}{
				"token_num": gorm.Expr("CASE WHEN token_num - ? >= 0 THEN token_num - ? ELSE 0 END", tokenNum, tokenNum),
				"chunk_num": gorm.Expr("CASE WHEN chunk_num - ? >= 0 THEN chunk_num - ? ELSE 0 END", chunkNum, chunkNum),
			})
		if result.Error != nil {
			return result.Error
		}
		if result.RowsAffected == 0 {
			return fmt.Errorf("knowledgebase not found")
		}
		return nil
	})
}

func (s *ChunkService) storeChunkImage(bucket, chunkID string, imageBinary []byte) error {
	if s.storeChunkImageFunc != nil {
		return s.storeChunkImageFunc(bucket, chunkID, imageBinary)
	}
	storageImpl := storage.GetStorageFactory().GetStorage()
	if storageImpl == nil {
		return fmt.Errorf("storage not initialized")
	}
	lockKey := bucket + "/" + chunkID
	lock := acquireChunkImageMergeLock(lockKey)
	lock.mu.Lock()
	defer func() {
		lock.mu.Unlock()
		releaseChunkImageMergeLock(lockKey)
	}()

	if !storageImpl.ObjExist(bucket, chunkID) {
		return storageImpl.Put(bucket, chunkID, imageBinary)
	}

	oldBinary, err := storageImpl.Get(bucket, chunkID)
	if err != nil {
		return err
	}
	oldImage, _, err := image.Decode(bytes.NewReader(oldBinary))
	if err != nil {
		return err
	}
	newImage, _, err := image.Decode(bytes.NewReader(imageBinary))
	if err != nil {
		return err
	}
	oldBounds, newBounds := oldImage.Bounds(), newImage.Bounds()
	width := oldBounds.Dx()
	if newBounds.Dx() > width {
		width = newBounds.Dx()
	}
	height := oldBounds.Dy() + newBounds.Dy()
	combined := image.NewRGBA(image.Rect(0, 0, width, height))
	draw.Draw(combined, combined.Bounds(), &image.Uniform{C: color.White}, image.Point{}, draw.Src)
	draw.Draw(combined, oldBounds, oldImage, oldBounds.Min, draw.Src)
	draw.Draw(combined, image.Rect(0, oldBounds.Dy(), newBounds.Dx(), oldBounds.Dy()+newBounds.Dy()), newImage, newBounds.Min, draw.Src)

	var buf bytes.Buffer
	if err := jpeg.Encode(&buf, combined, nil); err != nil {
		return err
	}
	return storageImpl.Put(bucket, chunkID, buf.Bytes())
}

func acquireChunkImageMergeLock(key string) *chunkImageMergeLock {
	chunkImageMergeLocks.Lock()
	defer chunkImageMergeLocks.Unlock()

	lock := chunkImageMergeLocks.locks[key]
	if lock == nil {
		lock = &chunkImageMergeLock{}
		chunkImageMergeLocks.locks[key] = lock
	}
	lock.refs++
	return lock
}

func releaseChunkImageMergeLock(key string) {
	chunkImageMergeLocks.Lock()
	defer chunkImageMergeLocks.Unlock()

	lock := chunkImageMergeLocks.locks[key]
	if lock == nil {
		return
	}
	lock.refs--
	if lock.refs == 0 {
		delete(chunkImageMergeLocks.locks, key)
	}
}

// chunkDocRunText maps the document run code to its text form, mirroring
// Python's _map_doc run_mapping.
func chunkDocRunText(run *string) interface{} {
	if run == nil {
		return nil
	}
	switch *run {
	case "0":
		return "UNSTART"
	case "1":
		return "RUNNING"
	case "2":
		return "CANCEL"
	case "3":
		return "DONE"
	case "4":
		return "FAIL"
	}
	return *run
}
