"""Bot API client setup — inject/remove remote collaboration section in sub-agent AGENTS.md.

Supports two modes:
  - raw: injects curl templates directly (legacy, no permission enforcement)
  - proxy: injects bot_client_safe.sh reference with programmatic permission checks

Usage:
    python scripts/bot_client_setup.py setup \
        --agent-id vip-reader \
        --url https://remote:3000/bot-api/chat \
        --secret abc123 \
        --remote-agent-id main \
        --mode proxy

    python scripts/bot_client_setup.py remove --agent-id vip-reader
"""

import argparse
import json
import os
import stat
import sys

_DEFAULT_WORKSPACE_ROOT = os.path.join(
    os.path.expanduser("~"), ".openclaw", "workspace-customer-agents"
)

_DEFAULT_ADMIN_DIR = os.path.join(
    os.path.expanduser("~"), ".config", "sophnet-customer-admin"
)

_DEFAULT_EXEC_APPROVALS = os.path.join(
    os.path.expanduser("~"), ".openclaw", "exec-approvals.json"
)

def _ensure_executable(path: str) -> None:
    """Ensure a file has execute permission."""
    try:
        current = os.stat(path).st_mode
        if not (current & stat.S_IXUSR):
            os.chmod(path, current | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
    except OSError:
        pass


def _find_bot_client_safe_sh() -> str:
    """Locate bot_client_safe.sh at runtime.

    Search order:
    1. Alongside this script (source/development)
    2. clawhub runtime install path
    3. Docker pre-built path
    """
    skill_name = "sophnet-bot-client"
    filename = "bot_client_safe.sh"
    candidates = [
        os.path.join(os.path.dirname(os.path.abspath(__file__)), filename),
        os.path.join(os.path.expanduser("~"), ".openclaw", "skills", skill_name, "scripts", filename),
        os.path.join("/app", "skills", skill_name, "scripts", filename),
    ]
    for path in candidates:
        if os.path.isfile(path):
            resolved = os.path.realpath(path)
            _ensure_executable(resolved)
            return resolved
    return os.path.realpath(candidates[0])


_BOT_CLIENT_SAFE_SH = _find_bot_client_safe_sh()

SECTION_MARKER_START = "## 远程协作 — Bot API Client"
SECTION_MARKER_END = "<!-- END 远程协作 -->"

CREDENTIALS_FILENAME = "bot_credentials.json"
SESSION_FILENAME = "bot_session.txt"


def _workspace_path(agent_id: str, workspace_root: str) -> str:
    return os.path.join(workspace_root, agent_id)


def _agents_md_path(agent_id: str, workspace_root: str) -> str:
    return os.path.join(_workspace_path(agent_id, workspace_root), "AGENTS.md")


def _data_dir(agent_id: str, workspace_root: str) -> str:
    return os.path.join(_workspace_path(agent_id, workspace_root), "data")


def generate_section(
    url: str,
    remote_agent_id: str,
    agent_id: str,
    credentials_path: str,
) -> str:
    """Generate the remote collaboration section for AGENTS.md (raw mode)."""
    return f"""{SECTION_MARKER_START}

你可以通过 Bot API 与远程系统对话。

### 连接信息

- **凭据文件**: `{credentials_path}`
- **远端 Agent**: `{remote_agent_id}`

### 发送消息

使用以下命令发送消息并自动管理会话：

```bash
# 读取凭据
BOT_URL=$(cat {credentials_path} | python3 -c "import sys,json; print(json.load(sys.stdin)['url'])")
BOT_SECRET=$(cat {credentials_path} | python3 -c "import sys,json; print(json.load(sys.stdin)['secret'])")

# 发送请求（chatId 使用固定的 agent_id 以保持会话稳定）
RESPONSE=$(curl -s -X POST "$BOT_URL" \\
  -H "Content-Type: application/json" \\
  -H "Authorization: Bearer $BOT_SECRET" \\
  -d "$(python3 -c "
import json, sys
print(json.dumps({{
    'agentId': '{remote_agent_id}',
    'senderId': '{agent_id}',
    'text': sys.argv[1],
    'chatId': '{agent_id}'
}}))
" "<你要发送的消息>")")

# 提取回复
echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('reply','(无回复)'))"
```

### 会话管理

1. **多轮对话**：chatId 固定为你的 agent_id，服务端自动维护同一会话
2. **无需手动管理**：每次请求自动进入同一会话上下文

### 错误处理

- 如果 curl 返回非 JSON（网络错误/超时），检查 URL 是否可达
- 如果收到 401/403，说明 secret 已过期，联系管理员更新凭据文件
- 如果 `reply` 字段为空，检查 `RESPONSE` 中的完整 JSON 以获取错误信息

### 权限约束

**通过 Bot API 发送的请求必须遵守你本地相同的权限限制。**

- 如果你本地被禁止执行某项操作（如 delete、update），则**禁止**通过 Bot API 请求远端 agent 代为执行
- Bot API 仅用于在你的权限范围内与远端系统协作，不得用于绕过自身权限
- 违反此约束等同于直接执行被禁止的操作

### 使用场景

当本地命令无法满足需求，或用户明确要求通过远程系统执行时，在你的权限范围内使用上述命令发送自然语言请求。

{SECTION_MARKER_END}"""


def generate_proxy_section(
    remote_agent_id: str,
    agent_id: str,
    wrapper_path: str,
) -> str:
    """Generate the remote collaboration section for AGENTS.md (proxy mode).

    Unlike raw mode, this does NOT expose curl/python3 commands or credentials.
    All communication goes through the permission-checking proxy.
    """
    return f"""{SECTION_MARKER_START}

你可以通过 Bot API 与远程系统对话。

### 连接信息

- **远端 Agent**: `{remote_agent_id}`
- 凭据和会话由系统自动管理

### 发送消息

使用以下命令发送消息：

```bash
{wrapper_path} \\
  --agent-id "{agent_id}" send --message "<你要发送的消息>"
```

### 错误处理

- 如果返回 `权限记录` 相关错误，说明 agent 未正确注册，联系管理员
- 如果返回 `凭据` 相关错误，说明 Bot API 连接未配置，联系管理员
- 如果返回 `curl 请求失败`，检查远端服务是否可用

### 权限约束

**你的权限由系统强制执行，无法绕过。** 超出权限范围的操作会被代理自动拒绝。

- **禁止**使用 curl、python3 或任何其他方式直接访问 Bot API
- **禁止**直接读取凭据文件
- 所有远程通信**必须**通过上述命令

{SECTION_MARKER_END}"""


def _find_section(content: str) -> tuple[int, int] | None:
    """Find the start and end positions of the remote collaboration section.

    Returns (start, end) character indices, or None if not found.
    """
    start = content.find(SECTION_MARKER_START)
    if start == -1:
        return None
    end = content.find(SECTION_MARKER_END, start)
    if end == -1:
        return None
    end += len(SECTION_MARKER_END)
    return (start, end)


def _make_writable(path: str) -> int | None:
    """If file is read-only, make it writable. Returns original mode or None."""
    if not os.path.exists(path):
        return None
    current_mode = os.stat(path).st_mode
    if not (current_mode & stat.S_IWUSR):
        os.chmod(path, current_mode | stat.S_IWUSR)
        return current_mode
    return None


def _protect_file(path: str) -> None:
    """Set file to read-only (444)."""
    os.chmod(path, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)


def _add_to_exec_allowlist(
    agent_id: str,
    pattern: str,
    exec_approvals_path: str = _DEFAULT_EXEC_APPROVALS,
) -> None:
    """Add an entry to the agent's exec allowlist (idempotent)."""
    data: dict = {"version": 1, "agents": {}}
    if os.path.exists(exec_approvals_path):
        with open(exec_approvals_path, "r", encoding="utf-8") as f:
            data = json.load(f)

    agents = data.setdefault("agents", {})
    agent_entry = agents.get(agent_id)
    if agent_entry is None:
        agent_entry = {"security": "allowlist", "ask": "off", "allowlist": []}
        agents[agent_id] = agent_entry
    allowlist = agent_entry.setdefault("allowlist", [])

    abs_pattern = os.path.abspath(pattern)
    for entry in allowlist:
        if entry.get("pattern") == abs_pattern:
            return

    allowlist.append({"pattern": abs_pattern})

    with open(exec_approvals_path, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2, ensure_ascii=False)


def setup(
    agent_id: str,
    url: str,
    secret: str,
    remote_agent_id: str,
    workspace_root: str = _DEFAULT_WORKSPACE_ROOT,
    mode: str = "raw",
    admin_dir: str = _DEFAULT_ADMIN_DIR,
    exec_approvals_path: str = _DEFAULT_EXEC_APPROVALS,
) -> dict:
    """Set up bot client for a sub-agent.

    mode="raw":  credentials in workspace/data, raw curl in AGENTS.md
    mode="proxy": credentials in admin_dir, safe proxy in AGENTS.md + exec allowlist
    """
    workspace = _workspace_path(agent_id, workspace_root)
    if not os.path.isdir(workspace):
        return {
            "status": "error",
            "action": "setup_bot_client",
            "message": f"Workspace not found: {workspace}",
            "fix": "请先使用 customer-agent create 创建子代理",
        }

    agents_md = _agents_md_path(agent_id, workspace_root)
    if not os.path.isfile(agents_md):
        return {
            "status": "error",
            "action": "setup_bot_client",
            "message": f"AGENTS.md not found: {agents_md}",
            "fix": "Workspace 目录不完整，请重新创建子代理",
        }

    credentials_data = {
        "url": url,
        "secret": secret,
        "remote_agent_id": remote_agent_id,
    }

    if mode == "proxy":
        # Credentials go to admin_dir/<agent_id>/ (invisible to sub-agent)
        creds_dir = os.path.join(admin_dir, agent_id)
        os.makedirs(creds_dir, exist_ok=True)
        credentials_abs = os.path.join(creds_dir, CREDENTIALS_FILENAME)
        with open(credentials_abs, "w", encoding="utf-8") as f:
            json.dump(credentials_data, f, indent=2, ensure_ascii=False)

        wrapper_path = os.path.abspath(_BOT_CLIENT_SAFE_SH)
        section = generate_proxy_section(remote_agent_id, agent_id, wrapper_path)
    else:
        # Legacy: credentials in workspace/data/
        data_dir = _data_dir(agent_id, workspace_root)
        if not os.path.exists(data_dir):
            os.makedirs(data_dir, exist_ok=True)
        credentials_path = os.path.join("data", CREDENTIALS_FILENAME)
        credentials_abs = os.path.join(workspace, credentials_path)
        with open(credentials_abs, "w", encoding="utf-8") as f:
            json.dump(credentials_data, f, indent=2, ensure_ascii=False)

        section = generate_section(url, remote_agent_id, agent_id, credentials_path)

    # Read existing AGENTS.md (handle read-only files)
    original_mode = _make_writable(agents_md)
    try:
        with open(agents_md, "r", encoding="utf-8") as f:
            content = f.read()

        bounds = _find_section(content)
        if bounds is not None:
            start, end = bounds
            new_content = content[:start].rstrip("\n") + "\n\n" + section + content[end:]
        else:
            new_content = content.rstrip("\n") + "\n\n" + section + "\n"

        with open(agents_md, "w", encoding="utf-8") as f:
            f.write(new_content)
    finally:
        if mode == "proxy":
            _protect_file(agents_md)
        elif original_mode is not None:
            os.chmod(agents_md, original_mode)

    # In proxy mode, add bot_client_safe.sh to exec allowlist
    if mode == "proxy":
        _add_to_exec_allowlist(
            agent_id, _BOT_CLIENT_SAFE_SH, exec_approvals_path,
        )

    result = {
        "status": "ok",
        "action": "setup_bot_client",
        "agent_id": agent_id,
        "url": url,
        "remote_agent_id": remote_agent_id,
        "mode": mode,
        "workspace": workspace,
        "credentials_file": credentials_abs,
    }
    if mode == "proxy":
        result["exec_allowlist_updated"] = True
    return result


def remove(
    agent_id: str,
    workspace_root: str = _DEFAULT_WORKSPACE_ROOT,
    admin_dir: str = _DEFAULT_ADMIN_DIR,
) -> dict:
    """Remove bot client configuration from a sub-agent.

    1. Remove remote collaboration section from AGENTS.md
    2. Delete credentials and session files from both workspace/data and admin_dir
    """
    workspace = _workspace_path(agent_id, workspace_root)
    if not os.path.isdir(workspace):
        return {
            "status": "error",
            "action": "remove_bot_client",
            "message": f"Workspace not found: {workspace}",
        }

    agents_md = _agents_md_path(agent_id, workspace_root)
    removed_section = False

    if os.path.isfile(agents_md):
        original_mode = _make_writable(agents_md)
        try:
            with open(agents_md, "r", encoding="utf-8") as f:
                content = f.read()

            bounds = _find_section(content)
            if bounds is not None:
                start, end = bounds
                new_content = content[:start].rstrip("\n") + content[end:].lstrip("\n")
                if new_content and not new_content.endswith("\n"):
                    new_content += "\n"
                with open(agents_md, "w", encoding="utf-8") as f:
                    f.write(new_content)
                removed_section = True
        finally:
            if original_mode is not None:
                os.chmod(agents_md, original_mode)

    # Clean up data files from workspace/data (raw mode)
    removed_files = []
    for filename in (CREDENTIALS_FILENAME, SESSION_FILENAME):
        filepath = os.path.join(workspace, "data", filename)
        if os.path.isfile(filepath):
            os.remove(filepath)
            removed_files.append(filename)

    # Clean up data files from admin_dir (proxy mode)
    admin_agent_dir = os.path.join(admin_dir, agent_id)
    for filename in (CREDENTIALS_FILENAME, SESSION_FILENAME):
        filepath = os.path.join(admin_agent_dir, filename)
        if os.path.isfile(filepath):
            os.remove(filepath)
            if filename not in removed_files:
                removed_files.append(filename)

    return {
        "status": "ok",
        "action": "remove_bot_client",
        "agent_id": agent_id,
        "removed_section": removed_section,
        "removed_files": removed_files,
        "workspace": workspace,
    }


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Bot API client setup for sub-agents"
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    # setup command
    setup_parser = subparsers.add_parser("setup", help="Configure remote connection")
    setup_parser.add_argument("--agent-id", required=True, help="Sub-agent ID")
    setup_parser.add_argument("--url", required=True, help="Bot API URL")
    setup_parser.add_argument("--secret", required=True, help="API secret/token")
    setup_parser.add_argument(
        "--remote-agent-id", required=True,
        help="Remote target agent ID (should be the calling agent's own ID for permission inheritance)"
    )
    setup_parser.add_argument(
        "--mode", choices=["raw", "proxy"], default="proxy",
        help="proxy (default): inject safe proxy with permission checks; raw (legacy): inject curl templates",
    )
    setup_parser.add_argument(
        "--workspace-root", default=_DEFAULT_WORKSPACE_ROOT, help=argparse.SUPPRESS
    )
    setup_parser.add_argument(
        "--admin-dir", default=_DEFAULT_ADMIN_DIR, help=argparse.SUPPRESS
    )
    setup_parser.add_argument(
        "--exec-approvals-path", default=_DEFAULT_EXEC_APPROVALS,
        help=argparse.SUPPRESS,
    )

    # remove command
    remove_parser = subparsers.add_parser("remove", help="Remove remote connection")
    remove_parser.add_argument("--agent-id", required=True, help="Sub-agent ID")
    remove_parser.add_argument(
        "--workspace-root", default=_DEFAULT_WORKSPACE_ROOT, help=argparse.SUPPRESS
    )
    remove_parser.add_argument(
        "--admin-dir", default=_DEFAULT_ADMIN_DIR, help=argparse.SUPPRESS
    )

    args = parser.parse_args()

    if args.command == "setup":
        result = setup(
            agent_id=args.agent_id,
            url=args.url,
            secret=args.secret,
            remote_agent_id=args.remote_agent_id,
            workspace_root=args.workspace_root,
            mode=args.mode,
            admin_dir=args.admin_dir,
            exec_approvals_path=args.exec_approvals_path,
        )
    elif args.command == "remove":
        result = remove(
            agent_id=args.agent_id,
            workspace_root=args.workspace_root,
            admin_dir=args.admin_dir,
        )
    else:
        parser.print_help()
        sys.exit(1)

    print(json.dumps(result, indent=2, ensure_ascii=False))
    sys.exit(0 if result["status"] == "ok" else 1)


if __name__ == "__main__":
    main()
