"""run_python: 在 subprocess 里执行 Python 代码 (Hybrid 范式的关键)。

JSON tool call 处理离散操作 (read/edit/shell),run_python 处理连续逻辑
(数据计算、批处理、生成 .pptx/.docx)。让模型自己挑工具。

阶段 1 (本地): subprocess + 工作目录限制 + 敏感环境变量过滤
阶段 2 (后期): Docker / E2B 替换执行后端
"""
from __future__ import annotations

import os
import subprocess
import sys
import tempfile
from pathlib import Path

from .base import Tool, compact_tool_output

_SENSITIVE_PATTERNS = ("API_KEY", "TOKEN", "SECRET", "PASSWORD", "PRIVATE_KEY")


class RunPythonTool(Tool):
    name = "run_python"
    description = (
        "Execute Python code in a subprocess. Returns stdout/stderr/exit_code.\n"
        "Use script_path for non-trivial code: write the .py under scripts/ (e.g. scripts/analyze.py) first, "
        "then execute it so the full source stays in files instead of conversation history.\n"
        "Use inline code only for short throwaway snippets (a quick calc / one-line probe) — these run from a temp file and leave no trace.\n"
        "Good for: data analysis, batch file ops, document generation (.pptx/.docx), "
        "matplotlib charts, or any task where Python is more natural than chaining tools.\n"
        "Working directory is the agent's base dir (task_dir); relative paths resolve against it. "
        "Keep process scripts in scripts/; write deliverables to task_dir root or the SKILL-specified path.\n"
        "Available libs (install with shell pip if missing): "
        "pandas, numpy, matplotlib, python-pptx, python-docx, requests, pypdf."
    )
    parameters = {
        "type": "object",
        "properties": {
            "code": {
                "type": "string",
                "description": "Short Python source. For longer code, write a .py file and pass script_path.",
            },
            "script_path": {
                "type": "string",
                "description": "Path to an existing .py file to execute (relative to task_dir). Prefer this for non-trivial code; keep such scripts under scripts/.",
            },
            "timeout": {"type": "integer", "default": 120, "description": "Seconds before kill"},
        },
        "required": [],
    }

    def execute(
        self,
        code: str | None = None,
        script_path: str | None = None,
        timeout: int = 120,
    ) -> str:
        cleanup_script = False
        if script_path:
            script = self._resolve(script_path)
            if not script.is_file():
                return f"[Error] script_path not found: {self._display(script)}"
        elif isinstance(code, str):
            # 写到临时文件,避免 -c 转义问题
            with tempfile.NamedTemporaryFile(
                suffix=".py", mode="w", delete=False, encoding="utf-8"
            ) as f:
                f.write(code)
                script = Path(f.name)
            cleanup_script = True
        else:
            return "[Error] run_python requires code or script_path"

        try:
            env = os.environ.copy()
            for k in list(env):
                u = k.upper()
                if any(p in u for p in _SENSITIVE_PATTERNS):
                    del env[k]
            env["PYTHONIOENCODING"] = "utf-8"
            env["PYTHONPATH"] = str(self.base_dir) + os.pathsep + env.get("PYTHONPATH", "")

            result = subprocess.run(
                [sys.executable, str(script)],
                cwd=str(self.base_dir),
                capture_output=True,
                timeout=timeout,
                text=True,
                encoding="utf-8",
                errors="replace",
                env=env,
            )
        except subprocess.TimeoutExpired:
            return f"[Error] python script timed out after {timeout}s"
        finally:
            if cleanup_script:
                try:
                    script.unlink()
                except OSError:
                    pass

        parts = []
        if result.stdout:
            parts.append(f"[stdout]\n{result.stdout.rstrip()}")
        if result.stderr:
            parts.append(f"[stderr]\n{result.stderr.rstrip()}")
        parts.append(f"[exit {result.returncode}]")
        return compact_tool_output("\n".join(parts))
