package agent

import (
	"context"
	"errors"
	"fmt"
	"log"
	"strings"
	"sync"
	"time"
	"unicode"
)

type Agent struct {
	cfg     Config
	llm     *LLMClient
	store   *SessionStore
	tools   *Toolset
	docker  *DockerConfigStore
	apiCfg  *APIConfigStore
	coop    *CoopManager
	locks   map[string]*sync.Mutex
	locksMu sync.Mutex
	liveMu  sync.Mutex
	live    map[string]*liveReg
}

// liveReg 记录某会话当前"在线"的 SSE 连接，用于推送后台任务完成通知。
type liveReg struct {
	emit func(Event)
}

type Event struct {
	Type string `json:"type"`
	Data any    `json:"data,omitempty"`
}

func NewAgent(cfg Config, store *SessionStore, dockerCfg *DockerConfigStore, apiCfg *APIConfigStore) *Agent {
	agent := &Agent{
		cfg:    cfg,
		llm:    NewLLMClient(cfg, apiCfg),
		store:  store,
		docker: dockerCfg,
		apiCfg: apiCfg,
		locks:  make(map[string]*sync.Mutex),
		live:   make(map[string]*liveReg),
	}
	agent.tools = NewToolset(cfg, dockerCfg, apiCfg)
	agent.coop = NewCoopManager()
	agent.tools.coop = agent.coop
	agent.coop.SetNotify(agent.notifyCoopDone)
	return agent
}

func (a *Agent) Run(ctx context.Context, sessionID, userContent string, emit func(Event)) error {
	if strings.TrimSpace(userContent) == "" {
		emit(Event{Type: "error", Data: map[string]any{"message": "消息内容不能为空"}})
		return errors.New("消息内容不能为空")
	}

	lock := a.sessionLock(sessionID)
	lock.Lock()
	defer lock.Unlock()

	session, ok := a.store.Get(sessionID)
	if !ok {
		emit(Event{Type: "error", Data: map[string]any{"message": "会话不存在"}})
		return errors.New("会话不存在")
	}
	// 在私有副本上操作，避免修改持久化对象与其他会话的序列化产生数据竞争
	session = cloneSession(session)

	if session.Title == "" || session.Title == "新对话" {
		session.Title = firstRunes(userContent, 30)
	}
	session.Messages = append(session.Messages, Message{
		Role:    "user",
		Content: stringPointer(userContent),
	})
	a.store.Save(session)

	emit(Event{Type: "meta", Data: map[string]any{
		"session_id": session.ID,
		"title":      session.Title,
	}})

	history := session.Messages
	compressed := false
	iterations := 0
	emptyRetries := 0

	for {
		iterations++
		systemMessage := Message{
			Role:    "system",
			Content: stringPointer(a.systemPrompt(sessionID)),
		}
		apiMessages := append([]Message{systemMessage}, history...)

		if estimateMessages(apiMessages) > a.cfg.MaxContextTokens {
			if summary, err := a.compressHistory(ctx, sessionID, history); err != nil {
				history = truncateHistory(history, a.cfg.KeepRecentMessages)
				compressed = true
				emit(Event{Type: "notice", Data: map[string]any{
					"text": "上下文较长，自动压缩失败，已降级为截断早期对话: " + err.Error(),
				}})
			} else {
				history = summary
				compressed = true
				emit(Event{Type: "notice", Data: map[string]any{
					"text": "上下文较长，已自动压缩早期对话（分块摘要），关键信息会保留。",
				}})
			}
		}

		apiMessages = append([]Message{systemMessage}, history...)
		assistant, err := a.streamAssistantReply(ctx, apiMessages, a.tools.Definitions(), emit)
		if err != nil {
			// 模型返回空内容：强制压缩上下文后重试，而非直接中断会话。
			// 长会话（如跑分循环累积大量工具输出）时模型容易因输入过大而返回空，
			// 压缩后通常可恢复；多次仍空则优雅退出，保留会话状态供用户继续。
			if errors.Is(err, errEmptyResponse) {
				emptyRetries++
				if emptyRetries <= 2 {
					emit(Event{Type: "notice", Data: map[string]any{
						"text": "模型返回空内容，正在压缩上下文后重试…",
					}})
					if summary, serr := a.compressHistory(ctx, sessionID, history); serr == nil {
						history = summary
					} else {
						history = truncateHistory(history, a.cfg.KeepRecentMessages)
					}
					session.Messages = history
					a.store.Save(session)
					continue
				}
				session.Messages = history
				a.store.Save(session)
				emit(Event{Type: "notice", Data: map[string]any{
					"text": "模型多次返回空内容，本轮已停止。你可以继续发消息让我接着处理（程序不会退出）。",
				}})
				emit(Event{Type: "done", Data: map[string]any{
					"compressed": compressed,
					"iterations": iterations,
				}})
				return nil
			}
			session.Messages = history
			a.store.Save(session)
			if errors.Is(err, context.Canceled) {
				// 用户主动停止（前端 stop 按钮 / Esc）：是正常中断，不按错误展示
				emit(Event{Type: "notice", Data: map[string]any{
					"text": "已停止本次生成，已完成的对话进度已保存，可继续发送新消息。",
				}})
			} else {
				emit(Event{Type: "error", Data: map[string]any{"message": err.Error()}})
			}
			return err
		}
		emptyRetries = 0

		history = append(history, assistant)
		if len(assistant.ToolCalls) == 0 {
			break
		}

		// 本轮是否成功下发了协作任务：分发完成后本轮直接收尾，
		// 避免控制层在 run_coop 之后继续空转（sleep 等待 / 反复轮询），
		// coop 完成后会以 user 身份推送完成通知并启动新一轮处理。
		dispatched := false
		for _, call := range assistant.ToolCalls {
			emit(Event{Type: "tool", Data: map[string]any{
				"call_id": call.ID,
				"name":    call.Function.Name,
				"input":   call.Function.Arguments,
			}})

			result := a.tools.Execute(ctx, sessionID, call.Function.Name, call.Function.Arguments)
			result.Output = truncateRunes(result.Output, a.cfg.MaxToolResultChars)
			history = append(history, Message{
				Role:       "tool",
				ToolCallID: call.ID,
				Content:    stringPointer(result.Output),
			})
			if call.Function.Name == "run_coop" && result.Success {
				dispatched = true
			}

			emit(Event{Type: "tool_result", Data: map[string]any{
				"call_id": call.ID,
				"name":    call.Function.Name,
				"output":  result.Output,
				"success": result.Success,
			}})
		}

		// 每轮迭代后落盘：长任务（如跑分循环）期间用户刷新页面也能看到
		// 已完成轮次的消息与工具结果，而不是只能等整轮 Run 结束。
		session.Messages = history
		a.store.Save(session)

		if dispatched {
			emit(Event{Type: "notice", Data: map[string]any{
				"text": "协作任务已全部下发，本轮收尾结束。任务完成后会自动以用户身份推送完成通知并启动新一轮处理，无需在等待上消耗本轮回合。",
			}})
			break
		}

		if iterations >= a.cfg.MaxIterations {
			emit(Event{Type: "notice", Data: map[string]any{
				"text": fmt.Sprintf("已达到 %d 次工具调用上限，本轮先基于已有结果收尾。你可以继续发消息让我接着处理（程序不会退出）。", a.cfg.MaxIterations),
			}})
			// 达到上限并不中断程序：用一次不带工具的收尾回答结束本回合，
			// 避免在工具链中途戛然而止、用户得不到任何总结。
			closingSystem := Message{
				Role: "system",
				Content: stringPointer(a.systemPrompt(sessionID) +
					"\n\n注意：本轮已达到工具调用次数上限。现在必须直接输出最终回答：总结目前已确定的结果、未完成事项与建议的下一步，不要再调用任何工具。"),
			}
			if closing, err := a.streamAssistantReply(ctx, append([]Message{closingSystem}, history...), nil, emit); err == nil {
				history = append(history, closing)
			}
			break
		}
	}

	session.Messages = history
	a.store.Save(session)
	emit(Event{Type: "done", Data: map[string]any{
		"compressed": compressed,
		"iterations": iterations,
	}})
	return nil
}

// errEmptyResponse 表示模型流式返回既无文本也无工具调用。
// 区别于普通错误：调用方应压缩上下文后重试，而非直接中断会话。
var errEmptyResponse = errors.New("模型没有返回任何内容")

func (a *Agent) streamAssistantReply(ctx context.Context, messages []Message, tools []ToolDefinition, emit func(Event)) (Message, error) {
	const maxRetries = 2
	for attempt := 0; attempt <= maxRetries; attempt++ {
		ch, err := a.llm.ChatStream(ctx, messages, tools)
		if err != nil {
			return Message{}, err
		}

		var content strings.Builder
		var calls []ToolCall
		for chunk := range ch {
			if chunk.Error != nil {
				return Message{}, chunk.Error
			}
			if chunk.Content != "" {
				content.WriteString(chunk.Content)
				emit(Event{Type: "message", Data: map[string]any{"delta": chunk.Content}})
			}
			for _, delta := range chunk.ToolCalls {
				calls = mergeToolCall(calls, delta)
			}
		}

		for i := range calls {
			if calls[i].ID == "" {
				calls[i].ID = "call_" + newID()
			}
		}

		assistant := Message{
			Role:    "assistant",
			Content: stringPointer(content.String()),
		}
		if len(calls) > 0 {
			assistant.ToolCalls = calls
			if content.Len() == 0 {
				assistant.Content = nil
			}
		}
		if content.Len() == 0 && len(calls) == 0 {
			if attempt < maxRetries {
				emit(Event{Type: "notice", Data: map[string]any{
					"text": fmt.Sprintf("模型返回空内容，正在重试（第 %d/%d 次）…", attempt+1, maxRetries),
				}})
				continue
			}
			return Message{}, errEmptyResponse
		}
		return assistant, nil
	}
	return Message{}, errEmptyResponse
}

// compressHistory 压缩 history 中除最近 keep 条之外的全部早期消息。
// 早期消息可能远超单次模型输入窗口，因此按压缩预算分块、逐块摘要后再合并；
// LLM 压缩失败时用 fallbackSummary 保留最近消息要点，避免直接整体截断。
// 合并后若仍超窗口，对合并结果再压缩一层（递归收敛），极端情况才截断。
func (a *Agent) compressHistory(ctx context.Context, sessionID string, history []Message) ([]Message, error) {
	keep := a.cfg.KeepRecentMessages
	if keep <= 0 {
		keep = 12
	}
	if len(history) <= keep {
		return history, nil
	}
	old := history[:len(history)-keep]
	recent := history[len(history)-keep:]

	summary, err := a.summarizeBlocks(ctx, old)
	if err != nil {
		// LLM 压缩失败：降级为文本要点摘要（纯文本，不产生孤儿 tool 消息）
		summary = fallbackSummary(old)
	}
	merged := append([]Message{{
		Role:    "system",
		Content: stringPointer("[早期对话摘要]\n" + summary),
	}}, recent...)

	// 压缩后校验：摘要 + 最近消息仍超窗口时，对合并结果再压缩一层（递归收敛）
	if estimateMessages(merged) > a.cfg.MaxContextTokens {
		if inner, inerr := a.compressHistory(ctx, sessionID, merged); inerr == nil {
			return inner, nil
		}
		merged = truncateHistory(merged, keep)
	}
	return merged, nil
}

// summarizeBlocks 把早期消息按单次压缩输入预算切成若干块，逐块交给 LLM 摘要，
// 最后把各块摘要按顺序合并返回。任一块失败即返回错误（由调用方降级）。
func (a *Agent) summarizeBlocks(ctx context.Context, history []Message) (string, error) {
	budget := a.compactionInputBudget()
	var summaries []string
	block := make([]Message, 0, 32)
	blockTokens := 0
	flush := func() error {
		if len(block) == 0 {
			return nil
		}
		sum, cerr := a.llm.Compress(ctx, block)
		block = block[:0]
		blockTokens = 0
		if cerr != nil {
			return cerr
		}
		summaries = append(summaries, sum)
		return nil
	}
	for _, msg := range history {
		tokens := estimateMessageTokens(msg)
		if len(block) > 0 && blockTokens+tokens > budget {
			if err := flush(); err != nil {
				return "", err
			}
		}
		block = append(block, msg)
		blockTokens += tokens
	}
	if err := flush(); err != nil {
		return "", err
	}
	return strings.Join(summaries, "\n\n"), nil
}

// compactionInputBudget 返回单次 LLM 压缩请求的输入 token 预算：
// 总窗口减去压缩输出（CompactionTokens）与常规输出（StreamOutputTokens）的预留，
// 再留出安全余量，避免把输入塞满窗口导致请求被拒。
func (a *Agent) compactionInputBudget() int {
	window := a.cfg.MaxContextTokens
	output := a.cfg.CompactionTokens + a.cfg.StreamOutputTokens
	budget := window - output - window/8
	if budget < 4096 {
		budget = 4096
	}
	return budget
}

// fallbackSummary 当 LLM 压缩失败时的兜底摘要：按时间顺序保留早期消息中
// 最近若干条的用户目标/助手结论/工具结果要点，输出纯文本（不产生孤立的
// tool 消息），保证后续请求协议合法、信息尽量不丢。
func fallbackSummary(history []Message) string {
	const maxLines = 10
	start := len(history) - maxLines
	if start < 0 {
		start = 0
	}
	labels := map[string]string{
		"user":      "用户",
		"assistant": "助手",
		"tool":      "工具结果",
	}
	var lines []string
	for i := start; i < len(history); i++ {
		msg := history[i]
		label, ok := labels[msg.Role]
		if !ok {
			label = msg.Role
		}
		text := ""
		if msg.Content != nil {
			text = strings.TrimSpace(*msg.Content)
		}
		if text == "" {
			if len(msg.ToolCalls) > 0 {
				text = "调用了 " + msg.ToolCalls[0].Function.Name + " 等工具"
			} else {
				continue
			}
		}
		if countRunes(text) > 200 {
			text = truncateRunes(text, 200)
		}
		lines = append(lines, fmt.Sprintf("[%s] %s", label, text))
	}
	if len(lines) == 0 {
		return "（早期对话内容较多，摘要失败后仅保留最近消息。）"
	}
	return "（早期对话压缩失败，以下为最近消息要点，更早内容已截断）\n" + strings.Join(lines, "\n")
}

// truncateHistory 在模型压缩失败时兜底：仅保留最近消息，并丢弃开头孤立的
// tool 结果消息，避免出现没有对应 assistant 调用声明的 tool 消息导致协议错乱。
func truncateHistory(history []Message, keep int) []Message {
	if len(history) <= keep {
		return history
	}
	kept := append([]Message(nil), history[len(history)-keep:]...)
	for len(kept) > 0 && kept[0].Role == "tool" {
		kept = kept[1:]
	}
	return kept
}

func (a *Agent) systemPrompt(sessionID string) string {
	return fmt.Sprintf(`你是 BlackBean，运行在用户本机上的 AI Agent，正在协助用户完成真实任务。
当前日期：%s
本会话工作目录（与其他会话隔离，相对路径与命令默认在此执行）：%s
项目根目录（可读取，含 agent 自身文件、协作黑板产物等共享内容）：%s

工作方式：
1. 先理解用户目标，必要时先用工具查看文件、目录和运行结果，再给出准确答案。
2. 你可以运行 Bash、读写文件、运行 Python。所有文件和命令默认都在本会话工作目录内进行，不要访问项目根目录之外的文件。
3. 使用工具时必须根据真实输出继续推理，绝不能编造命令结果、文件内容或错误信息。
4. 回答使用简洁的中文，重要代码、命令和路径用 Markdown 代码块呈现。
5. 需要执行可能造成不可逆影响的操作前，先说明风险和影响，再谨慎执行。
6. 如果一次工具调用没有解决问题，可以多次调用工具排查；不要在没有依据时下结论。


【任务调度规则】
- 解题类任务（CTF 题目、渗透测试、漏洞挖掘、Web 攻防、逆向、密码学、取证分析等）一律交由 coop 执行，你不负责具体解题，只做调度。
- 你的调度职责：
  1. 从用户表述中提取足够信息：目标地址/文件、约束条件、平台提交规则（如 TSec Benchmark 的 submit API、BENCHMARK_TOKEN、unique_code 等）；
  2. 调用 run_coop 工具，把任务描述与提交规则写清楚，交给 pi 协作单 Agent 执行；
  3. 任务完成后按完成通知中的结构化结果向用户汇报，并按约定规则提交 flag、回报得分。
- 主动并发调度（默认行为，无需用户提醒）：解题跑分类任务要最大化并发，始终把在跑的靶机/协作任务维持到平台并发上限（TSec Benchmark 通常为 3 个并发容器），不要一次只开一个靶机慢慢等：
  1. 开始解题时：先查看题目列表，选定多道未完成题目，用 start 接口一次性启动多个靶机，并逐一调用 run_coop 下发对应协作任务，把并发拉满；
  2. 每有任务完成/失败释放名额时：立即主动 close 已结束题目 → start 下一道未完成题目 → run_coop 下发，始终补满并发，不要空出名额等待；
  3. 只有平台已无可启动题目、或用户明确要求停止时才停止扩并发。
- 主动检测状态（默认行为，无需用户提醒）：在收到协作完成通知、下发新任务、或推进到关键节点时，主动用短命令（≤60s）查询平台进度（GET challenges）与运行中容器（docker_ps / run_bash），并向用户简要汇报「已通关题数/总分、运行中靶机、下一步计划」，不要等用户说「检测状态」才去查。
- 调用 run_coop 成功下发目标后，本轮会话立即收尾结束：直接输出一句简短总结即可，不要原地等待、不要 sleep、不要反复轮询黑板。coop 完成后系统会自动以「用户」身份在会话中追加完成通知并启动新一轮处理，届时你再读取黑板产物、提交 flag、规划下一题。
- coop 拿到 flag 后，必须按任务中约定的平台提交规则（如 TSec Benchmark 的 submit API、BENCHMARK_TOKEN、unique_code）用 curl 提交 flag，并把提交响应与得分回报给用户；提交失败要重试并说明原因，不能只汇报不提交。
- 不要用 sleep 或超时超过 60 秒的长等待命令原地等待 coop 任务完成：coop 是异步运行的，任务完成后系统会自动推送「协作任务完成通知」并触发你继续处理，无需 sleep 阻塞。等待期间可以用短命令（≤60s）检查黑板进度，或直接推进其他不依赖该任务的工作（如规划下一题）。
- 非解题类任务（文档撰写、代码开发、日常问答等）按常规方式由你自己完成，不必交给 coop。
`,
		time.Now().Format("2006-01-02"), a.tools.workspaceFor(sessionID), a.cfg.Workspace)
}

func (a *Agent) sessionLock(sessionID string) *sync.Mutex {
	a.locksMu.Lock()
	defer a.locksMu.Unlock()
	lock := a.locks[sessionID]
	if lock == nil {
		lock = &sync.Mutex{}
		a.locks[sessionID] = lock
	}
	return lock
}

// ForgetSession 释放会话持有的运行时资源（会话锁与 live 注册）。
// 在删除会话时调用，避免 locks / live map 随会话累积只增不减。
func (a *Agent) ForgetSession(sessionID string) {
	a.locksMu.Lock()
	delete(a.locks, sessionID)
	a.locksMu.Unlock()

	a.liveMu.Lock()
	delete(a.live, sessionID)
	a.liveMu.Unlock()
}

// RegisterLive 把某个 SSE 连接注册为会话的"在线接收者"，供后台协作任务完成
func (a *Agent) RegisterLive(sessionID string, emit func(Event)) func() {
	a.liveMu.Lock()
	reg := &liveReg{emit: emit}
	a.live[sessionID] = reg
	a.liveMu.Unlock()
	return func() {
		a.liveMu.Lock()
		if a.live[sessionID] == reg {
			delete(a.live, sessionID)
		}
		a.liveMu.Unlock()
	}
}

// CoopTasks 返回协作任务列表；sessionID 非空时只返回该会话的任务，
// 供 Web 页面实时展示容器运行状态。
func (a *Agent) CoopTasks(sessionID string) []*CoopTask {
	return a.coop.List(sessionID)
}

// liveEmitter 返回动态事件发射器：每次发送时实时查询该会话当前在线的连接。
// 与启动时一次性捕获 emit 相比，即使汇报轮在页面连接之前启动，页面打开后
// 也能实时收到流式事件（修复"汇报轮事件前端收不到"的问题）。
func (a *Agent) liveEmitter(sessionID string) func(Event) {
	return func(event Event) {
		a.liveMu.Lock()
		reg := a.live[sessionID]
		a.liveMu.Unlock()
		if reg != nil {
			reg.emit(event)
		}
	}
}

// notifyCoopDone 是后台协作任务完成后的回调：给会话注入一条
// "【协作任务完成通知】"用户消息，并触发主 agent 新一轮处理
// （有在线 SSE 连接则流式推送，否则在后台静默完成并持久化结果）。
// 通知基于 supervisor 的结构化结果（CoopResult）渲染，成功时携带 flag/摘要/
// 产物清单，失败时携带明确的续跑指令（关闭靶机 → 开下一题 → 重新下发 coop）。
// 汇报轮失败会在会话中持久化错误信息，避免"通知已到但流程静默停摆"。
func (a *Agent) notifyCoopDone(task *CoopTask) {
	notif := buildCoopNotification(task)

	emit := a.liveEmitter(task.SessionID)
	// 汇报轮加超时兜底：后台自动触发的 Run 不能永久持锁阻塞后续所有通知
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
	emit(Event{Type: "turn_start", Data: map[string]any{"user": notif}})
	// 使用独立后台上下文：即使前端已关闭页面，汇报轮仍会完成并持久化。
	// 注意：cancel 必须在 Run 结束后调用（defer 到 goroutine 内），
	// 否则本函数一返回 ctx 即被取消，汇报轮的首个 LLM 请求会立即报
	// "Post .../v1/messages: context canceled"（历史 bug：会话停在完成通知处）。
	go func() {
		defer cancel()
		if err := a.Run(ctx, task.SessionID, notif, emit); err != nil {
			// 汇报轮失败兜底：把错误持久化到会话，避免静默断流
			if session, ok := a.store.Get(task.SessionID); ok {
				fallback := "[系统] 协作完成通知的自动处理失败（" + err.Error() +
					"）。请先读取黑板产物（" + task.Blackboard + "）向用户汇报，再按 TSec 流程继续推进：close 已通关题目释放名额 → start 下一题 → run_coop 下发协作。"
				session.Messages = append(session.Messages, Message{Role: "user", Content: stringPointer(fallback)})
				a.store.Save(session)
			}
			log.Printf("[coop] 汇报轮失败 session=%s task=%s err=%v", task.SessionID, task.ID, err)
		}
	}()
}

// buildCoopNotification 根据任务的结构化结果渲染完成通知。
// 有 result.json 时按 status 判定成功/失败并附带 flag/产物清单；
// 缺失时回退到容器 stdout（老版本兼容）。
func buildCoopNotification(task *CoopTask) string {
	success := false
	status := "完成"
	var summary, flag string
	var artifacts []string

	if task.Result != nil {
		res := task.Result
		switch res.Status {
		case "solved":
			success = true
			status = "完成"
		case "unsolved":
			status = "未解出"
		case "timeout":
			status = "超时"
		default:
			status = "失败"
		}
		summary, flag, artifacts = res.Summary, res.Flag, res.Artifacts
	} else {
		// 老版本 / result.json 缺失时回退到容器日志
		detail := task.Output
		if detail == "" {
			detail = task.Error
		}
		failed := task.Error != "" || task.ExitCode != 0 ||
			strings.Contains(detail, "未解出") || strings.Contains(detail, "未完成")
		if failed {
			status = "未解出/失败"
		} else {
			success = true
		}
		summary = detail
	}

	var b strings.Builder
	fmt.Fprintf(&b, "【协作任务完成通知】你之前发起的 pi 协作任务（ID: %s）已%s。\n", task.ID, status)
	if success && flag != "" {
		fmt.Fprintf(&b, "\n【flag】%s\n", flag)
	}
	if strings.TrimSpace(summary) != "" {
		b.WriteString("\n" + truncateRunes(strings.TrimSpace(summary), 1200) + "\n")
	}
	if task.Blackboard != "" {
		fmt.Fprintf(&b, "\n黑板产物目录：%s", task.Blackboard)
		if len(artifacts) > 0 {
			b.WriteString("\n关键产物：")
			for _, artifact := range artifacts {
				if name := strings.TrimSpace(artifact); name != "" {
					b.WriteString("\n- " + name)
				}
			}
		}
		b.WriteString("\n可选用 read_file / list_directory 查看黑板产物，向用户汇报结果与关键文件。")
	}
	if success {
		b.WriteString("\n若产物中包含 flag 且尚未提交：请读取黑板下 task.md（内含平台提交规则 / BENCHMARK_TOKEN / unique_code），立即用 curl 向平台提交 flag 并给出提交响应与得分；不要只汇报而不提交。")
		b.WriteString("\n随后按 TSec 流程持续推进以拿更高总分：close 已通关题目释放名额 → 主动补满并发（一次性 start 多道未完成题目并逐一 run_coop，把在跑靶机数拉满到平台上限）→ 不要空出名额等待；若所有题目已完成或平台任务超时，则停止并向用户汇报总分。")
	} else {
		b.WriteString("\n该任务未解出/失败，请按 TSec 标准流程继续推进，不要空等：")
		b.WriteString("\n1) 用 POST {BENCHMARK_BASE_URL}/openapi/v1/challenges/close?unique_code=<该题编号> 关闭当前靶机容器释放名额（该题编号为 " + task.ChallengeCode + "，可核对黑板 task.md）；")
		b.WriteString("\n2) 用 GET {BENCHMARK_BASE_URL}/openapi/v1/challenges 查看剩余未完成题目，一次性选定多道未完成题目补满并发；")
		b.WriteString("\n3) 用 POST {BENCHMARK_BASE_URL}/openapi/v1/challenges/start?unique_code=<新题> 逐个启动新靶机，把在跑靶机数拉满到平台并发上限；")
		b.WriteString("\n4) 对每道新题调用 run_coop 工具下发协作解题，不要只开一个靶机空等。")
		b.WriteString("\n若所有题目已完成或平台任务已结束（接口持续 invalid_state），则停止并向用户汇报总分。")
	}
	return b.String()
}

func mergeToolCall(calls []ToolCall, delta ToolCallDelta) []ToolCall {
	index := delta.Index
	if index < 0 {
		index = len(calls)
	}
	for len(calls) <= index {
		calls = append(calls, ToolCall{Type: "function"})
	}
	call := &calls[index]
	if delta.ID != "" {
		call.ID = delta.ID
	}
	if delta.Name != "" {
		call.Function.Name = delta.Name
	}
	call.Function.Arguments += delta.ArgumentsDelta
	return calls
}

func estimateMessages(messages []Message) int {
	total := 0
	for _, message := range messages {
		total += estimateMessageTokens(message)
	}
	return total
}

// estimateMessageTokens 估算单条消息的 token 数（含 tool_calls 与消息开销）。
func estimateMessageTokens(message Message) int {
	total := 0
	if message.Content != nil {
		total += estimateTokens(*message.Content)
	}
	for _, call := range message.ToolCalls {
		total += estimateTokens(call.Function.Name + call.Function.Arguments)
	}
	total += 8
	return total
}

func estimateTokens(value string) int {
	runes := []rune(value)
	hanCount := 0
	for _, r := range runes {
		if unicode.Is(unicode.Han, r) {
			hanCount++
		}
	}
	other := len(runes) - hanCount
	return int(float64(other)/4.0+float64(hanCount)*0.8) + 4
}

func stringPointer(value string) *string {
	return &value
}
