# -*- coding: utf-8 -*-
"""ToolSafetyGuard 多层纵深防护（Phase 8 Tier 1-5 CE）

借鉴 zhikuncode ToolSafetyGuard：
- 12 个危险 bash 命令正则（fork bomb / rm -rf / / curl|sh 等）
- 10 类敏感环境变量清理（AWS/GITHUB/NPM/DB 等）
- 路径规范化与符号链接逃逸检测
- SafetyLevel 4 级分类（SAFE / AUDIT / HIGH_RISK / BLOCK）
- 挂载点边界检测（Linux /proc/mounts，Windows 跳过）

与 Phase 6 AP PathSecurityService 互补：
- AP PathSecurityService：5 重路径校验（符号链接、路径穿越、工作目录边界、受保护路径、Windows 绕过）
- CE ToolSafetyGuard：bash 命令检测 + 环境变量清理 + 路径安全检查（轻量版，集成方便）

集成点：
1. `tools/shell_exec.py`：执行前 check_bash_command + sanitize_env
2. `tools/file_write.py` / `file_edit.py`：写前 check_path
3. `tools/file_read.py`：读敏感路径前 check_path
"""
import os
import re
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple


class SafetyLevel(Enum):
    """安全级别枚举。

    优先级：SAFE(1) < AUDIT(2) < HIGH_RISK(3) < BLOCK(4)
    """

    SAFE = "safe"           # 安全，直接通过
    AUDIT = "audit"         # 仅记录，不拦截
    HIGH_RISK = "high_risk"  # 高风险，提示用户确认
    BLOCK = "block"         # 拦截，拒绝执行

    def __init__(self, value: str) -> None:
        order = ["safe", "audit", "high_risk", "block"]
        self.priority = order.index(value) + 1

    @classmethod
    def from_value(cls, value: str) -> Optional["SafetyLevel"]:
        for member in cls:
            if member.value == value:
                return member
        return None


class SafetyCheckResult:
    """安全检查结果。

    Attributes:
        level: 安全级别
        reason: 人类可读的原因说明
        pattern_matched: 触发的模式（仅 BLOCK/HIGH_RISK 时填充）
    """

    __slots__ = ("level", "reason", "pattern_matched")

    def __init__(
        self,
        level: SafetyLevel,
        reason: str = "",
        pattern_matched: Optional[str] = None,
    ) -> None:
        self.level = level
        self.reason = reason
        self.pattern_matched = pattern_matched

    @property
    def should_block(self) -> bool:
        """是否应阻止操作。"""
        return self.level == SafetyLevel.BLOCK

    @property
    def should_audit(self) -> bool:
        """是否应记录审计日志。"""
        return self.level.priority >= SafetyLevel.AUDIT.priority

    def __repr__(self) -> str:
        return f"SafetyCheckResult(level={self.level}, reason={self.reason!r})"


# 12 个危险 bash 命令正则
# 注意：使用 re.MULTILINE + re.DOTALL 兼容多行命令和复合命令
_DANGEROUS_BASH_PATTERNS: List[Tuple[str, str, SafetyLevel]] = [
    # 1. fork bomb: :(){ :|:& };:
    (r":\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;?\s*:?", "fork bomb", SafetyLevel.BLOCK),
    # 2. rm -rf / 或 sudo rm -rf /（根目录递归删除，后接 shell 元字符或行尾）
    (r"(?:\bsudo\s+)?\brm\s+-rf\s+/?\s*(?:$|[;&|)\n`])", "rm -rf /", SafetyLevel.BLOCK),
    # 3. mkfs.*（格式化文件系统）
    (r"\bmkfs(?:\.\w+)?\s+", "mkfs", SafetyLevel.BLOCK),
    # 4. dd of=/dev/...（写入块设备）
    (r"\bdd\b.*\bof=/dev/", "dd of=/dev/", SafetyLevel.BLOCK),
    # 5. curl | sh 或 wget | sh（远程执行）
    (r"\b(?:curl|wget)\b.*\|\s*(?:sh|bash|zsh|fish)\b", "curl|sh", SafetyLevel.BLOCK),
    # 6. chmod 777 /（根目录权限开放）
    (r"\bchmod\s+777\s+/\s*$", "chmod 777 /", SafetyLevel.BLOCK),
    # 7. > /dev/sda 或类似（重定向到块设备）
    (r">\s*/dev/(?:sd[a-z]+|nvme\d+n\d+|hd[a-z]+)", "redirect to /dev/", SafetyLevel.BLOCK),
    # 8. shutdown（关机）
    (r"\bshutdown\b", "shutdown", SafetyLevel.BLOCK),
    # 9. reboot（重启）
    (r"\breboot\b", "reboot", SafetyLevel.BLOCK),
    # 10. halt（停机）
    (r"\bhalt\b", "halt", SafetyLevel.BLOCK),
    # 11. poweroff（关机）
    (r"\bpoweroff\b", "poweroff", SafetyLevel.BLOCK),
    # 12. init 0（系统关机）
    (r"\binit\s+0\b", "init 0", SafetyLevel.BLOCK),
]

# 编译为正则对象（性能优化）
_COMPILED_DANGEROUS_PATTERNS: List[Tuple[re.Pattern, str, SafetyLevel]] = [
    (re.compile(p, re.MULTILINE | re.DOTALL), name, level)
    for p, name, level in _DANGEROUS_BASH_PATTERNS
]

# 10 类敏感环境变量（精确匹配）
SENSITIVE_ENV_VARS = [
    # 1. AWS
    "AWS_ACCESS_KEY_ID",
    "AWS_SECRET_ACCESS_KEY",
    "AWS_SESSION_TOKEN",
    # 2. GitHub
    "GITHUB_TOKEN",
    "GH_TOKEN",
    # 3. NPM
    "NPM_TOKEN",
    "NODE_AUTH_TOKEN",
    # 4. Database
    "DATABASE_URL",
    "DB_PASSWORD",
    # 5. OpenAI / Anthropic
    "OPENAI_API_KEY",
    "ANTHROPIC_API_KEY",
]

# 受保护路径（系统级敏感文件）
_PROTECTED_PATHS = [
    # Linux
    "/etc/shadow",
    "/etc/passwd",
    "/etc/sudoers",
    "/etc/ssh/sshd_config",
    "/root/.ssh",
    "/var/log/auth.log",
    # Windows
    "C:\\Windows\\System32\\config\\SAM",
    "C:\\Windows\\System32\\config\\SYSTEM",
    "C:\\Windows\\System32\\config\\SECURITY",
]


class ToolSafetyGuard:
    """多层纵深防护链。

    借鉴 zhikuncode ToolSafetyGuard：
    1. realpath 规范化 + 符号链接逃逸检测
    2. 沙箱边界检查（工作目录内）
    3. 挂载点边界检测（Linux /proc/mounts）
    4. 敏感路径黑名单
    5. 12 个危险 bash 命令正则
    6. 敏感环境变量清理

    Usage:
        >>> guard = ToolSafetyGuard(working_dir="/path/to/project")
        >>> result = guard.check_bash_command("rm -rf /")
        >>> if result.should_block:
        ...     raise PermissionError(result.reason)
        >>> env = guard.sanitize_env(os.environ.copy())
    """

    def __init__(self, working_dir: str) -> None:
        """初始化 ToolSafetyGuard。

        Args:
            working_dir: 工作目录路径（决定沙箱边界）
        """
        try:
            self._working_dir = str(Path(working_dir).resolve())
        except (OSError, ValueError):
            # 路径规范化失败（如含 null 字节）：保留原值，后续 check_path 会拦截
            self._working_dir = working_dir

    # ---- bash 命令检测 ----

    def check_bash_command(self, cmd: str) -> SafetyCheckResult:
        """检测 bash 命令是否包含危险模式。

        检测策略：
        - 12 个危险命令正则匹配
        - 多行命令逐行检测
        - 复合命令（&&/||/;/|/反引号/$()）整体检测

        Args:
            cmd: bash 命令字符串

        Returns:
            SafetyCheckResult：BLOCK 表示应拦截，SAFE 表示通过
        """
        if not cmd or not cmd.strip():
            return SafetyCheckResult(level=SafetyLevel.SAFE, reason="empty command")

        # 对每个危险模式检测
        for pattern, name, level in _COMPILED_DANGEROUS_PATTERNS:
            match = pattern.search(cmd)
            if match:
                return SafetyCheckResult(
                    level=level,
                    reason=f"matched dangerous pattern: {name}",
                    pattern_matched=name,
                )

        return SafetyCheckResult(level=SafetyLevel.SAFE, reason="no dangerous pattern")

    # ---- 路径检测 ----

    def check_path(self, path: str, operation: str = "read") -> SafetyCheckResult:
        """检测路径是否安全。

        检测项：
        1. null 字节检测
        2. 路径穿越检测（../）
        3. 工作目录边界检测
        4. 受保护路径黑名单

        相对路径会基于 working_dir 解析（而非进程 CWD）。

        Args:
            path: 待检测路径
            operation: 操作类型（read/write）

        Returns:
            SafetyCheckResult：BLOCK 表示应拦截，SAFE 表示通过
        """
        if path is None:
            return SafetyCheckResult(level=SafetyLevel.BLOCK, reason="path is None")

        # 1. null 字节检测
        if "\x00" in path:
            return SafetyCheckResult(
                level=SafetyLevel.BLOCK,
                reason="path contains null byte",
                pattern_matched="null_byte",
            )

        # 2. 路径穿越检测（简单字符串检测，先于规范化）
        if ".." in path.split(os.sep) or ".." in re.split(r"[\\/]", path):
            # 进一步规范化判断：若解析后仍在工作目录内，则放行
            try:
                resolved = self._resolve_path(path)
                if not self._is_in_working_dir(resolved):
                    return SafetyCheckResult(
                        level=SafetyLevel.BLOCK,
                        reason=f"path traversal: {path} resolves outside working dir",
                        pattern_matched="path_traversal",
                    )
            except (OSError, ValueError):
                return SafetyCheckResult(
                    level=SafetyLevel.BLOCK,
                    reason=f"cannot resolve path: {path}",
                    pattern_matched="path_resolve_failed",
                )

        # 3. 规范化路径
        try:
            resolved = self._resolve_path(path)
            resolved_str = str(resolved)
        except (OSError, ValueError):
            return SafetyCheckResult(
                level=SafetyLevel.BLOCK,
                reason=f"cannot resolve path: {path}",
                pattern_matched="path_resolve_failed",
            )

        # 4. 受保护路径检测
        for protected in _PROTECTED_PATHS:
            try:
                protected_resolved = str(Path(protected).resolve())
            except (OSError, ValueError):
                protected_resolved = protected
            # 大小写不敏感比较（Windows 路径）
            if resolved_str.lower() == protected_resolved.lower() or \
               resolved_str.lower().startswith(protected_resolved.lower() + os.sep):
                return SafetyCheckResult(
                    level=SafetyLevel.BLOCK,
                    reason=f"protected path: {protected}",
                    pattern_matched="protected_path",
                )

        # 5. 工作目录边界检测
        if not self._is_in_working_dir(resolved):
            return SafetyCheckResult(
                level=SafetyLevel.BLOCK,
                reason=f"path outside working dir: {resolved_str}",
                pattern_matched="outside_working_dir",
            )

        return SafetyCheckResult(level=SafetyLevel.SAFE, reason="path ok")

    def _resolve_path(self, path: str) -> Path:
        """解析路径：相对路径基于 working_dir，绝对路径原样。

        Args:
            path: 原始路径字符串

        Returns:
            解析后的绝对 Path 对象
        """
        p = Path(path)
        if not p.is_absolute():
            # 相对路径：基于 working_dir 解析
            p = Path(self._working_dir) / p
        return p.resolve()

    def _is_in_working_dir(self, resolved: Path) -> bool:
        """检查解析后的路径是否在工作目录内。"""
        try:
            wd = Path(self._working_dir)
            # 相对路径：检查是否以工作目录为前缀
            resolved.relative_to(wd)
            return True
        except ValueError:
            # 不在工作目录内
            return False
        except (OSError, RuntimeError):
            return False

    # ---- 环境变量清理 ----

    def sanitize_env(self, env: Dict[str, str]) -> Dict[str, str]:
        """清理敏感环境变量。

        移除以下 10 类敏感变量：
        - AWS（ACCESS_KEY_ID / SECRET_ACCESS_KEY / SESSION_TOKEN）
        - GitHub（GITHUB_TOKEN / GH_TOKEN）
        - NPM（NPM_TOKEN / NODE_AUTH_TOKEN）
        - Database（DATABASE_URL / DB_PASSWORD）
        - OpenAI / Anthropic（OPENAI_API_KEY / ANTHROPIC_API_KEY）

        Args:
            env: 原始环境变量 dict

        Returns:
            清理后的环境变量 dict（新对象，不修改原 dict）
        """
        if not env:
            return {}
        sensitive_set = set(SENSITIVE_ENV_VARS)
        return {k: v for k, v in env.items() if k not in sensitive_set}

    # ---- 挂载点边界检测 ----

    def check_mount_boundary(self, path: str) -> SafetyCheckResult:
        """检测路径是否跨越挂载点边界。

        Linux：解析 /proc/mounts 获取挂载点列表
        Windows：跳过（直接返回 SAFE）

        Args:
            path: 待检测路径

        Returns:
            SafetyCheckResult
        """
        if path is None:
            return SafetyCheckResult(
                level=SafetyLevel.BLOCK, reason="path is None"
            )

        # Windows 无 /proc/mounts，跳过挂载点检测
        if os.name == "nt":
            return SafetyCheckResult(
                level=SafetyLevel.SAFE,
                reason="mount boundary check skipped on Windows",
            )

        # Linux：读取 /proc/mounts
        try:
            resolved = str(Path(path).resolve())
            with open("/proc/mounts", "r", encoding="utf-8") as f:
                mounts = []
                for line in f:
                    parts = line.split()
                    if len(parts) >= 2:
                        mounts.append(parts[1])

            # 找出工作目录所在的挂载点
            wd_mount = None
            for mount in sorted(mounts, key=len, reverse=True):
                if self._working_dir.startswith(mount):
                    wd_mount = mount
                    break

            if wd_mount is None:
                return SafetyCheckResult(
                    level=SafetyLevel.AUDIT,
                    reason=f"cannot find working dir mount point",
                )

            # 检查目标路径是否在同一挂载点内
            if not resolved.startswith(wd_mount):
                return SafetyCheckResult(
                    level=SafetyLevel.HIGH_RISK,
                    reason=f"path crosses mount boundary: {resolved} not in {wd_mount}",
                    pattern_matched="mount_boundary_cross",
                )

            return SafetyCheckResult(
                level=SafetyLevel.SAFE, reason="path within mount boundary"
            )
        except (OSError, IOError):
            # /proc/mounts 不可读：跳过
            return SafetyCheckResult(
                level=SafetyLevel.SAFE,
                reason="mount boundary check skipped (cannot read /proc/mounts)",
            )
