# toolUtil.py —— 工具实现（无沙箱：不做路径/命令/网络限制）
import fnmatch
import json
import os
import re
import subprocess
import sys
import threading
from datetime import datetime
from pathlib import Path
from urllib.parse import urlencode
from urllib.request import Request, urlopen

try:
    from zoneinfo import ZoneInfo
except ImportError:
    ZoneInfo = None


# =========================================================
# 配置（无沙箱：只保留容量/超时等运行参数）
# =========================================================
class ToolConfig:
    def __init__(
            self,
            root: str = "/work",
            max_file_bytes: int = 5 * 1024 * 1024,   # 单文件最大 5MB
            max_output_chars: int = 20000,            # 输出截断
            default_timeout: int = 30,
            max_timeout: int = 120,
            audit_log: str = None,
    ):
        self.root = Path(root).resolve()
        try:
            self.root.mkdir(parents=True, exist_ok=True)
        except OSError:
            pass

        self.max_file_bytes = max_file_bytes
        self.max_output_chars = max_output_chars
        self.default_timeout = default_timeout
        self.max_timeout = max_timeout
        self.audit_log = Path(audit_log) if audit_log else (self.root / "audit.log")
        self._lock = threading.Lock()


CFG = ToolConfig()


# =========================================================
# 异常 & 审计
# =========================================================
class ToolError(Exception):
    pass


def _audit(tool: str, args: dict, ok: bool, extra: str = ""):
    if ZoneInfo is not None:
        try:
            _ts = datetime.now(ZoneInfo("Asia/Shanghai")).isoformat(timespec="seconds")
        except Exception:
            _ts = datetime.now().isoformat(timespec="seconds")
    else:
        _ts = datetime.now().isoformat(timespec="seconds")
    entry = {
        "ts": _ts,
        "tool": tool,
        "args": args,
        "ok": ok,
        "extra": extra,
    }
    try:
        with CFG._lock:
            log_path = CFG.audit_log
            try:
                if log_path.exists() and log_path.stat().st_size > 10 * 1024 * 1024:
                    backup = log_path.with_suffix(log_path.suffix + ".1")
                    if backup.exists():
                        try:
                            backup.unlink()
                        except OSError:
                            pass
                    try:
                        log_path.rename(backup)
                    except OSError:
                        pass
            except OSError:
                pass
            with log_path.open("a", encoding="utf-8") as f:
                f.write(json.dumps(entry, ensure_ascii=False) + "\n")
    except OSError:
        pass


# =========================================================
# 路径解析（相对路径基于 CFG.root，绝对路径原样使用；不做越界限制）
# =========================================================
def resolve_path(user_path: str, must_exist: bool = False) -> Path:
    p = Path(user_path or ".")
    if not p.is_absolute():
        p = CFG.root / p

    try:
        resolved = p.resolve(strict=False)
    except OSError as e:
        raise ToolError(f"路径解析失败: {e}") from e

    if must_exist and not resolved.exists():
        raise ToolError(f"路径不存在: {user_path}")

    return resolved


def _rel(p: Path) -> str:
    """尽量返回相对 CFG.root 的路径；在 root 之外时返回绝对路径。"""
    try:
        return str(p.relative_to(CFG.root))
    except ValueError:
        return str(p)


def _clip(text: str) -> str:
    limit = CFG.max_output_chars
    if len(text) <= limit:
        return text
    return text[:limit] + f"\n...[已截断，共 {len(text)} 字符]"


# =========================================================
# 文件工具
# =========================================================
def read_file(path: str, encoding: str = "utf-8"):
    p = resolve_path(path, must_exist=True)
    if not p.is_file():
        raise ToolError(f"不是文件: {path}")

    size = p.stat().st_size
    if size > CFG.max_file_bytes:
        raise ToolError(f"文件过大 ({size} bytes > {CFG.max_file_bytes})")

    content = p.read_text(encoding=encoding)
    _audit("read_file", {"path": path}, True)
    return f"已读取文件 {_rel(p)}（{size} 字节）：" + chr(10) + "---" + chr(10) + content


def write_file(path: str, content: str, encoding: str = "utf-8", append: bool = False):
    p = resolve_path(path)

    data = content.encode(encoding)
    if len(data) > CFG.max_file_bytes:
        raise ToolError(f"写入内容过大 ({len(data)} bytes)")

    p.parent.mkdir(parents=True, exist_ok=True)
    if append and p.exists():
        prev = p.read_text(encoding=encoding)
        if content.strip() and prev.rstrip().endswith(content.strip()):
            return {"path": _rel(p), "bytes": 0, "append": True, "dup": True}
        if prev and not prev.endswith(chr(10)):
            content = chr(10) + content
    mode = "a" if append else "w"
    with p.open(mode, encoding=encoding, newline="\n") as f:
        f.write(content)

    _audit("write_file", {"path": path, "append": append, "bytes": len(data)}, True)
    return {
        "path": _rel(p),
        "bytes": len(data),
        "append": append,
    }


def list_directory(path: str = ".", recursive: bool = False):
    p = resolve_path(path, must_exist=True)
    if not p.is_dir():
        raise ToolError(f"不是目录: {path}")

    items = []
    iterator = p.rglob("*") if recursive else p.iterdir()

    for child in iterator:
        try:
            stat = child.stat()
            _is_dir = child.is_dir()
            _name = child.relative_to(p).as_posix() if recursive else child.name
            if _is_dir:
                items.append("- " + _name + "（目录）")
            else:
                items.append("- " + _name + "（" + str(stat.st_size) + " 字节）")
        except OSError:
            continue
        if len(items) >= 500:
            break

    _audit("list_directory", {"path": path, "recursive": recursive}, True)
    return "目录 " + _rel(p) + " 共 " + str(len(items)) + " 项：" + chr(10) + chr(10).join(items)


def delete_file(path: str):
    p = resolve_path(path, must_exist=True)
    if p.is_dir():
        raise ToolError("delete_file 只支持文件，目录请用 delete_directory")

    p.unlink()
    _audit("delete_file", {"path": path}, True)
    return {"deleted": _rel(p)}


def delete_directory(path: str, recursive: bool = False):
    p = resolve_path(path, must_exist=True)
    if not p.is_dir():
        raise ToolError(f"不是目录: {path}")

    if recursive:
        for child in sorted(p.rglob("*"), key=lambda x: len(x.parts), reverse=True):
            try:
                if child.is_dir() and not child.is_symlink():
                    child.rmdir()
                else:
                    child.unlink()
            except OSError:
                pass
        p.rmdir()
    else:
        p.rmdir()

    _audit("delete_directory", {"path": path, "recursive": recursive}, True)
    return {"deleted": _rel(p), "recursive": recursive}


def search_files(pattern: str = "*", path: str = ".", max_results: int = 50):
    root = resolve_path(path, must_exist=True)
    results = []
    for p in root.rglob(pattern):
        results.append(_rel(p))
        if len(results) >= max_results:
            break

    _audit("search_files", {"pattern": pattern, "path": path}, True)
    return {"pattern": pattern, "count": len(results), "files": results}


def _compile_safe_regex(pattern: str):
    if len(pattern) > 2000:
        raise ToolError("正则表达式过长（>2000 字符）")
    try:
        return re.compile(pattern)
    except re.error as e:
        raise ToolError(f"正则表达式无效: {e}")


def grep(pattern: str, path: str = ".", file_glob: str = "*", max_results: int = 50):
    root = resolve_path(path, must_exist=True)
    regex = _compile_safe_regex(pattern)
    matches = []

    if root.is_file():
        candidates = [root]
    else:
        candidates = [p for p in root.rglob("*") if p.is_file()]

    for p in candidates:
        if not fnmatch.fnmatch(p.name, file_glob):
            continue

        try:
            text = p.read_text(encoding="utf-8", errors="replace")
        except OSError:
            continue

        for lineno, line in enumerate(text.splitlines(), 1):
            if regex.search(line):
                matches.append({
                    "file": _rel(p),
                    "line": lineno,
                    "text": _clip(line),
                })
                if len(matches) >= max_results:
                    _audit("grep", {"pattern": pattern}, True)
                    return {"pattern": pattern, "count": len(matches), "matches": matches}

    _audit("grep", {"pattern": pattern}, True)
    return {"pattern": pattern, "count": len(matches), "matches": matches}


# =========================================================
# Shell / Python 执行（无沙箱，只保留超时与输出截断）
# =========================================================
def execute_shell(command: str, cwd: str = None, timeout: int = None):
    if not command or not command.strip():
        raise ToolError("空命令")

    workdir = resolve_path(cwd or ".")
    if not workdir.is_dir():
        raise ToolError(f"工作目录无效: {cwd}")

    timeout = min(timeout or CFG.default_timeout, CFG.max_timeout)

    try:
        proc = subprocess.run(
            command,
            shell=True,
            cwd=str(workdir),
            capture_output=True,
            text=True,
            timeout=timeout,
            env=os.environ.copy(),
        )
        ok = proc.returncode == 0
        _audit("execute_shell", {"command": command, "cwd": cwd}, ok,
               f"exit={proc.returncode}")
        out = "命令执行" + ("完成" if ok else "失败") + "，退出码 " + str(proc.returncode) + "，输出如下：" + chr(10) + _clip(proc.stdout)
        if proc.stderr:
            out = out + chr(10) + "错误输出：" + chr(10) + _clip(proc.stderr)
        return out
    except subprocess.TimeoutExpired:
        _audit("execute_shell", {"command": command}, False, "timeout")
        raise ToolError(f"命令超时 (>{timeout}s)")


def python_exec(code: str, timeout: int = None):
    timeout = min(timeout or CFG.default_timeout, CFG.max_timeout)

    try:
        proc = subprocess.run(
            [sys.executable, "-c", code],
            capture_output=True,
            text=True,
            timeout=timeout,
            cwd=str(CFG.root),
            env=os.environ.copy(),
        )
        ok = proc.returncode == 0
        _audit("python_exec", {"code_len": len(code)}, ok,
               f"exit={proc.returncode}")
        out = "Python 执行" + ("完成" if ok else "失败") + "，退出码 " + str(proc.returncode) + "，输出如下：" + chr(10) + _clip(proc.stdout)
        if proc.stderr:
            out = out + chr(10) + "错误输出：" + chr(10) + _clip(proc.stderr)
        return out

    except subprocess.TimeoutExpired:
        _audit("python_exec", {"code_len": len(code)}, False, "timeout")
        raise ToolError(f"Python 执行超时 (>{timeout}s)")


# =========================================================
# 网络（无主机白名单）
# =========================================================
def http_request(
        url: str,
        method: str = "GET",
        headers: dict = None,
        params: dict = None,
        json_body: dict = None,
        data: str = None,
        timeout: int = 30,
):
    if params:
        sep = "&" if "?" in url else "?"
        url = f"{url}{sep}{urlencode(params)}"

    h = dict(headers or {})
    body = None

    if json_body is not None:
        body = json.dumps(json_body).encode("utf-8")
        h.setdefault("Content-Type", "application/json")
    elif data is not None:
        body = str(data).encode("utf-8")

    req = Request(url, data=body, headers=h, method=method.upper())

    try:
        with urlopen(req, timeout=min(timeout, CFG.max_timeout)) as resp:
            raw = resp.read(CFG.max_file_bytes)
            text = raw.decode("utf-8", errors="replace")
            parsed = None
            try:
                parsed = json.loads(text)
            except json.JSONDecodeError:
                pass

            _audit("http_request", {"url": url, "method": method}, True,
                   f"status={resp.status}")
            return {
                "status": resp.status,
                "headers": dict(resp.headers),
                "text": _clip(text),
                "json": parsed,
            }
    except Exception as e:
        _audit("http_request", {"url": url}, False, str(e))
        raise ToolError(f"HTTP 请求失败: {type(e).__name__}: {e}")


# =========================================================
# 无副作用工具
# =========================================================
def _safe_pow(a, b):
    if isinstance(b, (int, float)) and abs(b) > 1000:
        raise ToolError("指数过大")
    if isinstance(a, (int, float)) and isinstance(b, (int, float)):
        if abs(a) > 1 and b > 100:
            raise ToolError("幂运算结果可能过大")
    return a ** b


_BIN_OPS = {
    "Add": lambda a, b: a + b,
    "Sub": lambda a, b: a - b,
    "Mult": lambda a, b: a * b,
    "Div": lambda a, b: a / b,
    "FloorDiv": lambda a, b: a // b,
    "Mod": lambda a, b: a % b,
    "Pow": _safe_pow,
}
_UNARY_OPS = {"UAdd": lambda a: +a, "USub": lambda a: -a}


def git_info():
    """返回运行环境的基本信息，以及 CFG.root 的 Git 状态（如果适用）。"""
    import platform
    import shutil

    info = {
        "os_system": platform.system(),
        "os_release": platform.release(),
        "architecture": platform.machine(),
        "python_version": sys.version.split()[0],
        "work_dir": str(CFG.root),
    }

    if shutil.which("git") is None:
        info["git_status"] = "git_not_installed"
        _audit("git_info", {}, True)
        return info

    def _git(*args):
        proc = subprocess.run(
            ["git", *args],
            cwd=str(CFG.root),
            capture_output=True,
            text=True,
            timeout=3,
        )
        return proc.stdout.strip() if proc.returncode == 0 else None

    try:
        if _git("rev-parse", "--is-inside-work-tree") != "true":
            info["is_git_repo"] = False
            info["git_status"] = "not_a_git_repo"
        else:
            info["is_git_repo"] = True
            info["git_branch"] = _git("branch", "--show-current") or "detached HEAD"
            commit = _git("rev-parse", "--short", "HEAD")
            if commit:
                info["git_commit"] = commit
    except Exception:
        info["git_status"] = "check_failed"

    _audit("git_info", {}, True)
    return info


def _eval_node(node):
    import ast
    if isinstance(node, ast.Expression):
        return _eval_node(node.body)
    if isinstance(node, ast.Constant):
        if isinstance(node.value, (int, float)):
            return node.value
        raise ToolError("只支持数字常量")
    if isinstance(node, ast.BinOp):
        fn = _BIN_OPS.get(type(node.op).__name__)
        if not fn:
            raise ToolError("不支持的运算符")
        return fn(_eval_node(node.left), _eval_node(node.right))
    if isinstance(node, ast.UnaryOp):
        fn = _UNARY_OPS.get(type(node.op).__name__)
        if not fn:
            raise ToolError("不支持的一元运算符")
        return fn(_eval_node(node.operand))
    raise ToolError("表达式包含不支持的内容")


def calculate(expression: str):
    import ast
    tree = ast.parse(expression, mode="eval")
    result = _eval_node(tree)
    _audit("calculate", {"expression": expression}, True)
    return {"expression": expression, "result": result}


def get_current_time(timezone: str = "Asia/Shanghai"):
    if ZoneInfo is not None:
        try:
            now = datetime.now(ZoneInfo(timezone))
        except Exception:
            now = datetime.now()
    else:
        now = datetime.now()
    return {
        "timezone": timezone,
        "datetime": now.isoformat(),
        "timestamp": now.timestamp(),
    }


# =========================================================
# 记忆
# =========================================================
def _memory_file() -> Path:
    return Path("/app/user/MEMORY.MD")


def memory_save(value: str):
    one_line = " ".join(str(value).splitlines())
    p = _memory_file()
    p.parent.mkdir(parents=True, exist_ok=True)
    with CFG._lock:
        prev = p.read_text(encoding="utf-8") if p.exists() else ""
        with p.open("a", encoding="utf-8", newline="\n") as f:
            if prev and not prev.endswith(chr(10)):
                f.write(chr(10))
            print("- " + one_line, file=f)
    _audit("memory_save", {"value_len": len(one_line)}, True)
    return {"ok": True, "value": one_line}


def memory_recall():
    p = _memory_file()
    text = p.read_text(encoding="utf-8") if p.exists() else ""
    lines2 = [ln[2:].strip() for ln in text.splitlines() if ln.startswith("- ")]
    _audit("memory_recall", {"count": len(lines2)}, True)
    return {"count": len(lines2), "lines": lines2}


# =========================================================
# 工具注册（schema + 分发）
# =========================================================
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "read_file",
            "description": "读取文本文件。",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "文件路径"},
                    "encoding": {"type": "string",
                                 "enum": ["utf-8", "gbk", "ascii"],
                                 "default": "utf-8"},
                },
                "required": ["path"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "write_file",
            "description": "写入文件，可追加。",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string"},
                    "content": {"type": "string"},
                    "encoding": {"type": "string",
                                 "enum": ["utf-8", "gbk", "ascii"],
                                 "default": "utf-8"},
                    "append": {"type": "boolean", "default": False},
                },
                "required": ["path", "content"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "list_directory",
            "description": "列出目录。",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string", "default": "."},
                    "recursive": {"type": "boolean", "default": False},
                },
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "delete_file",
            "description": "删除文件。",
            "parameters": {
                "type": "object",
                "properties": {"path": {"type": "string"}},
                "required": ["path"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "delete_directory",
            "description": "删除目录（可递归）。",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string"},
                    "recursive": {"type": "boolean", "default": False},
                },
                "required": ["path"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "search_files",
            "description": "按 glob 搜索文件。",
            "parameters": {
                "type": "object",
                "properties": {
                    "pattern": {"type": "string", "default": "*"},
                    "path": {"type": "string", "default": "."},
                    "max_results": {"type": "integer", "default": 50},
                },
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "grep",
            "description": "按正则搜索文件内容。",
            "parameters": {
                "type": "object",
                "properties": {
                    "pattern": {"type": "string"},
                    "path": {"type": "string", "default": "."},
                    "file_glob": {"type": "string", "default": "*"},
                    "max_results": {"type": "integer", "default": 50},
                },
                "required": ["pattern"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "execute_shell",
            "description": "执行 shell 命令。",
            "parameters": {
                "type": "object",
                "properties": {
                    "command": {"type": "string"},
                    "cwd": {"type": "string"},
                    "timeout": {"type": "integer", "default": 30},
                },
                "required": ["command"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "python_exec",
            "description": "执行 Python 代码。",
            "parameters": {
                "type": "object",
                "properties": {
                    "code": {"type": "string"},
                    "timeout": {"type": "integer", "default": 10},
                },
                "required": ["code"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "http_request",
            "description": "发起 HTTP 请求。",
            "parameters": {
                "type": "object",
                "properties": {
                    "url": {"type": "string"},
                    "method": {"type": "string", "default": "GET"},
                    "headers": {"type": "object"},
                    "params": {"type": "object"},
                    "json_body": {"type": "object"},
                    "data": {"type": "string"},
                    "timeout": {"type": "integer", "default": 30},
                },
                "required": ["url"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "计算数学表达式。",
            "parameters": {
                "type": "object",
                "properties": {"expression": {"type": "string"}},
                "required": ["expression"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "get_current_time",
            "description": "获取当前时间。",
            "parameters": {
                "type": "object",
                "properties": {"timezone": {"type": "string",
                                            "default": "Asia/Shanghai"}},
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "memory_save",
            "description": "保存一条记忆（追加一行到 MEMORY.MD）。",
            "parameters": {
                "type": "object",
                "properties": {
                    "value": {"type": "string"},
                },
                "required": ["value"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "memory_recall",
            "description": "读取全部记忆（逐行返回）。",
            "parameters": {
                "type": "object",
                "properties": {},
                "required": [],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "git_info",
            "description": "获取运行环境的基本系统信息，以及工作目录的 Git 仓库状态。",
            "parameters": {
                "type": "object",
                "properties": {},
                "required": [],
            },
        },
    },
]

TOOL_FUNCTIONS = {
    "read_file": read_file,
    "write_file": write_file,
    "list_directory": list_directory,
    "delete_file": delete_file,
    "delete_directory": delete_directory,
    "search_files": search_files,
    "grep": grep,
    "execute_shell": execute_shell,
    "python_exec": python_exec,
    "http_request": http_request,
    "calculate": calculate,
    "get_current_time": get_current_time,
    "memory_save": memory_save,
    "memory_recall": memory_recall,
    "git_info": git_info,
}


def _fmt_result(name, result):
    """把工具返回值压成一行中文，方便本地模型/网页端阅读。"""
    if isinstance(result, str):
        return result
    if isinstance(result, dict):
        if result.get("ok") == 0 or ("error" in result and "ok" not in result):
            err = result.get("error") or result.get("message") or "未知错误"
            return name + " 失败：" + str(err) + "。"
        parts = []
        for k, v in result.items():
            if k in ("ok", "tool", "error"):
                continue
            parts.append(str(k) + "=" + str(v))
        tail = "，".join(parts)
        if tail:
            return name + " 执行完成：" + tail + "。"
        return name + " 执行完成。"
    return name + " 执行完成：" + str(result)


def execute_tool(name, arguments):
    if isinstance(arguments, str):
        try:
            arguments = json.loads(arguments)
        except json.JSONDecodeError as e:
            return name + " 失败：参数不是合法 JSON: " + str(e) + "。"
    fn = TOOL_FUNCTIONS.get(name)
    if not fn:
        return "未知工具 " + name + "。"
    if arguments is None:
        arguments = {}
    if not isinstance(arguments, dict):
        return name + " 失败：参数必须是 JSON 对象。"

    try:
        result = fn(**arguments)
    except ToolError as e:
        return name + " 失败：" + str(e) + "。"
    except TypeError as e:
        return name + " 失败：参数不匹配: " + str(e) + "。"
    except Exception as e:
        return name + " 失败：" + type(e).__name__ + ": " + str(e) + "。"
    return _fmt_result(name, result)
