"""子智能体任务管理（子进程模式）。"""

import json
import subprocess
import time
import uuid
import os
import shlex
import platform
import shutil
import signal
from pathlib import Path, PurePosixPath
from typing import Dict, List, Optional, Any, Tuple, TYPE_CHECKING

from config import (
    OUTPUT_FORMATS,
    SUB_AGENT_DEFAULT_TIMEOUT,
    SUB_AGENT_MAX_ACTIVE,
    SUB_AGENT_STATE_FILE,
    SUB_AGENT_STATUS_POLL_INTERVAL,
    SUB_AGENT_TASKS_BASE_DIR,
)
from utils.logger import setup_logger
import logging
from modules.host_sandbox_runner import build_host_sandbox_plan, host_sandbox_enabled, HostSandboxError

if TYPE_CHECKING:
    from modules.user_container_manager import ContainerHandle

# 静音子智能体日志（交由前端提示/brief_log处理）
logger = setup_logger(__name__)
logger.setLevel(logging.CRITICAL)
logger.disabled = True
logger.propagate = False
for h in list(logger.handlers):
    logger.removeHandler(h)
TERMINAL_STATUSES = {"completed", "failed", "timeout"}


class SubAgentManager:
    """负责主智能体与子智能体的任务调度（子进程模式）。"""

    def __init__(
        self,
        project_path: str,
        data_dir: str,
        container_session: Optional["ContainerHandle"] = None,
    ):
        self.project_path = Path(project_path).resolve()
        self.data_dir = Path(data_dir).resolve()
        self.base_dir = Path(SUB_AGENT_TASKS_BASE_DIR).resolve()
        self.state_file = Path(SUB_AGENT_STATE_FILE).resolve()
        self.container_session: Optional["ContainerHandle"] = container_session
        self.host_execution_mode: str = "sandbox"

        # easyagent批处理入口（优先使用项目目录内的副本）
        candidate_batch = self.project_path / "easyagent" / "src" / "batch" / "index.js"
        if candidate_batch.exists():
            self.easyagent_batch = candidate_batch
        else:
            self.easyagent_batch = Path(__file__).parent.parent / "easyagent" / "src" / "batch" / "index.js"

        self.base_dir.mkdir(parents=True, exist_ok=True)
        self.state_file.parent.mkdir(parents=True, exist_ok=True)

        self.tasks: Dict[str, Dict] = {}
        self.conversation_agents: Dict[str, List[int]] = {}
        self.processes: Dict[str, subprocess.Popen] = {}  # task_id -> Popen对象
        self._load_state()
        try:
            self.reconcile_task_states()
        except Exception:
            pass

    # ------------------------------------------------------------------
    # 公共方法
    # ------------------------------------------------------------------
    def create_sub_agent(
        self,
        *,
        agent_id: int,
        summary: str,
        task: str,
        deliverables_dir: str,
        timeout_seconds: Optional[int] = None,
        conversation_id: Optional[str] = None,
        run_in_background: bool = False,
        model_key: Optional[str] = None,
        thinking_mode: Optional[str] = None,
    ) -> Dict:
        """创建子智能体任务并启动子进程。"""
        validation_error = self._validate_create_params(agent_id, summary, task, deliverables_dir)
        if validation_error:
            return {"success": False, "error": validation_error}

        if not thinking_mode:
            return {"success": False, "error": "缺少 thinking_mode 参数，必须指定 fast 或 thinking"}
        if thinking_mode not in {"fast", "thinking"}:
            return {"success": False, "error": "thinking_mode 仅支持 fast 或 thinking"}

        if not conversation_id:
            return {"success": False, "error": "缺少对话ID，无法创建子智能体"}

        if not self._ensure_agent_slot_available(conversation_id, agent_id):
            return {
                "success": False,
                "error": f"该对话已使用过编号 {agent_id}，请更换新的子智能体代号。"
            }

        if self._active_task_count(conversation_id) >= SUB_AGENT_MAX_ACTIVE:
            return {
                "success": False,
                "error": f"该对话已存在 {SUB_AGENT_MAX_ACTIVE} 个运行中的子智能体，请稍后再试。",
            }

        task_id = self._generate_task_id(agent_id)
        task_root = self.base_dir / task_id
        task_root.mkdir(parents=True, exist_ok=True)

        # 解析deliverables_dir（相对于project_path）
        try:
            deliverables_path = self._resolve_deliverables_dir(deliverables_dir)
        except ValueError as exc:
            return {"success": False, "error": str(exc)}

        # 创建.subagent目录用于存储对话历史
        subagent_dir = deliverables_path / ".subagent"
        subagent_dir.mkdir(parents=True, exist_ok=True)

        # 准备文件路径
        task_file = task_root / "task.txt"
        system_prompt_file = task_root / "system_prompt.txt"
        output_file = task_root / "output.json"
        stats_file = task_root / "stats.json"
        progress_file = task_root / "progress.jsonl"

        # 构建用户消息
        prompt_workspace = self._get_runtime_path(self.project_path)
        deliverables_display = self._get_runtime_path(deliverables_path)
        user_message = self._build_user_message(agent_id, summary, task, deliverables_display, timeout_seconds)
        task_file.write_text(user_message, encoding="utf-8")

        # 构建系统提示
        system_prompt = self._build_system_prompt(prompt_workspace)
        system_prompt_file.write_text(system_prompt, encoding="utf-8")

        # 启动子进程
        timeout_seconds = timeout_seconds or SUB_AGENT_DEFAULT_TIMEOUT
        cmd = self._build_sub_agent_command(
            batch_path=str(self.easyagent_batch),
            workspace_path=str(self.project_path),
            task_file=str(task_file),
            system_prompt_file=str(system_prompt_file),
            output_file=str(output_file),
            stats_file=str(stats_file),
            progress_file=str(progress_file),
            agent_id=agent_id,
            timeout_seconds=timeout_seconds,
            model_key=model_key,
            thinking_mode=thinking_mode,
        )
        execution_mode = "host"
        container_name = None
        env = os.environ.copy()
        launch_cmd = list(cmd)
        launch_cwd = str(self.project_path)
        pass_fds: tuple[int, ...] = tuple()
        seccomp_fd: Optional[int] = None

        if self._should_use_container():
            container = self.container_session
            container_name = getattr(container, "container_name", None) if container else None
            docker_bin = (getattr(container, "sandbox_bin", None) if container else None) or shutil.which("docker")
            if not container_name:
                return {"success": False, "error": "容器模式下缺少 container_name，无法启动子智能体。"}
            if not docker_bin:
                return {"success": False, "error": "容器模式下未找到 docker，可执行子智能体失败。"}
            env["EASYAGENT_CONTAINER_NAME"] = container_name
            env["EASYAGENT_CONTAINER_MOUNT_PATH"] = (
                getattr(container, "mount_path", None) or "/workspace"
            )
            env["EASYAGENT_DOCKER_BIN"] = docker_bin
            env["EASYAGENT_CONTAINER_VENV"] = os.environ.get("CONTAINER_PYTHON_VENV", "/opt/agent-venv")
            env["EASYAGENT_CONTAINER_PYTHON"] = os.environ.get("CONTAINER_PYTHON_CMD", "/opt/agent-venv/bin/python3")
            env["EASYAGENT_CONTAINER_MODE"] = "1"
            execution_mode = "docker"
        else:
            use_host_sandbox = self.host_execution_mode != "direct"
            if use_host_sandbox:
                if not host_sandbox_enabled():
                    return {"success": False, "error": "宿主机子智能体执行被拒绝：HOST_SANDBOX_ENABLED=0"}
                try:
                    command_str = self._join_shell_words(cmd)
                    plan = build_host_sandbox_plan(command_str, self.project_path, env)
                    launch_cmd, pass_fds, seccomp_fd = self._materialize_seccomp_fd(
                        plan.command,
                        plan.seccomp_bpf_path,
                    )
                    launch_cwd = plan.cwd or str(self.project_path)
                    env = plan.env
                    execution_mode = "host_sandbox"
                except HostSandboxError as exc:
                    return {"success": False, "error": f"宿主机沙箱不可用，拒绝启动子智能体：{exc}"}
                except Exception as exc:
                    return {"success": False, "error": f"构建宿主机沙箱启动命令失败：{exc}"}

        try:
            process = subprocess.Popen(
                launch_cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                cwd=launch_cwd,
                env=env,
                pass_fds=pass_fds,
            )
        except Exception as exc:
            return {"success": False, "error": f"启动子智能体失败: {exc}"}
        finally:
            if seccomp_fd is not None:
                try:
                    os.close(seccomp_fd)
                except OSError:
                    pass

        # 记录任务
        task_record = {
            "task_id": task_id,
            "agent_id": agent_id,
            "summary": summary,
            "task": task,
            "status": "running",
            "deliverables_dir": str(deliverables_path),
            "subagent_dir": str(subagent_dir),
            "timeout_seconds": timeout_seconds,
            "thinking_mode": thinking_mode,
            "created_at": time.time(),
            "conversation_id": conversation_id,
            "run_in_background": run_in_background,
            "task_root": str(task_root),
            "output_file": str(output_file),
            "stats_file": str(stats_file),
            "progress_file": str(progress_file),
            "pid": process.pid,
            "execution_mode": execution_mode,
            "container_name": container_name,
        }
        self.tasks[task_id] = task_record
        self.processes[task_id] = process
        self._mark_agent_id_used(conversation_id, agent_id)
        self._save_state()

        message = f"子智能体{agent_id} 已创建，任务ID: {task_id}，PID: {process.pid}"
        print(f"{OUTPUT_FORMATS['info']} {message}")

        return {
            "success": True,
            "task_id": task_id,
            "agent_id": agent_id,
            "status": "running",
            "message": message,
            "deliverables_dir": str(deliverables_path),
            "run_in_background": run_in_background,
        }

    def wait_for_completion(
        self,
        *,
        task_id: Optional[str] = None,
        agent_id: Optional[int] = None,
        timeout_seconds: Optional[int] = None,
    ) -> Dict:
        """阻塞等待子智能体完成或超时。"""
        task = self._select_task(task_id, agent_id)
        if not task:
            return {"success": False, "error": "未找到对应的子智能体任务"}

        if task.get("status") in TERMINAL_STATUSES or task.get("status") == "terminated":
            if task.get("final_result"):
                return task["final_result"]
            return {"success": False, "status": task.get("status"), "message": "子智能体已结束。"}

        timeout_seconds = timeout_seconds or task.get("timeout_seconds") or SUB_AGENT_DEFAULT_TIMEOUT
        deadline = time.time() + timeout_seconds

        while time.time() < deadline:
            # 检查进程状态
            status_result = self._check_task_status(task)
            if status_result["status"] in TERMINAL_STATUSES:
                return status_result

            time.sleep(SUB_AGENT_STATUS_POLL_INTERVAL)

        # 超时
        return self._handle_timeout(task)

    def terminate_sub_agent(
        self,
        *,
        task_id: Optional[str] = None,
        agent_id: Optional[int] = None,
    ) -> Dict:
        """强制关闭指定子智能体。"""
        task = self._select_task(task_id, agent_id)
        if not task:
            return {"success": False, "error": "未找到对应的子智能体任务"}

        task_id = task["task_id"]
        process = self.processes.get(task_id)
        pid = task.get("pid")

        if process and process.poll() is None:
            # 进程还在运行，终止它
            try:
                process.terminate()
                try:
                    process.wait(timeout=5)
                except subprocess.TimeoutExpired:
                    process.kill()
                    process.wait()
            except Exception as exc:
                return {"success": False, "error": f"终止进程失败: {exc}"}
        elif self._is_pid_alive(pid):
            if not self._terminate_pid(pid):
                return {"success": False, "error": f"终止进程失败: PID {pid} 无法停止"}

        self.processes.pop(task_id, None)
        self._mark_task_terminated(
            task,
            message="子智能体已被强制关闭。",
            system_message=f"🛑 子智能体{task.get('agent_id')} 已被手动关闭。",
            notified=True,
        )
        self._save_state()

        return {
            "success": True,
            "task_id": task_id,
            "message": "子智能体已被强制关闭。",
            "system_message": f"🛑 子智能体{task.get('agent_id')} 已被手动关闭。",
        }

    def set_container_session(self, session: Optional["ContainerHandle"]):
        """更新容器会话信息。"""
        self.container_session = session

    def set_host_execution_mode(self, mode: str) -> None:
        normalized = str(mode or "").strip().lower()
        self.host_execution_mode = "direct" if normalized == "direct" else "sandbox"

    # ------------------------------------------------------------------
    # 内部工具方法
    # ------------------------------------------------------------------
    def _should_use_container(self) -> bool:
        return bool(self.container_session and getattr(self.container_session, "mode", None) == "docker")

    def _get_runtime_path(self, host_path: Path) -> str:
        """将宿主机路径映射为容器内路径（仅用于提示展示）。"""
        if not self._should_use_container():
            return str(host_path)
        mount_path = (getattr(self.container_session, "mount_path", None) or "/workspace").rstrip("/") or "/workspace"
        try:
            relative = host_path.resolve().relative_to(self.project_path)
        except Exception:
            return mount_path
        if str(relative) in {"", "."}:
            return mount_path
        return str(PurePosixPath(mount_path) / PurePosixPath(relative.as_posix()))

    def _build_sub_agent_command(
        self,
        *,
        batch_path: str,
        workspace_path: str,
        task_file: str,
        system_prompt_file: str,
        output_file: str,
        stats_file: str,
        progress_file: str,
        agent_id: int,
        timeout_seconds: int,
        model_key: Optional[str],
        thinking_mode: Optional[str],
    ) -> List[str]:
        cmd = [
            "node",
            batch_path,
            "--workspace", workspace_path,
            "--task-file", task_file,
            "--system-prompt-file", system_prompt_file,
            "--output-file", output_file,
            "--stats-file", stats_file,
            "--progress-file", progress_file,
            "--agent-id", str(agent_id),
            "--timeout", str(timeout_seconds),
        ]
        if model_key:
            cmd.extend(["--model-key", model_key])
        if thinking_mode:
            cmd.extend(["--thinking-mode", thinking_mode])
        return cmd

    @staticmethod
    def _join_shell_words(parts: List[str]) -> str:
        if platform.system() == "Windows":
            return subprocess.list2cmdline(parts)
        return " ".join(shlex.quote(p) for p in parts)

    @staticmethod
    def _materialize_seccomp_fd(
        plan_command: List[str], seccomp_path: Optional[str]
    ) -> Tuple[List[str], tuple[int, ...], Optional[int]]:
        if not seccomp_path:
            return plan_command, tuple(), None
        seccomp_fd = os.open(seccomp_path, os.O_RDONLY)
        fd_num = seccomp_fd
        cmd = [str(fd_num) if token == "__SECCOMP_FD__" else token for token in plan_command]
        return cmd, (fd_num,), seccomp_fd
    def _check_task_status(self, task: Dict) -> Dict:
        """检查任务状态，如果完成则解析输出。"""
        task_id = task["task_id"]
        process = self.processes.get(task_id)

        # 检查进程是否结束
        if process:
            returncode = process.poll()
            if returncode is None:
                # 进程还在运行
                return {"status": "running", "task_id": task_id}

        # 进程已结束，读取输出文件
        output_file = Path(task.get("output_file", ""))
        if not output_file.exists():
            # 输出文件不存在，可能是异常退出
            task["status"] = "failed"
            task["updated_at"] = time.time()
            result = {
                "success": False,
                "status": "failed",
                "task_id": task_id,
                "agent_id": task.get("agent_id"),
                "message": "子智能体异常退出，未生成输出文件。",
                "system_message": f"❌ 子智能体{task.get('agent_id')} 异常退出。",
            }
            task["final_result"] = result
            return result

        # 解析输出
        try:
            output = json.loads(output_file.read_text(encoding="utf-8"))
        except Exception as exc:
            task["status"] = "failed"
            task["updated_at"] = time.time()
            result = {
                "success": False,
                "status": "failed",
                "task_id": task_id,
                "agent_id": task.get("agent_id"),
                "message": f"输出文件解析失败: {exc}",
                "system_message": f"❌ 子智能体{task.get('agent_id')} 输出解析失败。",
            }
            task["final_result"] = result
            return result

        # 根据输出更新任务状态
        success = output.get("success", False)
        summary = output.get("summary", "")
        stats = output.get("stats", {})
        stats_summary = self._build_stats_summary(stats)

        if output.get("timeout"):
            status = "timeout"
        elif output.get("max_turns_exceeded"):
            status = "failed"
            summary = f"任务执行超过最大轮次限制。{summary}"
        elif success:
            status = "completed"
        else:
            status = "failed"

        task["status"] = status
        task["updated_at"] = time.time()
        elapsed_seconds = self._compute_elapsed_seconds(task)
        if status == "completed" and elapsed_seconds is not None:
            task["elapsed_seconds"] = elapsed_seconds
            task["runtime_seconds"] = elapsed_seconds

        # 构建系统消息
        agent_id = task.get("agent_id")
        task_summary = task.get("summary")
        deliverables_dir = task.get("deliverables_dir")

        if status == "completed":
            system_message = self._compose_sub_agent_message(
                prefix=f"✅ 子智能体{agent_id} 任务摘要：{task_summary} 已完成。",
                stats_summary=stats_summary,
                summary=summary,
                deliverables_dir=deliverables_dir,
                duration_seconds=elapsed_seconds,
            )
        elif status == "timeout":
            system_message = self._compose_sub_agent_message(
                prefix=f"⏱️ 子智能体{agent_id} 任务摘要：{task_summary} 超时未完成。",
                stats_summary=stats_summary,
                summary=summary,
            )
        else:
            system_message = self._compose_sub_agent_message(
                prefix=f"❌ 子智能体{agent_id} 任务摘要：{task_summary} 执行失败。",
                stats_summary=stats_summary,
                summary=summary,
            )

        result = {
            "success": success,
            "status": status,
            "task_id": task_id,
            "agent_id": agent_id,
            "message": summary,
            "deliverables_dir": deliverables_dir,
            "stats": stats,
            "stats_summary": stats_summary,
            "system_message": system_message,
        }
        if status == "completed" and elapsed_seconds is not None:
            result["elapsed_seconds"] = elapsed_seconds
            result["runtime_seconds"] = elapsed_seconds
        task["final_result"] = result
        return result

    def _handle_timeout(self, task: Dict) -> Dict:
        """处理任务超时。"""
        task_id = task["task_id"]
        process = self.processes.get(task_id)

        # 终止进程
        if process and process.poll() is None:
            try:
                process.terminate()
                try:
                    process.wait(timeout=5)
                except subprocess.TimeoutExpired:
                    process.kill()
                    process.wait()
            except Exception:
                pass

        task["status"] = "timeout"
        task["updated_at"] = time.time()

        stats = {}
        stats_file = Path(task.get("stats_file", ""))
        if stats_file.exists():
            try:
                stats = json.loads(stats_file.read_text(encoding="utf-8"))
            except Exception:
                stats = {}
        stats_summary = self._build_stats_summary(stats)
        system_message = self._compose_sub_agent_message(
            prefix=f"⏱️ 子智能体{task.get('agent_id')} 任务摘要：{task.get('summary')} 超时未完成。",
            stats_summary=stats_summary,
            summary="等待超时，子智能体已被终止。",
        )

        result = {
            "success": False,
            "status": "timeout",
            "task_id": task_id,
            "agent_id": task.get("agent_id"),
            "message": "等待超时，子智能体已被终止。",
            "stats": stats,
            "stats_summary": stats_summary,
            "system_message": system_message,
        }
        task["final_result"] = result
        self._save_state()
        return result

    def _build_user_message(
        self,
        agent_id: int,
        summary: str,
        task: str,
        deliverables_path: str,
        timeout_seconds: Optional[int],
    ) -> str:
        """构建发送给子智能体的用户消息。"""
        timeout_seconds = timeout_seconds or SUB_AGENT_DEFAULT_TIMEOUT
        return f"""你是子智能体 #{agent_id}，负责完成以下任务：

**任务摘要**：{summary}

**任务详情**：
{task}

**交付目录**：{deliverables_path}
请将所有生成的文件保存到此目录。对话历史会自动保存到 {deliverables_path}/.subagent/ 目录。

**超时时间**：{timeout_seconds} 秒

完成任务后，请调用 finish_task 工具提交完成报告。"""

    def _build_system_prompt(self, workspace_path: str) -> str:
        """构建子智能体的系统提示。"""
        import platform
        from datetime import datetime

        system_info = f"{platform.system()} {platform.release()}"
        current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        return f"""你是一个专注的子智能体，负责独立完成分配的任务。

# 身份定位

你是主智能体创建的子智能体，拥有完整的工具能力（读写文件、执行命令、搜索网页等）。你的职责是专注完成分配的单一任务，不要偏离任务目标。

# 工作流程

1. **理解任务**：仔细阅读任务描述，明确目标和要求
2. **制定计划**：规划完成任务的步骤
3. **执行任务**：使用工具完成各个步骤
4. **生成交付**：将所有结果文件放到指定的交付目录
5. **提交报告**：使用 finish_task 工具提交完成报告并退出

# 工作原则

## 专注性
- 只完成分配的任务，不要做额外的工作
- 不要尝试与用户对话或询问问题
- 遇到问题时，在能力范围内解决或在报告中说明

## 独立性
- 你与主智能体共享工作区，可以访问所有文件
- 你的工作范围应该与其他子智能体不重叠
- 不要修改任务描述之外的文件

## 效率性
- 直接开始工作，不要过度解释
- 合理使用工具，避免重复操作
- 注意超时限制，在时间内完成核心工作

## 完整性
- 确保交付目录中的文件完整可用
- 生成的文档要清晰、格式正确
- 代码要包含必要的注释和说明

# 交付要求

所有结果文件必须放在指定的交付目录中，包括：
- 主要成果文件（文档、代码、报告等）
- 支持文件（数据、配置、示例等）
- 不要在交付目录外创建文件

# 完成任务

任务完成后，必须调用 finish_task 工具：
- success: 是否成功完成
- summary: 完成摘要（说明做了什么、生成了什么）

调用 finish_task 后，你会立即退出，无法继续工作。

# 工具使用

你拥有以下工具能力：
- read_file: 读取文件内容
- write_file / edit_file: 创建或修改文件
- search_workspace: 搜索文件和代码
- run_command: 执行终端命令
- web_search / extract_webpage: 搜索和提取网页内容
- finish_task: 完成任务并退出（必须调用）

# 注意事项

1. **结果传达**：你在运行期间产生的记录与输出不会被直接传递给主智能体。务必把所有需要传达的信息写进 `finish_task` 工具的 `summary` 字段，以及交付目录中的落盘文件里。
1. **不要无限循环**：如果任务无法完成，说明原因并提交报告
2. **不要超出范围**：只操作任务描述中指定的文件/目录
3. **不要等待输入**：你是自主运行的，不会收到用户的进一步指令
4. **注意时间限制**：超时会被强制终止，优先完成核心工作

# 当前环境

- 工作区路径: {workspace_path}
- 系统: {system_info}
- 当前时间: {current_time}

现在开始执行任务。"""

    def _resolve_deliverables_dir(self, relative_dir: str) -> Path:
        """解析交付目录（相对于project_path）。"""
        relative_dir = relative_dir.strip() if relative_dir else ""
        if not relative_dir:
            raise ValueError("交付目录不能为空，必须指定")

        deliverables_path = (self.project_path / relative_dir).resolve()
        if not str(deliverables_path).startswith(str(self.project_path)):
            raise ValueError("交付目录必须位于项目目录内")

        if deliverables_path.exists():
            raise ValueError("交付目录必须为不存在的新目录")
        deliverables_path.mkdir(parents=True, exist_ok=True)
        return deliverables_path
    def _load_state(self):
        if self.state_file.exists():
            try:
                data = json.loads(self.state_file.read_text(encoding="utf-8"))
                self.tasks = data.get("tasks", {})
                self.conversation_agents = data.get("conversation_agents", {})
            except json.JSONDecodeError:
                logger.warning("子智能体状态文件损坏，已忽略。")
                self.tasks = {}
                self.conversation_agents = {}
        else:
            self.tasks = {}
            self.conversation_agents = {}
        if self.tasks:
            migrated = False
            for task in self.tasks.values():
                if task.get("parent_conversation_id"):
                    continue
                candidate = task.get("conversation_id") or (task.get("service_payload") or {}).get("parent_conversation_id")
                if candidate:
                    task["parent_conversation_id"] = candidate
                    migrated = True
            if migrated:
                self._save_state()

    @staticmethod
    def _coerce_pid(value: Any) -> Optional[int]:
        try:
            pid = int(value)
            return pid if pid > 0 else None
        except (TypeError, ValueError):
            return None

    def _is_pid_alive(self, pid: Any) -> bool:
        normalized = self._coerce_pid(pid)
        if not normalized:
            return False
        try:
            os.kill(normalized, 0)
        except ProcessLookupError:
            return False
        except PermissionError:
            return True
        except Exception:
            return False
        return True

    def _terminate_pid(self, pid: Any) -> bool:
        normalized = self._coerce_pid(pid)
        if not normalized:
            return False
        try:
            os.killpg(normalized, signal.SIGTERM)
        except Exception:
            try:
                os.kill(normalized, signal.SIGTERM)
            except Exception:
                return False
        deadline = time.time() + 5
        while time.time() < deadline:
            if not self._is_pid_alive(normalized):
                return True
            time.sleep(0.1)
        try:
            os.killpg(normalized, signal.SIGKILL)
        except Exception:
            try:
                os.kill(normalized, signal.SIGKILL)
            except Exception:
                return not self._is_pid_alive(normalized)
        return not self._is_pid_alive(normalized)

    def _mark_task_terminated(
        self,
        task: Dict[str, Any],
        *,
        message: str,
        system_message: Optional[str] = None,
        notified: bool = False,
    ) -> Dict[str, Any]:
        task["status"] = "terminated"
        task["updated_at"] = time.time()
        if notified:
            task["notified"] = True
        result = {
            "success": False,
            "status": "terminated",
            "task_id": task.get("task_id"),
            "agent_id": task.get("agent_id"),
            "message": message,
            "system_message": system_message or message,
        }
        task["final_result"] = result
        return result

    def _should_force_cleanup_stale_task(self, task: Dict[str, Any]) -> bool:
        try:
            created_at = float(task.get("created_at") or 0)
        except (TypeError, ValueError):
            created_at = 0
        if created_at <= 0:
            return False
        timeout_seconds = int(task.get("timeout_seconds") or SUB_AGENT_DEFAULT_TIMEOUT or 0)
        timeout_seconds = max(timeout_seconds, 1)
        grace_seconds = 120
        elapsed = time.time() - created_at
        if elapsed <= (timeout_seconds + grace_seconds):
            return False
        output_file = Path(task.get("output_file", ""))
        if output_file.exists():
            return False
        progress_file = Path(task.get("progress_file", ""))
        if progress_file.exists():
            try:
                stale_span = time.time() - progress_file.stat().st_mtime
                if stale_span <= grace_seconds:
                    return False
            except Exception:
                pass
        return True

    def _refresh_task_runtime_state(self, task: Dict[str, Any]) -> Dict[str, Any]:
        """刷新单个任务运行态（用于进程句柄丢失/重启后的兜底修正）。"""
        status = task.get("status")
        if status in TERMINAL_STATUSES.union({"terminated"}):
            return {"status": status, "task_id": task.get("task_id")}

        task_id = task.get("task_id")
        process = self.processes.get(task_id) if task_id else None
        if process:
            poll_result = process.poll()
            if poll_result is None:
                return {"status": "running", "task_id": task_id}
            return self._check_task_status(task)

        output_file = Path(task.get("output_file", ""))
        if output_file.exists():
            return self._check_task_status(task)

        pid = task.get("pid")
        if self._is_pid_alive(pid):
            if self._should_force_cleanup_stale_task(task):
                return self._mark_task_terminated(
                    task,
                    message="子智能体疑似僵尸任务，已超时自动清理运行状态。",
                    system_message="⚠️ 子智能体长时间未结束，系统已自动清理运行状态。",
                    notified=True,
                )
            return {"status": "running", "task_id": task_id}

        return self._mark_task_terminated(
            task,
            message="检测到子智能体进程已退出，已自动清理运行状态。",
            system_message="⚠️ 子智能体进程异常退出，系统已自动清理运行状态。",
            notified=True,
        )

    def reconcile_task_states(self, conversation_id: Optional[str] = None) -> int:
        """修正运行态任务状态，返回修正条目数。"""
        changed = 0
        for task in self.tasks.values():
            if not isinstance(task, dict):
                continue
            if conversation_id and task.get("conversation_id") != conversation_id:
                continue
            before_status = task.get("status")
            before_notified = task.get("notified")
            self._refresh_task_runtime_state(task)
            if task.get("status") != before_status or task.get("notified") != before_notified:
                changed += 1
        if changed:
            self._save_state()
        return changed

    def _save_state(self):
        payload = {
            "tasks": self.tasks,
            "conversation_agents": self.conversation_agents
        }
        self.state_file.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")

    def _generate_task_id(self, agent_id: int) -> str:
        suffix = uuid.uuid4().hex[:6]
        return f"sub_{agent_id}_{int(time.time())}_{suffix}"

    def _active_task_count(self, conversation_id: Optional[str] = None) -> int:
        self.reconcile_task_states(conversation_id=conversation_id)
        active = [
            t for t in self.tasks.values()
            if t.get("status") in {"pending", "running"}
        ]
        if conversation_id:
            active = [
                t for t in active
                if t.get("conversation_id") == conversation_id
            ]
        return len(active)

    def _copy_reference_files(self, references: List[str], dest_dir: Path) -> Tuple[List[str], List[str]]:
        copied = []
        errors = []
        for rel_path in references:
            rel_path = rel_path.strip()
            if not rel_path:
                continue
            try:
                source = self._resolve_project_file(rel_path)
            except ValueError as exc:
                errors.append(str(exc))
                continue

            if not source.exists():
                errors.append(f"参考文件不存在: {rel_path}")
                continue

            target_path = dest_dir / rel_path
            target_path.parent.mkdir(parents=True, exist_ok=True)
            try:
                shutil.copy2(source, target_path)
                copied.append(rel_path)
            except Exception as exc:
                errors.append(f"复制 {rel_path} 失败: {exc}")
        return copied, errors

    def _ensure_project_subdir(self, relative_dir: str) -> Path:
        relative_dir = relative_dir.strip() if relative_dir else ""
        if not relative_dir:
            relative_dir = "sub_agent_results"
        target = (self.project_path / relative_dir).resolve()
        if not str(target).startswith(str(self.project_path)):
            raise ValueError("指定文件夹必须位于项目目录内")
        target.mkdir(parents=True, exist_ok=True)
        return target

    def _resolve_project_file(self, relative_path: str) -> Path:
        relative_path = relative_path.strip()
        candidate = (self.project_path / relative_path).resolve()
        if not str(candidate).startswith(str(self.project_path)):
            raise ValueError(f"非法的参考文件路径: {relative_path}")
        return candidate

    def _select_task(self, task_id: Optional[str], agent_id: Optional[int]) -> Optional[Dict]:
        self.reconcile_task_states()
        if task_id:
            return self.tasks.get(task_id)

        if agent_id is None:
            return None

        # 返回最新的匹配任务
        candidates = [
            task for task in self.tasks.values()
            if task.get("agent_id") == agent_id and task.get("status") in {"pending", "running"}
        ]
        if candidates:
            candidates.sort(key=lambda item: item.get("created_at", 0), reverse=True)
            return candidates[0]
        return None

    def lookup_task(self, *, task_id: Optional[str] = None, agent_id: Optional[int] = None) -> Optional[Dict]:
        """只读查询任务信息，供 wait_sub_agent 自动调整超时时间。"""
        task = self._select_task(task_id, agent_id)
        if not task:
            return None
        return {
            "task_id": task.get("task_id"),
            "agent_id": task.get("agent_id"),
            "status": task.get("status"),
            "timeout_seconds": task.get("timeout_seconds"),
            "conversation_id": task.get("conversation_id"),
        }

    def poll_updates(self) -> List[Dict]:
        """检查运行中的子智能体任务，返回新完成的结果。"""
        updates: List[Dict] = []
        self.reconcile_task_states()
        pending_tasks = [
            task for task in self.tasks.values()
            if task.get("status") not in TERMINAL_STATUSES.union({"terminated"})
        ]
        logger.debug(f"[SubAgentManager] 待检查任务: {len(pending_tasks)}")
        if not pending_tasks:
            return updates

        state_changed = False
        for task in pending_tasks:
            result = self._check_task_status(task)
            if result["status"] in TERMINAL_STATUSES:
                updates.append(result)
                state_changed = True

        if state_changed:
            self._save_state()
        return updates

    def get_sub_agent_status(
        self,
        *,
        agent_ids: Optional[List[int]] = None,
    ) -> Dict:
        """获取指定子智能体的详细状态。"""
        if not agent_ids:
            return {"success": False, "error": "必须指定至少一个agent_id"}

        results = []
        for agent_id in agent_ids:
            task = self._select_task(None, agent_id)
            if not task:
                results.append({
                    "agent_id": agent_id,
                    "found": False,
                    "error": "未找到对应的子智能体任务"
                })
                continue

            # 如果任务还在运行，检查最新状态
            if task.get("status") not in TERMINAL_STATUSES.union({"terminated"}):
                self._check_task_status(task)

            # 读取统计信息
            stats = {}
            stats_file = Path(task.get("stats_file", ""))
            if stats_file.exists():
                try:
                    stats = json.loads(stats_file.read_text(encoding="utf-8"))
                except Exception:
                    pass
            stats_summary = self._build_stats_summary(stats)

            results.append({
                "agent_id": agent_id,
                "found": True,
                "task_id": task["task_id"],
                "status": task["status"],
                "summary": task.get("summary"),
                "created_at": task.get("created_at"),
                "updated_at": task.get("updated_at"),
                "deliverables_dir": task.get("deliverables_dir"),
                "stats": stats,
                "stats_summary": stats_summary,
                "final_result": task.get("final_result"),
            })

        return {
            "success": True,
            "results": results,
        }

    def _call_service(self, method: str, path: str, payload: Optional[Dict] = None, timeout: Optional[int] = None) -> Dict:
        url = f"{SUB_AGENT_SERVICE_BASE_URL.rstrip('/')}{path}"
        try:
            with httpx.Client(timeout=timeout or 10) as client:
                if method.upper() == "POST":
                    response = client.post(url, json=payload or {})
                else:
                    response = client.get(url)
            response.raise_for_status()
            return response.json()
        except httpx.RequestError as exc:
            logger.error(f"子智能体服务请求失败: {exc}")
            return {"success": False, "error": f"无法连接子智能体服务: {exc}"}
        except httpx.HTTPStatusError as exc:
            logger.error(f"子智能体服务返回错误: {exc}")
            try:
                return exc.response.json()
            except Exception:
                return {"success": False, "error": f"服务端错误: {exc.response.text}"}
        except json.JSONDecodeError:
            return {"success": False, "error": "子智能体服务返回格式错误"}

    def _finalize_task(self, task: Dict, service_payload: Dict, status: str) -> Dict:
        existing_result = task.get("final_result")
        if existing_result and task.get("status") in TERMINAL_STATUSES.union({"terminated"}):
            return existing_result

        task["status"] = status
        task["updated_at"] = time.time()
        message = service_payload.get("message") or service_payload.get("error") or ""
        deliverables_dir = Path(service_payload.get("deliverables_dir") or task.get("deliverables_dir", ""))
        logger.debug(f"[SubAgentManager] finalize task={task['task_id']} status={status}")

        if status == "terminated":
            system_message = service_payload.get("system_message") or "🛑 子智能体已被手动关闭。"
            result = {
                "success": False,
                "task_id": task["task_id"],
                "agent_id": task["agent_id"],
                "status": "terminated",
                "message": message or "子智能体已被手动关闭。",
                "details": service_payload,
                "sub_conversation_id": task.get("sub_conversation_id"),
                "system_message": system_message,
            }
            task["final_result"] = result
            return result

        if status != "completed":
            result = {
                "success": False,
                "task_id": task["task_id"],
                "agent_id": task["agent_id"],
                "status": status,
                "message": message or f"子智能体状态：{status}",
                "details": service_payload,
                "sub_conversation_id": task.get("sub_conversation_id"),
                "system_message": self._build_system_message(task, status, None, message),
            }
            task["final_result"] = result
            return result

        if not deliverables_dir.exists():
            result = {
                "success": False,
                "task_id": task["task_id"],
                "agent_id": task["agent_id"],
                "status": "failed",
                "error": f"未找到交付目录: {deliverables_dir}",
                "system_message": self._build_system_message(task, "failed", None, f"未找到交付目录: {deliverables_dir}"),
            }
            task["status"] = "failed"
            task["final_result"] = result
            return result

        result_md = deliverables_dir / "result.md"
        if not result_md.exists():
            result = {
                "success": False,
                "task_id": task["task_id"],
                "agent_id": task["agent_id"],
                "status": "failed",
                "error": "交付目录缺少 result.md，无法完成任务。",
                "system_message": self._build_system_message(task, "failed", None, "交付目录缺少 result.md"),
            }
            task["status"] = "failed"
            task["final_result"] = result
            return result

        copied_path = self._copy_deliverables_to_project(task, deliverables_dir)
        task["copied_path"] = str(copied_path)

        elapsed_seconds = self._compute_elapsed_seconds(task)
        if elapsed_seconds is not None:
            task["elapsed_seconds"] = elapsed_seconds
            task["runtime_seconds"] = elapsed_seconds

        system_message = self._build_system_message(task, status, copied_path, message)
        result = {
            "success": True,
            "task_id": task["task_id"],
            "agent_id": task["agent_id"],
            "status": status,
            "message": message or "子智能体已完成任务。",
            "deliverables_path": str(deliverables_dir),
            "copied_path": str(copied_path),
             "sub_conversation_id": task.get("sub_conversation_id"),
            "system_message": system_message,
            "details": service_payload,
        }
        if elapsed_seconds is not None:
            result["elapsed_seconds"] = elapsed_seconds
            result["runtime_seconds"] = elapsed_seconds
        task["final_result"] = result
        return result

    def _copy_deliverables_to_project(self, task: Dict, source_dir: Path) -> Path:
        """将交付文件复制到项目目录下的指定文件夹。"""
        target_dir = Path(task["target_project_dir"])
        target_dir.mkdir(parents=True, exist_ok=True)
        dest_dir = target_dir / f"{task['task_id']}_deliverables"

        if dest_dir.exists():
            shutil.rmtree(dest_dir)
        shutil.copytree(source_dir, dest_dir)

        return dest_dir

    def _cleanup_task_folder(self, task_root: Path):
        if task_root.exists():
            shutil.rmtree(task_root, ignore_errors=True)

    def _ensure_agent_slot_available(self, conversation_id: str, agent_id: int) -> bool:
        used = self.conversation_agents.setdefault(conversation_id, [])
        return agent_id not in used

    def _mark_agent_id_used(self, conversation_id: str, agent_id: int):
        used = self.conversation_agents.setdefault(conversation_id, [])
        if agent_id not in used:
            used.append(agent_id)

    def _validate_create_params(self, agent_id: Optional[int], summary: str, task: str, target_dir: str) -> Optional[str]:
        if agent_id is None:
            return "子智能体代号不能为空"
        try:
            agent_id = int(agent_id)
        except ValueError:
            return "子智能体代号必须是整数"
        if agent_id <= 0:
            return "子智能体代号必须为正整数"
        if not summary or not summary.strip():
            return "任务摘要不能为空"
        if not task or not task.strip():
            return "任务详情不能为空"
        if target_dir is None:
            return "指定文件夹不能为空"
        return None

    def _build_system_message(
        self,
        task: Dict,
        status: str,
        copied_path: Optional[Path],
        extra_message: Optional[str] = None,
    ) -> str:
        prefix = f"子智能体{task['agent_id']} 任务摘要：{task['summary']}"
        extra = (extra_message or "").strip()

        if status == "completed" and copied_path:
            elapsed_seconds = self._compute_elapsed_seconds(task)
            msg = f"{prefix} 已完成，成果已复制到 {copied_path}。"
            if elapsed_seconds is not None:
                msg += f" 运行了{elapsed_seconds}秒。"
            if extra:
                msg += f" ({extra})"
            return msg

        if status == "timeout":
            return f"{prefix} 超时未完成。" + (f" {extra}" if extra else "")

        if status == "failed":
            return f"{prefix} 执行失败：" + (extra if extra else "请检查交付目录或任务状态。")

        return f"{prefix} 状态：{status}。" + (extra if extra else "")

    @staticmethod
    def _coerce_stat_int(value: Any) -> int:
        try:
            return max(0, int(value))
        except (TypeError, ValueError):
            return 0

    @staticmethod
    def _compute_elapsed_seconds(task: Dict) -> Optional[int]:
        try:
            created_at = float(task.get("created_at") or 0)
            updated_at = float(task.get("updated_at") or time.time())
        except (TypeError, ValueError):
            return None
        if created_at <= 0:
            return None
        elapsed = max(0.0, updated_at - created_at)
        return int(round(elapsed))

    def _build_stats_summary(self, stats: Optional[Dict[str, Any]]) -> str:
        if not isinstance(stats, dict):
            stats = {}
        api_calls = self._coerce_stat_int(
            stats.get("api_calls")
            or stats.get("api_call_count")
            or stats.get("turn_count")
        )
        files_read = self._coerce_stat_int(stats.get("files_read"))
        edit_files = self._coerce_stat_int(stats.get("edit_files"))
        searches = self._coerce_stat_int(stats.get("searches"))
        web_pages = self._coerce_stat_int(stats.get("web_pages"))
        commands = self._coerce_stat_int(stats.get("commands"))
        lines = [
            f"调用了{api_calls}次",
            f"阅读了{files_read}次文件",
            f"编辑了{edit_files}次文件",
            f"搜索了{searches}次内容",
            f"查看了{web_pages}个网页",
            f"运行了{commands}个指令",
        ]
        return "\n".join(lines)

    def _compose_sub_agent_message(
        self,
        *,
        prefix: str,
        stats_summary: str,
        summary: str,
        deliverables_dir: Optional[str] = None,
        duration_seconds: Optional[int] = None,
    ) -> str:
        parts = [prefix]
        if stats_summary:
            parts.append(stats_summary)
        if duration_seconds is not None:
            parts.append(f"运行了{duration_seconds}秒")
        if summary:
            parts.append(summary)
        if deliverables_dir:
            parts.append(f"交付目录：{deliverables_dir}")
        return "\n\n".join(parts)

    def get_overview(self, conversation_id: Optional[str] = None) -> List[Dict[str, Any]]:
        """返回子智能体任务概览，用于前端展示。"""
        self.reconcile_task_states(conversation_id=conversation_id)
        overview: List[Dict[str, Any]] = []
        for task_id, task in self.tasks.items():
            if conversation_id and task.get("conversation_id") != conversation_id:
                continue

            snapshot = {
                "task_id": task_id,
                "agent_id": task.get("agent_id"),
                "summary": task.get("summary"),
                "status": task.get("status"),
                "created_at": task.get("created_at"),
                "updated_at": task.get("updated_at"),
                "target_dir": task.get("target_project_dir"),
                "last_tool": task.get("last_tool"),
                "deliverables_dir": task.get("deliverables_dir"),
                "copied_path": task.get("copied_path"),
                "conversation_id": task.get("conversation_id"),
                "sub_conversation_id": task.get("sub_conversation_id"),
            }

            if snapshot["status"] in TERMINAL_STATUSES or snapshot["status"] == "terminated":
                # 已结束的任务带上最终结果/系统消息，方便前端展示
                final_result = task.get("final_result") or {}
                snapshot["final_message"] = final_result.get("system_message") or final_result.get("message")
                snapshot["success"] = final_result.get("success")

            overview.append(snapshot)

        overview.sort(key=lambda item: item.get("created_at") or 0, reverse=True)
        return overview
