# -*- coding: utf-8 -*-
"""
大纲生成器组件
基于 LangGraph 的大纲生成实现

该组件包含：
1. LangGraph 节点定义（模板加载、大纲生成、相似推荐、知识点关联）
2. 条件边判断函数
3. 辅助工具方法

位于 component 层，被 workflows 层的 OutlineWorkflow 调用
"""

import asyncio
import copy
import json
from typing import Dict, Any, List, Optional
from langchain_core.messages import AIMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate
from foundation.observability.logger.loggering import write_logger as logger
from foundation.ai.agent.generate.model_generate import generate_model_client
from core.base.workflow_manager import ProgressManagerRegistry
from .state_models import OutlineGenerationState


class OutlineGenerator:
    """
    大纲生成器

    提供大纲生成的 LangGraph 节点实现
    """

    def __init__(self):
        """初始化大纲生成器"""
        self.template_manager = None  # 模板管理器实例（延迟初始化）
        self.similarity_matcher = None  # 相似度匹配器实例（延迟初始化）
        self._keyword_rules_cache = None  # keyword_rules 缓存，避免重复IO加载

    # ==================== LangGraph 节点定义 ====================

    async def start_node(self, state: OutlineGenerationState) -> OutlineGenerationState:
        """
        工作流开始节点

        初始化工作流状态，记录开始信息

        Args:
            state: 当前状态

        Returns:
            OutlineGenerationState: 更新后的状态
        """
        logger.info(f"大纲生成工作流启动: {state['callback_task_id']}")

        # 初始化进度
        await self._update_progress(
            state,
            current=0,
            stage_name="工作流启动",
            message="大纲生成工作流已启动"
        )

        return {
            "current_stage": "start",
            "overall_task_status": "processing",
            "messages": [AIMessage(content="大纲生成工作流启动")]
        }

    async def load_template_node(self, state: OutlineGenerationState) -> OutlineGenerationState:
        """
        模板加载节点

        根据工程类型和模板ID加载大纲模板

        Args:
            state: 当前状态

        Returns:
            OutlineGenerationState: 更新后的状态，包含加载的模板
        """
        try:
            logger.info(f"开始加载模板: template_id={state['template_id']}")

            # 检查终止信号
            if await self._check_terminate_signal(state):
                return self._create_terminated_state(state, "load_template")

            # 更新进度
            await self._update_progress(
                state,
                current=10,
                stage_name="模板加载",
                message="正在加载大纲模板..."
            )

            template = await self._load_template(
                template_id=state["template_id"],
                project_type=state["project_info"].get("project_type", "")
            )

            logger.info(f"模板加载完成: {state['template_id']}")

            return {
                "current_stage": "template_loaded",
                "template": template,
                "messages": [AIMessage(content=f"模板加载完成: {state['template_id']}")]
            }

        except Exception as e:
            logger.error(f"模板加载失败: {str(e)}", exc_info=True)
            return self._create_error_state(state, f"模板加载失败: {str(e)}")

    async def generate_outline_node(self, state: OutlineGenerationState) -> OutlineGenerationState:
        """
        大纲生成节点

        使用 AI 模型生成大纲结构

        Args:
            state: 当前状态

        Returns:
            OutlineGenerationState: 更新后的状态，包含生成的大纲结构
        """
        try:
            logger.info(f"开始生成大纲: {state['callback_task_id']}")

            # 检查终止信号
            if await self._check_terminate_signal(state):
                return self._create_terminated_state(state, "outline_generation")

            # 更新进度
            await self._update_progress(
                state,
                current=30,
                stage_name="大纲生成",
                message="正在生成大纲结构..."
            )

            # 获取配置（从 state 中直接获取新字段，替代原来的 outline_config）
            generation_chapterenum = state.get("generation_chapterenum", [])
            generation_template = state.get("generation_template", [])
            trace_id = state["callback_task_id"]
            project_info = state["project_info"]

            # 过滤模板结构，只保留需要生成的章节
            filtered_template = await self._filter_template_keep_structure(
                node_list=generation_template,
                target_codes=generation_chapterenum
            )

            logger.info(f"[大纲生成] trace_id: {trace_id}, 需要生成 {len(filtered_template)} 个一级章节")

            # 异步并发控制：使用信号量控制并发度
            # 信号数为1时，逻辑上等价于顺序遍历；信号数大于1时，并发执行
            concurrency_limit = 3  # 默认并发信号数，可根据实际情况调整
            semaphore = asyncio.Semaphore(concurrency_limit)

            # 计算所有叶子节点（末级节点）总数，用于进度计算
            def count_leaf_nodes(node_list):
                count = 0
                for node in node_list:
                    children = node.get("children", [])
                    if children:
                        count += count_leaf_nodes(children)
                    else:
                        count += 1
                return count

            total_leaf_chapters = count_leaf_nodes(filtered_template)
            logger.info(f"[大纲生成] trace_id: {trace_id}, 叶子节点总数: {total_leaf_chapters}")

            # 使用原子计数器统计已完成叶子节点数（使用列表包装以实现引用传递）
            completed_count = [0]
            completed_lock = asyncio.Lock()

            async def generate_with_semaphore(idx: int, chapter_node: dict) -> dict:
                async with semaphore:
                    return await self._generate_single_chapter(
                        trace_id=f"{trace_id}_{idx}",
                        project_info=project_info,
                        chapter_node=chapter_node,
                        completed_counter=(completed_count, completed_lock),
                        total_leaf_chapters=total_leaf_chapters
                    )

            # 统一使用异步并发结构，信号数控制实际并发度
            logger.info(f"[大纲生成] trace_id: {trace_id}, 并发信号数: {concurrency_limit}")
            chapter_tasks = [
                generate_with_semaphore(idx, chapter_node)
                for idx, chapter_node in enumerate(filtered_template)
            ]
            results = await asyncio.gather(*chapter_tasks, return_exceptions=True)
            # 过滤掉异常结果
            outline_structure = [r for r in results if not isinstance(r, Exception)]

            logger.info(f"[大纲生成] trace_id: {trace_id}, 完成，共生成 {len(outline_structure)} 个章节")

            return {
                "current_stage": "outline_generated",
                "outline_structure": outline_structure,
                "messages": [AIMessage(content="大纲生成完成")]
            }

        except Exception as e:
            logger.error(f"大纲生成失败: {str(e)}", exc_info=True)
            return self._create_error_state(state, f"大纲生成失败: {str(e)}")

    async def recommend_similar_cases_node(self, state: OutlineGenerationState) -> OutlineGenerationState:
        """
        相似案例推荐节点

        基于向量检索推荐相似施工方案案例

        Args:
            state: 当前状态

        Returns:
            OutlineGenerationState: 更新后的状态，包含相似案例
        """
        try:
            logger.info(f"开始推荐相似案例: {state['callback_task_id']}")

            # 检查终止信号
            if await self._check_terminate_signal(state):
                return self._create_terminated_state(state, "similar_cases")

            # 更新进度
            await self._update_progress(
                state,
                current=60,
                stage_name="相似案例推荐",
                message="正在推荐相似施工方案..."
            )

            # 获取配置
            similarity_config = state.get("similarity_config", {})
            max_cases = similarity_config.get("max_cases", 5)
            threshold = similarity_config.get("threshold", 0.75)

            # 模拟相似案例推荐（实际实现时调用向量检索）
            await asyncio.sleep(1.5)  # 模拟向量检索耗时
            similar_cases = await self._search_similar_cases(
                project_info=state["project_info"],
                max_cases=max_cases,
                threshold=threshold
            )

            logger.info(f"相似案例推荐完成: 找到 {len(similar_cases)} 个案例")

            return {
                "current_stage": "similar_cases_recommended",
                "similar_cases": similar_cases,
                "messages": [AIMessage(content=f"相似案例推荐完成: {len(similar_cases)} 个")]
            }

        except Exception as e:
            logger.error(f"相似案例推荐失败: {str(e)}", exc_info=True)
            # 非关键步骤，失败不中断流程
            return {
                "current_stage": "similar_cases_recommended",
                "similar_cases": [],
                "messages": [AIMessage(content=f"相似案例推荐失败: {str(e)}")]
            }

    async def recommend_similar_fragments_node(self, state: OutlineGenerationState) -> OutlineGenerationState:
        """
        相似片段推荐节点

        推荐与当前大纲相关的相似片段

        Args:
            state: 当前状态

        Returns:
            OutlineGenerationState: 更新后的状态，包含相似片段
        """
        try:
            logger.info(f"开始推荐相似片段: {state['callback_task_id']}")

            # 检查终止信号
            if await self._check_terminate_signal(state):
                return self._create_terminated_state(state, "similar_fragments")

            # 更新进度
            await self._update_progress(
                state,
                current=75,
                stage_name="相似片段推荐",
                message="正在推荐相似片段..."
            )

            # 获取配置
            similarity_config = state.get("similarity_config", {})
            max_fragments = similarity_config.get("max_fragments", 10)
            threshold = similarity_config.get("threshold", 0.75)

            # 模拟相似片段推荐
            await asyncio.sleep(1.5)  # 模拟片段检索耗时
            similar_fragments = await self._search_similar_fragments(
                outline_structure=state["outline_structure"],
                max_fragments=max_fragments,
                threshold=threshold
            )

            logger.info(f"相似片段推荐完成: 找到 {len(similar_fragments)} 个片段")

            return {
                "current_stage": "similar_fragments_recommended",
                "similar_fragments": similar_fragments,
                "messages": [AIMessage(content=f"相似片段推荐完成: {len(similar_fragments)} 个")]
            }

        except Exception as e:
            logger.error(f"相似片段推荐失败: {str(e)}", exc_info=True)
            # 非关键步骤，失败不中断流程
            return {
                "current_stage": "similar_fragments_recommended",
                "similar_fragments": [],
                "messages": [AIMessage(content=f"相似片段推荐失败: {str(e)}")]
            }

    async def link_knowledge_bases_node(self, state: OutlineGenerationState) -> OutlineGenerationState:
        """
        知识点关联节点

        使用 RAG 检索关联编制依据知识点

        Args:
            state: 当前状态

        Returns:
            OutlineGenerationState: 更新后的状态，包含关联的知识点
        """
        try:
            logger.info(f"开始关联知识点: {state['callback_task_id']}")

            # 检查终止信号
            if await self._check_terminate_signal(state):
                return self._create_terminated_state(state, "knowledge_bases")

            # 更新进度
            await self._update_progress(
                state,
                current=90,
                stage_name="知识点关联",
                message="正在关联编制依据知识点..."
            )

            # 获取配置
            knowledge_config = state.get("knowledge_config", {})
            enable_basis = knowledge_config.get("enable_basis", True)

            if not enable_basis:
                logger.info("知识点关联已禁用")
                return {
                    "current_stage": "knowledge_bases_linked",
                    "knowledge_bases": [],
                    "messages": [AIMessage(content="知识点关联已跳过")]
                }

            # 模拟知识点关联（实际实现时调用 RAG 检索）
            await asyncio.sleep(1.5)  # 模拟 RAG 检索耗时
            knowledge_bases = await self._search_knowledge_bases(
                outline_structure=state["outline_structure"],
                project_type=state["project_info"].get("project_type", "")
            )

            logger.info(f"知识点关联完成: 找到 {len(knowledge_bases)} 个知识点")

            return {
                "current_stage": "knowledge_bases_linked",
                "knowledge_bases": knowledge_bases,
                "messages": [AIMessage(content=f"知识点关联完成: {len(knowledge_bases)} 个")]
            }

        except Exception as e:
            logger.error(f"知识点关联失败: {str(e)}", exc_info=True)
            # 非关键步骤，失败不中断流程
            return {
                "current_stage": "knowledge_bases_linked",
                "knowledge_bases": [],
                "messages": [AIMessage(content=f"知识点关联失败: {str(e)}")]
            }

    async def complete_node(self, state: OutlineGenerationState) -> OutlineGenerationState:
        """
        工作流完成节点

        标记任务完成，整理最终结果

        Args:
            state: 当前状态

        Returns:
            OutlineGenerationState: 更新后的状态，标记完成
        """
        logger.info(f"大纲生成工作流完成: {state['callback_task_id']}")

        # 更新进度
        await self._update_progress(
            state,
            current=100,
            stage_name="大纲生成完成",
            message="大纲生成工作流已完成",
            status="completed"
        )

        return {
            "current_stage": "complete",
            "overall_task_status": "completed",
            "messages": [AIMessage(content="大纲生成工作流完成")]
        }

    # ==================== 条件边判断函数 ====================

    def should_recommend_similar(self, state: OutlineGenerationState) -> str:
        """
        条件判断：是否启用相似推荐

        Args:
            state: 当前状态

        Returns:
            str: "yes" 或 "no"
        """
        similarity_config = state.get("similarity_config")
        if similarity_config and similarity_config.get("max_cases", 0) > 0:
            return "yes"
        return "no"

    def should_link_knowledge(self, state: OutlineGenerationState) -> str:
        """
        条件判断：是否启用知识点关联

        Args:
            state: 当前状态

        Returns:
            str: "yes" 或 "no"
        """
        knowledge_config = state.get("knowledge_config", {})
        if knowledge_config.get("enable_basis", True):
            return "yes"
        return "no"

    def should_terminate_or_error(self, state: OutlineGenerationState) -> str:
        """
        条件判断：是否终止或发生错误

        Args:
            state: 当前状态

        Returns:
            str: "terminate", "error", 或 "continue"
        """
        # 检查终止状态
        if state.get("overall_task_status") == "terminated":
            return "terminate"

        # 检查错误状态
        if state.get("overall_task_status") == "failed" or state.get("error_message"):
            return "error"

        # 默认继续执行
        return "continue"

    # ==================== 辅助方法 ====================

    async def _update_progress(
        self,
        state: OutlineGenerationState,
        current: int,
        stage_name: str,
        message: str,
        status: str = "processing"
    ):
        """
        更新进度

        Args:
            state: 当前状态
            current: 当前进度百分比
            stage_name: 阶段名称
            message: 进度消息
            status: 状态
        """
        callback_task_id = state["callback_task_id"]
        # 通过 Registry 获取 progress_manager
        progress_manager = ProgressManagerRegistry.get_progress_manager(callback_task_id)
        logger.info(f"[进度更新] task_id={callback_task_id}, stage={stage_name}, progress={current}%, pm_exists={progress_manager is not None}")
        if progress_manager:
            await progress_manager.update_stage_progress(
                callback_task_id=callback_task_id,
                user_id=state["user_id"],
                stage_name=stage_name,
                current=current,
                status=status,
                message=message,
                overall_task_status=status
            )
            logger.info(f"[进度更新成功] task_id={callback_task_id}, stage={stage_name}")
        else:
            logger.warning(f"[进度更新失败] task_id={callback_task_id}, 未找到ProgressManager")

    async def _check_terminate_signal(self, state: OutlineGenerationState) -> bool:
        """
        检查是否有终止信号

        Args:
            state: 当前状态

        Returns:
            bool: 有终止信号返回 True
        """
        callback_task_id = state.get("callback_task_id", "")
        if not callback_task_id:
            return False

        try:
            # 从 Redis 检查终止信号
            from core.base.workflow_manager import workflow_manager
            return await workflow_manager.check_outline_terminate_signal(callback_task_id)
        except Exception as e:
            logger.error(f"检查终止信号失败: {str(e)}")
            return False

    def _create_terminated_state(self, state: OutlineGenerationState, stage: str) -> Dict[str, Any]:
        """
        创建终止状态

        Args:
            state: 当前状态
            stage: 当前阶段

        Returns:
            Dict: 终止状态
        """
        logger.warning(f"大纲生成任务被终止: {state['callback_task_id']} (阶段: {stage})")
        return {
            "current_stage": stage,
            "overall_task_status": "terminated",
            "messages": [AIMessage(content=f"任务被终止 (阶段: {stage})")]
        }

    def _create_error_state(self, state: OutlineGenerationState, error_message: str) -> Dict[str, Any]:
        """
        创建错误状态

        Args:
            state: 当前状态
            error_message: 错误信息

        Returns:
            Dict: 错误状态
        """
        logger.error(f"大纲生成任务失败: {state['callback_task_id']}, 错误: {error_message}")
        return {
            "current_stage": state.get("current_stage", "unknown"),
            "overall_task_status": "failed",
            "error_message": error_message,
            "messages": [AIMessage(content=f"任务失败: {error_message}")]
        }

    # ==================== 业务逻辑方法（模拟实现） ====================

    async def _load_template(self, template_id: str, project_type: str) -> Dict[str, Any]:
        """
        加载模板和 keyword_rules 配置

        同时加载模板和 keyword_rules_3.json，并将 keyword_rules 缓存到实例中，
        避免后续每次生成章节时重复IO加载。

        Args:
            template_id: 模板ID
            project_type: 工程类型

        Returns:
            Dict: 模板内容
        """
        import os

        # 1. 加载 keyword_rules_3.json（如果尚未缓存）
        if self._keyword_rules_cache is None:
            try:
                # 获取当前文件所在目录
                current_dir = os.path.dirname(os.path.abspath(__file__))
                keyword_rules_path = os.path.join(current_dir, "prompt", "keyword_rules_3.json")

                logger.info(f"[模板加载] 首次加载 keyword_rules: {keyword_rules_path}")

                with open(keyword_rules_path, "r", encoding="utf-8") as f:
                    keyword_rules_list = json.load(f)

                # 将列表转换为字典格式，便于快速查找：{code: prompt}
                keyword_rules_dict = {}
                for item in keyword_rules_list:
                    if isinstance(item, dict):
                        # 每个item是单键值对：{code: prompt}
                        for code, prompt in item.items():
                            keyword_rules_dict[code] = prompt

                self._keyword_rules_cache = keyword_rules_dict
                logger.info(f"[模板加载] keyword_rules 加载完成，共 {len(keyword_rules_dict)} 条规则")

            except Exception as e:
                logger.error(f"[模板加载] keyword_rules 加载失败: {str(e)}")
                self._keyword_rules_cache = {}  # 避免重复尝试加载

        # 2. 实际实现时从数据库或文件加载模板
        # 模拟返回
        return {
            "template_id": template_id,
            "project_type": project_type,
            "structure": {
                "chapters": [
                    {"id": "1", "title": "工程概况", "level": 1},
                    {"id": "2", "title": "编制依据", "level": 1},
                    {"id": "3", "title": "施工方案", "level": 1},
                    {"id": "4", "title": "安全措施", "level": 1},
                    {"id": "5", "title": "质量保证", "level": 1}
                ]
            }
        }

    def _get_keyword_rule(self, code: str) -> Optional[str]:
        """
        获取指定 code 的 keyword_rule 提示词

        Args:
            code: 章节代码（如 "overview_DesignSummary_ProjectIntroduction"）

        Returns:
            Optional[str]: 对应的提示词，如果未找到则返回 None
        """
        if self._keyword_rules_cache is None:
            return None
        return self._keyword_rules_cache.get(code)

    async def _call_llm(
        self,
        trace_id: str,
        prompt: str,
        references: Optional[List[Dict[str, Any]]] = None,
        timeout: Optional[int] = 60,
        model_name: Optional[str] = "qwen3_30b"
    ) -> str:
        """
        原子模型调用方法（非流式）

        使用 foundation.ai.agent.generate.model_generate 中的 generate_model_client
        调用大模型生成内容

        Args:
            trace_id: 追踪ID，用于日志和调试
            prompt: 提示词内容
            references: 参考内容列表，每个元素包含 content 和 source 字段（可选）
            timeout: 超时时间（秒），默认60秒
            model_name: 模型名称（可选），如 doubao, qwen, deepseek 等

        Returns:
            str: 生成的内容（generated_content）

        Raises:
            TimeoutError: 模型调用超时
            Exception: 模型调用异常

        Example:
            content = await self._call_llm(
                trace_id="task-001",
                prompt="生成工程概况内容",
                references=[{"content": "参考内容", "source": "文档A"}]
            )
        """
        try:
            # 构建参考内容字符串
            reference_text = ""
            if references:
                reference_parts = []
                for idx, ref in enumerate(references, 1):
                    content = ref.get("content", "")
                    source = ref.get("source", "")
                    if content:
                        ref_text = f"参考[{idx}]: {content}"
                        if source:
                            ref_text += f" (来源: {source})"
                        reference_parts.append(ref_text)
                if reference_parts:
                    reference_text = "\n\n".join(reference_parts)
                    reference_text = f"\n\n=== 参考内容 ===\n{reference_text}\n\n"

            # 构建完整的提示词模板
            system_prompt = "你是一个专业的施工方案编写助手，请根据要求生成高质量的内容。"
            user_prompt = f"{prompt}{reference_text}\n\n请生成内容："

            # 创建 ChatPromptTemplate
            chat_template = ChatPromptTemplate.from_messages([
                ("system", system_prompt),
                ("user", user_prompt)
            ])

            # 构建 task_prompt_info
            task_prompt_info = {
                "task_prompt": chat_template
            }

            logger.info(f"[LLM调用] trace_id: {trace_id}, 开始生成内容")

            # 调用模型生成（非流式）
            generated_content = await generate_model_client.get_model_generate_invoke(
                trace_id=trace_id,
                task_prompt_info=task_prompt_info,
                timeout=timeout,
                model_name=model_name
            )

            logger.info(f"[LLM调用] trace_id: {trace_id}, 生成完成，内容长度: {len(generated_content)}")

            return generated_content

        except TimeoutError as e:
            logger.error(f"[LLM调用] trace_id: {trace_id}, 超时: {str(e)}")
            raise
        except Exception as e:
            logger.error(f"[LLM调用] trace_id: {trace_id}, 异常: {str(e)}")
            raise

    async def _generate_single_chapter(
        self,
        trace_id: str,
        project_info: Dict[str, Any],
        chapter_node: Dict[str, Any],
        parent_index: str = "",
        completed_counter: tuple = None,
        total_leaf_chapters: int = 1
    ) -> Dict[str, Any]:
        """
        生成单个章节（支持内部异步并发生成子章节）

        根据架构设计文档的响应格式构建节点：
        - index: 章节编号
        - level: 层级
        - title: 章节标题
        - code: 章节代码
        - generated_content: AI生成的内容
        - similar_fragments: 相似片段（2级和3级节点）
        - key_points: 核心要点（末级节点）
        - knowledge_bases: 知识点（末级节点）
        - children: 子章节

        Args:
            trace_id: 追踪ID
            project_info: 项目信息
            chapter_node: 章节节点（包含 index, level, title, code, children）
            parent_index: 父章节编号（用于构建完整编号）

        Returns:
            Dict: 生成的章节节点，符合架构设计文档格式
        """
        # 构建当前节点的完整编号
        current_index = chapter_node.get("index", "")
        level = chapter_node.get("level", 1)
        title = chapter_node.get("title", "")
        code = chapter_node.get("code", "")
        children = chapter_node.get("children", [])

        logger.info(f"[章节生成] trace_id: {trace_id}, 开始生成 {current_index} {title}")

        # 检查终止信号 - 在章节生成开始前
        if await self._check_terminate_by_trace_id(trace_id):
            logger.warning(f"[章节生成] trace_id: {trace_id}, 检测到终止信号，停止生成 {current_index} {title}")
            raise asyncio.CancelledError(f"任务被终止: {current_index} {title}")

        # 1. 生成当前章节的 content
        chapter_content = await self._generate_chapter_content(
            trace_id=trace_id,
            project_info=project_info,
            chapter_title=title,
            chapter_code=code,
            level=level
        )

        # 检查终止信号 - 在 LLM 调用后
        if await self._check_terminate_by_trace_id(trace_id):
            logger.warning(f"[章节生成] trace_id: {trace_id}, LLM调用后检测到终止信号，停止生成 {current_index} {title}")
            raise asyncio.CancelledError(f"任务被终止: {current_index} {title}")

        # 2. 构建基础节点结构
        result_node = {
            "index": current_index,
            "level": level,
            "title": title,
            "code": code,
            "generated_content": chapter_content
        }

        # 3. 处理子章节（如果有）
        if children and len(children) > 0:
            # 内部异步并发：并行生成所有子章节
            logger.info(f"[章节生成] trace_id: {trace_id}, {current_index} 有 {len(children)} 个子章节，并发生成")

            child_tasks = [
                self._generate_single_chapter(
                    trace_id=f"{trace_id}_{idx}",
                    project_info=project_info,
                    chapter_node=child_node,
                    parent_index=current_index,
                    completed_counter=completed_counter,
                    total_leaf_chapters=total_leaf_chapters
                )
                for idx, child_node in enumerate(children)
            ]

            child_results = await asyncio.gather(*child_tasks, return_exceptions=True)

            # 检查是否有取消异常，向上传播
            for result in child_results:
                if isinstance(result, asyncio.CancelledError):
                    logger.warning(f"[章节生成] trace_id: {trace_id}, 子章节生成被取消，向上传播")
                    raise result

            # 过滤异常并添加子节点
            result_node["children"] = [
                r for r in child_results if not isinstance(r, Exception)
            ]

            # 2级和3级节点添加 similar_fragments
            if level in [2, 3]:
                result_node["similar_fragments"] = await self._get_similar_fragments_for_node(
                    trace_id=trace_id,
                    chapter_title=title,
                    chapter_code=code
                )
        else:
            # 末级节点：添加 key_points 和 knowledge_bases
            logger.info(f"[章节生成] trace_id: {trace_id}, {current_index} 是末级节点")
            result_node["key_points"] = await self._extract_key_points_for_node(
                trace_id=trace_id,
                content=chapter_content,
                chapter_code=code
            )
            result_node["knowledge_bases"] = await self._get_knowledge_bases_for_node(
                trace_id=trace_id,
                chapter_title=title,
                chapter_code=code
            )

            # 末级节点完成时，原子地增加计数并推送进度
            if completed_counter:
                completed_count_list, completed_lock = completed_counter
                async with completed_lock:
                    completed_count_list[0] += 1
                    current_completed = completed_count_list[0]
                await self._send_chapter_progress(
                    trace_id=trace_id,
                    chapter_index=current_index,
                    chapter_title=title,
                    user_id=project_info.get("user_id"),
                    completed_count=current_completed,
                    total_chapters=total_leaf_chapters
                )

        logger.info(f"[章节生成] trace_id: {trace_id}, {current_index} {title} 生成完成")

        return result_node

    async def _send_chapter_progress(self, trace_id: str, chapter_index: str, chapter_title: str, user_id: str = None, completed_count: int = 0, total_chapters: int = 1):
        """
        发送章节生成进度

        Args:
            trace_id: 追踪ID（格式: callback_task_id_索引）
            chapter_index: 章节编号
            chapter_title: 章节标题
            user_id: 用户ID
            completed_count: 已完成章节数
            total_chapters: 总章节数
        """
        try:
            # 提取 callback_task_id
            callback_task_id = self._extract_callback_task_id(trace_id)

            progress_manager = ProgressManagerRegistry.get_progress_manager(callback_task_id)
            logger.info(f"[章节进度调试] progress_manager={progress_manager}, registry_keys={list(ProgressManagerRegistry._registry.keys())}")
            if progress_manager:
                logger.info(f"大纲生成单章进度打印: task_id={callback_task_id}, 章节 [{chapter_index}] {chapter_title} 生成完成")
                # 计算当前进度：基于实际完成数计算，范围 0-100
                # 公式：(completed_count / total_chapters) * 100
                current_progress = int((completed_count / max(total_chapters, 1)) * 100)
                await progress_manager.update_stage_progress(
                    callback_task_id=callback_task_id,
                    user_id=user_id,
                    stage_name="大纲生成",
                    status="processing",
                    message=f"正在生成章节 [{chapter_index}] {chapter_title}...",
                    current=current_progress
                )
                logger.info(f"[章节进度] task_id={trace_id}, 章节 [{chapter_index}] {chapter_title} 生成完成, 进度={current_progress}%")
            else:
                logger.warning(f"[章节进度] 未找到 ProgressManager: callback_task_id={callback_task_id}")
        except Exception as e:
            logger.warning(f"[章节进度] 推送进度失败: {str(e)}")

    def _extract_callback_task_id(self, trace_id: str) -> Optional[str]:
        """
        从 trace_id 提取 callback_task_id

        trace_id 格式: callback_task_id_索引1_索引2...
        例如: outline_abc123_0, outline_abc123_0_1, outline_abc123_0_6_0

        Returns:
            Optional[str]: callback_task_id 或 None
        """
        if not trace_id:
            return None

        # callback_task_id 格式为: outline_{uuid}
        # 索引部分从第一个纯数字开始
        # 例如: outline_7da350bcdd094bb9_0_0 -> outline_7da350bcdd094bb9
        parts = trace_id.split("_")
        result_parts = []
        for part in parts:
            # 遇到纯数字索引，停止拼接
            if part.isdigit():
                break
            result_parts.append(part)

        if result_parts:
            return "_".join(result_parts)
        return trace_id

    async def _check_terminate_by_trace_id(self, trace_id: str) -> bool:
        """
        根据 trace_id 检查是否有终止信号

        Args:
            trace_id: 追踪ID

        Returns:
            bool: 有终止信号返回 True
        """
        callback_task_id = self._extract_callback_task_id(trace_id)
        if not callback_task_id:
            return False

        try:
            from core.base.workflow_manager import workflow_manager
            return await workflow_manager.check_outline_terminate_signal(callback_task_id)
        except Exception as e:
            logger.error(f"检查终止信号失败: {str(e)}")
            return False

    async def _generate_chapter_content(
        self,
        trace_id: str,
        project_info: Dict[str, Any],
        chapter_title: str,
        chapter_code: str,
        level: int
    ) -> str:
        """
        生成章节内容

        根据 chapter_code 从 keyword_rules 中提取对应的提示词作为编写要求。

        Args:
            trace_id: 追踪ID
            project_info: 项目信息
            chapter_title: 章节标题
            chapter_code: 章节代码
            level: 层级

        Returns:
            str: 生成的章节内容
        """
        project_name = project_info.get("project_name", "未命名项目")

        # 从 keyword_rules 获取该章节的编写要求
        keyword_rule = self._get_keyword_rule(chapter_code)
        if keyword_rule:
            writing_requirements = f"""【编写要求】
{keyword_rule}

"""
            logger.info(f"[章节生成] trace_id: {trace_id}, code={chapter_code}, 已加载 keyword_rule")
        else:
            writing_requirements = ""
            logger.warning(f"[章节生成] trace_id: {trace_id}, code={chapter_code}, 未找到 keyword_rule")

        # 构建提示词
        prompt = f"""请为施工方案生成【{chapter_title}】章节的内容。

项目名称：{project_name}
章节编号：{chapter_code}
章节层级：第{level}级

{writing_requirements}通用要求：
1. 内容专业、准确，符合公路桥梁施工规范
2. 语言简洁明了，条理清晰
3. 根据实际情况生成合理的施工方案内容
4. 字数控制在200-500字之间

请直接输出章节内容，不要包含标题。"""

        try:
            content = await self._call_llm(
                trace_id=trace_id,
                prompt=prompt,
                timeout=60
            )
            return content.strip()
        except Exception as e:
            logger.error(f"[章节生成] trace_id: {trace_id}, 生成内容失败: {str(e)}")
            # 返回默认内容
            return f"{project_name}的{chapter_title}内容（自动生成）..."

    async def _get_similar_fragments_for_node(
        self,
        trace_id: str,
        chapter_title: str,
        chapter_code: str
    ) -> List[Dict[str, Any]]:
        """
        获取节点的相似片段（用于2级和3级节点）

        Args:
            trace_id: 追踪ID
            chapter_title: 章节标题
            chapter_code: 章节代码

        Returns:
            List[Dict]: 相似片段列表
        """
        # TODO: 实际实现时调用向量检索
        # 模拟返回
        return [
            {
                "fragment_id": f"frag_{chapter_code}_001",
                "section_title": chapter_title,
                "fragment_content": f"与{chapter_title}相关的相似内容示例...",
                "similarity_score": 0.89,
                "source_document_title": "某特大桥施工方案"
            }
        ]

    async def _extract_key_points_for_node(
        self,
        trace_id: str,
        content: str,
        chapter_code: str
    ) -> List[str]:
        """
        提取节点的核心要点（用于末级节点）

        Args:
            trace_id: 追踪ID
            content: 章节内容
            chapter_code: 章节代码

        Returns:
            List[str]: 核心要点列表
        """
        # TODO: 实际实现时调用AI提取或使用规则提取
        # 模拟返回
        return ["要点1", "要点2", "要点3"]

    async def _get_knowledge_bases_for_node(
        self,
        trace_id: str,
        chapter_title: str,
        chapter_code: str
    ) -> List[str]:
        """
        获取节点的知识点/编制依据（用于末级节点）

        Args:
            trace_id: 追踪ID
            chapter_title: 章节标题
            chapter_code: 章节代码

        Returns:
            List[str]: 知识点列表
        """
        # TODO: 实际实现时调用知识库检索
        # 模拟返回
        return ["《公路桥涵设计通用规范》JTG D60-2015"]

    async def _search_similar_cases(
        self,
        project_info: Dict[str, Any],
        max_cases: int,
        threshold: float
    ) -> List[Dict[str, Any]]:
        """
        搜索相似案例

        Args:
            project_info: 项目信息
            max_cases: 最大案例数
            threshold: 相似度阈值

        Returns:
            List: 相似案例列表
        """
        # 实际实现时调用向量检索
        # 模拟返回
        return [
            {
                "case_id": "case_001",
                "case_title": "某大桥上部结构施工方案",
                "similarity_score": 0.89,
                "project_type": project_info.get("project_type", ""),
                "outline": []
            },
            {
                "case_id": "case_002",
                "case_title": "某特大桥主梁施工方案",
                "similarity_score": 0.82,
                "project_type": project_info.get("project_type", ""),
                "outline": []
            }
        ][:max_cases]

    async def _search_similar_fragments(
        self,
        outline_structure: Dict[str, Any],
        max_fragments: int,
        threshold: float
    ) -> List[Dict[str, Any]]:
        """
        搜索相似片段

        Args:
            outline_structure: 大纲结构
            max_fragments: 最大片段数
            threshold: 相似度阈值

        Returns:
            List: 相似片段列表
        """
        # 实际实现时调用向量检索
        # 模拟返回
        return [
            {
                "fragment_id": "frag_001",
                "section_path": "1.1",
                "section_title": "工程简介",
                "content": "工程简介示例内容...",
                "similarity_score": 0.91
            },
            {
                "fragment_id": "frag_002",
                "section_path": "3.1",
                "section_title": "施工工艺",
                "content": "施工工艺示例内容...",
                "similarity_score": 0.85
            }
        ][:max_fragments]

    async def _search_knowledge_bases(
        self,
        outline_structure: Dict[str, Any],
        project_type: str
    ) -> List[Dict[str, Any]]:
        """
        搜索知识点

        Args:
            outline_structure: 大纲结构
            project_type: 工程类型

        Returns:
            List: 知识点列表
        """
        # 实际实现时调用 RAG 检索
        # 模拟返回
        return [
            {
                "knowledge_id": "kb_001",
                "title": "《公路桥涵施工技术规范》",
                "content": "桥涵施工技术规范要点...",
                "relevance_score": 0.95,
                "category": "规范标准"
            },
            {
                "knowledge_id": "kb_002",
                "title": "桥梁上部结构施工要点",
                "content": "上部结构施工关键要点...",
                "relevance_score": 0.88,
                "category": "技术要点"
            }
        ]

    async def _filter_template_keep_structure(self, node_list, target_codes):
        """
        过滤模板结构（保留原始树形层级）
        仅移除：code不在target_codes中 且 无匹配子节点的节点
        :param node_list: 模板结构节点列表（初始传generation_template["structure"]）
        :param target_codes: 需保留的code列表（即generation_chapterenum）
        :return: 过滤后的树形结构列表（和原结构嵌套一致，仅剔除不匹配节点）
        """
        # 边界情况：空列表直接返回
        if not node_list:
            return []

        # 边界情况：空target_codes，返回原始结构的深拷贝
        if not target_codes:
            return copy.deepcopy(node_list)

        filtered_nodes = []
        for node in node_list:
            # 深拷贝节点，避免修改原数据（关键！防止原模板结构被污染）
            current_node = copy.deepcopy(node)

            # 1. 递归过滤当前节点的子节点，先处理子层再判断当前层（使用await）
            children = current_node.get("children", [])
            if isinstance(children, list) and len(children) > 0:
                filtered_children = await self._filter_template_keep_structure(children, target_codes)
                current_node["children"] = filtered_children  # 替换为过滤后的子节点

            # 2. 节点保留规则：自身code匹配 或 过滤后子节点非空 → 保留
            is_self_match = current_node.get("code") in target_codes
            has_valid_children = len(current_node.get("children", [])) > 0
            if is_self_match or has_valid_children:
                filtered_nodes.append(current_node)

        return filtered_nodes