package room

import (
	"context"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/sdaduanbilei/agent-team/internal/agent"
	"github.com/sdaduanbilei/agent-team/internal/llm"
	"github.com/sdaduanbilei/agent-team/internal/prompt"
	"github.com/sdaduanbilei/agent-team/internal/skill"
	"github.com/sdaduanbilei/agent-team/internal/store"
)

func Load(roomDir string, agentsDir string, skillsDir string, opts ...LoadOption) (*Room, error) {
	var lo loadOptions
	for _, opt := range opts {
		opt(&lo)
	}

	data, err := os.ReadFile(filepath.Join(roomDir, "room.md"))
	if err != nil {
		return nil, err
	}
	cfg, err := parseRoomConfig(data)
	if err != nil {
		return nil, err
	}

	r := &Room{Config: cfg, Dir: roomDir, members: make(map[string]*agent.Agent), Mode: "plan", Status: StatusPending}

	projectRoot := filepath.Dir(agentsDir)
	if data, err := os.ReadFile(filepath.Join(projectRoot, "SYSTEM.md")); err == nil {
		r.systemRules = string(data)
	}

	// 从 DB 预加载所有 agent 配置
	var dbConfigs []store.AgentConfig
	if lo.store != nil {
		dbConfigs, _ = lo.store.GetRoomAgentConfigs(cfg.Name)
	}
	dbConfigMap := make(map[string]map[string]string) // agent_name -> file_type -> content
	for _, c := range dbConfigs {
		if dbConfigMap[c.AgentName] == nil {
			dbConfigMap[c.AgentName] = make(map[string]string)
		}
		dbConfigMap[c.AgentName][c.FileType] = c.Content
	}

	if cfg.Master != "" {
		var knowledgeDir string
		if cfg.Team != "" {
			kd := filepath.Join(agentsDir, cfg.Team, "knowledge")
			if info, err := os.Stat(kd); err == nil && info.IsDir() {
				knowledgeDir = kd
			}
		}

		agentPath := resolveAgentPath(agentsDir, cfg.Team, cfg.Master)
		r.master, err = agent.Load(agentPath)
		if err != nil {
			return nil, fmt.Errorf("load master %s: %w", cfg.Master, err)
		}
		r.master.KnowledgeDir = knowledgeDir
		// DB 配置覆盖
		if files, ok := dbConfigMap[cfg.Master]; ok {
			if soul, ok := files["soul"]; ok {
				r.master.Soul = soul
				r.master.UseDBConfig = true
			}
			if agentDoc, ok := files["agent"]; ok {
				r.master.AgentDoc = agentDoc
				r.master.UseDBConfig = true
			}
		}

		for _, name := range cfg.Members {
			a, err := agent.Load(resolveAgentPath(agentsDir, cfg.Team, name))
			if err != nil {
				return nil, fmt.Errorf("load member %s: %w", name, err)
			}
			a.KnowledgeDir = knowledgeDir
			// DB 配置覆盖
			if files, ok := dbConfigMap[name]; ok {
				if soul, ok := files["soul"]; ok {
					a.Soul = soul
					a.UseDBConfig = true
				}
				if agentDoc, ok := files["agent"]; ok {
					a.AgentDoc = agentDoc
					a.UseDBConfig = true
				}
			}
			r.members[name] = a
		}
	}

	// 加载 TEAM.md 项目模板：优先从 DB，降级到文件系统
	teamMDBody := ""
	if teamDoc, ok := dbConfigMap["__team__"]["team_doc"]; ok && teamDoc != "" {
		teamMDBody = teamDoc
	} else if cfg.Team != "" {
		teamMDPath := filepath.Join(agentsDir, cfg.Team, "TEAM.md")
		if teamData, err := os.ReadFile(teamMDPath); err == nil {
			teamMDBody = string(teamData)
		}
	}
	if teamMDBody != "" {
		body := teamMDBody
		if strings.HasPrefix(body, "---") {
			parts := strings.SplitN(body, "---", 3)
			if len(parts) >= 3 {
				body = parts[2]
			}
		}
		if pt := parseProjectTemplate(body); pt != nil {
			r.projectTemplate = pt
			log.Printf("[room %s] 已加载项目模板，包含 %d 个文件定义", cfg.Name, len(pt.Files))
		}
		if wf := extractTeamWorkflow(body); wf != "" {
			r.teamWorkflow = wf
			log.Printf("[room %s] 已加载团队工作流描述 (%d 字符)", cfg.Name, len(wf))
		}
	}

	r.skillMeta, _ = skill.Discover(skillsDir)

	// 初始化 prompt 模板引擎
	r.Prompt = prompt.New()
	// 加载 room 级模板覆盖（如果存在）
	roomPromptDir := filepath.Join(roomDir, "prompts")
	r.Prompt.LoadOverrides(roomPromptDir)

	return r, nil
}

func (r *Room) emit(e Event) {
	e.RoomID = r.Config.Name
	if r.Broadcast != nil {
		r.Broadcast(e)
	}
	if r.Store != nil && !e.NoStore {
		gid := &r.currentGroupID
		switch e.Type {
		case EvtAgentMessage:
			if !e.Streaming && e.Content != "" && e.Role != "tool_use" &&
				!strings.HasPrefix(e.Content, "正在处理:") && !strings.HasPrefix(e.Content, "正在处理: ") {
				r.Store.InsertMessage(&store.Message{
					RoomID: r.Config.Name, Agent: e.Agent, Role: e.Role,
					Content: e.Content, GroupID: gid,
				})
			}
		case EvtArtifact:
			r.Store.InsertMessage(&store.Message{
				RoomID: r.Config.Name, Agent: e.Agent, Role: "artifact",
				Content: e.Title, Filename: e.Filename, Title: e.Title, GroupID: gid,
			})
		case EvtTaskAssign:
			r.Store.InsertMessage(&store.Message{
				RoomID: r.Config.Name, Agent: e.From, Role: "task_assign",
				Content: e.Task, Title: e.To, GroupID: gid,
			})
		}
	}
}

func (r *Room) emitUsage(agentName string, usage llm.Usage) {
	if usage.TotalTokens > 0 {
		r.emit(Event{
			Type:             EvtTokenUsage,
			Agent:            agentName,
			PromptTokens:     usage.PromptTokens,
			CompletionTokens: usage.CompletionTokens,
			TotalTokens:      usage.TotalTokens,
		})
		if r.Store != nil {
			r.Store.InsertTokenUsage(r.Config.Name, agentName, usage.PromptTokens, usage.CompletionTokens, usage.TotalTokens)
		}
	}
}

func (r *Room) setMode(mode string) {
	r.Mode = mode
	r.emit(Event{Type: EvtModeChange, Mode: mode})
}

func (r *Room) setStatus(s Status, activeAgent, action string) {
	r.Status = s
	r.ActiveAgent = activeAgent
	r.emit(Event{Type: EvtRoomStatus, Status: s, ActiveAgent: activeAgent, Action: action})
}

func (r *Room) Stop() {
	r.cancelMu.Lock()
	defer r.cancelMu.Unlock()
	if r.cancelFunc != nil {
		r.cancelFunc()
		r.cancelFunc = nil
	}
	r.setStatus(StatusPending, "", "")
}

// AppendHistory persists a message to today's history file.
func (r *Room) AppendHistory(role, agentName, content string) {
	dir := filepath.Join(r.Dir, "history")
	os.MkdirAll(dir, 0755)
	filename := filepath.Join(dir, time.Now().Format("2006-01-02")+".md")
	line := fmt.Sprintf("\n**[%s] %s** (%s)\n\n%s\n", time.Now().Format("15:04:05"), agentName, role, content)
	f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return
	}
	defer f.Close()
	f.WriteString(line)
}

// contextMaxTokens 是模型上下文窗口限制（留 8K 给输出）
const contextMaxTokens = 120000

// estimateTokens 估算消息列表的 token 数（中文约 1.5 token/字，英文约 0.75 token/word）
func estimateTokens(msgs []llm.Message) int {
	total := 0
	for _, m := range msgs {
		// 粗略估算：每个字符约 1 token（中英混合取均值）
		total += len([]rune(m.Content))
	}
	return total
}

// compressMessages 当 token 数超限时，保留 system prompt + 最近的消息，中间部分压缩为摘要
func compressMessages(msgs []llm.Message, maxTokens int) []llm.Message {
	if len(msgs) <= 3 {
		return msgs
	}
	est := estimateTokens(msgs)
	if est <= maxTokens {
		return msgs
	}

	log.Printf("[context] 当前 %d tokens (估算), 超过 %d 限制, 压缩历史", est, maxTokens)

	// 保留: system(第0条) + 最近的消息
	system := msgs[0]
	middle := msgs[1:]

	// 从后往前保留消息，直到 token 数在限制内
	// 留 system 的 token + 安全余量
	sysTokens := len([]rune(system.Content))
	budget := maxTokens - sysTokens - 2000 // 留 2K 余量

	var kept []llm.Message
	keptTokens := 0
	for i := len(middle) - 1; i >= 0; i-- {
		msgTokens := len([]rune(middle[i].Content))
		if keptTokens+msgTokens > budget {
			break
		}
		kept = append([]llm.Message{middle[i]}, kept...)
		keptTokens += msgTokens
	}

	// 被截掉的部分生成摘要提示
	droppedCount := len(middle) - len(kept)
	if droppedCount > 0 {
		summary := llm.NewMsg("user", fmt.Sprintf("[系统提示] 之前有 %d 条对话消息因上下文长度限制已被压缩。请基于当前可见的消息继续工作。", droppedCount))
		// NOTE: compressMessages 是无状态函数，无法访问 r.Prompt，保留 fmt 拼接
		result := make([]llm.Message, 0, 2+len(kept))
		result = append(result, system, summary)
		result = append(result, kept...)
		log.Printf("[context] 压缩完成: 保留 %d 条, 丢弃 %d 条, 估算 %d tokens", len(kept), droppedCount, estimateTokens(result))
		return result
	}
	return msgs
}

// Handle processes a user message through master orchestration.
func (r *Room) Handle(ctx context.Context, userMsg string) error {
	return r.HandleUserMessage(ctx, "user", userMsg)
}

// HandleUserMessage 处理用户消息。
func (r *Room) HandleUserMessage(ctx context.Context, userName, userMsg string) error {
	if r.master == nil {
		return fmt.Errorf("room has no master agent configured")
	}
	r.AppendHistory("user", userName, userMsg)

	if r.Store != nil {
		id, _ := r.Store.InsertMessage(&store.Message{
			RoomID: r.Config.Name, Agent: userName, Role: "user", Content: userMsg,
		})
		r.currentGroupID = id
	}

	ctx, cancel := context.WithCancel(ctx)
	r.cancelMu.Lock()
	r.cancelFunc = cancel
	r.cancelMu.Unlock()
	defer func() {
		r.cancelMu.Lock()
		r.cancelFunc = nil
		r.cancelMu.Unlock()
	}()

	// 检测用户直接 @master：去掉前缀，作为普通对话处理
	if r.master != nil {
		masterName := r.master.Config.Name
		prefix := "@" + masterName
		trimmed := strings.TrimSpace(userMsg)
		if strings.HasPrefix(trimmed, prefix) {
			question := strings.TrimSpace(strings.TrimPrefix(trimmed, prefix))
			if question != "" {
				// 清除 lastActiveMember，确保消息发给 master 而非成员
				r.lastActiveMember = ""
				userMsg = question
			}
		}
	}

	// 检测用户直接 @agent（成员）
	userAssignments := parseUserMentions(userMsg, r.members)
	if len(userAssignments) > 0 {
		// 区分对话和任务分配：短消息/问候视为对话
		if len(userAssignments) == 1 {
			for name, task := range userAssignments {
				if isConversational(task) {
					r.lastActiveMember = name
					return r.handleMemberConversation(ctx, userName, task)
				}
			}
		}
		return r.handleDirectAssign(ctx, userAssignments)
	}

	// Build 模式下执行暂存任务
	if r.Mode == "build" && len(r.pendingAssignments) > 0 {
		log.Printf("[room %s] build 模式，执行 %d 个暂存任务", r.Config.Name, len(r.pendingAssignments))
		assignments := r.pendingAssignments
		r.pendingAssignments = nil
		r.pendingPlanReply = ""

		skillXML := skill.ToXML(r.skillMeta)
		board := &SharedBoard{}
		r.setStatus(StatusWorking, "", "")
		r.runMembersParallel(ctx, assignments, board, skillXML)
		// 只在写作阶段（前置材料全部完成后）触发审读
		if r.allStaticFilesDone() {
			r.runChallengeRound(ctx, board, skillXML)
		}

		r.setStatus(StatusPending, "", "")
		return nil
	}

	// Build 模式下成员对话续接
	if r.Mode == "build" && r.lastActiveMember != "" {
		return r.handleMemberConversation(ctx, userName, userMsg)
	}

	r.setStatus(StatusThinking, "", "")

	// 构建 system prompt
	teamXML := r.buildTeamXML()
	skillXML := skill.ToXML(r.skillMeta)
	skillSummary := r.buildSkillSummary()
	var userXML string
	if r.User != nil {
		userXML = r.User.BuildUserXML()
	}
	extraContext := userXML + "\n\n" + teamXML + "\n\n" + skillSummary
	if r.systemRules != "" {
		extraContext = r.systemRules + "\n\n" + extraContext
	}
	if r.teamWorkflow != "" {
		extraContext = extraContext + "\n\n<team_workflow>\n" + r.teamWorkflow + "\n</team_workflow>"
	}
	if projectCtx := r.buildProjectContext(r.master.Config.Name); projectCtx != "" {
		extraContext = extraContext + "\n\n" + projectCtx
	}
	if wsCtx := r.buildWorkspaceContext(); wsCtx != "" {
		extraContext = extraContext + "\n\n" + wsCtx
	}
	systemPrompt := r.master.BuildSystemPrompt(extraContext)
	modeInfo := fmt.Sprintf("\n\n当前用户：%s\n当前模式：%s", userName, r.Mode)
	if r.Mode == "build" {
		modeInfo += "\n\n" + r.Prompt.R("mode_build_rule")
	} else {
		modeInfo += "\n\n" + r.Prompt.R("mode_plan_rule")
	}
	sysMsg := llm.NewMsg("system", systemPrompt+modeInfo)

	r.historyMu.Lock()
	if len(r.masterHistory) == 0 {
		r.masterHistory = []llm.Message{sysMsg}
	} else {
		r.masterHistory[0] = sysMsg
	}
	r.masterHistory = append(r.masterHistory, llm.NewMsg("user", userMsg))
	// token 感知的历史压缩（替代简单的消息数截断）
	r.masterHistory = compressMessages(r.masterHistory, contextMaxTokens)
	masterMsgs := make([]llm.Message, len(r.masterHistory))
	copy(masterMsgs, r.masterHistory)
	r.historyMu.Unlock()

	// Master 规划循环：简化为最多 3 轮（安全上限），每次用户消息通常只跑 1 轮
	const maxIterations = 3
	for iteration := 0; iteration < maxIterations; iteration++ {
		log.Printf("[room %s] master iteration %d, sending to LLM...", r.Config.Name, iteration)

		if r.projectTemplate != nil {
			if !r.masterCallerDecidedIteration(ctx, &masterMsgs, skillXML, iteration) {
				break
			}
		} else {
			if !r.masterLegacyIteration(ctx, &masterMsgs, skillXML, iteration) {
				break
			}
		}
	}

	r.setStatus(StatusPending, "", "")
	go r.updateMasterMemory(context.Background(), userMsg, masterMsgs)
	return nil
}

// masterCallerDecidedIteration 执行一次 Caller-Decided 路径的 master 迭代。
// 返回 true 表示继续循环，false 表示 break。
func (r *Room) masterCallerDecidedIteration(ctx context.Context, masterMsgs *[]llm.Message, skillXML string, iteration int) bool {
	if iteration > 0 {
		r.setStatus(StatusThinking, r.master.Config.Name, "正在规划...")
	}

	// 调用前压缩 context
	*masterMsgs = compressMessages(*masterMsgs, contextMaxTokens)

	// CHAT CALL: 纯规划+分配
	var masterReply strings.Builder
	_, usage, err := r.master.ChatWithUsage(ctx, *masterMsgs, func(token string) {
		masterReply.WriteString(token)
		r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: token, Streaming: true})
	})
	if err != nil {
		log.Printf("[room %s] master chat error: %v", r.Config.Name, err)
		return false
	}
	r.emitUsage(r.master.Config.Name, usage)
	reply := masterReply.String()
	log.Printf("[room %s] master chat reply (%d chars): %.100s...", r.Config.Name, len(reply), reply)

	// 安全网：如果 master 的聊天回复中混入了文档正文，剥离并替换显示
	persistContent := reply
	if r.projectTemplate != nil && isDocument(reply) {
		stripped := r.stripDocuments(reply)
		if stripped != "" && stripped != reply {
			persistContent = stripped
			r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: stripped, Streaming: false, Action: "replace"})
			log.Printf("[room %s] master 聊天中混入文档内容，已剥离", r.Config.Name)
		} else {
			r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: "", Streaming: false})
		}
	} else {
		r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: "", Streaming: false})
	}

	// 存为 text part
	if r.Store != nil {
		r.Store.InsertMessage(&store.Message{
			RoomID: r.Config.Name, Agent: r.master.Config.Name, Role: "master",
			Content: persistContent, PartType: "text",
			GroupID: &r.currentGroupID,
		})
	}
	assistantMsg := llm.NewMsg("assistant", reply)
	*masterMsgs = append(*masterMsgs, assistantMsg)
	r.historyMu.Lock()
	r.masterHistory = append(r.masterHistory, assistantMsg)
	r.historyMu.Unlock()
	r.AppendHistory("master", r.master.Config.Name, reply)

	// Plan 模式下：master 只允许聊天，禁止一切文件操作和任务分配
	if r.Mode != "build" {
		// 检查是否有 @分配 → 提醒用户切换模式
		allMentions := parseAssignments(reply)
		hasAssignment := false
		for name := range allMentions {
			if _, isMember := r.members[name]; isMember {
				hasAssignment = true
				break
			}
		}
		if hasAssignment {
			r.emit(Event{Type: EvtAgentMessage, Agent: "system", Role: "master",
				Content: r.Prompt.R("plan_mode_blocked")})
		}
		return false // plan 模式下每次只回复一条，不循环
	}

	// 增量编辑（仅 build 模式）
	if editFile, edited := r.applyDocumentEdit(reply); edited {
		editTitle := strings.TrimSuffix(editFile, ".md")
		r.emit(Event{Type: EvtArtifact, Agent: r.master.Config.Name, Filename: editFile, Title: editTitle})
		if r.Store != nil {
			r.Store.InsertMessage(&store.Message{
				RoomID: r.Config.Name, Agent: r.master.Config.Name, Role: "master",
				Content: editTitle, Filename: editFile, PartType: "document",
				GroupID: &r.currentGroupID,
			})
		}
	}

	// 安全网：如果 master 在聊天中直接输出了文档内容，自动拦截保存
	if isDocument(reply) && r.allStaticFilesDone() {
		if dynDir, dynOwner, _ := r.getDynamicFileInfo(); dynDir != "" && dynOwner == r.master.Config.Name {
			chapterFilename := r.extractChapterFilename(reply, dynDir)
			content := strings.TrimSpace(reply)
			if !strings.HasPrefix(content, "# ") {
				content = "# " + strings.TrimSuffix(chapterFilename, ".md") + "\n\n" + content
			}
			r.saveWorkspace(chapterFilename, content)
			docName := strings.TrimSuffix(chapterFilename, ".md")
			r.emit(Event{Type: EvtArtifact, Agent: r.master.Config.Name, Filename: chapterFilename, Title: docName})
			if r.Store != nil {
				r.Store.InsertMessage(&store.Message{
					RoomID: r.Config.Name, Agent: r.master.Config.Name, Role: "master",
					Content: docName, Filename: chapterFilename, PartType: "document",
					GroupID: &r.currentGroupID,
				})
			}
			log.Printf("[room %s] 拦截 master 聊天中的章节内容，保存到 %s", r.Config.Name, chapterFilename)
		}
	}

	// 解析 @ 分配
	allMentions := parseAssignments(reply)
	assignments := make(map[string]string)
	for name, task := range allMentions {
		if _, isMember := r.members[name]; isMember {
			assignments[name] = task
		}
	}

	// Phase 轻量校验：只在分配写手（phase:2 任务）时检查前置材料是否就绪
	if len(assignments) > 0 && r.projectTemplate != nil {
		blocked := r.validatePhaseAssignments(assignments)
		if len(blocked) > 0 {
			var blockedList strings.Builder
			for name, reason := range blocked {
				blockedList.WriteString(fmt.Sprintf("- @%s: %s\n", name, reason))
				delete(assignments, name)
			}
			blockedContent := r.Prompt.Render("phase_blocked", map[string]string{
				"BlockedList": blockedList.String(),
			})
			r.emit(Event{Type: EvtAgentMessage, Agent: "system", Role: "master", Content: blockedContent})
			log.Printf("[room %s] phase 校验阻止了 %d 个任务分配", r.Config.Name, len(blocked))

			// 注入反馈让 master 知道被阻止了
			phaseBlockMsg := llm.NewMsg("user", blockedContent)
			*masterMsgs = append(*masterMsgs, phaseBlockMsg)
			r.historyMu.Lock()
			r.masterHistory = append(r.masterHistory, phaseBlockMsg)
			r.historyMu.Unlock()
		}
	}

	if len(assignments) > 0 && r.Mode != "build" {
		r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master",
			Content: r.Prompt.R("plan_mode_blocked")})
		return false
	}

	// 执行成员任务
	if len(assignments) > 0 && r.Mode == "build" {
		board := &SharedBoard{}
		results := r.runMembersParallel(ctx, assignments, board, skillXML)
		// 只在写作阶段（前置材料全部完成后）触发审读
		if r.allStaticFilesDone() {
			r.runChallengeRound(ctx, board, skillXML)
		}

		r.setStatus(StatusThinking, r.master.Config.Name, "正在审阅成员结果...")
		var resultsStr strings.Builder
		for memberName, result := range results {
			resultsStr.WriteString(fmt.Sprintf("[%s] %s\n", memberName, result))
		}
		feedbackMsg := r.Prompt.Render("feedback_results", map[string]string{
			"Results":          resultsStr.String(),
			"BoardContext":     board.ToContext(),
			"WorkspaceContext": r.buildWorkspaceContext(),
			"WorkflowStep":    r.buildWorkflowStep(),
		})

		feedbackLLMMsg := llm.NewMsg("user", feedbackMsg)
		*masterMsgs = append(*masterMsgs, feedbackLLMMsg)
		r.historyMu.Lock()
		r.masterHistory = append(r.masterHistory, feedbackLLMMsg)
		r.historyMu.Unlock()
		r.updateTasks(*masterMsgs)
	}

	// FILE UPDATE CALLS: master 提到要更新已存在的文件
	if r.Mode == "build" {
		updateFiles := r.parseMasterUpdateIntent(reply)
		if len(updateFiles) > 0 {
			for _, file := range updateFiles {
				r.masterFileUpdateCall(ctx, masterMsgs, file, reply)
			}
		}
	}

	// FILE CALLS: master 负责的文件
	pendingFiles := r.findPendingMasterFiles()
	if len(pendingFiles) > 0 && r.Mode == "build" {
		for _, file := range pendingFiles {
			r.masterFileCall(ctx, masterMsgs, file)
		}
	} else if r.Mode == "build" && r.allStaticFilesDone() {
		// 所有静态文件完成，检查是否有动态章节需要写
		if dynDir, dynOwner, _ := r.getDynamicFileInfo(); dynDir != "" && dynOwner == r.master.Config.Name {
			// 用 master 的聊天回复作为章节规划提示
			r.masterChapterFileCall(ctx, masterMsgs, dynDir, reply)
		}
	}

	// 每次用户消息最多执行一轮分配 + 一次 file call，然后停下等用户
	return false
}

// masterLegacyIteration 执行一次旧路径的 master 迭代（无项目模板）。
func (r *Room) masterLegacyIteration(ctx context.Context, masterMsgs *[]llm.Message, skillXML string, iteration int) bool {
	if iteration > 0 {
		r.setStatus(StatusWorking, r.master.Config.Name, "正在编写文档...")
	}
	// 调用前压缩 context
	*masterMsgs = compressMessages(*masterMsgs, contextMaxTokens)

	var masterReply strings.Builder
	_, usage, err := r.master.ChatWithUsage(ctx, *masterMsgs, func(token string) {
		masterReply.WriteString(token)
		r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: token, Streaming: true})
	})
	if err != nil {
		log.Printf("[room %s] master chat error: %v", r.Config.Name, err)
		return false
	}
	r.emitUsage(r.master.Config.Name, usage)
	reply := masterReply.String()
	log.Printf("[room %s] master reply (%d chars): %.100s...", r.Config.Name, len(reply), reply)

	// Plan 模式下：只聊天，禁止一切后续操作
	if r.Mode != "build" {
		r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: "", Streaming: false})
		if r.Store != nil {
			r.Store.InsertMessage(&store.Message{
				RoomID: r.Config.Name, Agent: r.master.Config.Name, Role: "master",
				Content: reply, PartType: "text",
				GroupID: &r.currentGroupID,
			})
		}
		assistantMsg := llm.NewMsg("assistant", reply)
		*masterMsgs = append(*masterMsgs, assistantMsg)
		r.historyMu.Lock()
		r.masterHistory = append(r.masterHistory, assistantMsg)
		r.historyMu.Unlock()
		r.AppendHistory("master", r.master.Config.Name, reply)

		// 检查是否尝试了分配或文档
		allMentions := parseAssignments(reply)
		hasAssignment := false
		for name := range allMentions {
			if _, isMember := r.members[name]; isMember {
				hasAssignment = true
				break
			}
		}
		if hasAssignment {
			r.emit(Event{Type: EvtAgentMessage, Agent: "system", Role: "master",
				Content: r.Prompt.R("plan_mode_blocked")})
		}
		return false
	}

	var savedDocTitles []string
	persistContent := reply

	// Build 模式下执行文件产出
	{
		if editFile, edited := r.applyDocumentEdit(reply); edited {
			persistContent = fmt.Sprintf("已更新《%s》", strings.TrimSuffix(editFile, ".md"))
			savedDocTitles = append(savedDocTitles, strings.TrimSuffix(editFile, ".md"))
		} else if docs := splitDocuments(reply); len(docs) > 0 {
			for _, doc := range docs {
				title := extractTitle(doc)
				filename := titleToFilename(title, r.master.Config.Name)
				r.saveWorkspace(filename, doc)
				r.emit(Event{Type: EvtArtifact, Agent: r.master.Config.Name, Filename: filename, Title: title})
				savedDocTitles = append(savedDocTitles, title)
			}
		}
	}

	if len(savedDocTitles) > 0 {
		r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: persistContent, Streaming: false, Action: "replace"})
	} else {
		r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: "", Streaming: false})
	}

	if r.Store != nil && persistContent != "" {
		r.Store.InsertMessage(&store.Message{
			RoomID: r.Config.Name, Agent: r.master.Config.Name, Role: "master",
			Content: persistContent, PartType: "text",
			GroupID: &r.currentGroupID,
		})
	}
	assistantMsg := llm.NewMsg("assistant", reply)
	*masterMsgs = append(*masterMsgs, assistantMsg)
	r.historyMu.Lock()
	r.masterHistory = append(r.masterHistory, assistantMsg)
	r.historyMu.Unlock()
	r.AppendHistory("master", r.master.Config.Name, reply)

	allMentions := parseAssignments(reply)
	assignments := make(map[string]string)
	for name, task := range allMentions {
		if _, isMember := r.members[name]; isMember {
			assignments[name] = task
		}
	}

	if len(assignments) == 0 {
		if len(savedDocTitles) > 0 && r.Mode == "build" {
			continueMsg := r.Prompt.R("continue_legacy")
			continueLLMMsg := llm.NewMsg("user", continueMsg)
			*masterMsgs = append(*masterMsgs, continueLLMMsg)
			r.historyMu.Lock()
			r.masterHistory = append(r.masterHistory, continueLLMMsg)
			r.historyMu.Unlock()
			return true // continue
		}
		return false // break
	}

	if r.Mode != "build" {
		r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master",
			Content: r.Prompt.R("plan_mode_blocked")})
		return false
	}

	board := &SharedBoard{}
	results := r.runMembersParallel(ctx, assignments, board, skillXML)
	// 只在写作阶段（前置材料全部完成后）触发审读
	if r.allStaticFilesDone() {
		r.runChallengeRound(ctx, board, skillXML)
	}

	r.setStatus(StatusThinking, r.master.Config.Name, "正在审阅成员结果...")
	var resultsStr strings.Builder
	for memberName, result := range results {
		resultsStr.WriteString(fmt.Sprintf("[%s] %s\n", memberName, result))
	}
	var wsFilesList string
	if wsFiles := r.listWorkspaceFiles(); len(wsFiles) > 0 {
		wsFilesList = "\n\n当前产出物文件：\n"
		for _, f := range wsFiles {
			wsFilesList += "- " + f + "\n"
		}
	}
	feedbackMsg := r.Prompt.Render("feedback_legacy", map[string]string{
		"Results":        resultsStr.String(),
		"WorkspaceFiles": wsFilesList,
	})
	feedbackLLMMsg := llm.NewMsg("user", feedbackMsg)
	*masterMsgs = append(*masterMsgs, feedbackLLMMsg)
	r.historyMu.Lock()
	r.masterHistory = append(r.masterHistory, feedbackLLMMsg)
	r.historyMu.Unlock()
	r.updateTasks(*masterMsgs)
	return true // continue
}

// handleMemberConversation 处理用户与成员的对话续接
func (r *Room) handleMemberConversation(ctx context.Context, userName, userMsg string) error {
	memberName := r.lastActiveMember
	member, ok := r.members[memberName]
	if !ok {
		return fmt.Errorf("member %s not found", memberName)
	}
	log.Printf("[room %s] 用户与 %s 对话", r.Config.Name, memberName)
	r.setStatus(StatusWorking, member.Config.Name, "")

	if r.memberConvos == nil {
		r.memberConvos = make(map[string][]llm.Message)
	}

	// 如果该成员没有现有对话上下文，初始化系统提示
	if len(r.memberConvos[memberName]) == 0 {
		extraCtx := r.buildTeamXML()
		if r.systemRules != "" {
			extraCtx = r.systemRules + "\n\n" + extraCtx
		}
		if wsCtx := r.buildWorkspaceContext(); wsCtx != "" {
			extraCtx += "\n\n" + wsCtx
		}
		systemPrompt := member.BuildSystemPrompt(extraCtx)
		r.memberConvos[memberName] = []llm.Message{
			llm.NewMsg("system", systemPrompt+"\n\n"+r.Prompt.R("member_conversation")),
		}
	}

	r.memberConvos[memberName] = append(r.memberConvos[memberName], llm.NewMsg("user", userMsg))

	var memberReply strings.Builder
	_, usage, err := member.ChatWithUsage(ctx, r.memberConvos[memberName], func(token string) {
		memberReply.WriteString(token)
		r.emit(Event{Type: EvtAgentMessage, Agent: memberName, Role: "member", Content: token, Streaming: true})
	})
	if err != nil {
		r.setStatus(StatusPending, "", "")
		return err
	}
	r.emitUsage(memberName, usage)
	result := memberReply.String()

	// 结束流式
	r.emit(Event{Type: EvtAgentMessage, Agent: memberName, Role: "member", Content: "", Streaming: false})

	r.memberConvos[memberName] = append(r.memberConvos[memberName], llm.NewMsg("assistant", result))
	r.AppendHistory("member", memberName, result)

	// 存储到数据库
	if r.Store != nil && strings.TrimSpace(result) != "" {
		r.Store.InsertMessage(&store.Message{
			RoomID: r.Config.Name, Agent: memberName, Role: "member",
			Content: result, PartType: "text",
			GroupID: &r.currentGroupID,
		})
	}

	_, routed := r.saveAgentOutput(memberName, result, "")
	if routed {
		r.lastActiveMember = ""
	} else if !isDocument(result) {
		// lastActiveMember 保持不变，用户可继续对话
	} else {
		r.lastActiveMember = ""
	}

	r.setStatus(StatusPending, "", "")
	return nil
}

// handleDirectAssign 处理用户直接 @agent 指派的任务
func (r *Room) handleDirectAssign(ctx context.Context, assignments map[string]string) error {
	// Plan 模式下阻止任务执行
	if r.Mode != "build" {
		r.emit(Event{Type: EvtAgentMessage, Agent: "system", Role: "master",
			Content: r.Prompt.R("plan_mode_blocked")})
		r.setStatus(StatusPending, "", "")
		return nil
	}

	skillXML := skill.ToXML(r.skillMeta)
	board := &SharedBoard{}

	r.runMembersParallel(ctx, assignments, board, skillXML)
	// 只在写作阶段（前置材料全部完成后）触发审读
	if r.allStaticFilesDone() {
		r.runChallengeRound(ctx, board, skillXML)
	}

	r.setStatus(StatusPending, "", "")
	return nil
}
