package graph

import (
	"context"
	"encoding/json"
	"fmt"

	"ragflow/internal/common"
	"ragflow/internal/engine"
	"ragflow/internal/engine/types"
	modelModule "ragflow/internal/entity/models"
)

// Retrieval performs a full knowledge graph retrieval and returns
// a synthetic chunk. Convenience wrapper around Pipeline.
func Retrieval(
	ctx context.Context,
	docEngine engine.DocEngine,
	chatModel *modelModule.ChatModel,
	embModel *modelModule.EmbeddingModel,
	kbIDs []string,
	tenantIDs []string,
	question string,
) (map[string]interface{}, error) {
	p := &Pipeline{
		docEngine:       docEngine,
		chatModel:       chatModel,
		embModel:        embModel,
		kbIDs:           kbIDs,
		idxnms:          makeIndexNames(tenantIDs),
		question:        question,
		entSimThreshold: defaultSimThreshold,
		relSimThreshold: defaultSimThreshold,
		denseTopK:       defaultDenseTopK,
		entTopN:         6,
		relTopN:         6,
		commTopN:        1,
		maxToken:        8196,
	}
	return p.Retrieval(ctx)
}

// makeIndexNames converts tenant IDs to search index names.
func makeIndexNames(tenantIDs []string) []string {
	idxnms := make([]string, len(tenantIDs))
	for i, tid := range tenantIDs {
		idxnms[i] = indexName(tid)
	}
	return idxnms
}

// indexName builds the search index name from a tenant ID.
func indexName(tenantID string) string {
	return "ragflow_" + tenantID
}

// searchTypeSamples searches for ty2ents data.
func searchTypeSamples(ctx context.Context, docEngine engine.DocEngine, idxnms []string, kbIDs []string) (map[string][]string, error) {
	req := &types.SearchRequest{
		IndexNames:   idxnms,
		KbIDs:        kbIDs,
		SelectFields: []string{"content_with_weight"},
		Limit:        10000,
		Filter:       map[string]interface{}{"knowledge_graph_kwd": "ty2ents"},
	}
	result, err := docEngine.Search(ctx, req)
	if err != nil {
		return nil, err
	}
	typeMap := make(map[string][]string)
	for _, chunk := range result.Chunks {
		content, ok := chunk["content_with_weight"].(string)
		if !ok || content == "" {
			continue
		}
		var parsed map[string][]string
		if err := json.Unmarshal([]byte(content), &parsed); err != nil {
			continue
		}
		for typ, entities := range parsed {
			typeMap[typ] = append(typeMap[typ], entities...)
		}
	}
	return typeMap, nil
}

// searchCommunityContent searches for community reports and formats them.
func searchCommunityContent(ctx context.Context, docEngine engine.DocEngine, idxnms []string, kbIDs []string, scoredEnts []ScoredEntity, topN int, maxToken *int) string {
	if maxToken == nil || len(scoredEnts) == 0 || *maxToken <= 0 {
		return ""
	}
	entityNames := make([]string, len(scoredEnts))
	for i, e := range scoredEnts {
		entityNames[i] = e.Entity
	}
	req := &types.SearchRequest{
		IndexNames:   idxnms,
		KbIDs:        kbIDs,
		SelectFields: []string{"docnm_kwd", "content_with_weight", "weight_flt", "entities_kwd"},
		Limit:        topN,
		Filter:       map[string]interface{}{"knowledge_graph_kwd": "community_report"},
		OrderBy:      (&types.OrderByExpr{}).Desc("weight_flt"),
	}
	if len(entityNames) > 0 {
		filters := make([]interface{}, len(entityNames))
		for i, name := range entityNames {
			filters[i] = name
		}
		req.Filter["entities_kwd"] = filters
	}
	result, err := docEngine.Search(ctx, req)
	if err != nil || len(result.Chunks) == 0 || *maxToken <= 0 {
		return ""
	}

	var bld string
	for idx, chunk := range result.Chunks {
		title, _ := chunk["docnm_kwd"].(string)
		raw, _ := chunk["content_with_weight"].(string)
		if title == "" && raw == "" {
			continue
		}
		report := raw
		evidence := ""
		var parsed map[string]interface{}
		if err := json.Unmarshal([]byte(raw), &parsed); err == nil {
			if r, ok := parsed["report"].(string); ok {
				report = r
			}
			if e, ok := parsed["evidences"].(string); ok {
				evidence = e
			}
		}
		section := fmt.Sprintf("\n# %d. %s\n## Content\n%s\n## Evidences\n%s\n", idx+1, title, report, evidence)
		tokens := NumTokensFromString(section)
		if *maxToken-tokens <= 0 {
			break
		}
		bld += section
		*maxToken -= tokens
	}
	return bld
}

// entityFromChunk parses a single entity chunk into a KGEntity.
func entityFromChunk(name string, chunk map[string]interface{}) KGEntity {
	e := KGEntity{}
	if v, ok := chunk["_score"].(float64); ok {
		e.Similarity = v
	} else if v, ok := chunk["score"].(float64); ok {
		e.Similarity = v
	}
	if v, ok := chunk["rank_flt"].(float64); ok {
		e.PageRank = v
	}
	e.Description, _ = chunk["content_with_weight"].(string)
	if raw, ok := chunk["n_hop_with_weight"].(string); ok && raw != "" {
		var nhopData []struct {
			Path    []string  `json:"path"`
			Weights []float64 `json:"weights"`
		}
		if err := json.Unmarshal([]byte(raw), &nhopData); err == nil {
			for _, item := range nhopData {
				e.NhopEnts = append(e.NhopEnts, NhopEntity{
					Path:    item.Path,
					Weights: item.Weights,
				})
			}
		}
	}
	return e
}

// relationFromChunk parses a single relation chunk into a KGRelation.
func relationFromChunk(chunk map[string]interface{}) (Edge, KGRelation) {
	r := KGRelation{}
	r.Description, _ = chunk["content_with_weight"].(string)
	if v, ok := chunk["_score"].(float64); ok {
		r.Sim = v
	} else if v, ok := chunk["score"].(float64); ok {
		r.Sim = v
	}
	if v, ok := chunk["weight_int"].(float64); ok {
		r.PageRank = float64(v)
	} else if v, ok := chunk["weight_int"].(int); ok {
		r.PageRank = float64(v)
	}
	from, _ := chunk["from_entity_kwd"].(string)
	to, _ := chunk["to_entity_kwd"].(string)
	return Edge{From: from, To: to}, r
}

// buildSearchExprs constructs MatchExprs for KG entity/relation search.
// When embModel is nil, returns text-only match expression.
// When embModel is non-nil, embeds the question and returns hybrid
// (text + dense + fusion) expressions for vector+keyword search.
func buildSearchExprs(ctx context.Context, embModel *modelModule.EmbeddingModel, matchText *types.MatchTextExpr, simThreshold float64, denseTopK int) []interface{} {
	if embModel == nil || embModel.ModelDriver == nil {
		return []interface{}{matchText}
	}
	embeddingConfig := &modelModule.EmbeddingConfig{Dimension: 0}
	embeddings, err := embModel.ModelDriver.Embed(ctx, embModel.ModelName, []string{matchText.MatchingText}, embModel.APIConfig, embeddingConfig, nil)
	if err != nil || len(embeddings) == 0 {
		return []interface{}{matchText}
	}
	denseExpr := buildMatchDenseExpr(embeddings[0].Embedding, denseTopK, simThreshold)
	fusionExpr := buildFusionExpr(defaultTextWeight, defaultVectorWeight, matchText.TopN)
	return []interface{}{matchText, denseExpr, fusionExpr}
}

// buildMatchDenseExpr constructs a MatchDenseExpr from an embedding vector.
func buildMatchDenseExpr(vector []float64, topN int, similarity float64) *types.MatchDenseExpr {
	vectorColumnName := fmt.Sprintf("q_%d_vec", len(vector))
	return &types.MatchDenseExpr{
		VectorColumnName:  vectorColumnName,
		EmbeddingData:     vector,
		EmbeddingDataType: "float",
		DistanceType:      "cosine",
		TopN:              topN,
		ExtraOptions:      map[string]interface{}{"similarity": similarity},
	}
}

// buildFusionExpr constructs a FusionExpr for weighted-sum hybrid search.
func buildFusionExpr(textWeight, vectorWeight float64, topN int) *types.FusionExpr {
	return &types.FusionExpr{
		Method: "weighted_sum",
		TopN:   topN,
		FusionParams: map[string]interface{}{
			"weights": fmt.Sprintf("%.2f,%.2f", textWeight, vectorWeight),
		},
	}
}

// queryRewrite attempts LLM-based query rewrite, falling back to raw question.
func queryRewrite(ctx context.Context, chatModel *modelModule.ChatModel, question string, ty2entsJSON string) (typeKeywords, entities []string) {
	if question == "" {
		return nil, nil
	}
	if chatModel != nil && chatModel.ModelName != nil && chatModel.APIConfig != nil {
		prompt := common.BuildQueryRewritePrompt(question, ty2entsJSON)
		messages := []modelModule.Message{
			{Role: "system", Content: prompt},
			{Role: "user", Content: "Output:"},
		}
		response, err := chatModel.ModelDriver.ChatWithMessages(ctx, *chatModel.ModelName, messages, chatModel.APIConfig, nil, nil)
		if err == nil && response != nil && response.Answer != nil {
			result, parseErr := common.ParseQueryRewriteResponse(*response.Answer)
			if parseErr == nil && result != nil {
				return result.TypeKeywords, result.Entities
			}
		}
	}
	return nil, []string{question}
}

// Python alignment defaults
const (
	defaultSimThreshold = 0.3
	defaultDenseTopK    = 1024
	// defaultTextWeight / defaultVectorWeight are fusion weights for hybrid search (equal by default).
	defaultTextWeight   = 0.5
	defaultVectorWeight = 0.5
)
