package graph

import (
	"context"
	"fmt"

	"encoding/json"
	"ragflow/internal/engine"
	"ragflow/internal/engine/types"
	modelModule "ragflow/internal/entity/models"
)

// NhopEntityNames extracts unique entity names from an n_hop_with_weight JSON string.
func NhopEntityNames(nHopJSON string) []string {
	if nHopJSON == "" {
		return nil
	}
	var nhopData []struct {
		Path    []string  `json:"path"`
		Weights []float64 `json:"weights"`
	}
	if err := json.Unmarshal([]byte(nHopJSON), &nhopData); err != nil {
		return nil
	}
	seen := make(map[string]struct{})
	for _, item := range nhopData {
		for _, name := range item.Path {
			seen[name] = struct{}{}
		}
	}
	result := make([]string, 0, len(seen))
	for name := range seen {
		result = append(result, name)
	}
	return result
}

// SearchEntities searches for KG entities matching a question.
func SearchEntities(ctx context.Context, docEngine engine.DocEngine, kbIDs []string, question string, embModel *modelModule.EmbeddingModel, topN int) ([]KGEntity, error) {
	dense, err := buildDenseExpr(ctx, embModel, question, topN)
	if err != nil {
		return nil, err
	}
	searchReq := buildEntitySearchRequest(kbIDs, question, dense, topN)
	result, err := docEngine.Search(ctx, searchReq)
	if err != nil {
		return nil, fmt.Errorf("KG entity search failed: %w", err)
	}
	return ParseEntityChunks(result.Chunks), nil
}

// SearchEntitiesByTypes searches for KG entities by type keywords.
func SearchEntitiesByTypes(ctx context.Context, docEngine engine.DocEngine, kbIDs []string, typeKeywords []string, topN int) ([]KGEntity, error) {
	searchReq := buildEntityTypeSearchRequest(kbIDs, typeKeywords, topN)
	result, err := docEngine.Search(ctx, searchReq)
	if err != nil {
		return nil, fmt.Errorf("KG entity type search failed: %w", err)
	}
	return ParseEntityChunks(result.Chunks), nil
}

// SearchRelations searches for KG relations matching a question.
func SearchRelations(ctx context.Context, docEngine engine.DocEngine, kbIDs []string, question string, embModel *modelModule.EmbeddingModel, topN int) ([]KGRelation, error) {
	dense, err := buildDenseExpr(ctx, embModel, question, topN)
	if err != nil {
		return nil, err
	}
	searchReq := buildRelationSearchRequest(kbIDs, question, dense, topN)
	result, err := docEngine.Search(ctx, searchReq)
	if err != nil {
		return nil, fmt.Errorf("KG relation search failed: %w", err)
	}
	return ParseRelationChunks(result.Chunks), nil
}

// SearchCommunityReports searches for community reports related to given entities.
func SearchCommunityReports(ctx context.Context, docEngine engine.DocEngine, kbIDs []string, entityNames []string, topN int) ([]KGCommunityReport, error) {
	searchReq := buildCommunitySearchRequest(kbIDs, entityNames, topN)
	result, err := docEngine.Search(ctx, searchReq)
	if err != nil {
		return nil, fmt.Errorf("KG community search failed: %w", err)
	}
	return ParseCommunityReportChunks(result.Chunks), nil
}

// SearchTypeSamples retrieves the typeu2192entities mapping from ES.
func SearchTypeSamples(ctx context.Context, docEngine engine.DocEngine, kbIDs []string) (map[string][]string, error) {
	searchReq := buildTypeSamplesSearchRequest(kbIDs)
	result, err := docEngine.Search(ctx, searchReq)
	if err != nil {
		return nil, err
	}
	return ParseTypeSamplesChunks(result.Chunks), nil
}

// buildDenseExpr computes the query vector and returns a MatchDenseExpr.
func buildDenseExpr(ctx context.Context, embModel *modelModule.EmbeddingModel, question string, topN int) (*types.MatchDenseExpr, error) {
	if embModel == nil || question == "" {
		return nil, nil
	}
	embCfg := &modelModule.EmbeddingConfig{Dimension: 0}
	embeddings, err := embModel.ModelDriver.Embed(ctx, embModel.ModelName, []string{question}, embModel.APIConfig, embCfg, nil)
	if err != nil {
		return nil, fmt.Errorf("KG entity embed failed: %w", err)
	}
	if len(embeddings) == 0 || len(embeddings[0].Embedding) == 0 {
		return nil, nil
	}
	vector := embeddings[0].Embedding
	return &types.MatchDenseExpr{
		VectorColumnName:  fmt.Sprintf("q_%d_vec", len(vector)),
		EmbeddingData:     vector,
		EmbeddingDataType: "float",
		DistanceType:      "cosine",
		TopN:              topN,
		ExtraOptions:      map[string]interface{}{"similarity": 0.3},
	}, nil
}

// buildHybridExpr returns MatchExprs for hybrid search (dense + text + fusion).
func buildHybridExpr(dense *types.MatchDenseExpr, text *types.MatchTextExpr, topN int) []interface{} {
	if dense == nil {
		return []interface{}{text}
	}
	fusion := buildFusionExpr(defaultTextWeight, defaultVectorWeight, topN)
	return []interface{}{dense, text, fusion}
}

// buildEntitySearchRequest constructs a SearchRequest for KG entities.
func buildEntitySearchRequest(kbIDs []string, question string, dense *types.MatchDenseExpr, topN int) *types.SearchRequest {
	req := &types.SearchRequest{
		KbIDs:        kbIDs,
		SelectFields: []string{"entity_kwd", "entity_type_kwd", "rank_flt", "content_with_weight", "n_hop_with_weight", "_score"},
		Limit:        topN,
		Filter:       map[string]interface{}{"knowledge_graph_kwd": "entity"},
	}
	if question != "" {
		textExpr := &types.MatchTextExpr{
			Fields:       []string{"entity_kwd^10", "content_ltks^2"},
			MatchingText: question,
			TopN:         topN,
		}
		req.MatchExprs = buildHybridExpr(dense, textExpr, topN)
	}
	return req
}

// buildEntityTypeSearchRequest constructs a SearchRequest for KG entities by type.
func buildEntityTypeSearchRequest(kbIDs []string, typeKeywords []string, topN int) *types.SearchRequest {
	req := &types.SearchRequest{
		KbIDs:        kbIDs,
		SelectFields: []string{"entity_kwd", "entity_type_kwd"},
		Limit:        topN,
		Filter:       map[string]interface{}{"knowledge_graph_kwd": "entity"},
	}
	if len(typeKeywords) > 0 {
		filters := make([]interface{}, len(typeKeywords))
		for i, t := range typeKeywords {
			filters[i] = t
		}
		req.Filter["entity_type_kwd"] = filters
	}
	return req
}

// buildRelationSearchRequest constructs a SearchRequest for KG relations.
func buildRelationSearchRequest(kbIDs []string, question string, dense *types.MatchDenseExpr, topN int) *types.SearchRequest {
	req := &types.SearchRequest{
		KbIDs:        kbIDs,
		SelectFields: []string{"from_entity_kwd", "to_entity_kwd", "weight_int", "content_with_weight", "_score"},
		Limit:        topN,
		Filter:       map[string]interface{}{"knowledge_graph_kwd": "relation"},
	}
	if question != "" {
		textExpr := &types.MatchTextExpr{
			Fields:       []string{"content_ltks", "from_entity_kwd", "to_entity_kwd"},
			MatchingText: question,
			TopN:         topN,
		}
		req.MatchExprs = buildHybridExpr(dense, textExpr, topN)
	}
	return req
}

// buildCommunitySearchRequest constructs a SearchRequest for KG community reports.
func buildCommunitySearchRequest(kbIDs []string, entityNames []string, topN int) *types.SearchRequest {
	req := &types.SearchRequest{
		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
	}
	return req
}

// buildTypeSamplesSearchRequest constructs a SearchRequest for type samples.
func buildTypeSamplesSearchRequest(kbIDs []string) *types.SearchRequest {
	return &types.SearchRequest{
		KbIDs:        kbIDs,
		SelectFields: []string{"content_with_weight"},
		Limit:        10000,
		Filter:       map[string]interface{}{"knowledge_graph_kwd": "ty2ents"},
	}
}

// ParseEntityChunks converts raw search result chunks into KGEntity slices.
func ParseEntityChunks(chunks []map[string]interface{}) []KGEntity {
	var entities []KGEntity
	for _, chunk := range chunks {
		name, _ := chunk["entity_kwd"].(string)
		if name == "" {
			// Try extracting from list
			if list, ok := chunk["entity_kwd"].([]interface{}); ok && len(list) > 0 {
				name, _ = list[0].(string)
			}
		}
		if name == "" {
			continue
		}
		typ, _ := chunk["entity_type_kwd"].(string)
		e := KGEntity{Name: name, Type: typ}
		if v, ok := chunk["rank_flt"].(float64); ok {
			e.PageRank = v
		}
		if v, ok := chunk["_score"].(float64); ok {
			e.Similarity = v
		} else if v, ok := chunk["score"].(float64); ok {
			e.Similarity = v
		}
		e.Description, _ = chunk["content_with_weight"].(string)
		entities = append(entities, e)
	}
	return entities
}

// ParseRelationChunks converts raw search result chunks into KGRelation slices.
func ParseRelationChunks(chunks []map[string]interface{}) []KGRelation {
	var relations []KGRelation
	for _, chunk := range chunks {
		from, _ := chunk["from_entity_kwd"].(string)
		to, _ := chunk["to_entity_kwd"].(string)
		if from == "" || to == "" {
			continue
		}
		r := KGRelation{From: from, To: to}
		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 = v
		} else if v, ok := chunk["weight_int"].(int); ok {
			r.PageRank = float64(v)
		}
		r.Description, _ = chunk["content_with_weight"].(string)
		relations = append(relations, r)
	}
	return relations
}

// ParseCommunityReportChunks converts raw search result chunks into KGCommunityReport slices.
func ParseCommunityReportChunks(chunks []map[string]interface{}) []KGCommunityReport {
	var reports []KGCommunityReport
	for _, chunk := range chunks {
		title, _ := chunk["docnm_kwd"].(string)
		content, _ := chunk["content_with_weight"].(string)
		if title == "" && content == "" {
			continue
		}
		r := KGCommunityReport{Title: title, Content: content}
		if v, ok := chunk["weight_flt"].(float64); ok {
			r.Weight = v
		}
		r.Entities, _ = chunk["entities_kwd"].(string)
		reports = append(reports, r)
	}
	return reports
}

// ParseTypeSamplesChunks converts raw search result chunks into a typeu2192entities map.
func ParseTypeSamplesChunks(chunks []map[string]interface{}) map[string][]string {
	typeMap := make(map[string][]string)
	for _, chunk := range 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
}
