import base64
import hashlib
import json
import os
import random
import time
import uuid
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Tuple
from urllib.parse import quote

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

from app.log import logger
from app.utils.http import RequestUtils


@dataclass
class ILinkIncomingMessage:
    """iLink 归一化入站文本消息。"""

    user_id: str
    text: str
    username: Optional[str] = None
    message_id: Optional[str] = None
    chat_id: Optional[str] = None
    context_token: Optional[str] = None
    raw: Dict[str, Any] = field(default_factory=dict)


class ILinkClient:
    """iLink HTTP 客户端（MVP：二维码登录、文本发送、长轮询收消息）。"""

    def __init__(
        self,
        base_url: str,
        bot_token: Optional[str] = None,
        account_id: Optional[str] = None,
        sync_buf: Optional[str] = None,
        timeout: int = 20,
        log_func: Optional[Callable[[str, str], None]] = None,
    ):
        self.base_url = (base_url or "https://ilinkai.weixin.qq.com").rstrip("/")
        self.bot_token = bot_token
        self.account_id = account_id
        self.sync_buf = sync_buf
        self.timeout = timeout
        self._log_func = log_func
        self.channel_version = "1.0.2"
        self.cdn_base_url = "https://novac2c.cdn.weixin.qq.com/c2c"

    def _log(self, level: str, message: str):
        """输出客户端日志，优先写入插件日志缓冲。"""
        if self._log_func:
            try:
                self._log_func(level, f"[ILinkClient] {message}")
                return
            except Exception:
                pass

        lv = (level or "info").lower()
        if lv == "debug":
            logger.debug(f"[WechatClawBot][ILinkClient] {message}")
        elif lv == "warning":
            logger.warning(f"[WechatClawBot][ILinkClient] {message}")
        elif lv == "error":
            logger.error(f"[WechatClawBot][ILinkClient] {message}")
        else:
            logger.info(f"[WechatClawBot][ILinkClient] {message}")

    def set_credentials(
        self,
        bot_token: Optional[str],
        account_id: Optional[str] = None,
        sync_buf: Optional[str] = None,
    ) -> None:
        self.bot_token = bot_token
        self.account_id = account_id
        if sync_buf is not None:
            self.sync_buf = sync_buf

    def _headers(self, auth_required: bool = True) -> Dict[str, str]:
        headers = {
            "Content-Type": "application/json",
            "Accept": "application/json, text/plain, */*",
            "User-Agent": "MoviePilot-WechatClawBot/0.1",
        }
        if auth_required and self.bot_token:
            headers["AuthorizationType"] = "ilink_bot_token"
            headers["Authorization"] = f"Bearer {self.bot_token}"
            headers["X-WECHAT-UIN"] = self._build_wechat_uin()
        return headers

    @staticmethod
    def _build_wechat_uin() -> str:
        """生成 iLink 要求的 X-WECHAT-UIN（base64(random_uint32_decimal_string)）。"""
        random_u32 = random.getrandbits(32)
        return base64.b64encode(str(random_u32).encode("utf-8")).decode("ascii")

    def _with_base_info(self, body: Optional[Dict[str, Any]]) -> Dict[str, Any]:
        payload = dict(body or {})
        base_info = payload.get("base_info")
        if not isinstance(base_info, dict):
            base_info = {}
        base_info.setdefault("channel_version", self.channel_version)
        payload["base_info"] = base_info
        return payload

    @staticmethod
    def _json(resp) -> Dict[str, Any]:
        if not resp:
            return {}
        try:
            return resp.json() or {}
        except Exception:
            text = (getattr(resp, "text", "") or "").strip()
            if not text:
                return {}
            try:
                return json.loads(text)
            except Exception:
                return {}

    @staticmethod
    def _ok(payload: Dict[str, Any]) -> bool:
        if not payload:
            return False
        code = payload.get("errcode")
        if code is None:
            code = payload.get("code")
        if code is None:
            code = payload.get("ret")
        if code is None:
            err = payload.get("errmsg") or payload.get("error") or payload.get("error_msg")
            if err and str(err).strip().lower() not in {"ok", "success", "succeed"}:
                return False
            state = payload.get("status") or payload.get("state")
            if isinstance(state, str) and state.strip().lower() in {"error", "failed", "fail"}:
                return False
            return True
        try:
            return int(str(code)) == 0
        except Exception:
            return str(code).strip().lower() in {"0", "ok", "success", "succeed"}

    @staticmethod
    def _short_text(value: Any, max_len: int = 240) -> str:
        """将任意响应内容缩短为单行日志，避免日志过长。"""
        if value is None:
            return ""
        if isinstance(value, (dict, list)):
            try:
                text = json.dumps(value, ensure_ascii=False)
            except Exception:
                text = str(value)
        else:
            text = str(value)
        text = text.replace("\n", " ").replace("\r", " ").strip()
        if len(text) > max_len:
            return f"{text[:max_len]}..."
        return text

    def _is_send_success(self, payload: Dict[str, Any]) -> bool:
        """发送接口成功判定，兼容不同返回格式。"""
        if not payload:
            return False

        # 优先使用显式状态码判断。
        code = self._find_first_value(payload, ["errcode", "code", "ret", "result_code", "status_code"])
        if code is not None:
            try:
                return int(str(code)) == 0
            except Exception:
                return str(code).strip().lower() in {"0", "ok", "success", "succeed"}

        # 兼容布尔成功标记。
        success_flag = self._find_first_value(payload, ["success", "ok", "is_success", "sent"])
        if isinstance(success_flag, bool):
            return success_flag
        if success_flag is not None:
            if str(success_flag).strip().lower() in {"1", "true", "ok", "success", "succeed", "sent"}:
                return True

        # 兼容状态字段。
        state = self._find_first_value(payload, ["status", "state", "send_status"])
        if state is not None:
            if str(state).strip().lower() in {"ok", "success", "succeed", "sent", "done"}:
                return True
            if str(state).strip().lower() in {"failed", "error", "denied"}:
                return False

        # 兜底：明确错误信息判定失败。
        err_text = self._find_first_value(payload, ["errmsg", "error", "error_msg", "detail"])
        if err_text is not None and str(err_text).strip():
            err_s = str(err_text).strip().lower()
            if err_s not in {"ok", "success", "succeed", "sent"}:
                return False

        return False

    def _is_send_explicit_failure(self, payload: Dict[str, Any]) -> bool:
        """判断返回是否明确表示失败。"""
        if not payload:
            return False

        code = self._find_first_value(payload, ["errcode", "code", "ret", "result_code", "status_code"])
        if code is not None:
            try:
                return int(str(code)) != 0
            except Exception:
                return str(code).strip().lower() not in {"0", "ok", "success", "succeed"}

        success_flag = self._find_first_value(payload, ["success", "ok", "is_success", "sent"])
        if isinstance(success_flag, bool):
            return not success_flag
        if success_flag is not None:
            val = str(success_flag).strip().lower()
            if val in {"0", "false", "fail", "failed", "error", "denied"}:
                return True

        state = self._find_first_value(payload, ["status", "state", "send_status"])
        if state is not None:
            val = str(state).strip().lower()
            if val in {"failed", "error", "denied", "forbidden", "blocked"}:
                return True

        err_text = self._find_first_value(payload, ["errmsg", "error", "error_msg", "detail"])
        if err_text is not None and str(err_text).strip():
            val = str(err_text).strip().lower()
            if val not in {"ok", "success", "succeed", "sent"}:
                return True

        return False

    def _is_send_http_success(self, resp: Any, payload: Dict[str, Any]) -> bool:
        """官方实现以 HTTP 成功为准；仅在返回明确失败时判为失败。"""
        if resp is None:
            return False
        status_code = getattr(resp, "status_code", None)
        if status_code is None:
            return False
        try:
            status_ok = 200 <= int(status_code) < 300
        except Exception:
            status_ok = False
        if not status_ok:
            return False
        if not payload:
            return True
        return not self._is_send_explicit_failure(payload)

    @staticmethod
    def _build_user_candidates(to_user: str) -> List[str]:
        """构造多种收件人ID格式，兼容 iLink 接口差异。"""
        raw = str(to_user or "").strip()
        if not raw:
            return []

        candidates: List[str] = [raw]
        if "@" in raw:
            candidates.append(raw.split("@", 1)[0])
        if raw.endswith("@im.wechat"):
            candidates.append(raw[:-len("@im.wechat")])
        else:
            candidates.append(f"{raw}@im.wechat")

        uniq: List[str] = []
        for item in candidates:
            value = str(item or "").strip()
            if value and value not in uniq:
                uniq.append(value)
        return uniq

    @staticmethod
    def _build_text_payloads(user_id: str, text: str) -> List[Dict[str, Any]]:
        """构造多种发送请求体，提升 sendmessage 协议兼容性。"""
        return [
            {
                "to_user": user_id,
                "msg_type": "text",
                "text": {"content": text},
            },
            {
                "to_user": user_id,
                "msg_type": "text",
                "text": text,
            },
            {
                "touser": user_id,
                "msgtype": "text",
                "text": {"content": text},
            },
            {
                "touser": user_id,
                "msgtype": "text",
                "text": text,
            },
            {
                "to": user_id,
                "type": "text",
                "content": text,
            },
            {
                "to_user_id": user_id,
                "msg_type": "text",
                "content": text,
            },
            {
                "receiver": user_id,
                "msg_type": "text",
                "text": {"content": text},
            },
            {
                "to_user": user_id,
                "message_type": "text",
                "content": text,
            },
            {
                "to_user": user_id,
                "msg_type": 1,
                "text": {"content": text},
            },
        ]

    @staticmethod
    def _aes_ecb_padded_size(plaintext_size: int) -> int:
        return ((int(plaintext_size) + 1 + 15) // 16) * 16

    @staticmethod
    def _encrypt_aes_ecb(plaintext: bytes, key: bytes) -> bytes:
        cipher = AES.new(key, AES.MODE_ECB)
        return cipher.encrypt(pad(plaintext, AES.block_size))

    @staticmethod
    def _encode_media_aes_key(aeskey: bytes) -> str:
        """与官方 openclaw-weixin 保持一致：base64(hex_string)。"""
        return base64.b64encode(aeskey.hex().encode("ascii")).decode("ascii")

    def _build_protocol_msg_payload(self, user_id: str, text: str, context_token: Optional[str]) -> Dict[str, Any]:
        msg = {
            "from_user_id": str(self.account_id or ""),
            "to_user_id": user_id,
            "client_id": f"mp-{uuid.uuid4()}",
            "message_type": 2,
            "message_state": 2,
            "item_list": [
                {
                    "type": 1,
                    "text_item": {
                        "text": text,
                    },
                }
            ],
        }
        if context_token:
            msg["context_token"] = context_token
        return {"msg": msg}

    def _build_protocol_image_payload(
        self,
        user_id: str,
        context_token: Optional[str],
        download_param: str,
        aeskey_b64: str,
        cipher_size: int,
    ) -> Dict[str, Any]:
        msg: Dict[str, Any] = {
            "from_user_id": "",
            "to_user_id": user_id,
            "client_id": f"mp-{uuid.uuid4()}",
            "message_type": 2,
            "message_state": 2,
            "item_list": [
                {
                    "type": 2,
                    "image_item": {
                        "media": {
                            "encrypt_query_param": download_param,
                            "aes_key": aeskey_b64,
                            "encrypt_type": 1,
                        },
                        "mid_size": int(cipher_size),
                    },
                }
            ],
        }
        if context_token:
            msg["context_token"] = context_token
        return {"msg": msg}

    def _request_upload_param(
        self,
        to_user: str,
        plaintext: bytes,
    ) -> Tuple[Optional[str], Optional[str], Optional[bytes], Optional[int], Optional[str]]:
        rawsize = len(plaintext)
        rawfilemd5 = hashlib.md5(plaintext).hexdigest()
        filesize = self._aes_ecb_padded_size(rawsize)
        filekey = os.urandom(16).hex()
        aeskey = os.urandom(16)

        body = self._with_base_info(
            {
                "filekey": filekey,
                "media_type": 1,
                "to_user_id": to_user,
                "rawsize": rawsize,
                "rawfilemd5": rawfilemd5,
                "filesize": filesize,
                "no_need_thumb": True,
                "aeskey": aeskey.hex(),
            }
        )

        url = f"{self.base_url}/ilink/bot/getuploadurl"
        resp = RequestUtils(headers=self._headers(auth_required=True), timeout=self.timeout).post(url, json=body)
        payload = self._json(resp)
        upload_param = (
            self._find_first_value(payload, ["upload_param", "uploadParam"])
            if payload
            else None
        )
        upload_full_url = (
            self._find_first_value(payload, ["upload_full_url", "uploadFullUrl", "full_url"])
            if payload
            else None
        )
        if not upload_param and not upload_full_url:
            self._log(
                "warning",
                f"getuploadurl 失败: http={getattr(resp, 'status_code', None)}, resp={self._short_text(payload or getattr(resp, 'text', ''))}",
            )
            return None, None, None, None, None

        return (
            str(upload_param) if upload_param else None,
            str(upload_full_url) if upload_full_url else None,
            aeskey,
            filesize,
            filekey,
        )

    def _upload_encrypted_to_cdn(
        self,
        upload_param: Optional[str],
        upload_full_url: Optional[str],
        filekey: str,
        plaintext: bytes,
        aeskey: bytes,
    ) -> Tuple[Optional[str], Optional[int]]:
        ciphertext = self._encrypt_aes_ecb(plaintext, aeskey)
        if upload_full_url:
            upload_url = str(upload_full_url).strip()
        elif upload_param:
            upload_url = (
                f"{self.cdn_base_url}/upload?encrypted_query_param={quote(str(upload_param), safe='')}&"
                f"filekey={quote(filekey, safe='')}"
            )
        else:
            self._log("warning", "CDN 上传失败: 缺少 upload_url 参数")
            return None, None

        resp = RequestUtils(
            headers={"Content-Type": "application/octet-stream"},
            timeout=self.timeout,
        ).post(upload_url, data=ciphertext)

        status_code = getattr(resp, "status_code", None)
        if status_code != 200:
            self._log(
                "warning",
                f"CDN 上传失败: http={status_code}, err={self._short_text(getattr(resp, 'text', ''))}",
            )
            return None, None

        download_param = None
        if resp is not None and getattr(resp, "headers", None):
            download_param = resp.headers.get("x-encrypted-param")
        if not download_param:
            self._log("warning", "CDN 上传成功但缺少 x-encrypted-param")
            return None, None

        return str(download_param), len(ciphertext)

    def get_qrcode(self) -> Dict[str, Any]:
        url = f"{self.base_url}/ilink/bot/get_bot_qrcode?bot_type=3"
        self._log("debug", f"请求二维码: {url}")
        resp = RequestUtils(headers=self._headers(auth_required=False), timeout=self.timeout).get_res(url)
        payload = self._json(resp)
        if not payload:
            self._log("warning", "二维码接口返回空响应")
            return {"success": False, "message": "获取二维码失败"}

        data = payload.get("data") or payload.get("result") or payload
        qrcode = (
            data.get("qrcode")
            or data.get("qr_code")
            or data.get("qrcode_id")
            or data.get("ticket")
        )
        qrcode_url = (
            data.get("qrcode_url")
            or data.get("url")
            or data.get("qrcodeUrl")
            or data.get("qr_url")
            or data.get("qrcode_img_content")
            or data.get("qrcode_img_url")
            or data.get("qr_img")
        )

        # 某些返回仅给出 qrcode id，补一个已知可扫码链接兜底。
        if not qrcode_url and qrcode:
            qrcode_url = f"https://liteapp.weixin.qq.com/q/7GiQu1?qrcode={qrcode}&bot_type=3"

        result = {
            "success": self._ok(payload) and bool(qrcode or qrcode_url),
            "qrcode": qrcode,
            "qrcode_url": qrcode_url,
            "raw": payload,
            "message": payload.get("errmsg") or payload.get("message"),
        }
        self._log(
            "info" if result.get("success") else "warning",
            f"二维码解析结果: success={result.get('success')}, qrcode={result.get('qrcode')}, has_url={bool(result.get('qrcode_url'))}",
        )
        return result

    def get_qrcode_status(self, qrcode: str) -> Dict[str, Any]:
        url = f"{self.base_url}/ilink/bot/get_qrcode_status"
        self._log("debug", f"查询二维码状态: qrcode={qrcode}")
        resp = RequestUtils(headers=self._headers(auth_required=False), timeout=self.timeout).get_res(
            url, params={"qrcode": qrcode}
        )
        payload = self._json(resp)
        if not payload:
            # 某些代理场景下 params 透传异常，补一次显式 query 兜底。
            retry_resp = RequestUtils(headers=self._headers(auth_required=False), timeout=self.timeout).get_res(
                f"{url}?qrcode={qrcode}"
            )
            payload = self._json(retry_resp)
        if not payload:
            self._log("warning", "二维码状态接口返回空响应")
            return {
                "success": False,
                "status": "waiting",
                "token": None,
                "account_id": None,
                "raw": {},
                "message": "二维码状态接口返回空响应",
            }

        data = payload.get("data") or payload.get("result") or payload

        token = (
            data.get("bot_token")
            or data.get("token")
            or data.get("access_token")
            or self._find_first_value(data, ["bot_token", "access_token", "token", "jwt", "auth_token"])
        )
        account_id = (
            data.get("account_id")
            or data.get("ilink_bot_id")
            or data.get("wxid")
            or data.get("uid")
            or data.get("user_id")
            or self._find_first_value(data, ["account_id", "ilink_bot_id", "wxid", "uid", "user_id", "from_user", "from_uid"])
        )
        base_url = (
            data.get("baseurl")
            or data.get("base_url")
            or payload.get("baseurl")
            or payload.get("base_url")
        )

        if token:
            self.bot_token = token
        if account_id:
            self.account_id = str(account_id)

        state = (
            data.get("status")
            or data.get("state")
            or payload.get("status")
            or payload.get("state")
            or self._find_first_value(data, ["status", "state", "scan_status"])
            or "waiting"
        )

        result = {
            "success": self._ok(payload),
            "status": str(state).lower(),
            "token": token,
            "account_id": account_id,
            "base_url": base_url,
            "raw": payload,
            "message": payload.get("errmsg") or payload.get("message"),
        }
        self._log(
            "debug",
            f"二维码状态结果: success={result.get('success')}, status={result.get('status')}, has_token={bool(result.get('token'))}",
        )
        return result

    def send_text(self, to_user: str, text: str, context_token: Optional[str] = None) -> bool:
        if not self.bot_token:
            self._log("warning", "发送消息失败：bot token 未配置")
            return False
        if not to_user or not text:
            self._log("warning", "发送消息失败：to_user 或 text 为空")
            return False

        url_candidates = [
            f"{self.base_url}/ilink/bot/sendmessage",
            f"{self.base_url}/ilink/bot/sendmessage?bot_type=3",
        ]
        user_candidates = self._build_user_candidates(to_user)

        last_error = ""
        for user_id in user_candidates:
            payload_candidates = [
                self._build_protocol_msg_payload(user_id=user_id, text=text, context_token=context_token),
                *self._build_text_payloads(user_id=user_id, text=text),
            ]
            for url in url_candidates:
                for idx, body in enumerate(payload_candidates, start=1):
                    request_body = self._with_base_info(body)
                    resp = RequestUtils(headers=self._headers(auth_required=True), timeout=self.timeout).post(
                        url,
                        json=request_body,
                    )
                    payload = self._json(resp)
                    if self._is_send_success(payload) or self._is_send_http_success(resp, payload):
                        self._log("info", f"发送消息成功: to_user={user_id}, variant={idx}")
                        return True

                    http_code = getattr(resp, "status_code", None)
                    err_msg = (
                        self._find_first_value(payload, ["errmsg", "message", "error", "detail"])
                        if payload
                        else None
                    )
                    if not err_msg and resp is not None:
                        err_msg = self._short_text(getattr(resp, "text", ""))

                    last_error = f"http={http_code}, err={self._short_text(err_msg)}"
                    self._log(
                        "debug",
                        f"发送候选失败: to_user={user_id}, variant={idx}, {last_error}, req={self._short_text(request_body)}, resp={self._short_text(payload)}",
                    )

        self._log("warning", f"发送消息失败: to_user={to_user}, {last_error}")
        return False

    def send_image_text_png(
        self,
        to_user: str,
        image_bytes: bytes,
        text: str,
        context_token: Optional[str] = None,
    ) -> bool:
        """发送图文消息（兼容模式：文本与图片分两条发送）。"""
        if not self.bot_token:
            self._log("warning", "发送图文失败：bot token 未配置")
            return False
        if not to_user or not image_bytes or not text:
            self._log("warning", "发送图文失败：to_user 或 image_bytes 或 text 为空")
            return False

        url_candidates = [
            f"{self.base_url}/ilink/bot/sendmessage",
            f"{self.base_url}/ilink/bot/sendmessage?bot_type=3",
        ]

        last_error = ""
        for user_id in self._build_user_candidates(to_user):
            upload_param, upload_full_url, aeskey, _, filekey = self._request_upload_param(user_id, image_bytes)
            if (not upload_param and not upload_full_url) or not aeskey or not filekey:
                continue

            download_param, cipher_size = self._upload_encrypted_to_cdn(
                upload_param=upload_param,
                upload_full_url=upload_full_url,
                filekey=filekey,
                plaintext=image_bytes,
                aeskey=aeskey,
            )
            if not download_param or not cipher_size:
                continue

            aeskey_b64 = self._encode_media_aes_key(aeskey)
            message_items: List[Dict[str, Any]] = [
                {
                    "type": 1,
                    "text_item": {
                        "text": text,
                    },
                },
                {
                    "type": 2,
                    "image_item": {
                        "media": {
                            "encrypt_query_param": download_param,
                            "aes_key": aeskey_b64,
                            "encrypt_type": 1,
                        },
                        "mid_size": int(cipher_size),
                    },
                },
            ]

            sent_all = True
            for item in message_items:
                item_sent = False
                item_type = item.get("type")
                for url in url_candidates:
                    msg: Dict[str, Any] = {
                        "from_user_id": "",
                        "to_user_id": user_id,
                        "client_id": f"mp-{uuid.uuid4()}",
                        "message_type": 2,
                        "message_state": 2,
                        "item_list": [item],
                    }
                    if context_token:
                        msg["context_token"] = context_token

                    body = {"msg": msg}
                    request_body = self._with_base_info(body)
                    resp = RequestUtils(headers=self._headers(auth_required=True), timeout=self.timeout).post(
                        url,
                        json=request_body,
                    )
                    payload = self._json(resp)
                    if self._is_send_success(payload) or self._is_send_http_success(resp, payload):
                        item_sent = True
                        break

                    http_code = getattr(resp, "status_code", None)
                    err_msg = (
                        self._find_first_value(payload, ["errmsg", "message", "error", "detail"])
                        if payload
                        else None
                    )
                    if not err_msg and resp is not None:
                        err_msg = self._short_text(getattr(resp, "text", ""))
                    last_error = f"http={http_code}, err={self._short_text(err_msg)}"
                    self._log(
                        "debug",
                        f"发送图文子消息失败: to_user={user_id}, item_type={item_type}, {last_error}, req={self._short_text(request_body)}, resp={self._short_text(payload)}",
                    )

                if not item_sent:
                    sent_all = False
                    break

            if sent_all:
                self._log("info", f"发送图文成功: to_user={user_id}, mode=split_items")
                return True

        self._log("warning", f"发送图文失败: to_user={to_user}, {last_error}")
        return False

    def send_image_png(self, to_user: str, image_bytes: bytes, context_token: Optional[str] = None) -> bool:
        if not self.bot_token:
            self._log("warning", "发送图片失败：bot token 未配置")
            return False
        if not to_user or not image_bytes:
            self._log("warning", "发送图片失败：to_user 或 image_bytes 为空")
            return False

        url_candidates = [
            f"{self.base_url}/ilink/bot/sendmessage",
            f"{self.base_url}/ilink/bot/sendmessage?bot_type=3",
        ]

        last_error = ""
        for user_id in self._build_user_candidates(to_user):
            upload_param, upload_full_url, aeskey, _, filekey = self._request_upload_param(user_id, image_bytes)
            if (not upload_param and not upload_full_url) or not aeskey or not filekey:
                continue

            download_param, cipher_size = self._upload_encrypted_to_cdn(
                upload_param=upload_param,
                upload_full_url=upload_full_url,
                filekey=filekey,
                plaintext=image_bytes,
                aeskey=aeskey,
            )
            if not download_param or not cipher_size:
                continue

            aeskey_b64 = self._encode_media_aes_key(aeskey)
            body = self._build_protocol_image_payload(
                user_id=user_id,
                context_token=context_token,
                download_param=download_param,
                aeskey_b64=aeskey_b64,
                cipher_size=cipher_size,
            )

            for url in url_candidates:
                request_body = self._with_base_info(body)
                resp = RequestUtils(headers=self._headers(auth_required=True), timeout=self.timeout).post(
                    url,
                    json=request_body,
                )
                payload = self._json(resp)
                if self._is_send_success(payload) or self._is_send_http_success(resp, payload):
                    self._log("info", f"发送图片成功: to_user={user_id}")
                    return True

                http_code = getattr(resp, "status_code", None)
                err_msg = (
                    self._find_first_value(payload, ["errmsg", "message", "error", "detail"])
                    if payload
                    else None
                )
                if not err_msg and resp is not None:
                    err_msg = self._short_text(getattr(resp, "text", ""))
                last_error = f"http={http_code}, err={self._short_text(err_msg)}"
                self._log(
                    "debug",
                    f"发送图片失败: to_user={user_id}, {last_error}, req={self._short_text(request_body)}, resp={self._short_text(payload)}",
                )

        self._log("warning", f"发送图片失败: to_user={to_user}, {last_error}")
        return False

    def _extract_updates(self, payload: Dict[str, Any]) -> Tuple[List[Dict[str, Any]], Optional[str]]:
        data = payload.get("data") or payload.get("result") or payload
        sync_buf = (
            data.get("get_updates_buf")
            or payload.get("get_updates_buf")
            or data.get("sync_buf")
            or data.get("syncBuf")
            or payload.get("sync_buf")
            or payload.get("syncBuf")
            or self._find_first_value(data, ["get_updates_buf", "sync_buf", "syncBuf", "cursor", "offset", "next_sync_buf"])
        )

        list_keys = [
            "msgs",
            "updates",
            "messages",
            "items",
            "events",
            "msg_list",
            "msgList",
            "add_msgs",
            "addMsgs",
            "records",
            "list",
        ]

        for obj in [data, payload]:
            for key in list_keys:
                value = obj.get(key)
                if isinstance(value, list):
                    return value, sync_buf

        nested = self._find_first_list(data, prefer_keys=list_keys)
        if isinstance(nested, list):
            return nested, sync_buf

        if isinstance(data, list):
            return data, sync_buf

        # 少数接口返回单条消息对象。
        if isinstance(data, dict):
            for key in ["message", "msg", "event", "item"]:
                item = data.get(key)
                if isinstance(item, dict):
                    return [item], sync_buf
        return [], sync_buf

    @staticmethod
    def _pick_value(obj: Dict[str, Any], keys: List[str]) -> Optional[Any]:
        for key in keys:
            if key in obj and obj.get(key) not in (None, ""):
                return obj.get(key)
        return None

    @classmethod
    def _find_first_value(cls, data: Any, keys: List[str], max_depth: int = 5) -> Optional[Any]:
        if max_depth < 0 or data is None:
            return None
        if isinstance(data, dict):
            direct = cls._pick_value(data, keys)
            if direct not in (None, ""):
                return direct
            for value in data.values():
                found = cls._find_first_value(value, keys, max_depth - 1)
                if found not in (None, ""):
                    return found
        elif isinstance(data, list):
            for value in data:
                found = cls._find_first_value(value, keys, max_depth - 1)
                if found not in (None, ""):
                    return found
        return None

    @classmethod
    def _find_first_list(cls, data: Any, prefer_keys: List[str], max_depth: int = 5) -> Optional[List[Any]]:
        if max_depth < 0 or data is None:
            return None
        if isinstance(data, dict):
            for key in prefer_keys:
                value = data.get(key)
                if isinstance(value, list):
                    return value
            for value in data.values():
                found = cls._find_first_list(value, prefer_keys, max_depth - 1)
                if found is not None:
                    return found
        elif isinstance(data, list):
            if data and all(isinstance(item, dict) for item in data):
                return data
            for value in data:
                found = cls._find_first_list(value, prefer_keys, max_depth - 1)
                if found is not None:
                    return found
        return None

    @staticmethod
    def _as_scalar(value: Any) -> Optional[Any]:
        if value in (None, ""):
            return None
        if isinstance(value, (dict, list, tuple, set)):
            return None
        return value

    def _parse_incoming(self, item: Dict[str, Any]) -> Optional[ILinkIncomingMessage]:
        if not isinstance(item, dict):
            return None

        message = item
        for key in ["message", "msg", "event", "payload", "data"]:
            child = item.get(key)
            if isinstance(child, dict):
                message = child
                break

        sender = (
            message.get("from")
            if isinstance(message.get("from"), dict)
            else message.get("sender")
            if isinstance(message.get("sender"), dict)
            else message.get("user")
            if isinstance(message.get("user"), dict)
            else message.get("from_user")
            if isinstance(message.get("from_user"), dict)
            else {}
        )

        user_id = (
            self._pick_value(sender, ["user_id", "id", "wxid", "uid"])
            or self._pick_value(message, [
                "from_user",
                "from_user_id",
                "user_id",
                "uid",
                "wxid",
                "from_uid",
                "fromUser",
                "fromUserId",
                "openid",
            ])
            or self._pick_value(item, [
                "from_user",
                "from_user_id",
                "user_id",
                "uid",
                "wxid",
                "from_uid",
                "fromUser",
                "fromUserId",
                "openid",
            ])
            or self._find_first_value(message, [
                "from_user",
                "from_user_id",
                "user_id",
                "sender_id",
                "uid",
                "wxid",
                "from_uid",
                "fromUserId",
                "openid",
            ])
        )
        user_id = self._as_scalar(user_id)
        if not user_id:
            return None

        text = None
        item_list = message.get("item_list") if isinstance(message.get("item_list"), list) else []
        for one in item_list:
            if not isinstance(one, dict):
                continue
            item_type = one.get("type")
            if item_type == 1 and isinstance(one.get("text_item"), dict):
                text = self._pick_value(one.get("text_item") or {}, ["text", "content"])
                if text:
                    break

        if isinstance(message.get("text"), dict):
            text = self._pick_value(message.get("text") or {}, ["content", "text", "value", "msg"])
        if not text:
            text = self._pick_value(message, ["content", "message", "msg", "text", "body", "msg_content", "msgContent"])
        if not text:
            text = self._pick_value(item, ["content", "message", "msg", "text", "body", "msg_content", "msgContent"])
        if not text:
            text = self._find_first_value(message, ["content", "text", "message", "msg", "body", "cmd"]) 

        if not text:
            return None

        if isinstance(text, dict):
            text = self._pick_value(text, ["content", "text", "value", "message"])
        if not isinstance(text, str):
            text = str(text)

        username = (
            self._pick_value(sender, ["name", "nickname", "username", "remark"])
            or self._pick_value(message, ["username", "nickname", "from_name", "fromNick", "sender_name"])
            or str(user_id)
        )
        message_id = (
            self._pick_value(message, ["message_id", "msg_id", "id", "client_msg_id", "msgId"])
            or self._pick_value(item, ["message_id", "msg_id", "id", "client_msg_id", "msgId"])
        )
        chat_id = (
            self._pick_value(message, ["chat_id", "conversation_id", "room_id", "chatId", "conversationId", "roomId"])
            or self._pick_value(item, ["chat_id", "conversation_id", "room_id", "chatId", "conversationId", "roomId"])
        )
        context_token = (
            self._pick_value(message, ["context_token", "contextToken"])
            or self._pick_value(item, ["context_token", "contextToken"])
        )

        return ILinkIncomingMessage(
            user_id=str(user_id),
            text=str(text),
            username=str(username) if username else None,
            message_id=str(message_id) if message_id else None,
            chat_id=str(chat_id) if chat_id else None,
            context_token=str(context_token) if context_token else None,
            raw=item,
        )

    def poll_updates(self, timeout_seconds: int = 25) -> Tuple[List[ILinkIncomingMessage], Optional[str], Dict[str, Any]]:
        if not self.bot_token:
            self._log("warning", "轮询失败：bot token 未配置")
            return [], self.sync_buf, {"success": False, "message": "bot token 未配置"}

        url = f"{self.base_url}/ilink/bot/getupdates"
        payload = {}
        body_candidates = [
            {
                "get_updates_buf": self.sync_buf or "",
            },
            {
                "sync_buf": self.sync_buf,
                "timeout": timeout_seconds,
            },
            {
                "syncBuf": self.sync_buf,
                "timeout": timeout_seconds,
            },
            {
                "sync_buf": self.sync_buf,
                "wait": timeout_seconds,
            },
        ]

        for body in body_candidates:
            request_body = self._with_base_info(body)
            resp = RequestUtils(headers=self._headers(auth_required=True), timeout=timeout_seconds + 10).post(
                url,
                json=request_body,
            )
            payload = self._json(resp)
            if payload and self._ok(payload):
                break
            if payload and self._find_first_list(payload, prefer_keys=["updates", "messages", "items", "events", "add_msgs", "msgs"]):
                break

        if not payload:
            self._log("warning", "轮询接口返回空响应")
            return [], self.sync_buf, {"success": False, "message": "轮询返回空响应"}

        items, sync_buf = self._extract_updates(payload)
        parsed: List[ILinkIncomingMessage] = []
        for item in items:
            msg = self._parse_incoming(item)
            if msg:
                parsed.append(msg)

        if sync_buf is not None:
            self.sync_buf = str(sync_buf)

        result = {
            "success": self._ok(payload),
            "raw": payload,
            "message": payload.get("errmsg") or payload.get("message"),
            "item_count": len(items),
            "parsed_count": len(parsed),
        }
        if items and not parsed:
            sample = items[0] if items else {}
            sample_keys = []
            if isinstance(sample, dict):
                sample_keys = list(sample.keys())[:12]
            self._log(
                "warning",
                f"轮询收到原始消息但未解析到文本: raw_items={len(items)}, sample_keys={sample_keys}",
            )
        elif parsed:
            self._log("info", f"轮询收到文本消息: count={len(parsed)}")
        else:
            self._log("debug", "轮询结果: 无新消息")
        return parsed, self.sync_buf, result

    def test_connection(self) -> Tuple[bool, str]:
        if not self.bot_token:
            self._log("warning", "连接测试失败：未登录")
            return False, "未登录，缺少 bot token"

        url = f"{self.base_url}/ilink/bot/getconfig"
        resp = RequestUtils(headers=self._headers(auth_required=True), timeout=self.timeout).post(url, json={})
        payload = self._json(resp)
        if self._ok(payload):
            self._log("info", "连接测试通过")
            return True, "连接正常"
        message = payload.get("errmsg") or payload.get("message") or "连接失败"
        self._log("warning", f"连接测试失败: {message}")
        return False, message
