"""
departments/manager.py - 部门管理器
====================================
管理部门体系（无限层级），包含:
  - 部门 CRUD（支持无限层级嵌套）
  - 部门介绍信息（dept.md）
  - 部门知识库（knowledge/）
  - 部门成员管理（关联 Agent）
  - 部门负责人管理
  - 自动关联群聊（通过 GroupManager）

目录结构:
  ~/.myagent/data/departments/
    ├── tech/                     # 技术部
    │   ├── dept.md               # 部门介绍
    │   ├── _meta.json            # 元数据
    │   ├── knowledge/            # 知识库
    │   │   └── knowledge_index.json
    │   ├── frontend/             # 子部门: 前端组
    │   │   ├── dept.md
    │   │   ├── _meta.json
    │   │   └── knowledge/
    │   └── backend/              # 子部门: 后端组
    │       ├── dept.md
    │       ├── _meta.json
    │       └── knowledge/
    └── marketing/                # 市场部
        ├── dept.md
        ├── _meta.json
        └── knowledge/
"""
from __future__ import annotations

import json
import shutil
from pathlib import Path
from typing import Any, Dict, List, Optional

from core.logger import get_logger
from core.utils import timestamp

logger = get_logger("myagent.departments")

# 支持上传的文件扩展名
SUPPORTED_EXTENSIONS = {".md", ".txt", ".json", ".csv", ".py", ".js", ".html"}

# 部门介绍模板
DEPT_INFO_TEMPLATE = """# {name}

## 部门简介
<!-- 在此填写部门简介 -->

## 核心职责
<!-- 在此填写核心职责 -->

## 团队成员
<!-- 在此填写团队成员信息 -->

## 注意事项
<!-- 在此填写其他注意事项 -->
"""


class DepartmentManager:
    """
    部门管理器。

    使用示例:
        dm = DepartmentManager(
            data_dir="~/.myagent/data",
            group_manager=group_mgr,
        )
        dm.initialize()

        # 创建部门
        dm.create_dept(name="技术部", emoji="💻", description="负责技术研发")

        # 创建子部门
        dm.create_dept(parent="tech", name="前端组", emoji="🎨")

        # 获取部门树
        tree = dm.get_dept_tree()

        # 分配 Agent
        dm.assign_agent("tech", agents=["coder"], action="add")
    """

    def __init__(
        self,
        data_dir: str | Path = "",
        group_manager: Any = None,
        agent_storage: Any = None,
    ):
        self.data_dir = Path(data_dir) if data_dir else Path()
        self._group_manager = group_manager
        self._agent_storage = agent_storage
        self._root_dir: Optional[Path] = None

    def initialize(self):
        """初始化部门根目录"""
        self._root_dir = self.data_dir / "departments"
        self._root_dir.mkdir(parents=True, exist_ok=True)
        logger.info(f"部门管理器已初始化 (dir={self._root_dir})")

    # ==========================================================================
    # 路径与安全
    # ==========================================================================

    def _validate_path(self, path: str) -> bool:
        """
        校验部门路径安全性。

        Args:
            path: 部门路径（如 "tech", "tech/frontend"）

        Returns:
            是否合法
        """
        if not path:
            return True  # 空路径表示根目录
        if ".." in path:
            return False
        if path.startswith("/"):
            return False
        # 不允许路径段为空（连续的 //）
        if "//" in path:
            return False
        # 路径段只允许字母、数字、下划线、连字符和中文字符
        for seg in path.split("/"):
            if not seg:
                return False
        return True

    def _dept_dir(self, path: str) -> Path:
        """根据部门路径返回目录"""
        if not path:
            return self._root_dir
        return self._root_dir / path

    def _meta_path(self, path: str) -> Path:
        """根据部门路径返回 _meta.json 路径"""
        return self._dept_dir(path) / "_meta.json"

    def _dept_info_path(self, path: str) -> Path:
        """根据部门路径返回 dept.md 路径"""
        return self._dept_dir(path) / "dept.md"

    def _knowledge_dir(self, path: str) -> Path:
        """根据部门路径返回知识库目录"""
        return self._dept_dir(path) / "knowledge"

    def _knowledge_index_path(self, path: str) -> Path:
        """根据部门路径返回知识库索引路径"""
        return self._knowledge_dir(path) / "knowledge_index.json"

    # ==========================================================================
    # 元数据读写
    # ==========================================================================

    def _load_meta(self, path: str) -> Optional[Dict[str, Any]]:
        """加载部门元数据"""
        meta_file = self._meta_path(path)
        if not meta_file.exists():
            return None
        try:
            return json.loads(meta_file.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, IOError) as e:
            logger.warning(f"加载部门元数据失败 ({path}): {e}")
            return None

    def _save_meta(self, path: str, meta: Dict[str, Any]):
        """保存部门元数据"""
        meta_file = self._meta_path(path)
        try:
            meta_file.write_text(
                json.dumps(meta, ensure_ascii=False, indent=2),
                encoding="utf-8",
            )
        except Exception as e:
            logger.error(f"保存部门元数据失败 ({path}): {e}")

    # ==========================================================================
    # 部门 CRUD
    # ==========================================================================

    def create_dept(
        self,
        name: str,
        emoji: str = "",
        description: str = "",
        parent: str = "",
    ) -> Dict[str, Any]:
        """
        创建部门。

        Args:
            name: 部门名称（同时作为目录名）
            emoji: 部门 emoji 标识
            description: 部门描述
            parent: 父部门路径（空字符串表示顶级部门）

        Returns:
            {ok, path, message, meta}
        """
        if not name:
            return {"ok": False, "message": "部门名称不能为空"}

        # 清理部门名称：移除空格和特殊字符，保留中英文、数字、下划线、连字符
        import re
        clean_name = re.sub(r'[^\w\u4e00-\u9fff-]', '', name)
        if not clean_name:
            return {"ok": False, "message": "部门名称包含无效字符"}

        # 安全校验
        if not self._validate_path(parent):
            return {"ok": False, "message": "非法的父部门路径"}
        if not self._validate_path(clean_name):
            return {"ok": False, "message": "非法的部门名称"}
        if "/" in clean_name or "\\" in clean_name:
            return {"ok": False, "message": "部门名称不能包含 / 或 \\"}

        # 构建完整路径（使用清理后的名称作为目录名）
        dept_path = f"{parent}/{clean_name}" if parent else clean_name

        # 检查是否已存在
        dept_dir = self._dept_dir(dept_path)
        if dept_dir.exists():
            return {"ok": False, "message": f"部门已存在: {dept_path}"}

        # 检查父部门是否存在
        if parent:
            parent_meta = self._load_meta(parent)
            if not parent_meta:
                return {"ok": False, "message": f"父部门不存在: {parent}"}

        # 创建目录结构
        try:
            dept_dir.mkdir(parents=True, exist_ok=True)
            knowledge_dir = self._knowledge_dir(dept_path)
            knowledge_dir.mkdir(parents=True, exist_ok=True)

            # 创建部门介绍
            info_path = self._dept_info_path(dept_path)
            info_content = DEPT_INFO_TEMPLATE.format(name=name)
            info_path.write_text(info_content, encoding="utf-8")

            # 创建知识库索引
            index_path = self._knowledge_index_path(dept_path)
            index_path.write_text(
                json.dumps({}, ensure_ascii=False, indent=2),
                encoding="utf-8",
            )

            # 自动创建群聊（部门群不带默认 owner，成员由 assign_agent 添加）
            chat_group_id = ""
            if self._group_manager:
                try:
                    group_name = f"部门: {name}"
                    group = self._group_manager.create_group(
                        name=group_name,
                        owner="",  # 部门群无默认 owner
                        description=description or f"{name} 部门群聊",
                        avatar_emoji=emoji or "🏢",
                    )
                    chat_group_id = group.id
                    logger.info(f"已为部门 {dept_path} 创建群聊: {group.id}")
                except Exception as e:
                    logger.warning(f"为部门 {dept_path} 创建群聊失败: {e}")

            # 创建元数据
            now = timestamp()
            meta = {
                "name": name,
                "emoji": emoji or "",
                "description": description,
                "head": "",
                "agents": [],
                "chat_group_id": chat_group_id,
                "created_at": now,
                "updated_at": now,
            }
            self._save_meta(dept_path, meta)

            logger.info(f"部门已创建: {dept_path} (parent={parent or '无'})")
            return {"ok": True, "path": dept_path, "message": "创建成功", "meta": meta}
        except Exception as e:
            # 回滚：删除已创建的目录
            if dept_dir.exists():
                shutil.rmtree(dept_dir, ignore_errors=True)
            logger.error(f"创建部门失败 ({dept_path}): {e}")
            return {"ok": False, "message": f"创建失败: {e}"}

    def get_dept(self, path: str) -> Optional[Dict[str, Any]]:
        """
        获取部门详情。

        Args:
            path: 部门路径

        Returns:
            部门信息字典，不存在返回 None
        """
        if not self._validate_path(path):
            return None

        meta = self._load_meta(path)
        if not meta:
            return None

        result = {
            "path": path,
            **meta,
        }

        # 确保 agent_count 字段存在（meta 中不一定有，需要动态计算）
        if "agent_count" not in result:
            result["agent_count"] = len(meta.get("agents", []))

        # 列出子部门
        sub_depts = self.list_sub_depts(path)
        result["children"] = sub_depts

        return result

    def update_dept(
        self,
        path: str,
        name: str = "",
        emoji: str = None,
        description: str = None,
    ) -> Dict[str, Any]:
        """
        更新部门元数据。

        Args:
            path: 部门路径
            name: 新名称（空=不修改）
            emoji: 新 emoji（None=不修改）
            description: 新描述（None=不修改）

        Returns:
            {ok, meta, message}
        """
        if not self._validate_path(path):
            return {"ok": False, "message": "非法路径"}

        meta = self._load_meta(path)
        if not meta:
            return {"ok": False, "message": f"部门不存在: {path}"}

        changed = False
        if name and name != meta.get("name"):
            # 检查名称合法性
            if "/" in name or "\\" in name:
                return {"ok": False, "message": "部门名称不能包含 / 或 \\"}
            meta["name"] = name
            changed = True

        if emoji is not None and emoji != meta.get("emoji"):
            meta["emoji"] = emoji
            changed = True

        if description is not None and description != meta.get("description"):
            meta["description"] = description
            changed = True

        if changed:
            meta["updated_at"] = timestamp()
            self._save_meta(path, meta)
            logger.info(f"部门已更新: {path}")

        return {"ok": True, "meta": meta, "message": "更新成功"}

    def delete_dept(self, path: str) -> Dict[str, Any]:
        """
        删除部门（递归删除所有子部门）。

        Args:
            path: 部门路径

        Returns:
            {ok, message, deleted_count}
        """
        if not self._validate_path(path):
            return {"ok": False, "message": "非法路径"}

        dept_dir = self._dept_dir(path)
        if not dept_dir.exists():
            return {"ok": False, "message": f"部门不存在: {path}"}

        meta = self._load_meta(path)
        if not meta:
            return {"ok": False, "message": f"部门元数据不存在: {path}"}

        # 递归收集所有子部门的 chat_group_id
        all_group_ids: List[str] = []
        self._collect_group_ids(path, all_group_ids)

        try:
            # 递归删除目录
            deleted_count = self._count_files(dept_dir)
            shutil.rmtree(dept_dir)

            # 自动解散所有关联的群聊
            if self._group_manager:
                for gid in all_group_ids:
                    if gid:
                        try:
                            self._group_manager.delete_group(gid)
                            logger.info(f"已解散部门群聊: {gid}")
                        except Exception as e:
                            logger.warning(f"解散群聊失败 ({gid}): {e}")

            logger.info(f"部门已删除: {path} ({deleted_count} 个文件)")
            return {
                "ok": True,
                "message": f"已删除 {deleted_count} 个文件",
                "deleted_count": deleted_count,
            }
        except Exception as e:
            logger.error(f"删除部门失败 ({path}): {e}")
            return {"ok": False, "message": f"删除失败: {e}"}

    def _collect_group_ids(self, path: str, group_ids: List[str]):
        """递归收集所有子部门的 chat_group_id"""
        meta = self._load_meta(path)
        if meta and meta.get("chat_group_id"):
            group_ids.append(meta["chat_group_id"])

        sub_depts = self.list_sub_depts(path)
        for sub in sub_depts:
            self._collect_group_ids(sub["path"], group_ids)

    def _count_files(self, dir_path: Path) -> int:
        """统计目录中的文件数量"""
        count = 0
        for f in dir_path.rglob("*"):
            if f.is_file():
                count += 1
        return count

    # ==========================================================================
    # 部门树
    # ==========================================================================

    def list_sub_depts(self, path: str) -> List[Dict[str, Any]]:
        """
        列出直接子部门。

        Args:
            path: 父部门路径（空字符串表示根级）

        Returns:
            子部门列表 [{path, name, emoji, description, head, agent_count, ...}]
        """
        if not self._validate_path(path):
            return []

        parent_dir = self._dept_dir(path)
        if not parent_dir.exists():
            return []

        children = []
        for d in sorted(parent_dir.iterdir()):
            if not d.is_dir():
                continue
            meta_file = d / "_meta.json"
            if not meta_file.exists():
                continue

            try:
                meta = json.loads(meta_file.read_text(encoding="utf-8"))
            except (json.JSONDecodeError, IOError):
                continue

            agents_list = meta.get("agents", [])
            child_path = f"{path}/{d.name}" if path else d.name
            children.append({
                "path": child_path,
                "name": meta.get("name", d.name),
                "emoji": meta.get("emoji", ""),
                "description": meta.get("description", ""),
                "head": meta.get("head", ""),
                "agents": agents_list,
                "agent_count": len(agents_list),
                "chat_group_id": meta.get("chat_group_id", ""),
                "created_at": meta.get("created_at", ""),
                "updated_at": meta.get("updated_at", ""),
            })

        return children

    def get_dept_tree(self) -> List[Dict[str, Any]]:
        """
        获取完整的部门树（递归）。

        Returns:
            部门树列表
        """
        flat = self._scan_flat("")
        return self._build_tree(flat)

    def _scan_flat(self, parent: str) -> List[Dict[str, Any]]:
        """递归扫描所有部门，返回扁平列表"""
        result = []
        for sub in self.list_sub_depts(parent):
            item = {**sub, "children": []}
            result.append(item)
            # 递归子部门
            children = self._scan_flat(sub["path"])
            result.extend(children)
        return result

    def _build_tree(self, flat: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        """将扁平列表构建为树结构"""
        by_path = {item["path"]: item for item in flat}
        roots = []
        for item in flat:
            path = item["path"]
            parent_path = "/".join(path.split("/")[:-1]) if "/" in path else ""
            node = by_path[path]
            if parent_path and parent_path in by_path:
                by_path[parent_path]["children"].append(node)
            else:
                roots.append(node)
        return roots

    # ==========================================================================
    # 成员管理
    # ==========================================================================

    def assign_agent(
        self,
        path: str,
        agents: List[str],
        action: str = "add",
    ) -> Dict[str, Any]:
        """
        管理部门成员（Agent）。

        Args:
            path: 部门路径
            agents: Agent 路径列表
            action: 操作类型 ("add" | "remove" | "set")

        Returns:
            {ok, meta, message}
        """
        if not self._validate_path(path):
            return {"ok": False, "message": "非法路径"}
        if action not in ("add", "remove", "set"):
            return {"ok": False, "message": f"不支持的操作: {action}"}

        meta = self._load_meta(path)
        if not meta:
            return {"ok": False, "message": f"部门不存在: {path}"}

        current_agents: List[str] = meta.get("agents", [])
        chat_group_id = meta.get("chat_group_id", "")

        if action == "add":
            for agent in agents:
                if agent not in current_agents:
                    current_agents.append(agent)
                    # 自动添加到部门群聊
                    if chat_group_id and self._group_manager:
                        try:
                            self._group_manager.add_member(
                                chat_group_id, agent
                            )
                        except Exception as e:
                            logger.warning(
                                f"添加 {agent} 到群聊失败 ({chat_group_id}): {e}"
                            )
                        # 如果部门群没有 owner，将第一个 agent 设为 owner
                        if not meta.get("head") and self._group_manager:
                            try:
                                group = self._group_manager.get_group(chat_group_id)
                                if group and not group.owner:
                                    group.owner = agent
                                    self._group_manager.add_member(
                                        chat_group_id, agent, role="owner"
                                    )
                                    meta["head"] = agent
                            except Exception as e:
                                logger.warning(
                                    f"设置部门群 owner 失败 ({chat_group_id}): {e}"
                                )
            msg = f"已添加 {len(agents)} 个成员"
        elif action == "remove":
            removed = [a for a in agents if a in current_agents]
            current_agents = [a for a in current_agents if a not in agents]
            # 自动从部门群聊移除
            if chat_group_id and self._group_manager:
                for agent in removed:
                    try:
                        self._group_manager.remove_member(
                            chat_group_id, agent
                        )
                    except Exception as e:
                        logger.warning(
                            f"从群聊移除 {agent} 失败 ({chat_group_id}): {e}"
                        )
            msg = f"已移除 {len(removed)} 个成员"
        elif action == "set":
            old_agents = set(current_agents)
            new_agents = set(agents)
            # 新增的
            to_add = new_agents - old_agents
            # 移除的
            to_remove = old_agents - new_agents
            current_agents = list(agents)
            # 自动同步群聊成员
            if chat_group_id and self._group_manager:
                for agent in to_add:
                    try:
                        self._group_manager.add_member(
                            chat_group_id, agent
                        )
                    except Exception as e:
                        logger.warning(
                            f"添加 {agent} 到群聊失败 ({chat_group_id}): {e}"
                        )
                for agent in to_remove:
                    try:
                        self._group_manager.remove_member(
                            chat_group_id, agent
                        )
                    except Exception as e:
                        logger.warning(
                            f"从群聊移除 {agent} 失败 ({chat_group_id}): {e}"
                        )
            msg = f"已设置 {len(agents)} 个成员"

        meta["agents"] = current_agents
        meta["updated_at"] = timestamp()
        self._save_meta(path, meta)

        # [v1.34.4] 同步更新数据库中的 agent-department 关联
        if self._agent_storage:
            try:
                if action == "set":
                    # set 模式：直接设置最终列表
                    self._agent_storage.set_department_agents(path, current_agents)
                elif action == "add":
                    # add 模式：批量添加
                    for agent in agents:
                        self._agent_storage.add_agent_to_department(agent, path)
                elif action == "remove":
                    # remove 模式：批量移除
                    for agent in agents:
                        self._agent_storage.remove_agent_from_department(agent, path)
                logger.debug(f"部门 {path} 的 agent 关联已同步到数据库")
            except Exception as e:
                logger.warning(f"同步部门关联到数据库失败: {e}")

        logger.info(f"部门 {path} 成员已更新: {msg}")
        return {"ok": True, "meta": meta, "message": msg}

    def set_head(self, path: str, head: str = "") -> Dict[str, Any]:
        """
        设置部门负责人。

        Args:
            path: 部门路径
            head: 负责人 Agent 路径（空字符串表示取消负责人）

        Returns:
            {ok, meta, message}
        """
        if not self._validate_path(path):
            return {"ok": False, "message": "非法路径"}

        meta = self._load_meta(path)
        if not meta:
            return {"ok": False, "message": f"部门不存在: {path}"}

        meta["head"] = head
        meta["updated_at"] = timestamp()
        self._save_meta(path, meta)

        action = "设置" if head else "取消"
        logger.info(f"部门 {path} 负责人已{action}: {head or '无'}")
        return {"ok": True, "meta": meta, "message": f"已{action}负责人"}

    def update_dept_meta(
        self,
        path: str,
        description: str = None,
        head: str = None,
    ) -> Dict[str, Any]:
        """
        更新部门元数据（描述、负责人等），不修改 dept.md。

        Args:
            path: 部门路径
            description: 新描述（None=不修改）
            head: 负责人 Agent 路径（None=不修改）

        Returns:
            {ok, meta, message}
        """
        if not self._validate_path(path):
            return {"ok": False, "message": "非法路径"}

        meta = self._load_meta(path)
        if not meta:
            return {"ok": False, "message": f"部门不存在: {path}"}

        if description is not None:
            meta["description"] = description
        if head is not None:
            meta["head"] = head
        meta["updated_at"] = timestamp()
        self._save_meta(path, meta)

        logger.info(f"部门 {path} 元数据已更新")
        return {"ok": True, "meta": meta, "message": "已更新"}

    # ==========================================================================
    # 部门介绍（dept.md）
    # ==========================================================================

    def get_dept_info(self, path: str) -> Optional[str]:
        """
        获取部门介绍内容（dept.md）。

        Args:
            path: 部门路径

        Returns:
            Markdown 内容，不存在返回 None
        """
        if not self._validate_path(path):
            return None

        info_path = self._dept_info_path(path)
        if not info_path.exists():
            return None

        try:
            return info_path.read_text(encoding="utf-8")
        except Exception as e:
            logger.error(f"读取部门介绍失败 ({path}): {e}")
            return None

    def update_dept_info(self, path: str, content: str) -> Dict[str, Any]:
        """
        更新部门介绍内容（dept.md）。

        Args:
            path: 部门路径
            content: 新的 Markdown 内容

        Returns:
            {ok, message}
        """
        if not self._validate_path(path):
            return {"ok": False, "message": "非法路径"}

        meta = self._load_meta(path)
        if not meta:
            return {"ok": False, "message": f"部门不存在: {path}"}

        info_path = self._dept_info_path(path)
        try:
            info_path.write_text(content, encoding="utf-8")
            meta["updated_at"] = timestamp()
            self._save_meta(path, meta)
            logger.info(f"部门介绍已更新: {path}")
            return {"ok": True, "message": "更新成功"}
        except Exception as e:
            logger.error(f"更新部门介绍失败 ({path}): {e}")
            return {"ok": False, "message": f"更新失败: {e}"}

    # ==========================================================================
    # 知识库索引
    # ==========================================================================

    def _load_knowledge_index(self, path: str) -> Dict[str, Any]:
        """加载部门知识库索引"""
        index_path = self._knowledge_index_path(path)
        if not index_path.exists():
            return {}
        try:
            return json.loads(index_path.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, IOError) as e:
            logger.warning(f"加载知识库索引失败 ({path}): {e}")
            return {}

    def _save_knowledge_index(self, path: str, index: Dict[str, Any]):
        """保存部门知识库索引"""
        index_path = self._knowledge_index_path(path)
        try:
            index_path.write_text(
                json.dumps(index, ensure_ascii=False, indent=2),
                encoding="utf-8",
            )
        except Exception as e:
            logger.error(f"保存知识库索引失败 ({path}): {e}")

    # ==========================================================================
    # 知识库文件管理
    # ==========================================================================

    def list_knowledge(self, path: str) -> List[Dict[str, Any]]:
        """
        列出部门知识库中的所有文件。

        Args:
            path: 部门路径

        Returns:
            文件列表 [{name, path, type, size, updated_at, uploaded_by}]
        """
        if not self._validate_path(path):
            return []

        kb_dir = self._knowledge_dir(path)
        if not kb_dir.exists():
            return []

        index = self._load_knowledge_index(path)
        files = []

        for item in sorted(kb_dir.iterdir()):
            # 跳过索引文件
            if item.name == "knowledge_index.json":
                continue

            if item.is_file():
                rel_path = item.name
                file_idx = index.get(rel_path, {})
                stat = item.stat()
                files.append({
                    "name": item.name,
                    "path": rel_path,
                    "type": "file",
                    "size": stat.st_size,
                    "updated_at": file_idx.get("updated_at", timestamp()),
                    "uploaded_by": file_idx.get("uploaded_by", ""),
                })
            elif item.is_dir():
                # 递归列出子目录中的文件
                sub_files = [
                    f for f in item.rglob("*")
                    if f.is_file() and f.suffix.lower() in SUPPORTED_EXTENSIONS
                ]
                files.append({
                    "name": item.name,
                    "path": item.name,
                    "type": "dir",
                    "size": 0,
                    "file_count": len(sub_files),
                })
                for f in sub_files:
                    rel_path = str(f.relative_to(kb_dir))
                    file_idx = index.get(rel_path, {})
                    try:
                        stat = f.stat()
                        files.append({
                            "name": f.name,
                            "path": rel_path,
                            "type": "file",
                            "size": stat.st_size,
                            "updated_at": file_idx.get("updated_at", timestamp()),
                            "uploaded_by": file_idx.get("uploaded_by", ""),
                        })
                    except OSError:
                        pass

        return files

    def upload_knowledge(
        self,
        path: str,
        filename: str,
        content: str,
        uploaded_by: str = "",
    ) -> Dict[str, Any]:
        """
        上传文件到部门知识库。

        Args:
            path: 部门路径
            filename: 文件名（支持子目录，如 "policies/leave.md"）
            content: 文件内容
            uploaded_by: 上传者标识（agent path）

        Returns:
            {ok, path, message}
        """
        if not self._validate_path(path):
            return {"ok": False, "message": "非法部门路径"}

        meta = self._load_meta(path)
        if not meta:
            return {"ok": False, "message": f"部门不存在: {path}"}

        kb_dir = self._knowledge_dir(path)

        # 安全校验: 不允许路径遍历
        if ".." in filename or filename.startswith("/"):
            return {"ok": False, "message": "非法文件名"}

        safe_name = Path(filename).name
        ext = Path(safe_name).suffix.lower()
        if ext and ext not in SUPPORTED_EXTENSIONS:
            return {"ok": False, "message": f"不支持的文件类型: {ext}"}

        # 处理子目录
        if "/" in filename:
            parts = filename.split("/")
            parent_dir = kb_dir / "/".join(parts[:-1])
            parent_dir.mkdir(parents=True, exist_ok=True)
            file_name = parts[-1]
            file_path = parent_dir / file_name
        else:
            file_name = safe_name
            file_path = kb_dir / safe_name

        # 写入文件
        try:
            file_path.write_text(content, encoding="utf-8")
        except Exception as e:
            return {"ok": False, "message": f"写入文件失败: {e}"}

        # 更新索引
        rel_path = str(file_path.relative_to(kb_dir))
        index = self._load_knowledge_index(path)
        index[rel_path] = {
            "filename": file_name,
            "size": len(content),
            "updated_at": timestamp(),
            "uploaded_by": uploaded_by,
        }
        self._save_knowledge_index(path, index)

        # 更新部门 updated_at
        meta["updated_at"] = timestamp()
        self._save_meta(path, meta)

        logger.info(f"知识库文件已上传: {path}/{rel_path} (by={uploaded_by})")
        return {"ok": True, "path": rel_path, "message": "上传成功"}

    def delete_knowledge(self, path: str, file_path: str) -> Dict[str, Any]:
        """
        删除部门知识库中的文件或文件夹。

        Args:
            path: 部门路径
            file_path: 相对路径（如 "policies/leave.md" 或 "policies"）

        Returns:
            {ok, message, deleted_count}
        """
        if not self._validate_path(path):
            return {"ok": False, "message": "非法部门路径"}

        kb_dir = self._knowledge_dir(path)
        if not kb_dir.exists():
            return {"ok": False, "message": "知识库不存在"}

        if ".." in file_path or file_path.startswith("/"):
            return {"ok": False, "message": "非法路径"}

        target = kb_dir / file_path
        if not target.exists():
            return {"ok": False, "message": f"文件不存在: {file_path}"}

        index_path = self._knowledge_index_path(path)
        if target == index_path:
            return {"ok": False, "message": "不允许删除索引文件"}

        try:
            deleted_count = 0
            if target.is_dir():
                for f in target.rglob("*"):
                    if f.is_file():
                        deleted_count += 1
                shutil.rmtree(target)
            else:
                deleted_count = 1
                target.unlink()

            # 更新索引
            index = self._load_knowledge_index(path)
            to_remove = [
                k for k in index
                if k == file_path or k.startswith(file_path + "/")
            ]
            for k in to_remove:
                del index[k]
            self._save_knowledge_index(path, index)

            logger.info(
                f"知识库文件已删除: {path}/{file_path} ({deleted_count} 个文件)"
            )
            return {
                "ok": True,
                "message": f"已删除 {deleted_count} 个文件",
                "deleted_count": deleted_count,
            }
        except Exception as e:
            return {"ok": False, "message": f"删除失败: {e}"}

    def read_knowledge(self, path: str, file_path: str) -> Optional[str]:
        """
        读取部门知识库文件内容。

        Args:
            path: 部门路径
            file_path: 相对路径

        Returns:
            文件内容，不存在返回 None
        """
        if not self._validate_path(path):
            return None

        if ".." in file_path or file_path.startswith("/"):
            return None

        kb_dir = self._knowledge_dir(path)
        if not kb_dir.exists():
            return None

        target = kb_dir / file_path
        if not target.exists() or not target.is_file():
            return None

        try:
            return target.read_text(encoding="utf-8", errors="replace")
        except Exception:
            return None
