#!/usr/bin/env python3
"""智能客服在线 Bug：组单并通过虾友 DM 发送给研发值班。"""

from __future__ import annotations

import argparse
import base64
import json
import os
import sys
import time
import urllib.error
import urllib.request
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Optional
import sophnet_tools

DEFAULT_TIMEOUT = 30
DEFAULT_API_BASE_URL = "https://yagent.sophnet.com/api"
DEFAULT_JWT_PATH = Path("/home/node/.openclaw/jwt.json")
DEFAULT_OPENCLAW_BASE_PATH = Path("/home/node/.openclaw/.base.json")
OSS_UPLOAD_TIMEOUT = 60

CONFIG_ALIASES: dict[str, tuple[str, ...]] = {
    "dev_friend_id": ("dev_friend_id", "DEV_FRIEND_ID", "friend_id"),
    "session_url_template": (
        "session_url_template",
        "SESSION_URL_TEMPLATE",
        "bug_session_url_template",
    ),
    "api_base_url": ("api_base_url", "API_BASE_URL", "base_url"),
    "openclaw_base_url": (
        "openclaw_base_url",
        "OPENCLAW_BASE_URL",
        "sophclaw_base_url",
    ),
}


def get_sessions_dir(agent_id: str) -> Path:
    """根据 agentId 构造 sessions 目录路径。"""
    if not agent_id.strip():
        raise AppError("agent-id 不能为空")
    return Path("/home/node/.openclaw/agents") / agent_id.strip() / "sessions"


class AppError(RuntimeError):
    pass


def configure_stdio() -> None:
    for name in ("stdout", "stderr"):
        stream = getattr(sys, name, None)
        if stream is not None and hasattr(stream, "reconfigure"):
            stream.reconfigure(encoding="utf-8", errors="replace")


def print_json(payload: Any) -> None:
    print(json.dumps(payload, ensure_ascii=False, indent=2))


def normalize_http_url(url: str, *, field_name: str = "url") -> str:
    value = (url or "").strip().rstrip("/")
    if not value:
        raise AppError(f"{field_name} 不能为空")
    if not value.startswith(("http://", "https://")):
        raise AppError(f"{field_name} 必须以 http:// 或 https:// 开头")
    return value


def load_json_file(path: Path) -> dict[str, Any]:
    try:
        with path.open("r", encoding="utf-8") as fp:
            data = json.load(fp)
    except OSError as exc:
        raise AppError(f"无法读取文件 {path}: {exc}") from exc
    except json.JSONDecodeError as exc:
        raise AppError(f"文件不是有效 JSON: {path}: {exc}") from exc
    if not isinstance(data, dict):
        raise AppError(f"JSON 根对象必须是对象: {path}")
    return data


def read_openclaw_base_url(path: Path) -> Optional[str]:
    if not path.is_file():
        return None
    try:
        data = load_json_file(path)
    except AppError:
        return None
    raw = data.get("base_url")
    if not isinstance(raw, str) or not raw.strip():
        return None
    return raw.strip().rstrip("/")


def skill_src_dir() -> Path:
    """Skill 可打包目录（与 prepublish 仅复制 src/ 一致）。"""
    return Path(__file__).resolve().parent.parent


def default_cred_file() -> Path:
    return skill_src_dir() / "secrets" / "bug-report.json"


def resolve_cred_path(explicit: Optional[str]) -> Optional[Path]:
    if explicit and str(explicit).strip():
        return Path(explicit).expanduser()
    default = default_cred_file()
    return default if default.is_file() else None


def load_cred_file(path: Optional[str], *, required: bool = False) -> dict[str, Any]:
    file_path = resolve_cred_path(path)
    if file_path is None:
        if required:
            raise AppError(
                f"未找到凭证文件，请使用 --cred-file 或确认存在：{default_cred_file()}"
            )
        return {}
    if not file_path.is_file():
        raise AppError(f"凭证文件不存在: {file_path}")
    return load_json_file(file_path)


def cfg_pick(cred: dict[str, Any], key: str, env_name: Optional[str] = None) -> Optional[str]:
    for alias in CONFIG_ALIASES.get(key, (key,)):
        value = cred.get(alias)
        if isinstance(value, str) and value.strip():
            return value.strip()
        if isinstance(value, (int, float)) and key == "dev_friend_id":
            return str(int(value))
    if env_name:
        env_val = os.environ.get(env_name)
        if isinstance(env_val, str) and env_val.strip():
            return env_val.strip()
    return None


def resolve_session_url(
    session_key: Optional[str],
    session_url: Optional[str],
    template: Optional[str],
    openclaw_base_url: Optional[str],
) -> Optional[str]:
    if isinstance(session_url, str) and session_url.strip():
        return session_url.strip()
    if not session_key or not template or not openclaw_base_url:
        return None
    try:
        return template.format(
            base_url=openclaw_base_url.rstrip("/"),
            session_key=session_key.strip(),
        )
    except (KeyError, ValueError) as exc:
        raise AppError(f"session_url_template 格式无效: {exc}") from exc


@dataclass
class BugReport:
    sender_id: str
    sender_name: str
    user_description: str
    phone_number: str
    openclaw_base_url: Optional[str] = None
    session_key: Optional[str] = None
    session_url: Optional[str] = None
    model: Optional[str] = None
    model_provider: Optional[str] = None
    context_tokens: Optional[int] = None
    total_tokens: Optional[int] = None
    oss_urls: list[str] = field(default_factory=list)
    reported_at: Optional[str] = None

    def validate(self) -> None:
        if not self.sender_id.strip():
            raise AppError("sender-id 不能为空")
        if not self.sender_name.strip():
            raise AppError("sender-name 不能为空")
        if not self.user_description.strip():
            raise AppError("description 不能为空")
        if not self.phone_number.strip():
            raise AppError("phone 不能为空")
        for url in self.oss_urls:
            normalize_http_url(url, field_name="oss-url")


def format_bug_message(report: BugReport) -> str:
    ts = report.reported_at or datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    lines = [
        f"【在线 Bug】{ts}",
        f"▎发件人：{report.sender_name.strip()}",
        f"▎ID: {report.sender_id.strip()}",
        f"▎联系电话：{report.phone_number.strip()}",
        f"▎会话：{report.session_key.strip() if report.session_key else '（无）'}",
    ]
    if report.openclaw_base_url:
        lines.append(f"▎Base URL: {report.openclaw_base_url}")
    if report.model and report.model.strip():
        model_line = report.model.strip()
        if report.model_provider and report.model_provider.strip():
            model_line = f"{report.model_provider.strip()}/{report.model.strip()}"
        lines.append(f"▎模型: {model_line}")
    if report.context_tokens is not None:
        lines.append(f"▎上下文大小: {report.context_tokens}")
    if report.total_tokens is not None:
        lines.append(f"▎当前上下文占用: {report.total_tokens}")

    if report.oss_urls:
        lines.append(f"▎附件：{report.oss_urls[0]}")
        for u in report.oss_urls[1:]:
            lines.append(f"    {u}")

    lines.append(f"▎用户描述：")
    lines.append(f"    {report.user_description.strip()}")

    return "\n".join(lines).rstrip() + "\n"


def read_bearer_token(jwt_path: Path) -> str:
    data = load_json_file(jwt_path)
    token = data.get("web_jwt")
    if not isinstance(token, str) or not token.strip():
        raise AppError(f"JWT 文件缺少有效 web_jwt: {jwt_path}")
    token = token.strip()
    if token.lower().startswith("bearer "):
        token = token[7:].strip()
    return token


def jwt_exp_unix(token: str) -> Optional[int]:
    try:
        parts = token.split(".")
        if len(parts) != 3:
            return None
        payload = parts[1] + "=" * (-len(parts[1]) % 4)
        raw = base64.urlsafe_b64decode(payload.encode("ascii"))
        data = json.loads(raw)
        exp = data.get("exp")
        return int(exp) if isinstance(exp, (int, float)) else None
    except Exception:
        return None


def ensure_jwt_valid(token: str, jwt_path: Path, allow_expired: bool) -> None:
    if allow_expired:
        return
    exp = jwt_exp_unix(token)
    if exp is None:
        return
    now = int(time.time())
    if now >= exp + 60:
        raise AppError(f"JWT 已过期，请重新登录后刷新 {jwt_path}")


def request_json(
    method: str,
    url: str,
    token: str,
    timeout: int,
    payload: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
    headers = {"Authorization": f"Bearer {token}", "Accept": "application/json"}
    body = None
    if payload is not None:
        body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
        headers["Content-Type"] = "application/json"
    req = urllib.request.Request(url=url, data=body, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            raw = resp.read().decode("utf-8", errors="replace")
    except urllib.error.HTTPError as exc:
        err_body = exc.read().decode("utf-8", errors="replace")
        hint = ""
        if exc.code == 401:
            hint = f"（请检查 {jwt_path_hint()} 中的 web_jwt）"
        raise AppError(f"HTTP {exc.code} {url}: {err_body}{hint}") from exc
    except urllib.error.URLError as exc:
        raise AppError(f"请求失败 {url}: {exc}") from exc

    try:
        data = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise AppError(f"接口返回不是有效 JSON: {url}") from exc
    if isinstance(data, dict) and data.get("status") not in (None, 0):
        raise AppError(
            f"接口错误: status={data.get('status')} message={data.get('message')!r}"
        )
    return data if isinstance(data, dict) else {"result": data}


def jwt_path_hint() -> str:
    return str(DEFAULT_JWT_PATH)


def unwrap_result(data: dict[str, Any]) -> Any:
    return data.get("result") if "result" in data else data


def get_or_create_dm(api_base: str, token: str, friend_id: int, timeout: int) -> str:
    url = f"{api_base}/sys/openclaw/friend/dm"
    data = request_json("POST", url, token, timeout, payload={"friendId": friend_id})
    result = unwrap_result(data) or {}
    channel = result.get("channel") if isinstance(result, dict) else None
    if not isinstance(channel, dict):
        channel = result if isinstance(result, dict) else {}
    rid = channel.get("_id")
    if not rid:
        raise AppError("DM 创建成功但缺少 result.channel._id")
    return str(rid)


def send_im_message(api_base: str, token: str, rid: str, text: str, timeout: int) -> dict[str, Any]:
    url = f"{api_base}/sys/openclaw/im/chat.sendMessage"
    data = request_json("POST", url, token, timeout, payload={"rid": rid, "msg": text})
    result = unwrap_result(data) or {}
    message = result.get("message") if isinstance(result, dict) else None
    return message if isinstance(message, dict) else {}


def load_friends(api_base: str, token: str, timeout: int) -> list[dict[str, Any]]:
    url = f"{api_base}/sys/openclaw/friend/list"
    data = request_json("GET", url, token, timeout)
    result = unwrap_result(data)
    if not isinstance(result, dict):
        raise AppError("虾友列表接口返回格式异常")
    friends = result.get("friends")
    if not isinstance(friends, list):
        raise AppError("虾友列表缺少 friends 数组")
    out: list[dict[str, Any]] = []
    for item in friends:
        if not isinstance(item, dict):
            continue
        fid = item.get("friendId")
        if fid is None:
            continue
        display = str(item.get("remark") or item.get("nickname") or "未命名虾友").strip()
        out.append({"friendId": fid, "displayName": display})
    return out


def find_nearest_agent_config(start_dir: Optional[Path] = None) -> Optional[Path]:
    current = (start_dir or Path.cwd()).resolve()
    for candidate in [current, *current.parents]:
        config_path = candidate / ".config.json"
        if config_path.is_file():
            return config_path
    return None


def read_model_from_agent_config(config_path: Optional[Path] = None) -> Optional[str]:
    path = config_path or find_nearest_agent_config()
    if path is None:
        return None
    try:
        data = load_json_file(path)
    except AppError:
        return None
    llm = data.get("llm")
    if isinstance(llm, str) and llm.strip():
        return llm.strip()
    model = data.get("model")
    if isinstance(model, str) and model.strip():
        return model.strip()
    return None


def resolve_sender_from_jwt(jwt_path: Path = DEFAULT_JWT_PATH) -> tuple[str, str]:
    """从 JWT payload 中解析 userId 和 name 作为 sender_id / sender_name。"""
    data = load_json_file(jwt_path)
    token = data.get("web_jwt")
    if not isinstance(token, str) or not token.strip():
        raise AppError(f"JWT 文件缺少有效 web_jwt: {jwt_path}")
    token = token.strip()
    if token.lower().startswith("bearer "):
        token = token[7:].strip()
    parts = token.split(".")
    if len(parts) != 3:
        raise AppError("web_jwt 不是有效的 JWT 格式")
    payload_b64 = parts[1]
    remainder = len(payload_b64) % 4
    if remainder:
        payload_b64 += "=" * (4 - remainder)
    try:
        raw = base64.urlsafe_b64decode(payload_b64.encode("ascii"))
        payload = json.loads(raw)
    except (ValueError, UnicodeDecodeError, json.JSONDecodeError) as exc:
        raise AppError(f"JWT payload 解码失败: {exc}") from exc
    user_id = payload.get("userId")
    if user_id is None:
        raise AppError("JWT payload 中未找到 userId 字段")
    sender_id = str(int(user_id))
    sender_name = str(payload.get("name") or "")
    if not sender_name.strip():
        sender_name = sender_id
    return sender_id, sender_name


def resolve_session_file(session_key: str, agent_id: str) -> tuple[Path, dict[str, Any]]:
    """根据 sessionKey 查找 .jsonl 会话文件，同时返回 session 元信息。"""
    if not session_key.strip():
        raise AppError("session-key 不能为空")
    if not agent_id.strip():
        raise AppError("agent-id 不能为空")
    sessions_dir = get_sessions_dir(agent_id)
    sessions_json = sessions_dir / "sessions.json"
    if not sessions_json.is_file():
        raise AppError(f"sessions.json 不存在: {sessions_json}")
    data = load_json_file(sessions_json)
    session_entry = data.get(session_key.strip())
    if not isinstance(session_entry, dict):
        raise AppError(f"sessions.json 中未找到 sessionKey={session_key} 的记录")
    session_file = Path(session_entry.get("sessionFile") or "")
    if not session_file.is_file():
        raise AppError(f"session 文件不存在: {session_file}")
    meta = {
        "modelProvider": session_entry.get("modelProvider"),
        "model": session_entry.get("model"),
        "contextTokens": session_entry.get("contextTokens"),
        "totalTokens": session_entry.get("totalTokens"),
    }
    return session_file, meta


def upload_session_to_oss(session_file: Path, timeout: int = OSS_UPLOAD_TIMEOUT) -> str:
    """上传 session 文件到 OSS，返回签名下载 URL。"""
    signed_url = sophnet_tools.upload_oss(str(session_file), timeout=timeout)
    if not signed_url:
        raise AppError("OSS 上传失败：未返回下载 URL")
    return signed_url


def build_report_from_args(args: argparse.Namespace) -> BugReport:
    cred = load_cred_file(args.cred_file)

    # sender: 显式传入优先，否则从 JWT 自动解析
    sender_id = (args.sender_id or "").strip()
    sender_name = (args.sender_name or "").strip()
    if not sender_id or not sender_name:
        jwt_id, jwt_name = resolve_sender_from_jwt()
        if not sender_id:
            sender_id = jwt_id
        if not sender_name:
            sender_name = jwt_name

    openclaw_base = (
        (args.openclaw_base_url or "").strip()
        or cfg_pick(cred, "openclaw_base_url", "OPENCLAW_BASE_URL")
        or read_openclaw_base_url(Path(args.openclaw_base_path).expanduser())
    )

    session_key = (args.session_key or "").strip() or None
    agent_id = (args.agent_id or "").strip() or None

    # OSS URL: 显式传入优先，否则根据 session_key 自动上传
    explicit_oss_urls = [normalize_http_url(u, field_name="oss-url") for u in (args.oss_url or [])]
    session_meta: dict[str, Any] = {}
    if explicit_oss_urls:
        oss_urls = explicit_oss_urls
    elif session_key and agent_id:
        session_file, session_meta = resolve_session_file(session_key, agent_id)
        oss_url = upload_session_to_oss(session_file)
        oss_urls = [oss_url]
    else:
        oss_urls = []

    template = (args.session_url_template or "").strip() or cfg_pick(
        cred, "session_url_template", "BUG_SESSION_URL_TEMPLATE"
    )
    explicit_session_url = (args.session_url or "").strip() or None
    session_url = resolve_session_url(
        session_key,
        explicit_session_url,
        template,
        openclaw_base,
    )

    model = (
        (args.model or "").strip()
        or session_meta.get("model")
        or cfg_pick(cred, "default_model", "BUG_MODEL")
        or read_model_from_agent_config()
    ) or None
    model_provider = (
        session_meta.get("modelProvider")
        or cfg_pick(cred, "model_provider")
    ) or None

    def _session_int(key: str) -> Optional[int]:
        v = session_meta.get(key)
        return int(v) if isinstance(v, (int, float)) else None

    return BugReport(
        sender_id=sender_id,
        sender_name=sender_name,
        user_description=args.description,
        phone_number=args.phone,
        openclaw_base_url=openclaw_base,
        session_key=session_key,
        session_url=session_url,
        model=model,
        model_provider=model_provider,
        context_tokens=_session_int("contextTokens"),
        total_tokens=_session_int("totalTokens"),
        oss_urls=oss_urls,
        reported_at=(args.reported_at or "").strip() or None,
    )


def resolve_dev_friend(args: argparse.Namespace, cred: dict[str, Any]) -> int:
    if args.friend_id is not None and args.friend_id > 0:
        friend_id = int(args.friend_id)
    else:
        raw = cfg_pick(cred, "dev_friend_id", "BUG_DEV_FRIEND_ID")
        if not raw:
            raise AppError("未配置研发虾友：请使用 --friend-id 或凭证文件 dev_friend_id")
        try:
            friend_id = int(raw)
        except ValueError as exc:
            raise AppError(f"dev_friend_id 不是有效整数: {raw}") from exc
    if friend_id <= 0:
        raise AppError("friend-id 必须为正整数")
    return friend_id


def request_friend(api_base: str, token: str, friend_id: int, timeout: int) -> dict[str, Any]:
    """发起好友请求。"""
    url = f"{api_base}/sys/openclaw/friend/request"
    return request_json("POST", url, token, timeout, payload={"friendId": friend_id})


def resolve_api_base(
    args: argparse.Namespace,
    cred: dict[str, Any],
) -> str:
    """从参数和凭证中解析 api_base_url，三处调用共用。"""
    return normalize_http_url(
        (args.api_base_url or "").strip()
        or cfg_pick(cred, "api_base_url", "BUG_API_BASE_URL")
        or DEFAULT_API_BASE_URL,
        field_name="api-base-url",
    )


def cmd_ensure_friend(args: argparse.Namespace) -> int:
    """ensure-friend 子命令：检查 dev_friend_id 是否已是好友，否则发起好友请求。

    退出码: 0=已是好友, 2=刚发起好友请求, 1=异常
    """
    cred = load_cred_file(getattr(args, "cred_file", None), required=True)
    friend_id = resolve_dev_friend(args, cred)
    api_base = resolve_api_base(args, cred)
    jwt_path = Path(args.jwt_path).expanduser()
    token = read_bearer_token(jwt_path)
    ensure_jwt_valid(token, jwt_path, args.allow_expired_jwt)

    friends = load_friends(api_base, token, args.timeout)
    friend_ids = {f.get("friendId") for f in friends}

    if friend_id in friend_ids:
        result = {"status": "already_friend", "friendId": friend_id}
        print_json(result)
        return 0

    request_friend(api_base, token, friend_id, args.timeout)
    result = {"status": "request_sent", "friendId": friend_id}
    print_json(result)
    return 2


def add_report_args(parser: argparse.ArgumentParser) -> None:
    parser.add_argument(
        "--cred-file",
        "-c",
        help="凭证 JSON 路径（默认 src/secrets/bug-report.json）",
    )
    parser.add_argument("--sender-id", default="", help="发件人 ID（默认从 JWT 自动解析）")
    parser.add_argument("--sender-name", default="", help="发件人展示名（默认从 JWT 自动解析）")
    parser.add_argument("--description", required=True, help="用户 Bug 描述")
    parser.add_argument("--phone", required=True, help="用户联系电话")
    parser.add_argument("--session-key", default="", help="当前会话 sessionKey")
    parser.add_argument("--agent-id", default="", help="当前 Agent ID（用于定位 sessions 目录）")
    parser.add_argument(
        "--session-url",
        default="",
        help="Session 可点开链接（优先于 template）",
    )
    parser.add_argument(
        "--session-url-template",
        default="",
        help="Session URL 模板，占位符 {base_url} {session_key}",
    )
    parser.add_argument(
        "--openclaw-base-url",
        default="",
        help="OpenClaw 实例根 URL（默认读 ~/.openclaw/.base.json）",
    )
    parser.add_argument(
        "--openclaw-base-path",
        default=str(DEFAULT_OPENCLAW_BASE_PATH),
        help="base_url JSON 路径",
    )
    parser.add_argument("--model",
        default="",
        help="用户/客服会话使用的模型（默认读凭证 default_model 或就近 .config.json 的 llm）",
    )
    parser.add_argument(
        "--oss-url",
        action="append",
        default=[],
        help="OSS 附件链接，可重复传入",
    )
    parser.add_argument("--reported-at", default="", help="工单时间，默认当前时间")


def add_http_args(parser: argparse.ArgumentParser) -> None:
    parser.add_argument(
        "--api-base-url",
        default="",
        help=f"虾友 API 根地址（默认 {DEFAULT_API_BASE_URL}）",
    )
    parser.add_argument("--jwt-path", default=str(DEFAULT_JWT_PATH))
    parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT)
    parser.add_argument("--allow-expired-jwt", action="store_true")
    parser.add_argument("--friend-id", type=int, help="研发值班虾友 friendId")


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="在线 Bug：组单并发送给研发虾友")
    sub = parser.add_subparsers(dest="command", required=True)

    p_preview = sub.add_parser("preview", help="预览工单正文（不发送）")
    add_report_args(p_preview)
    p_preview.add_argument("--json", action="store_true", help="JSON 输出")

    p_send = sub.add_parser("send", help="发送工单到研发虾友")
    add_report_args(p_send)
    add_http_args(p_send)
    p_send.add_argument("--json", action="store_true", help="JSON 输出")

    p_ensure = sub.add_parser("ensure-friend", help="校验研发虾友是否为好友，否则发起好友请求")
    p_ensure.add_argument(
        "--cred-file",
        "-c",
        help="凭证 JSON 路径（默认 src/secrets/bug-report.json）",
    )
    add_http_args(p_ensure)
    p_ensure.add_argument("--json", action="store_true", help="JSON 输出")

    p_friends = sub.add_parser("list-friends", help="列出虾友（配置 dev_friend_id 时用）")
    p_friends.add_argument(
        "--cred-file",
        "-c",
        help="凭证 JSON 路径（默认 src/secrets/bug-report.json）",
    )
    add_http_args(p_friends)

    return parser


def main(argv: Optional[list[str]] = None) -> int:
    configure_stdio()
    args = build_parser().parse_args(argv)
    try:
        if getattr(args, "timeout", DEFAULT_TIMEOUT) <= 0:
            raise AppError("timeout 必须为正整数")

        if args.command == "ensure-friend":
            return cmd_ensure_friend(args)

        if args.command == "list-friends":
            cred = load_cred_file(getattr(args, "cred_file", None))
            api_base = resolve_api_base(args, cred)
            token = read_bearer_token(Path(args.jwt_path).expanduser())
            ensure_jwt_valid(token, Path(args.jwt_path).expanduser(), args.allow_expired_jwt)
            friends = load_friends(api_base, token, args.timeout)
            print_json({"friends": friends})
            return 0

        report = build_report_from_args(args)
        report.validate()
        message = format_bug_message(report)

        if args.command == "preview":
            payload = {
                "message": message,
                "report": {
                    "senderId": report.sender_id,
                    "senderName": report.sender_name,
                    "phoneNumber": report.phone_number,
                    "model": report.model,
                    "modelProvider": report.model_provider,
                    "contextTokens": report.context_tokens,
                    "totalTokens": report.total_tokens,
                    "sessionKey": report.session_key,
                    "sessionUrl": report.session_url,
                    "ossUrls": report.oss_urls,
                },
            }
            if args.json:
                print_json(payload)
            else:
                sys.stdout.write(message)
            return 0

        cred = load_cred_file(args.cred_file, required=True)
        friend_id = resolve_dev_friend(args, cred)
        api_base = resolve_api_base(args, cred)
        jwt_path = Path(args.jwt_path).expanduser()
        token = read_bearer_token(jwt_path)
        ensure_jwt_valid(token, jwt_path, args.allow_expired_jwt)
        rid = get_or_create_dm(api_base, token, friend_id, args.timeout)
        sent = send_im_message(api_base, token, rid, message, args.timeout)
        result = {
            "success": True,
            "friend": {"friendId": friend_id},
            "rid": rid,
            "messageId": sent.get("_id"),
            "message": message,
        }
        if args.json:
            print_json(result)
        else:
            print("✅ Bug 工单已发送给研发虾友。")
            print(f"收件人 friendId={friend_id}")
            if sent.get("_id"):
                print(f"messageId: {sent.get('_id')}")
        return 0
    except AppError as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
