package eino

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"strings"
	"time"

	"github.com/cloudwego/eino/components/tool"
	"github.com/cloudwego/eino/compose"
	"github.com/cloudwego/eino/flow/agent/react"
	"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"
)

// reactMaxStep 限制 ReAct 推理-工具循环的步数上限（控成本/时延）。
// 研究型任务（搜索→抓取→推理 多轮）8 步偏紧易触顶报错；默认 12，可经 REACT_MAX_STEP 调。
func reactMaxStep() int { return envInt("REACT_MAX_STEP", 12) }

// streamHasToolCall 扫描模型整段流输出，任一片段含 tool call 即判定为工具调用。
// 比默认"只看首片段"鲁棒（兼容先吐文本/思考再给 tool call 的模型，如 deepseek）。
// 契约要求：返回前必须 Close 传入的流。
func streamHasToolCall(_ context.Context, sr *schema.StreamReader[*schema.Message]) (bool, error) {
	defer sr.Close()
	for {
		msg, err := sr.Recv()
		if err == io.EOF {
			return false, nil
		}
		if err != nil {
			return false, err
		}
		if len(msg.ToolCalls) > 0 {
			return true, nil
		}
	}
}

// mcpTool 把一个 MCP 工具（NATS 那头）适配成 Eino InvokableTool：
// 模型给的参数(JSON) + 运行时注入的绑定参数(uid/kb，不暴露给模型) 合并后经 NATS 调 MCP。
type mcpTool struct {
	info    *schema.ToolInfo
	mcpName string              // MCP 侧真实工具名（可与 info.Name 不同）
	subject func(string) string // contract.ToolSubjectGo/Py
	bind    map[string]any      // 运行时注入参数
	caller  ToolCaller
	taskID  string
	tr      *execTracer
}

func (m *mcpTool) Info(_ context.Context) (*schema.ToolInfo, error) { return m.info, nil }

// InvokableRun 执行一次工具调用：合并参数 → NATS 调 MCP → 返回观察给模型；
// 调用本身落一条 ExecEvent 轨迹（"模型自己调了哪个工具"在观测里可见）。
func (m *mcpTool) InvokableRun(ctx context.Context, argsJSON string, _ ...tool.Option) (string, error) {
	args := map[string]any{}
	if argsJSON != "" {
		_ = json.Unmarshal([]byte(argsJSON), &args)
	}
	for k, v := range m.bind {
		args[k] = v
	}
	log.Printf("[react] 模型自主调用工具 %s (mcp=%s) task=%s args=%s", m.info.Name, m.mcpName, m.taskID, truncate(argsJSON, 120))
	end := m.tr.span("tool:"+m.mcpName, "tool", "模型自主调用 "+m.info.Name)
	cctx, cancel := context.WithTimeout(ctx, toolCallTimeout)
	defer cancel()
	res, err := m.caller.CallTool(cctx, m.subject(m.mcpName), &contract.ToolCall{Tool: m.mcpName, TaskID: m.taskID, Args: args})
	if err != nil {
		end("调用失败", err)
		return "工具调用失败：" + err.Error(), nil // 作为观察返回，不中断 ReAct
	}
	if res == nil || !res.OK {
		msg := "工具无结果"
		if res != nil && res.Error != "" {
			msg = res.Error
		}
		end(msg, nil)
		return msg, nil
	}
	end("入参 "+truncate(argsJSON, 120)+" → "+truncate(res.Content, 160), nil)
	return res.Content, nil
}

// toolCatalogEntry 是 MCP list_tools 上报的一条工具元信息（与 mcp-go listTools 输出对齐）。
type toolCatalogEntry struct {
	Name      string `json:"name"`
	CN        string `json:"cn"`
	Desc      string `json:"desc"`
	Agent     bool   `json:"agent_exposed"`
	AgentName string `json:"agent_name"`
	Params    []struct {
		Name     string `json:"name"`
		Type     string `json:"type"`
		Desc     string `json:"desc"`
		Required bool   `json:"required"`
	} `json:"params"`
	Inject []string `json:"inject"`
}

// agentTools 动态构建 ReAct 可用的工具集：分别向 mcp-go / mcp-py 探 list_tools 自描述目录，
// 取 agent_exposed 的工具按上报参数 schema 建 InvokableTool；inject 参数（user_id/session_id/
// kb/task_id）服务端运行时绑定、不暴露给模型。某台 MCP 离线即跳过（降级）。
// 新增工具只需在对应 MCP 注册表标 agent，无需改这里（杜绝硬编码）。
func (o *Orchestrator) agentTools(b *board, taskID string, tr *execTracer) []tool.BaseTool {
	if o.tools == nil {
		return nil
	}
	var out []tool.BaseTool
	out = append(out, o.discoverTools(contract.ToolSubjectGo, b, taskID, tr)...)
	out = append(out, o.discoverTools(contract.ToolSubjectPy, b, taskID, tr)...)
	return out
}

// discoverTools 向某台 MCP（subject 前缀决定 go/py）探 list_tools，把 agent_exposed 的工具
// 转成 Eino InvokableTool。该 MCP 不可用 / 无应答时返回空（不阻断）。
func (o *Orchestrator) discoverTools(subject func(string) string, b *board, taskID string, tr *execTracer) []tool.BaseTool {
	cctx, cancel := context.WithTimeout(context.Background(), toolCallTimeout)
	defer cancel()
	res, err := o.tools.CallTool(cctx, subject("list_tools"), &contract.ToolCall{Tool: "list_tools"})
	if err != nil || res == nil || !res.OK {
		return nil
	}
	var cat struct {
		Tools []toolCatalogEntry `json:"tools"`
	}
	if json.Unmarshal([]byte(res.Content), &cat) != nil {
		return nil
	}
	injectVal := map[string]any{"user_id": b.uid, "session_id": b.sid, "task_id": taskID, "kb": b.kb}

	var out []tool.BaseTool
	for _, e := range cat.Tools {
		if !e.Agent {
			continue
		}
		params := map[string]*schema.ParameterInfo{}
		for _, p := range e.Params {
			params[p.Name] = &schema.ParameterInfo{Type: schema.DataType(p.Type), Desc: p.Desc, Required: p.Required}
		}
		bind := map[string]any{}
		for _, inj := range e.Inject {
			if v, ok := injectVal[inj]; ok && v != "" {
				bind[inj] = v
			}
		}
		name := e.AgentName
		if name == "" {
			name = e.Name
		}
		out = append(out, &mcpTool{
			mcpName: e.Name,
			subject: subject,
			caller:  o.tools, taskID: taskID, tr: tr,
			bind: bind,
			info: &schema.ToolInfo{
				Name: name, Desc: e.Desc,
				ParamsOneOf: schema.NewParamsOneOfByParams(params),
			},
		})
	}
	return out
}

// runReactAgent 执行带"自主工具"的 agent 节点：模型在 ReAct 循环里自行决定调哪些 MCP 工具。
// 模型不支持函数调用 / 无工具时降级回普通 runAgent。最终答复流式回流；工具调用由适配器落轨迹。
func (o *Orchestrator) runReactAgent(ctx context.Context, taskID string, b *board, system string, n dsl.Node, tr *execTracer, node string) {
	tcm := o.pool.ToolCallingModel()
	tools := o.agentTools(b, taskID, tr)
	if tcm == nil || len(tools) == 0 {
		tr.info(node, "system", "ReAct 降级", "模型不支持函数调用或无可用工具，退回普通对话")
		o.runAgent(ctx, taskID, b, system, tr, node)
		return
	}

	ag, err := react.NewAgent(ctx, &react.AgentConfig{
		ToolCallingModel: tcm,
		ToolsConfig:      compose.ToolsNodeConfig{Tools: tools},
		MaxStep:          reactMaxStep(),
		// 默认检查器只看首个流片段；deepseek 等常先吐文本再给 tool call → 漏判。
		// 改成扫描整段流，任一片段含 tool call 即判定为工具调用。
		StreamToolCallChecker: streamHasToolCall,
	})
	if err != nil {
		tr.emit(node, "model", "error", "构建 ReAct 智能体", err.Error(), 0)
		o.runAgent(ctx, taskID, b, system, tr, node)
		return
	}

	rc := &RunCtx{
		UserID: b.uid, SessionID: b.sid,
		System: firstNonEmpty(system, defaultAgentSystem),
		Query:  b.query,
		// 自主 agent 不预注入画像：让它经 recall_user_memory 工具按需自取（否则模型直接答、不调工具）。
		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", "ReAct 智能体（自主调工具）", fmt.Sprintf("%d 个工具可用", len(tools)), 0)
	t0 := time.Now()
	sr, err := ag.Stream(ctx, msgs)
	if err != nil {
		tr.emit(node, "model", "error", "ReAct 智能体", err.Error(), time.Since(t0).Milliseconds())
		return
	}
	defer sr.Close()
	o.streamAgentReply(ctx, taskID, b, sr, tr, node, "ReAct 智能体", t0)
}

// streamAgentReply 把一个 agent / 协调者的流式输出回流到 sink：跨分片脱敏 + 计输出 token +
// 落产出(供下游接力) + 轨迹收尾。runReactAgent 与 runCoordinator 共用，杜绝两路尾段漂移。
func (o *Orchestrator) streamAgentReply(ctx context.Context, taskID string, b *board, sr *schema.StreamReader[*schema.Message], tr *execTracer, node, label string, t0 time.Time) {
	chunks := 0
	var produced strings.Builder       // 本节点产出（供下游 agent 接力）
	red := harness.NewStreamRedactor() // 输出护栏：跨分片脱敏，杜绝密钥被切断而漏检
	emit := func(safe string) {
		if safe == "" {
			return
		}
		_ = o.sink.PublishToken(taskID, []byte(safe))
		produced.WriteString(safe)
		chunks++
	}
	for {
		chunk, rerr := sr.Recv()
		if rerr == io.EOF {
			break
		}
		if rerr != nil {
			tr.emit(node, "model", "error", label, rerr.Error(), time.Since(t0).Milliseconds())
			return
		}
		if chunk.Content == "" {
			continue // 工具调用片段无正文，跳过；正文只来自模型答复
		}
		emit(red.Push(chunk.Content))
	}
	emit(red.Flush()) // 吐出暂留尾部
	if bud := harness.BudgetFrom(ctx); bud != nil {
		bud.AddComplete(produced.String()) // 成本护栏：计入输出 token
	}
	o.recordAgentOutput(b, produced.String())
	tr.emit(node, "model", "end", label,
		fmt.Sprintf("%d 段输出 / %d 字", chunks, len([]rune(produced.String()))), time.Since(t0).Milliseconds())
}
