#!/usr/bin/env python3
"""Appia Claw HTTP API: agent.groups.get, agent.message.send (plaintext + md + chunking)."""

from __future__ import annotations

import argparse
import json
import os
import sys
from typing import Any
from urllib import error, parse, request

DEFAULT_CHUNK_SIZE = 3500
# 未配置 APPIA_BASE_URL / APPIA_HOST（凭证与环境均无）时使用。
DEFAULT_APPIA_BASE_URL = "https://sophgo.appia.cn"

# 凭证文件解析后的规范字段（键）→ 值；未使用凭证文件时为 None。
_CRED: dict[str, str] | None = None

# (规范 env 名, JSON / 文件中允许的别名)。文件中同一逻辑字段任一别名非空即可写入规范键。
_CRED_CANON_ALIASES: tuple[tuple[str, tuple[str, ...]], ...] = (
    ("APPIA_BASE_URL", ("APPIA_BASE_URL", "appia_base_url", "base_url", "APPIA_HOST", "appia_host")),
    ("CLAW_JWT", ("CLAW_JWT", "claw_jwt", "jwt", "APPIA_CLAW_JWT", "appia_claw_jwt")),
    ("APP_AGENT_ID", ("APP_AGENT_ID", "app_agent_id", "agent_id", "AGENT_ID")),
    ("CLAW_USER_ID", ("CLAW_USER_ID", "claw_user_id", "user_id", "USER_ID", "creator_user_id", "APPIA_USER_ID", "appia_user_id")),
    ("TARGET_RID", ("TARGET_RID", "target_rid")),
    ("TARGET_GROUP_NAME", ("TARGET_GROUP_NAME", "target_group_name")),
    ("TARGET_GROUP_MATCH", ("TARGET_GROUP_MATCH", "target_group_match")),
    ("APPIA_MSG_CHUNK_SIZE", ("APPIA_MSG_CHUNK_SIZE", "appia_msg_chunk_size")),
)


def _scalar_from_raw(raw: dict[str, Any], aliases: tuple[str, ...]) -> str:
    for key in aliases:
        if key not in raw:
            continue
        val = raw[key]
        if val is None:
            continue
        s = str(val).strip()
        if s:
            return s
    return ""


def load_cred_file(path: str) -> dict[str, str]:
    exp = os.path.expanduser(path)
    try:
        with open(exp, encoding="utf-8") as f:
            raw_any: Any = json.load(f)
    except OSError as exc:
        sys.exit(f"无法读取凭证文件 {exp}: {exc}")
    except json.JSONDecodeError as exc:
        sys.exit(f"凭证文件 JSON 无效 {exp}: {exc}")
    if not isinstance(raw_any, dict):
        sys.exit("凭证文件顶层必须是 JSON 对象")
    raw = raw_any
    out: dict[str, str] = {}
    for canon, aliases in _CRED_CANON_ALIASES:
        v = _scalar_from_raw(raw, aliases)
        if v:
            out[canon] = v
    return out


def _env(name: str, *fallbacks: str) -> str:
    for key in (name,) + fallbacks:
        v = os.environ.get(key, "").strip()
        if v:
            return v
    return ""


def _cfg(canon: str, *env_fallbacks: str) -> str:
    """凭证文件中非空字段优先，否则回退到环境变量（与原先 _env 链一致）。"""
    if _CRED:
        v = _CRED.get(canon, "").strip()
        if v:
            return v
    return _env(canon, *env_fallbacks)


def _base_url() -> str:
    base = _cfg("APPIA_BASE_URL", "APPIA_HOST").rstrip("/")
    if not base:
        base = DEFAULT_APPIA_BASE_URL.rstrip("/")
    return base


def _jwt() -> str:
    tok = _cfg("CLAW_JWT", "APPIA_CLAW_JWT")
    if not tok:
        sys.exit("缺少配置：凭证文件或环境变量 CLAW_JWT（或 APPIA_CLAW_JWT）")
    return tok


def _agent_id() -> str:
    aid = _cfg("APP_AGENT_ID", "AGENT_ID")
    if not aid:
        sys.exit("缺少配置：凭证文件或环境变量 APP_AGENT_ID（或 AGENT_ID）")
    return aid


def _user_id() -> str:
    """创建者 userId：agent.groups.get（query）与 agent.message.send（body）必填。"""
    uid = _cfg("CLAW_USER_ID", "USER_ID", "APPIA_USER_ID")
    if not uid:
        sys.exit(
            "缺少配置：凭证文件或环境变量 CLAW_USER_ID（或 USER_ID、APPIA_USER_ID、creator_user_id）"
        )
    return uid


def _groups_get(timeout: float) -> dict[str, Any]:
    base = _base_url()
    aid = _agent_id()
    user_id = _user_id()
    q = parse.urlencode({"agentId": aid, "userId": user_id})
    url = f"{base}/api/v1/claw/agent.groups.get?{q}"
    req = request.Request(
        url,
        headers={
            "Authorization": f"Bearer {_jwt()}",
            "Accept": "application/json",
        },
        method="GET",
    )
    try:
        with request.urlopen(req, timeout=timeout) as resp:
            body = resp.read().decode("utf-8")
    except error.HTTPError as exc:
        body = exc.read().decode("utf-8", errors="replace")
        raise RuntimeError(f"HTTP {exc.code}: {body}") from exc
    data = json.loads(body)
    if not data.get("success"):
        raise RuntimeError(json.dumps(data, ensure_ascii=False))
    return data


def cmd_groups(args: argparse.Namespace) -> int:
    data = _groups_get(args.timeout)
    if args.json:
        print(json.dumps(data, ensure_ascii=False, indent=2))
        return 0
    payload = data.get("data") or {}
    groups = payload.get("groups") or []
    print(f"agentId={payload.get('agentId')} total={payload.get('total', len(groups))}")
    for g in groups:
        if isinstance(g, dict):
            print(f"- rid={g.get('rid')} name={g.get('name')}")
    return 0


def cmd_verify_target(args: argparse.Namespace) -> int:
    rid = _cfg("TARGET_RID")
    name = _cfg("TARGET_GROUP_NAME")
    if not rid or not name:
        print(
            "⚠️ verify-target 需要 TARGET_RID 与 TARGET_GROUP_NAME（凭证文件或环境变量）",
            file=sys.stderr,
        )
        return 1
    mode = (_cfg("TARGET_GROUP_MATCH") or "exact").lower()
    if mode not in ("exact", "contains"):
        mode = "exact"
    data = _groups_get(args.timeout)
    payload = data.get("data") or {}
    groups = payload.get("groups") or []
    hit: dict[str, Any] | None = None
    for g in groups:
        if isinstance(g, dict) and g.get("rid") == rid:
            hit = g
            break
    if not hit:
        print("⚠️ 校验失败：TARGET_RID 不在当前机器人已加入的群列表中。", file=sys.stderr)
        return 1
    gname = (hit.get("name") or "").strip()
    name = name.strip()
    if mode == "contains":
        ok = name.lower() in gname.lower()
    else:
        ok = gname == name
    if not ok:
        print(
            f"⚠️ 校验失败：群名与 TARGET_GROUP_NAME 不一致。期望匹配模式={mode} 配置名=[{name}] 实际=[{gname}]",
            file=sys.stderr,
        )
        return 1
    print("✅ TARGET_RID 与 TARGET_GROUP_NAME 双因子校验通过。")
    return 0


def _send_request(body_obj: dict[str, Any], timeout: float) -> dict[str, Any]:
    """通用发送请求，支持 msg 或 md。"""
    base = _base_url()
    url = f"{base}/api/v1/claw/agent.message.send"
    raw = json.dumps(body_obj, ensure_ascii=False).encode("utf-8")
    req = request.Request(
        url,
        data=raw,
        headers={
            "Authorization": f"Bearer {_jwt()}",
            "Content-Type": "application/json; charset=utf-8",
            "Accept": "application/json",
        },
        method="POST",
    )
    try:
        with request.urlopen(req, timeout=timeout) as resp:
            out = resp.read().decode("utf-8")
    except error.HTTPError as exc:
        out = exc.read().decode("utf-8", errors="replace")
        raise RuntimeError(f"HTTP {exc.code}: {out}") from exc
    data = json.loads(out)
    if not data.get("success"):
        raise RuntimeError(json.dumps(data, ensure_ascii=False))
    return data


def _send_plain(rid: str, msg: str, timeout: float) -> dict[str, Any]:
    """发送纯文本消息。"""
    aid = _agent_id()
    body_obj: dict[str, Any] = {
        "rid": rid,
        "agentId": aid,
        "userId": _user_id(),
        "msg": msg,
    }
    return _send_request(body_obj, timeout)


def _send_md(rid: str, md: list[dict[str, Any]], timeout: float) -> dict[str, Any]:
    """发送 Markdown AST 消息。"""
    aid = _agent_id()
    body_obj: dict[str, Any] = {
        "rid": rid,
        "agentId": aid,
        "userId": _user_id(),
        "md": md,
    }
    return _send_request(body_obj, timeout)


def _chunk_text(text: str, size: int) -> list[str]:
    if size <= 0:
        return [text]
    if len(text) <= size:
        return [text]
    return [text[i : i + size] for i in range(0, len(text), size)]


def cmd_send(args: argparse.Namespace) -> int:
    """发送纯文本消息（支持分段）。"""
    rid = args.rid or _cfg("TARGET_RID")
    if not rid:
        print("⚠️ send 需要 TARGET_RID 或 --rid", file=sys.stderr)
        return 1
    text = ""
    if args.file:
        path = os.path.expanduser(args.file)
        with open(path, encoding="utf-8") as f:
            text = f.read()
    elif args.text is not None:
        text = args.text
    else:
        print("⚠️ 需要 --file 或 --text", file=sys.stderr)
        return 1
    chunk_raw = _cfg("APPIA_MSG_CHUNK_SIZE")
    chunk_size = int(chunk_raw) if chunk_raw else DEFAULT_CHUNK_SIZE
    parts = _chunk_text(text, chunk_size)
    for idx, part in enumerate(parts):
        data = _send_plain(rid, part, args.timeout)
        if args.json:
            print(json.dumps(data, ensure_ascii=False, indent=2))
        elif len(parts) > 1:
            print(f"[{idx + 1}/{len(parts)}] ok: {data.get('data', data)!r}")
        else:
            print(json.dumps(data, ensure_ascii=False, indent=2))
    return 0


def cmd_send_md(args: argparse.Namespace) -> int:
    """发送 Markdown AST 消息。"""
    rid = args.rid or _cfg("TARGET_RID")
    if not rid:
        print("⚠️ send-md 需要 TARGET_RID 或 --rid", file=sys.stderr)
        return 1

    md: list[dict[str, Any]] = []

    if args.md_json:
        # 从 JSON 字符串解析
        try:
            md = json.loads(args.md_json)
        except json.JSONDecodeError as exc:
            print(f"⚠️ --md-json 解析失败: {exc}", file=sys.stderr)
            return 1
    elif args.md_file:
        # 从 JSON 文件读取
        path = os.path.expanduser(args.md_file)
        try:
            with open(path, encoding="utf-8") as f:
                md = json.load(f)
        except (OSError, json.JSONDecodeError) as exc:
            print(f"⚠️ 读取 --md-file 失败: {exc}", file=sys.stderr)
            return 1
    elif args.text:
        # 从纯文本自动构造简单 md 结构
        md = [
            {
                "type": "PARAGRAPH",
                "value": [{"type": "PLAIN_TEXT", "value": args.text}]
            }
        ]
    else:
        print("⚠️ 需要 --md-json, --md-file 或 --text", file=sys.stderr)
        return 1

    if not isinstance(md, list):
        print("⚠️ md 必须是数组格式", file=sys.stderr)
        return 1

    data = _send_md(rid, md, args.timeout)
    if args.json:
        print(json.dumps(data, ensure_ascii=False, indent=2))
    else:
        print(json.dumps(data, ensure_ascii=False, indent=2))
    return 0


def main() -> int:
    global _CRED
    p = argparse.ArgumentParser(description="Appia Claw (groups.get / message.send)")
    p.add_argument("--timeout", type=float, default=60.0)
    p.add_argument(
        "--cred-file",
        "-c",
        metavar="PATH",
        help="JSON 凭证文件（含 Appia 地址、JWT、agentId 等）；文件中非空字段优先于环境变量",
    )
    sub = p.add_subparsers(dest="cmd", required=True)

    gp = sub.add_parser("groups", help="GET agent.groups.get")
    gp.add_argument("--json", action="store_true")
    gp.set_defaults(_run=cmd_groups)

    vp = sub.add_parser("verify-target", help="校验 TARGET_RID + TARGET_GROUP_NAME")
    vp.set_defaults(_run=cmd_verify_target)

    sp = sub.add_parser("send", help="POST agent.message.send（纯文本 msg；超出 APPIA_MSG_CHUNK_SIZE 则分多条）")
    sp.add_argument("--file", "-f", help="长文本文件")
    sp.add_argument("--text", "-t", help="短文本")
    sp.add_argument("--rid", help="覆盖 TARGET_RID")
    sp.add_argument("--json", action="store_true")
    sp.set_defaults(_run=cmd_send)

    smp = sub.add_parser("send-md", help="POST agent.message.send（Markdown AST md）")
    smp.add_argument("--md-json", "-j", help="md JSON 字符串")
    smp.add_argument("--md-file", "-f", help="md JSON 文件路径")
    smp.add_argument("--text", "-t", help="纯文本（自动转为简单 md 结构）")
    smp.add_argument("--rid", help="覆盖 TARGET_RID")
    smp.add_argument("--json", action="store_true")
    smp.set_defaults(_run=cmd_send_md)

    args = p.parse_args()
    if args.cred_file:
        _CRED = load_cred_file(args.cred_file)
    runner = getattr(args, "_run", None)
    if runner is None:
        return 1
    return int(runner(args))


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except Exception as exc:  # pragma: no cover - CLI
        print(f"⚠️ {exc}", file=sys.stderr)
        raise SystemExit(1)
