// Package rag 实现 RAG 核心链：embedding(provider 抽象) + Milvus 向量库 + 入库/检索。
// 是 LLM Wiki 混合检索的向量路；Bleve/Neo4j 融合为后续扩展。
package rag

import (
	"context"
	"errors"
	"fmt"
	"log"
	"os"
	"strconv"
	"sync"

	"github.com/sundynix/sundynix-shared/contract"
)

const (
	embedBatch       = 10 // 每批向量化的块数（兼顾进度可观测 + provider 单批上限）
	embedConcurrency = 4  // 并发批数：几十万字上千块串行太慢，限并发跑快数倍且不打爆 provider 速率
)

// 图谱抽取窗口化参数（env 可配，便于按文档体量/成本调）：取代"整篇喂 LLM"（几十万字必爆上下文）。
// 把已切的语义块合并成 ~graphWindowRunes 的窗口，逐窗并发抽三元组；实体按 kb+name 在 Neo4j 去重。
func graphWindowRunes() int { return envInt("GRAPH_WINDOW_RUNES", 4000) } // 每窗字数（远小于模型上下文）
func graphMaxWindows() int  { return envInt("GRAPH_MAX_WINDOWS", 60) }    // 封顶窗口数（控成本；大文档超出略过+告警）
func graphConcurrency() int { return envInt("GRAPH_CONCURRENCY", 3) }     // 并发抽取窗口数

// envInt 读正整数 env，缺省/非法时返回 def。
func envInt(key string, def int) int {
	if v := os.Getenv(key); v != "" {
		if n, err := strconv.Atoi(v); err == nil && n > 0 {
			return n
		}
	}
	return def
}

// Config 是 RAG 引擎的初始化配置。
type Config struct {
	MilvusAddr                         string
	EmbedBase, EmbedKey, EmbedModel    string
	RerankBase, RerankKey, RerankModel string
	Neo4jURI, Neo4jUser, Neo4jPass     string
}

// Engine 聚合 embedding + Milvus(向量) + Bleve(全文) + Neo4j(图谱) → RRF 融合 + 可选 rerank。
// embedding 与 chat(图谱抽取用)可热更新（控制面下发）。
type Engine struct {
	mu        sync.RWMutex
	emb       *embedClient
	chat      *chatClient // 控制面下发的主对话模型
	graphChat *chatClient // 可选：图谱抽取专用便宜模型（env GRAPH_CHAT_*）；未配则回退主 chat
	mv        *milvusStore
	bleve     *bleveStore
	rerank    *rerankClient
	graph     *graphStore
}

// SetEmbedding 热更新 embedding 配置（控制面变更时调用）。空配置=关闭向量检索。
func (e *Engine) SetEmbedding(base, key, model string) {
	e.mu.Lock()
	defer e.mu.Unlock()
	if base == "" || model == "" {
		e.emb = nil
		return
	}
	e.emb = newEmbedClient(base, key, model)
	log.Printf("[rag] embedding 配置: %s model=%s", base, model)
}

// SetChat 热更新对话模型配置（图谱实体抽取用，复用控制面 chat 模型）。
func (e *Engine) SetChat(base, key, model string) {
	e.mu.Lock()
	defer e.mu.Unlock()
	e.chat = newChatClient(base, key, model)
	if e.chat.ready() {
		log.Printf("[rag] 图谱抽取模型: %s model=%s", base, model)
	}
}

func (e *Engine) embed() *embedClient {
	e.mu.RLock()
	defer e.mu.RUnlock()
	return e.emb
}

func (e *Engine) chatClient() *chatClient {
	e.mu.RLock()
	defer e.mu.RUnlock()
	return e.chat
}

// graphChatClient 返回图谱抽取用的模型：优先专用便宜模型(env GRAPH_CHAT_*)，未配则回退主 chat。
// 让"每篇最多 60 次图谱抽取"走便宜模型，主对话仍用好模型——成本与质量解耦。
func (e *Engine) graphChatClient() *chatClient {
	e.mu.RLock()
	defer e.mu.RUnlock()
	if e.graphChat.ready() {
		return e.graphChat
	}
	return e.chat
}

// Open 建立 RAG 引擎。各路连不上 → 降级（不阻断工具服务）。
func Open(ctx context.Context, cfg Config) *Engine {
	e := &Engine{
		bleve:  openBleve(),
		rerank: newRerankClient(cfg.RerankBase, cfg.RerankKey, cfg.RerankModel),
		graph:  openGraph(ctx, cfg.Neo4jURI, cfg.Neo4jUser, cfg.Neo4jPass),
	}
	// 图谱抽取专用便宜模型（可选，env GRAPH_CHAT_*）：未配则图谱抽取回退控制面主 chat。
	if gb := os.Getenv("GRAPH_CHAT_BASE"); gb != "" {
		e.graphChat = newChatClient(gb, os.Getenv("GRAPH_CHAT_KEY"), os.Getenv("GRAPH_CHAT_MODEL"))
		if e.graphChat.ready() {
			log.Printf("[rag] 图谱抽取专用便宜模型: %s model=%s", gb, os.Getenv("GRAPH_CHAT_MODEL"))
		}
	}
	if e.rerank.ready() {
		log.Printf("[rag] rerank: %s model=%s", cfg.RerankBase, cfg.RerankModel)
	}
	if cfg.EmbedBase != "" && cfg.EmbedModel != "" {
		e.SetEmbedding(cfg.EmbedBase, cfg.EmbedKey, cfg.EmbedModel)
	} else {
		log.Println("[rag] embedding 未配置（待控制面下发），向量检索暂降级")
	}
	if cfg.MilvusAddr != "" {
		mv, err := openMilvus(ctx, cfg.MilvusAddr)
		if err != nil {
			log.Printf("[rag] Milvus 不可用，向量检索降级: %v", err)
		} else {
			e.mv = mv
			log.Printf("[rag] Milvus connected %s", cfg.MilvusAddr)
		}
	}
	return e
}

// Triples 返回某 kb 的图谱三元组（供 UI 可视化）。
func (e *Engine) Triples(ctx context.Context, kb string, limit int) []Triple {
	return e.graph.triples(ctx, kb, limit)
}

// Ready 报告 RAG 是否可用（embedding + Milvus 均就绪）。
func (e *Engine) Ready() bool { return e.embed().ready() && e.mv != nil }

// Status 报告各依赖子系统的就绪情况（供 health 工具 → 控制台健康灯）。
func (e *Engine) Status() map[string]bool {
	return map[string]bool{
		"milvus":    e.mv != nil,
		"neo4j":     e.graph.ready(),
		"embedding": e.embed().ready(),
	}
}

// Ingest 把一段文本切块 → 分批向量化 → 写 Milvus + Bleve，返回块数。
// doc 非空表示这是某篇文档/笔记（按 doc 先删旧块再写，支持编辑替换，不重复累积）。
// onProgress 非空时逐阶段/逐批回调进度（用于实时入库监控）。
func (e *Engine) Ingest(ctx context.Context, kb, doc, text string, onProgress func(contract.IngestEvent)) (int, error) {
	emit := func(ev contract.IngestEvent) {
		if onProgress != nil {
			onProgress(ev)
		}
	}
	if !e.Ready() {
		return 0, errors.New("rag 未配置（需 embedding + Milvus）")
	}
	chunks := chunk(text)
	if len(chunks) == 0 {
		return 0, nil
	}
	emit(contract.IngestEvent{Stage: "切块", Total: len(chunks), Chunks: previews(chunks), Msg: "拆为 " + itoa(len(chunks)) + " 块"})

	// 并发分批向量化（保序，逐批回报进度）—— 几十万字上千块时比串行快数倍。
	vecs, err := e.embedAll(ctx, chunks, emit)
	if err != nil {
		emit(contract.IngestEvent{Stage: "失败", Error: "向量化: " + err.Error()})
		return 0, err
	}

	emit(contract.IngestEvent{Stage: "写Milvus", Msg: "向量库写入中"})
	if len(vecs) > 0 {
		e.mv.deleteDoc(ctx, kb, doc, len(vecs[0])) // 编辑/重入库：先清该 doc 旧块
	}
	if err := e.mv.insert(ctx, kb, doc, chunks, vecs); err != nil {
		emit(contract.IngestEvent{Stage: "失败", Error: "写Milvus: " + err.Error()})
		return 0, err
	}
	emit(contract.IngestEvent{Stage: "写Bleve", Msg: "全文索引写入中"})
	e.bleve.deleteDoc(kb, doc)
	_ = e.bleve.index(kb, doc, chunks) // 同步写全文索引（失败不阻断向量入库）

	// 图谱路：窗口化并发抽实体/关系 → Neo4j（可降级，不阻断向量入库）。
	// 几十万字按 ~graphWindowRunes 分窗逐窗抽，全覆盖；实体在 Neo4j 按 kb+name 去重。
	if e.graph.ready() && e.graphChatClient().ready() {
		e.extractGraphWindowed(ctx, kb, doc, chunks, emit) // doc=file_id：图谱关系按它标源，供级联删
	}

	return len(chunks), nil
}

// previews 取每块的前若干字作为预览（供 UI 展示拆分情况）。
func previews(chunks []string) []string {
	out := make([]string, len(chunks))
	for i, c := range chunks {
		r := []rune(c)
		if len(r) > 50 {
			out[i] = string(r[:50]) + "…"
		} else {
			out[i] = c
		}
	}
	return out
}

func itoa(n int) string {
	if n == 0 {
		return "0"
	}
	var b []byte
	for n > 0 {
		b = append([]byte{byte('0' + n%10)}, b...)
		n /= 10
	}
	return string(b)
}

// Search 混合检索：Milvus(向量) + Bleve(全文) → RRF 融合 → 可选 rerank → topK。降级时返回空。
func (e *Engine) Search(ctx context.Context, kb, query string, topK int) ([]Hit, error) {
	if !e.Ready() {
		return nil, nil
	}
	if topK <= 0 {
		topK = 5
	}
	fanout := topK * 3

	vecHits, ftHits, graphHits := e.searchPaths(ctx, kb, query, fanout)
	// RRF 融合（三路，按文本去重）
	cand := rrf([][]Hit{vecHits, ftHits, graphHits}, fanout)
	log.Printf("[rag] hybrid: 向量=%d 全文=%d 图谱=%d → 融合=%d", len(vecHits), len(ftHits), len(graphHits), len(cand))

	// 可选 rerank：对融合候选重排取 topK
	if e.rerank.ready() && len(cand) > 1 {
		if rr, rerr := e.rerank.rerank(ctx, query, cand, topK); rerr == nil {
			return rr, nil
		} else {
			log.Printf("[rag] rerank 降级（用 RRF 结果）: %v", rerr)
		}
	}
	if len(cand) > topK {
		cand = cand[:topK]
	}
	return cand, nil
}

// DeleteDoc 按 file_id 级联删某文档在三库的痕迹：Milvus 向量块 + Bleve 全文块 + Neo4j 关系。
// DeleteDoc 按 file_id 级联删三库痕迹。三库**全试一遍**（最大化清理，不因一处失败漏删另两处），
// 聚合各库错误上返。三库删均幂等（删不存在=no-op），故上层可安全重试。
func (e *Engine) DeleteDoc(ctx context.Context, kb, fileID string) error {
	if fileID == "" {
		return errors.New("file_id 必填")
	}
	var errs []error
	if err := e.mv.deleteByFile(ctx, kb, fileID); err != nil {
		errs = append(errs, fmt.Errorf("向量: %w", err))
	}
	if err := e.bleve.deleteDoc(kb, fileID); err != nil {
		errs = append(errs, fmt.Errorf("全文: %w", err))
	}
	if err := e.graph.deleteByFile(ctx, kb, fileID); err != nil {
		errs = append(errs, fmt.Errorf("图谱: %w", err))
	}
	if len(errs) > 0 {
		return fmt.Errorf("级联删部分失败（幂等，可重试）: %w", errors.Join(errs...))
	}
	log.Printf("[rag] 已删除文档痕迹 kb=%s file_id=%s（向量/全文/图谱）", kb, fileID)
	return nil
}

// searchPaths 跑三路召回，返回各路命中（供混合融合与离线评测按单路对比）。
func (e *Engine) searchPaths(ctx context.Context, kb, query string, fanout int) (vec, ft, graph []Hit) {
	if vecs, err := e.embed().Embed(ctx, []string{query}); err == nil && len(vecs) > 0 {
		vec, _ = e.mv.search(ctx, kb, vecs[0], fanout)
	}
	ft = e.bleve.search(kb, query, fanout)
	graph = e.graph.search(ctx, kb, query, fanout)
	return
}

// SearchByMode 按指定模式返回 topK（评测用，纯检索不 rerank，便于公平对比）。
// mode: vector|fulltext|graph|hybrid(RRF)。
func (e *Engine) SearchByMode(ctx context.Context, kb, query string, topK int, mode string) []Hit {
	if !e.Ready() || topK <= 0 {
		if topK <= 0 {
			topK = 5
		}
	}
	fanout := topK * 3
	vec, ft, graph := e.searchPaths(ctx, kb, query, fanout)
	var hits []Hit
	switch mode {
	case "vector":
		hits = vec
	case "fulltext":
		hits = ft
	case "graph":
		hits = graph
	default: // hybrid
		hits = rrf([][]Hit{vec, ft, graph}, fanout)
	}
	if len(hits) > topK {
		hits = hits[:topK]
	}
	return hits
}

func (e *Engine) Close() {
	if e.mv != nil {
		e.mv.close()
	}
	if e.bleve != nil {
		e.bleve.close() // 落盘版释放锁并刷盘
	}
	e.graph.close(context.Background())
}

// chunk 的实现已移到 chunk.go（递归 + 句界 + 重叠 + rune 安全的语义切块）。
