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

// retrieval_nlp.go — NLPRetrievalAdapter wiring.
//
// The agent tool layer (tool/retrieval_service.go) declares a
// minimal RetrievalService interface. Until this file landed, the
// only registered implementation was the stub that returns
// ErrRetrievalServiceMissing. NLPRetrievalAdapter bridges the
// agent-side interface to the production nlp.RetrievalService —
// the same service that powers chat / dataset search / chunk
// retrieval across the rest of the codebase.
//
// Wiring is one line at boot:
//
//	tool.SetRetrievalService(tool.NewNLPRetrievalAdapter(
//	    nlp.NewRetrievalService(docEngine, documentDAO),
//	))
//
// Translation rules:
//
//   tool.RetrievalRequest.Query      → nlp.RetrievalRequest.Question
//   tool.RetrievalRequest.DatasetIDs → nlp.RetrievalRequest.KbIDs
//   tool.RetrievalRequest.TopN       → nlp.RetrievalRequest.PageSize
//                                       (Page=1, Top=TopN*4 so rerank
//                                        has headroom)
//   tool.RetrievalRequest.UseKG      → ErrGraphRAGNotSupported (out of
//                                       scope per plan  + §9 Q3)
//
// Chunk shape translation: nlp's Chunks are []map[string]any with
// keys chunk_id, doc_id, docnm_kwd, content_with_weight,
// content_ltks, similarity, term_similarity, vector_similarity. The
// tool side wants a flat RetrievalChunk{ID, Content, DocumentID,
// Score}. We pick the most user-facing fields:
//   - ID         ← chunk_id
//   - Content    ← content_with_weight (fallback to content_ltks)
//   - DocumentID ← doc_id
//   - Score      ← similarity (fallback to avg of term+vector)
//
// Defensive defaults: missing or wrong-typed chunk fields become
// empty strings / 0.0 rather than panicking — a single malformed
// chunk from the doc engine shouldn't take down the whole
// retrieval call.

package tool

import (
	"context"

	"ragflow/internal/dao"
	"ragflow/internal/engine"
	"ragflow/internal/entity"
	"ragflow/internal/service/nlp"
)

// NLPRetrievalAdapter wraps *nlp.RetrievalService behind the
// agent-tool RetrievalService interface. The adapter is safe to
// share across goroutines — the wrapped service is stateless
// beyond its docEngine + documentDAO handles, both of which the
// nlp package treats as concurrent-safe.
type NLPRetrievalAdapter struct {
	svc   *nlp.RetrievalService
	kbDAO knowledgebaseLookup
}

type knowledgebaseLookup interface {
	GetByIDs(ids []string) ([]*entity.Knowledgebase, error)
}

// NewNLPRetrievalAdapter wraps an already-constructed
// *nlp.RetrievalService.
func NewNLPRetrievalAdapter(svc *nlp.RetrievalService) *NLPRetrievalAdapter {
	return &NLPRetrievalAdapter{
		svc:   svc,
		kbDAO: dao.NewKnowledgebaseDAO(),
	}
}

// NewNLPRetrievalAdapterFromDeps is the convenience constructor
// for the common boot path:
//
//	tool.SetRetrievalService(tool.NewNLPRetrievalAdapterFromDeps(docEngine, docDAO))
//
// matches chat_session.go's newChatSessionServiceWithRetrieval
// call site.
func NewNLPRetrievalAdapterFromDeps(docEngine engine.DocEngine, documentDAO *dao.DocumentDAO) *NLPRetrievalAdapter {
	return &NLPRetrievalAdapter{
		svc:   nlp.NewRetrievalService(docEngine, documentDAO),
		kbDAO: dao.NewKnowledgebaseDAO(),
	}
}

// Search implements RetrievalService. The translation rules live
// at the top of this file.
func (a *NLPRetrievalAdapter) Search(ctx context.Context, req RetrievalRequest) ([]RetrievalChunk, error) {
	if a == nil || a.svc == nil {
		return nil, ErrRetrievalServiceMissing
	}
	if req.UseKG {
		// Plan  + §9 Q3: GraphRAG is out of scope for the
		// Go Canvas. The tool layer also returns the error; we
		// surface it here so any future direct caller of the
		// adapter (bypassing the tool envelope) sees the same
		// contract.
		return nil, ErrGraphRAGNotSupported
	}
	if req.Query == "" {
		return nil, nil
	}
	topN := req.TopN
	if topN <= 0 {
		topN = 8
	}

	// nlp.Retrieval applies its own defaults for SimilarityThreshold
	// (0.2), VectorSimilarityWeight (0.3), RankFeature, etc. We
	// surface only the fields the agent tool actually controls:
	// Page=1, PageSize=TopN, KbIDs=DatasetIDs, Top=TopN*4 (rerank
	// headroom — matches the chat_session.go call pattern).
	nlpReq := &nlp.RetrievalRequest{
		Question:  req.Query,
		TenantIDs: a.resolveTenantIDs(req),
		KbIDs:     append([]string(nil), req.DatasetIDs...),
		Page:      1,
		PageSize:  topN,
		Aggs:      boolPtr(false),
		Highlight: boolPtr(false),
	}
	if topN > 0 {
		rerankBudget := topN * 4
		nlpReq.Top = &rerankBudget
	}
	if req.SimilarityThreshold > 0 {
		nlpReq.SimilarityThreshold = &req.SimilarityThreshold
	}

	res, err := a.svc.Retrieval(ctx, nlpReq)
	if err != nil {
		return nil, err
	}
	if res == nil || len(res.Chunks) == 0 {
		return []RetrievalChunk{}, nil
	}
	out := make([]RetrievalChunk, 0, len(res.Chunks))
	for _, raw := range res.Chunks {
		out = append(out, translateChunk(raw))
	}
	return out, nil
}

func (a *NLPRetrievalAdapter) resolveTenantIDs(req RetrievalRequest) []string {
	seen := map[string]struct{}{}
	tenantIDs := make([]string, 0, 1)
	appendTenantID := func(tenantID string) {
		if tenantID == "" {
			return
		}
		if _, ok := seen[tenantID]; ok {
			return
		}
		seen[tenantID] = struct{}{}
		tenantIDs = append(tenantIDs, tenantID)
	}

	appendTenantID(req.TenantID)
	return tenantIDs
}

// translateChunk converts one nlp chunk map into a RetrievalChunk.
// Tolerates missing fields (returns zero values) and wrong types
// (returns zero values) so a single bad chunk from the doc engine
// can't break the whole result list.
func translateChunk(raw map[string]any) RetrievalChunk {
	return RetrievalChunk{
		ID:         stringFromMap(raw, "chunk_id"),
		Content:    contentFromMap(raw),
		DocumentID: stringFromMap(raw, "doc_id"),
		Score:      scoreFromMap(raw),
	}
}

// stringFromMap returns raw[key].(string) or "" if missing / wrong
// type. Keeps the translator compact.
func stringFromMap(raw map[string]any, key string) string {
	if v, ok := raw[key]; ok {
		if s, ok := v.(string); ok {
			return s
		}
	}
	return ""
}

// contentFromMap picks the most user-facing content field. nlp
// chunks carry content_with_weight (the highlightable string) and
// content_ltks (the tokenised form). content_with_weight is what
// the model sees in Python; we use it here too. Empty / missing →
// fall back to content_ltks; both empty → empty string.
func contentFromMap(raw map[string]any) string {
	if v := stringFromMap(raw, "content_with_weight"); v != "" {
		return v
	}
	return stringFromMap(raw, "content_ltks")
}

// scoreFromMap returns the chunk's similarity score. nlp populates
// three fields — similarity (combined), term_similarity (BM25),
// vector_similarity (cosine). We prefer similarity; if absent or
// zero, average the two sub-scores. Wrong-type values → fall through
// to sub-scores; missing sub-scores → 0.
func scoreFromMap(raw map[string]any) float64 {
	if f, ok := numberFromMap(raw, "similarity"); ok {
		return f
	}
	term, termOK := numberFromMap(raw, "term_similarity")
	vec, vecOK := numberFromMap(raw, "vector_similarity")
	if termOK && vecOK {
		return (term + vec) / 2
	}
	if termOK {
		return term
	}
	if vecOK {
		return vec
	}
	return 0
}

// numberFromMap returns raw[key].(float64) with a tolerant path
// for ints. JSON unmarshaling can produce either.
func numberFromMap(raw map[string]any, key string) (float64, bool) {
	v, ok := raw[key]
	if !ok {
		return 0, false
	}
	switch x := v.(type) {
	case float64:
		return x, true
	case float32:
		return float64(x), true
	case int:
		return float64(x), true
	case int64:
		return float64(x), true
	}
	return 0, false
}

func boolPtr(b bool) *bool { return &b }
