"""
agentpaas.engine.mcp_registry — MCP server 注册中心（docs/MCP_SKILL_DESIGN.md P1）。

单一权威源：`~/.agentpaas/mcp_servers.json`（人可编辑、可迁移）。所有写操作
走带文件锁的读-改-写（评审#12），不并发覆盖。DB 只缓存 probe 结果，不存配置。

connection 注入运行时（评审#14 在 agentpaas 侧天然成立）：每条聊天消息是
独立 run、按 config_hash 重新编译 term，禁用 server 不注入 config → 下次 run
立即生效，无需穿透 lambdagent 编译缓存。

凭证（评审#13）：auth 只存 env_key 变量名，值走 ~/.agentpaas/.env；list/probe
返回与日志只出现变量名，不出现值。

stdio 命令加固（评审#16）：command_argv 是数组（无 shell），可执行名走 allowlist。
"""
from __future__ import annotations

import json
import logging
import os
import time
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)

MCP_FILE = os.path.join(os.path.expanduser("~"), ".agentpaas", "mcp_servers.json")

# stdio 可执行 allowlist（评审#16）：已知 MCP 运行器；其他需显式加白
DEFAULT_ARGV_ALLOWLIST = {"npx", "node", "python", "python3", "uvx", "uv",
                          "docker", "deno", "bunx"}

# 破坏性动词 → 工具风险升 HIGH（与 tool_gateway 一致）
_HIGH_VERBS = ("delete", "remove", "drop", "send", "email", "exec", "execute",
               "write", "payment", "pay", "transfer", "purchase", "deploy",
               "publish", "post", "create_issue")
_ID_OK = lambda s: bool(s) and all(c.isalnum() or c in "-_." for c in s)


# ── 文件锁读写（评审#12）──────────────────────────────────────────────────────

def _load() -> Dict[str, dict]:
    try:
        with open(MCP_FILE, encoding="utf-8") as f:
            return json.load(f) or {}
    except FileNotFoundError:
        return {}
    except Exception as e:
        logger.warning("mcp_servers.json unreadable: %s", e)
        return {}


def _atomic_update(mutate) -> Dict[str, dict]:
    """带文件锁的读-改-写。mutate(data) 原地改 data；返回最终 data。"""
    import fcntl
    os.makedirs(os.path.dirname(MCP_FILE), exist_ok=True)
    # 用单独的锁文件，避免 truncate 与读冲突
    lock_path = MCP_FILE + ".lock"
    with open(lock_path, "w") as lock:
        fcntl.flock(lock, fcntl.LOCK_EX)
        try:
            data = _load()
            mutate(data)
            tmp = MCP_FILE + ".tmp"
            with open(tmp, "w", encoding="utf-8") as f:
                json.dump(data, f, ensure_ascii=False, indent=2)
            os.replace(tmp, MCP_FILE)
            return data
        finally:
            fcntl.flock(lock, fcntl.LOCK_UN)


# ── 工具风险预分级（评审#15）─────────────────────────────────────────────────

def classify_mcp_tool(tool_name: str, overrides: Dict[str, str]) -> str:
    """返回 low|medium|high。override 最高优先；破坏性动词升 high；否则 medium。"""
    if tool_name in (overrides or {}):
        return overrides[tool_name]
    low = tool_name.lower()
    if any(v in low for v in _HIGH_VERBS):
        return "high"
    return "medium"


# ── CRUD ─────────────────────────────────────────────────────────────────────

def list_servers(include_secrets: bool = False) -> List[dict]:
    """列出所有 server（默认脱敏：auth 只出现 env_key 名，不出现值）。"""
    out = []
    for sid, s in _load().items():
        out.append(_view(sid, s, include_secrets))
    return out


def get_server(server_id: str, include_secrets: bool = False) -> Optional[dict]:
    s = _load().get(server_id)
    return _view(server_id, s, include_secrets) if s else None


def _view(sid: str, s: dict, include_secrets: bool) -> dict:
    auth = dict(s.get("auth") or {})
    # 评审#13：永不返回凭证值（env_key 是变量名，安全；这里确保没有 value 字段）
    auth.pop("value", None)
    return {
        "id": sid,
        "name": s.get("name", sid),
        "transport": s.get("transport", "stdio"),
        "command_argv": s.get("command_argv", []),
        "url": s.get("url", ""),
        "auth": auth,
        "enabled": bool(s.get("enabled", True)),
        "tool_risk_overrides": s.get("tool_risk_overrides", {}),
    }


def validate_server(spec: dict, argv_allowlist: Optional[set] = None) -> List[str]:
    """校验 server spec，返回错误列表（空=通过）。"""
    errs: List[str] = []
    sid = spec.get("id", "")
    if not _ID_OK(sid):
        errs.append(f"id {sid!r} 非法（只允许字母数字 -_.）")
    transport = spec.get("transport", "")
    if transport not in ("stdio", "sse", "http"):
        errs.append(f"transport 必须是 stdio|sse|http，得到 {transport!r}")
    if transport == "stdio":
        argv = spec.get("command_argv") or []
        if not isinstance(argv, list) or not argv:
            errs.append("stdio 需要非空 command_argv 数组")
        else:
            exe = os.path.basename(str(argv[0]))
            allow = argv_allowlist if argv_allowlist is not None else DEFAULT_ARGV_ALLOWLIST
            if exe not in allow:
                errs.append(f"可执行 {exe!r} 不在 allowlist；如信任请显式加白")
    elif transport in ("sse", "http"):
        if not spec.get("url"):
            errs.append(f"{transport} 需要 url")
    return errs


def upsert_server(spec: dict, argv_allowlist: Optional[set] = None) -> dict:
    """新增/更新一个 server（已校验）。返回脱敏视图。"""
    errs = validate_server(spec, argv_allowlist)
    if errs:
        raise ValueError("; ".join(errs))
    sid = spec["id"]

    def _mut(data):
        existing = data.get(sid, {})
        data[sid] = {
            "name": spec.get("name", existing.get("name", sid)),
            "transport": spec["transport"],
            "command_argv": spec.get("command_argv", existing.get("command_argv", [])),
            "url": spec.get("url", existing.get("url", "")),
            "auth": spec.get("auth", existing.get("auth", {"kind": "none"})),
            "enabled": bool(spec.get("enabled", existing.get("enabled", True))),
            "tool_risk_overrides": spec.get("tool_risk_overrides",
                                            existing.get("tool_risk_overrides", {})),
        }
    _atomic_update(_mut)
    return get_server(sid)


def set_enabled(server_id: str, enabled: bool) -> bool:
    found = {"v": False}

    def _mut(data):
        if server_id in data:
            data[server_id]["enabled"] = bool(enabled)
            found["v"] = True
    _atomic_update(_mut)
    return found["v"]


def remove_server(server_id: str) -> bool:
    found = {"v": False}

    def _mut(data):
        if data.pop(server_id, None) is not None:
            found["v"] = True
    _atomic_update(_mut)
    return found["v"]


# ── probe：连接测试 + 工具发现 ───────────────────────────────────────────────

def probe_server(server_id: str, timeout: float = 8.0) -> dict:
    """连接 server、拉工具清单、预分级风险。返回 {status, tools, error}。

    status: ok | unreachable | auth_failed | unsupported。
    P1 实现 http/sse 的 JSON-RPC tools/list；stdio 标记 registered（启动验证）。
    """
    s = _load().get(server_id)
    if not s:
        return {"status": "unreachable", "tools": [], "error": "server 未注册"}
    transport = s.get("transport", "stdio")
    overrides = s.get("tool_risk_overrides", {})

    if transport in ("http", "sse"):
        return _probe_http(s, overrides, timeout)
    # stdio：P1 不在 probe 里 spawn 子进程（重且平台相关），返回 registered
    return {"status": "registered",
            "tools": [], "error": "stdio server 将在 agent 运行时启动验证"}


def _probe_http(s: dict, overrides: dict, timeout: float) -> dict:
    import urllib.request
    import urllib.error
    url = s.get("url", "").rstrip("/")
    headers = {"Content-Type": "application/json"}
    # 凭证：从 env 取值注入 header（评审#13：值不落库不回显）
    auth = s.get("auth") or {}
    if auth.get("kind") in ("env_bearer", "env_header"):
        val = os.environ.get(auth.get("env_key", ""), "")
        if val:
            hname = auth.get("header_name", "Authorization")
            prefix = "Bearer " if auth["kind"] == "env_bearer" else ""
            headers[hname] = f"{prefix}{val}"
    body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/list",
                       "params": {}}).encode("utf-8")
    try:
        req = urllib.request.Request(url, data=body, headers=headers, method="POST")
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            data = json.loads(resp.read().decode("utf-8"))
        raw_tools = (data.get("result") or {}).get("tools", [])
        tools = []
        for t in raw_tools:
            tname = t.get("name", "")
            tools.append({
                "name": tname,
                "description": (t.get("description") or "")[:160],
                "risk": classify_mcp_tool(tname, overrides),
            })
        return {"status": "ok", "tools": tools, "error": ""}
    except urllib.error.HTTPError as e:
        st = "auth_failed" if e.code in (401, 403) else "unreachable"
        return {"status": st, "tools": [], "error": f"HTTP {e.code}"}
    except Exception as e:
        return {"status": "unreachable", "tools": [], "error": str(e)[:120]}


# ── 运行时桥：把启用的 server 注入 agent config ──────────────────────────────

def inject_into_config(config: dict) -> dict:
    """执行 agent 前调用：把启用的 MCP server 连接信息注入 config，
    供 lambdagent `_compile_mcp_caller` 解析。只注入 enabled=true 的 server；
    禁用的不注入 → 下次 run 重编译时该工具不可用（评审#14 天然穿透）。

    现有 _compile_mcp_caller 从 config.app.mcp.custom.nodes 取连接；这里
    按其 schema 填。凭证从 env 取值注入 headers（值不落 config 持久化，
    config 仅在内存中流转给 from_config）。
    """
    servers = _load()
    if not servers:
        return config
    nodes: Dict[str, dict] = {}
    for sid, s in servers.items():
        if not s.get("enabled", True):
            continue
        if s.get("transport") not in ("http", "sse"):
            continue  # P1 运行时桥只接 http/sse；stdio 留后续
        node = {"url": s.get("url", ""), "endpoint": "", "headers": {}, "timeout": 30}
        auth = s.get("auth") or {}
        if auth.get("kind") in ("env_bearer", "env_header"):
            val = os.environ.get(auth.get("env_key", ""), "")
            if val:
                hname = auth.get("header_name", "Authorization")
                prefix = "Bearer " if auth["kind"] == "env_bearer" else ""
                node["headers"][hname] = f"{prefix}{val}"
        nodes[sid] = node
    if not nodes:
        return config
    # 深合并进 config.app.mcp.custom.nodes（不覆盖已有同名）
    cfg = dict(config)
    app = dict(cfg.get("app") or {})
    mcp = dict(app.get("mcp") or {})
    custom = dict(mcp.get("custom") or {})
    existing_nodes = dict(custom.get("nodes") or {})
    for k, v in nodes.items():
        existing_nodes.setdefault(k, v)
    custom["nodes"] = existing_nodes
    mcp["custom"] = custom
    app["mcp"] = mcp
    cfg["app"] = app
    return cfg
