package eino

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/cloudwego/eino/schema"

	"github.com/sundynix/sundynix-dispatcher/internal/dsl"
	"github.com/sundynix/sundynix-dispatcher/internal/harness"
	"github.com/sundynix/sundynix-shared/contract"
)

// defaultAgentSystem 是 agent 节点未填系统提示词时的兜底。
const defaultAgentSystem = "你是 sundynix-agentix 平台的 AI 助手。"

// board 是图执行的"黑板"：节点按拓扑序流转时读写它。
type board struct {
	uid, sid string
	query    string
	profile  string
	history  []*schema.Message
	kb       string          // 最近一个检索节点的 owner 作用域库名（供 map 并行各项检索）
	refs     []string        // 检索 / 聚合得到的参考资料
	toolOut  []string        // 工具节点产出
	sections []reportSection // map 并行 fan-out 产出的分项成稿（供 render 多章渲染）
	answer   string          // 当前成稿（多 agent 协作时 = 最近一个 agent 的产出 = 成品）
	agentOut []string        // 各上游 agent 的产出（按序），注入下游 agent 上下文以实现接力协作
	rejected bool            // HITL 审批节点拒绝/超时 → 置位，执行中止并返回 errRejected
	fatalErr error           // agent 节点 LLM 调用失败 → 置位，执行中止并上抛 → 任务判 failed（而非 done-空）
}

// 注：本文件原有自研图解释器 runGraph 已退役（编排引擎统一为 compose，见 compose_compiler.go
// 的 execComposeGraph）。以下是被 compose 各节点 lambda 复用的节点执行器与黑板辅助函数。

// refsOf 汇总本次执行的检索来源（检索资料 + 工具产出），供忠实度评测。
func refsOf(b *board) []string {
	out := append([]string{}, b.refs...)
	return append(out, b.toolOut...)
}

// retrieverNode 执行检索节点：kb 按 owner 作用域 → kb_search → 累计参考资料。
func (o *Orchestrator) retrieverNode(ctx context.Context, n dsl.Node, b *board, tr *execTracer) {
	kb := cstr(n.Config, "kb")
	scoped := kb
	if b.uid != "" && kb != "" && !strings.Contains(kb, "/") {
		scoped = b.uid + "/" + kb
	}
	if scoped != "" {
		b.kb = scoped // 记下作用域库名，供后续 map 并行各项检索复用
	}
	end := tr.span("retriever:"+n.ID, "tool", labelOf(n, "检索"))
	refs := o.retrieve(ctx, scoped, b.query)
	if refs != "" {
		b.refs = append(b.refs, refs)
	}
	end(fmt.Sprintf("kb=%s · 命中 %d 段", firstNonEmpty(kb, "（未指定）"), countLines(refs)), nil)
}

// mapNode 执行并行 fan-out：把主题拆成若干项，各项有界并发撰写（复用 report 的 writeSections），
// 成稿存入黑板（结构化 sections + 拼进 answer），并流式呈现进度。
func (o *Orchestrator) mapNode(ctx context.Context, taskID string, n dsl.Node, b *board, tr *execTracer) {
	end := tr.span("map:"+n.ID, "plan", labelOf(n, "并行 fan-out"))
	items := o.planItems(ctx, b.query, cstr(n.Config, "splitBy"))
	end(fmt.Sprintf("拆出 %d 项：%s", len(items), strings.Join(items, " / ")), nil)

	o.emit(taskID, fmt.Sprintf("\n> 并行处理 %d 项…\n\n", len(items)))
	secs, failed := o.writeSections(ctx, b.query, b.kb, items, tr) // 有界并发，trace 出 section:i 各项
	b.sections = secs
	for _, s := range secs {
		chunk := "## " + s.Heading + "\n\n" + s.Body + "\n\n"
		o.emit(taskID, chunk)
		b.answer += chunk
		b.refs = append(b.refs, s.Heading+"："+s.Body)
	}
	// 错误传播：并行子项不再静默丢失——汇总成败；全失败则置致命错，让任务判 failed 而非 done-空。
	if failed > 0 {
		endMap := tr.span("map:"+n.ID+":result", "plan", "并行结果汇总")
		if failed >= len(items) && len(items) > 0 {
			if b.fatalErr == nil {
				b.fatalErr = fmt.Errorf("并行 fan-out 全部 %d 项撰写失败", failed)
			}
			endMap(fmt.Sprintf("全部 %d 项失败", failed), b.fatalErr)
		} else {
			note := fmt.Sprintf("%d/%d 项失败（已标注），其余成功", failed, len(items))
			endMap(note, nil)
			o.emit(taskID, fmt.Sprintf("\n> ⚠️ %s\n\n", note))
		}
	}
}

// execToolNode 执行工具节点：调 MCP 工具，产出累计进黑板；失败降级不阻断。
func (o *Orchestrator) execToolNode(ctx context.Context, taskID string, n dsl.Node, b *board, tr *execTracer) {
	tool, args := dsl.ToolBinding(n)
	if tool == "" {
		return
	}
	node := "tool:" + tool
	if o.tools == nil {
		tr.info(node, "tool", "工具 "+tool, "工具总线未接入，跳过")
		return
	}
	call := map[string]any{}
	for k, v := range args {
		call[k] = v
	}
	if call["q"] == nil && call["query"] == nil {
		call["q"] = b.query
	}
	if b.uid != "" {
		if kbv, ok := call["kb"].(string); ok && kbv != "" && !strings.Contains(kbv, "/") {
			call["kb"] = b.uid + "/" + kbv
		}
	}
	end := tr.span(node, "tool", "调用工具 "+tool)
	cctx, cancel := context.WithTimeout(ctx, toolCallTimeout)
	defer cancel()
	res, err := o.tools.CallTool(cctx, contract.ToolSubjectGo(tool), &contract.ToolCall{Tool: tool, TaskID: taskID, Args: call})
	if err != nil {
		end("调用失败，降级跳过", err)
		return
	}
	if res == nil || !res.OK || res.Content == "" {
		end("无结果，降级跳过", nil)
		return
	}
	end("入参 "+previewArgs(call)+" → 产出 "+truncate(res.Content, 160), nil)
	b.toolOut = append(b.toolOut, "["+tool+"] "+res.Content)
}

// runAgent 执行 agent/模型节点：据黑板拼消息 → 流式回流 token → 累计成稿。
func (o *Orchestrator) runAgent(ctx context.Context, taskID string, b *board, system string, tr *execTracer, node string) {
	rc := &RunCtx{
		System:   firstNonEmpty(system, defaultAgentSystem),
		Query:    b.query,
		Profile:  b.profile,
		History:  b.history,
		ToolOut:  append(append([]string{}, b.toolOut...), b.refs...),
		Upstream: append([]string{}, b.agentOut...), // 前序协作 agent 产出 → 接力
	}
	msgs, _ := buildMessages(ctx, rc)
	// 成本护栏：计入本节点输入 token；若已触顶则中止整图（防失控成本）。
	if bud := harness.BudgetFrom(ctx); bud != nil {
		for _, m := range msgs {
			bud.AddPrompt(m.Content)
		}
		if bud.Exceeded() {
			if b.fatalErr == nil {
				b.fatalErr = errBudget
			}
			tr.emit(node, "system", "error", "token 预算", "已达单任务预算上限，中止", 0)
			return
		}
	}
	tr.emit(node, "model", "start", "模型流式推理", "", 0)
	t0 := time.Now()
	n := 0
	var produced strings.Builder // 本节点自身产出（用于沿图向下游传递）
	// 输出护栏：有状态流式脱敏，跨分片缓冲，杜绝密钥被切断而漏检（逐片正则会漏跨片密钥）。
	red := harness.NewStreamRedactor()
	emit := func(safe string) {
		if safe == "" {
			return
		}
		_ = o.sink.PublishToken(taskID, []byte(safe))
		produced.WriteString(safe)
		n++
	}
	send := func(s string) { emit(red.Push(s)) }
	// reasoning 模型（本地 Qwen3 思考 / DeepSeek-R1 / QwQ 等）的「思考过程」：不进答案、
	// 累计后 surface 到观测轨迹（让用户看到模型在想什么，又不污染最终回答）。
	var reasoning strings.Builder
	onReasoning := func(s string) { reasoning.WriteString(s) }
	var err error
	if o.pool.Ready() {
		err = o.pool.ChatStream(ctx, toChatMessages(msgs), send, onReasoning)
	} else {
		err = o.pool.StreamText(ctx, replyFor(msgs), func(tok []byte) { send(string(tok)) })
	}
	if rc := reasoning.String(); rc != "" {
		tr.info(node, "model", "推理过程", fmt.Sprintf("思考 %d 字：%s", len([]rune(rc)), truncate(rc, 200)))
	}
	if err != nil {
		tr.emit(node, "model", "error", "模型流式推理", err.Error(), time.Since(t0).Milliseconds())
		// 未产出任何 token 即失败 → 标记致命错，让任务判 failed（暴露原因，便于监控告警），
		// 而非静默 done-空。已流出部分 token 的中断也算失败（结果不完整）。
		if b.fatalErr == nil {
			b.fatalErr = fmt.Errorf("agent 模型推理失败: %w", err)
		}
		return
	}
	emit(red.Flush()) // 吐出暂留的尾部（最后一段疑似密钥的判定）
	if bud := harness.BudgetFrom(ctx); bud != nil {
		bud.AddComplete(produced.String()) // 成本护栏：计入本节点输出 token
	}
	if red.Hits() > 0 {
		tr.info(node, "system", "输出护栏", fmt.Sprintf("已脱敏 %d 处疑似密钥/PII", red.Hits()))
	}
	o.recordAgentOutput(b, produced.String()) // 产出入黑板：成当前成稿 + 供下游接力
	tr.emit(node, "model", "end", "模型流式推理",
		fmt.Sprintf("%d tokens / %d 字", n, len([]rune(produced.String()))), time.Since(t0).Milliseconds())
}

// recordAgentOutput 把一个 agent 节点的产出记入黑板：append 到 agentOut（供下游 agent 注入接力），
// 并设为当前成稿 answer（多 agent 协作时，最后一个 agent 的产出即最终成品）。空产出忽略。
func (o *Orchestrator) recordAgentOutput(b *board, out string) {
	if strings.TrimSpace(out) == "" {
		return
	}
	b.agentOut = append(b.agentOut, out)
	b.answer = out
}

// renderNode 执行渲染节点：把当前成稿渲染成 Word（经 mcp-go report_render）。
func (o *Orchestrator) renderNode(ctx context.Context, taskID string, n dsl.Node, b *board, tr *execTracer) {
	if strings.TrimSpace(b.answer) == "" {
		tr.info("render:"+n.ID, "render", labelOf(n, "渲染"), "暂无正文可渲染（render 前需有 agent 产出）")
		return
	}
	format := firstNonEmpty(cstr(n.Config, "format"), "docx")
	end := tr.span("render:"+n.ID, "render", labelOf(n, "渲染 "+format))
	title := truncate(b.query, 40)
	secs := b.sections // map 产出的多章优先；否则把整段成稿当单章
	if len(secs) == 0 {
		secs = []reportSection{{Heading: title, Body: b.answer}}
	}
	if path := o.renderReport(ctx, taskID, title, secs); path != "" {
		end("已落盘："+path, nil)
		o.emit(taskID, "\n\n---\n✅ 已渲染 "+format+" 文档，可在「下载」获取。\n")
	} else {
		end("渲染服务不可用", fmt.Errorf("render unavailable"))
	}
}

// branchNode 执行分支节点：求值条件，按出边的 true/false 标签选出要激活的下游。
// 边带 sourceHandle("true"/"false") 时按标签精确选路（前端 Phase 2 给 branch 的边打标）；
// 无标签的旧图退回"出边顺序 [true,false]"约定，保持向后兼容。
func (o *Orchestrator) branchNode(n dsl.Node, b *board, outs []dsl.Edge, byID map[string]dsl.Node, tr *execTracer) []string {
	cond := cstr(n.Config, "condition")
	res := evalCondition(cond, b)

	var truthy, falsy, deflt []string
	labeled := false
	for _, e := range outs {
		switch e.SourceHandle {
		case "true":
			truthy = append(truthy, e.Target)
			labeled = true
		case "false":
			falsy = append(falsy, e.Target)
			labeled = true
		case "default", "else": // 显式兜底边：主选择落空时走它
			deflt = append(deflt, e.Target)
		}
	}

	var chosen []string
	mode := "边标签"
	if labeled {
		if res {
			chosen = truthy
		} else {
			chosen = falsy
		}
	} else {
		mode = "边序约定[true,false]"
		targets := targetsOf(outs)
		switch {
		case len(targets) >= 2:
			if res {
				chosen = targets[:1]
			} else {
				chosen = targets[1:2]
			}
		case len(targets) == 1 && !res:
			chosen = nil
		default:
			chosen = targets
		}
	}

	// else 兜底：主选择未命中任何下游时，走显式 default/else 边（若配了），避免悄悄落 END。
	usedDefault := false
	if len(chosen) == 0 && len(deflt) > 0 {
		chosen = deflt
		usedDefault = true
	}

	names := make([]string, 0, len(chosen))
	for _, id := range chosen {
		names = append(names, labelOf(byID[id], id))
	}
	// 明确记录选路：走 default / 未匹配收口结束 / 正常选路，杜绝"静默落 END"被误当 bug。
	dest := "[" + strings.Join(names, ", ") + "]"
	switch {
	case usedDefault:
		dest = "default → " + dest
	case len(chosen) == 0:
		dest = "未匹配任何下游 → 收口结束(END)"
	}
	tr.info("branch:"+n.ID, "system", labelOf(n, "分支"),
		fmt.Sprintf("条件「%s」→ %v ⇒ 走 %s（%s）",
			firstNonEmpty(cond, "(空=真)"), res, dest, mode))
	return chosen
}

// approvalNode 是 HITL 人工审批中断：执行到此暂停，把待审摘要推给 UI（exec 事件 kind=approval/phase=await
// + 状态 waiting），阻塞等人工批准/拒绝（带超时，安全默认拒绝）。
// 批准 → 状态回 running 并放行下游；拒绝/超时 → 置 b.rejected 中止全图。返回应激活的下游（拒绝=空）。
func (o *Orchestrator) approvalNode(ctx context.Context, taskID string, n dsl.Node, b *board, tr *execTracer, outs []dsl.Edge) []string {
	title, summary := approvalSummary(n, b)

	// 未接审批通道（单测/降级）→ 自动放行，避免无人应答卡死。
	if o.approval == nil {
		tr.info("approval:"+n.ID, "approval", title, "未接审批通道，自动放行")
		return targetsOf(outs)
	}

	// 暂停：发待审事件（UI 据 kind=approval & phase=await 弹批准/拒绝）+ 置任务 waiting。
	tr.emit("approval:"+n.ID, "approval", "await", title, summary, 0)
	o.setStatus(taskID, contract.TaskWaiting, title)

	dec, err := o.approval.WaitApproval(ctx, taskID, approvalTimeout)
	if err != nil { // 超时 / ctx 取消 → 安全默认拒绝
		b.rejected = true
		b.answer = "❌ 审批超时未决，已自动拒绝：" + title
		tr.emit("approval:"+n.ID, "approval", "error", title, "审批超时，自动拒绝", 0)
		return nil
	}
	// 批准/拒绝的落盘与终态事件与中断式审批共用，杜绝两路文案/语义漂移。
	o.applyApprovalDecision(taskID, n, b, dec, dec.Approved, tr)
	if b.rejected {
		return nil
	}
	return targetsOf(outs)
}

// approvalSummary 据审批节点配置 + 当前成稿，算出待审标题与摘要（带产出预览，便于审批人判断）。
func approvalSummary(n dsl.Node, b *board) (title, summary string) {
	title = firstNonEmpty(cstr(n.Config, "title"), labelOf(n, "人工审批"))
	prompt := firstNonEmpty(cstr(n.Config, "prompt"), "请审批是否继续执行后续步骤")
	summary = prompt
	if b.answer != "" {
		summary = prompt + "\n—— 当前产出预览 ——\n" + truncate(b.answer, 400)
	}
	return title, summary
}

// applyApprovalDecision 把一次人工决定落到黑板并发终态事件：拒绝 → 置 rejected + 拒绝语；
// 批准 → 状态回 running 放行。阻塞式(approvalNode)与中断式(approvalInterruptLambda)审批共用。
func (o *Orchestrator) applyApprovalDecision(taskID string, n dsl.Node, b *board, dec *contract.ApprovalDecision, approved bool, tr *execTracer) {
	title := firstNonEmpty(cstr(n.Config, "title"), labelOf(n, "人工审批"))
	if !approved {
		note := "审批人拒绝"
		if dec != nil {
			note = firstNonEmpty(dec.Note, note)
		}
		b.rejected = true
		b.answer = "❌ 已被拒绝：" + note
		tr.emit("approval:"+n.ID, "approval", "end", title, "拒绝："+note, 0)
		return
	}
	note := "放行"
	if dec != nil {
		note = firstNonEmpty(dec.Note, note)
	}
	o.setStatus(taskID, contract.TaskRunning, "审批通过，继续执行")
	tr.emit("approval:"+n.ID, "approval", "end", title, "批准："+note, 0)
}

// targetsOf 取一组边的目标节点 ID（保持顺序）。
func targetsOf(edges []dsl.Edge) []string {
	out := make([]string, 0, len(edges))
	for _, e := range edges {
		out = append(out, e.Target)
	}
	return out
}

// evalCondition 求值 branch 条件。支持：
//
//	空 → true；关键字 refs/tools/answer/profile 作左值（取数量/字数）；
//	形如 "a op b"（op: >= <= == != > <）数值比较；其余非空 → 默认真。
func evalCondition(cond string, b *board) bool {
	cond = strings.TrimSpace(cond)
	if cond == "" {
		return true
	}
	for _, op := range []string{">=", "<=", "==", "!=", ">", "<"} {
		if i := strings.Index(cond, op); i >= 0 {
			l := resolveOperand(strings.TrimSpace(cond[:i]), b)
			r := resolveOperand(strings.TrimSpace(cond[i+len(op):]), b)
			switch op {
			case ">":
				return l > r
			case "<":
				return l < r
			case ">=":
				return l >= r
			case "<=":
				return l <= r
			case "==":
				return l == r
			case "!=":
				return l != r
			}
		}
	}
	return true
}

// resolveOperand 把条件里的左/右值解析为数值（关键字取运行时数量，否则按字面量）。
func resolveOperand(s string, b *board) float64 {
	switch strings.ToLower(s) {
	case "refs":
		return float64(len(b.refs))
	case "tools":
		return float64(len(b.toolOut))
	case "answer":
		return float64(len([]rune(b.answer)))
	case "profile":
		return float64(len([]rune(b.profile)))
	}
	f, _ := strconv.ParseFloat(s, 64)
	return f
}

// aggregate 按策略合并多段参考资料为一段。
func aggregate(strategy string, parts []string) []string {
	var nonEmpty []string
	for _, p := range parts {
		if strings.TrimSpace(p) != "" {
			nonEmpty = append(nonEmpty, p)
		}
	}
	if len(nonEmpty) == 0 {
		return nil
	}
	switch strategy {
	case "去重合并":
		seen := map[string]bool{}
		var uniq []string
		for _, p := range nonEmpty {
			if !seen[p] {
				seen[p] = true
				uniq = append(uniq, p)
			}
		}
		return []string{strings.Join(uniq, "\n---\n")}
	case "摘要":
		return []string{truncate(strings.Join(nonEmpty, "\n"), 800)}
	default: // 拼接
		return []string{strings.Join(nonEmpty, "\n---\n")}
	}
}

// ---- 小工具 ----

func meta(t *contract.Task, key string) string {
	v, _ := t.Meta[key].(string)
	return v
}

func cstr(cfg map[string]any, key string) string {
	if cfg == nil {
		return ""
	}
	v, ok := cfg[key]
	if !ok || v == nil {
		return ""
	}
	if s, ok := v.(string); ok {
		return strings.TrimSpace(s)
	}
	return strings.TrimSpace(fmt.Sprint(v))
}

func cbool(cfg map[string]any, key string) bool {
	if cfg == nil {
		return false
	}
	if v, ok := cfg[key].(bool); ok {
		return v
	}
	return false
}

func labelOf(n dsl.Node, def string) string {
	if strings.TrimSpace(n.Label) != "" {
		return n.Label
	}
	return def
}

func countLines(s string) int {
	s = strings.TrimSpace(s)
	if s == "" {
		return 0
	}
	return strings.Count(s, "\n") + 1
}
