"""企业微信 IM 适配器 (U12)。

实现 :class:`MessageAdapter` 协议，对接企业微信回调 webhook。

关键设计决策：
- EncodingAESKey（43 字符 base64）→ 32 字节 AES-256-CBC 密钥。
- 签名：sha1(sorted([token, timestamp, nonce, encrypt])) 与 ``msg_signature`` 比对。
- 加密协议：AES-256-CBC，明文 = random(16) + msg_len(4, 大端) + msg + app_id。
  app_id 校验需匹配 ``corp_id``。
- ``access_token`` 简单 TTL 缓存（7200s）。
- XML 解析使用 stdlib ``xml.etree.ElementTree``。
- httpx 客户端懒构造。
"""

from __future__ import annotations

import base64
import hashlib
import hmac
import logging
import os
import time
import xml.etree.ElementTree as ET

import httpx

from agentkit.channels.base import (
    ChannelType,
    IncomingMessage,
    MessageAdapter,
    OutgoingMessage,
    header_get,
)

logger = logging.getLogger(__name__)

# 企微签名时间戳允许的最大偏移（秒）— 5 分钟窗口防重放
_SIGNATURE_MAX_AGE_SECONDS = 300

# access_token 缓存 TTL（秒）— 企微 token 有效期 2 小时
_TOKEN_CACHE_TTL = 7200.0

# 企微 API 端点
_GET_TOKEN_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
_SEND_MESSAGE_URL = "https://qyapi.weixin.qq.com/cgi-bin/message/send"


class WeComURLVerification(Exception):
    """企微 URL 验证事件 — webhook 端点需返回 XML 响应。

    企微在配置回调时发送含 ``EchoStr`` 的加密事件，服务端需解密、
    重新加密后包装成 XML 响应返回。
    """

    def __init__(self, response_xml: str) -> None:
        super().__init__("WeCom URL verification")
        self.response_xml = response_xml


class WeComError(Exception):
    """企微消息处理错误（解密失败、app_id 不匹配等）。"""


class WeComMessageAdapter(MessageAdapter):
    """企业微信 IM 适配器。

    生命周期：
        ``__init__`` → :meth:`verify_signature` → :meth:`receive_message`
        → :meth:`send_message` → :meth:`close`

    Args:
        corp_id: 企业 ID（解密时校验 app_id）。
        agent_id: 应用 AgentID（发送消息时使用）。
        corp_secret: 应用凭证密钥（获取 access_token）。
        token: 回调配置的 Token（签名校验）。
        encoding_aes_key: 43 字符 EncodingAESKey（AES 密钥来源）。
    """

    def __init__(
        self,
        corp_id: str,
        agent_id: int,
        corp_secret: str,
        token: str,
        encoding_aes_key: str,
    ) -> None:
        super().__init__()
        self.corp_id = corp_id
        self.agent_id = agent_id
        self.corp_secret = corp_secret
        self.token = token
        self.encoding_aes_key = encoding_aes_key
        # ponytail: 简单 TTL 缓存。天花板：单实例内存；升级路径：Redis 共享。
        self._token_cache: tuple[str, float] | None = None

    # ------------------------------------------------------------------
    # AES 密钥 / 加解密
    # ------------------------------------------------------------------

    def _decode_aes_key(self) -> bytes:
        """EncodingAESKey（43 字符）→ 32 字节 AES 密钥。"""
        return base64.b64decode(self.encoding_aes_key + "=")

    def _encrypt(self, plaintext_str: str) -> str:
        """AES-256-CBC 加密 — 返回 base64(IV + 密文)。

        明文结构：random(16) + msg_len(4, 大端) + msg + corp_id，PKCS7 填充。
        """
        from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
        from cryptography.hazmat.primitives.padding import PKCS7

        key = self._decode_aes_key()
        iv = os.urandom(16)
        random_prefix = os.urandom(16)
        msg_bytes = plaintext_str.encode("utf-8")
        msg_len = len(msg_bytes).to_bytes(4, "big")
        app_id = self.corp_id.encode("utf-8")
        plaintext = random_prefix + msg_len + msg_bytes + app_id

        padder = PKCS7(algorithms.AES.block_size).padder()
        padded = padder.update(plaintext) + padder.finalize()

        cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
        encryptor = cipher.encryptor()
        ciphertext = encryptor.update(padded) + encryptor.finalize()

        return base64.b64encode(iv + ciphertext).decode("utf-8")

    def _decrypt(self, encrypt_b64: str) -> str:
        """AES-256-CBC 解密 — 校验 app_id 后返回 msg 明文。

        Raises:
            WeComError: app_id 不匹配 corp_id，或密文损坏。
        """
        from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
        from cryptography.hazmat.primitives.padding import PKCS7

        key = self._decode_aes_key()
        ciphertext = base64.b64decode(encrypt_b64)
        if len(ciphertext) < 17:  # IV(16) + 至少 1 字节密文
            raise WeComError("企微密文长度不足")

        iv = ciphertext[:16]
        encrypted = ciphertext[16:]

        cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
        decryptor = cipher.decryptor()
        padded = decryptor.update(encrypted) + decryptor.finalize()

        unpadder = PKCS7(algorithms.AES.block_size).unpadder()
        plaintext = unpadder.update(padded) + unpadder.finalize()

        # plaintext = random(16) + msg_len(4, 大端) + msg + app_id
        if len(plaintext) < 20:
            raise WeComError("企微解密后明文长度不足")
        msg_len = int.from_bytes(plaintext[16:20], "big")
        msg = plaintext[20 : 20 + msg_len]
        app_id = plaintext[20 + msg_len :].decode("utf-8", errors="replace")

        if app_id != self.corp_id:
            raise WeComError(f"app_id 不匹配: {app_id} != {self.corp_id}")

        return msg.decode("utf-8")

    # ------------------------------------------------------------------
    # XML 解析辅助
    # ------------------------------------------------------------------

    def _extract_encrypt(self, body: bytes) -> str | None:
        """从外层 XML body 提取 ``Encrypt`` 字段。"""
        try:
            root = ET.fromstring(body.decode("utf-8"))
        except (ET.ParseError, UnicodeDecodeError):
            return None
        encrypt_elem = root.find("Encrypt")
        if encrypt_elem is None or encrypt_elem.text is None:
            return None
        return encrypt_elem.text

    def _parse_xml(self, xml_str: str) -> dict[str, str]:
        """解析 XML 为 ``{tag: text}`` 字典（无命名空间）。"""
        root = ET.fromstring(xml_str)
        result: dict[str, str] = {}
        for child in root:
            result[child.tag] = child.text or ""
        return result

    # ------------------------------------------------------------------
    # 签名验证
    # ------------------------------------------------------------------

    async def verify_signature(self, headers: dict[str, str], body: bytes) -> bool:
        """验证企微 webhook 签名。

        签名 = sha1(sorted([token, timestamp, nonce, encrypt]).join(""))，
        与 query 参数 ``msg_signature`` 比对。timestamp/nonce/msg_signature
        由 webhook 端点从 query 参数合并到 headers dict 中。

        Args:
            headers: HTTP 请求头（含已合并的 query 参数）。
            body: 原始请求体字节（XML）。

        Returns:
            True 表示签名校验通过。
        """
        msg_signature = header_get(headers, "msg_signature")
        timestamp = header_get(headers, "timestamp")
        nonce = header_get(headers, "nonce")
        if not msg_signature or not timestamp or not nonce:
            return False

        # 时间戳新鲜度校验 — 5 分钟窗口防重放攻击
        try:
            ts_int = int(timestamp)
        except (TypeError, ValueError):
            return False
        now = int(time.time())
        if abs(now - ts_int) > _SIGNATURE_MAX_AGE_SECONDS:
            logger.warning(
                "企微 webhook 时间戳超出 %ds 窗口: ts=%s now=%d",
                _SIGNATURE_MAX_AGE_SECONDS, timestamp, now,
            )
            return False

        encrypt = self._extract_encrypt(body)
        if not encrypt:
            return False

        # sha1(sorted([token, timestamp, nonce, encrypt]).join(""))
        parts = sorted([self.token, timestamp, nonce, encrypt])
        expected = hashlib.sha1("".join(parts).encode("utf-8")).hexdigest()
        return hmac.compare_digest(msg_signature, expected)

    # ------------------------------------------------------------------
    # 消息接收 / 解析
    # ------------------------------------------------------------------

    async def receive_message(self, headers: dict[str, str], body: bytes) -> IncomingMessage:
        """解析企微 webhook 事件为标准化 :class:`IncomingMessage`。

        - URL 验证流程（解密后含 ``EchoStr``）：抛 :class:`WeComURLVerification`。
        - 普通消息：解密后解析内部 XML，提取 FromUserName/MsgId/Content。

        Raises:
            WeComURLVerification: URL 验证事件，携带 XML 响应。
            WeComError: 缺少 Encrypt 字段或解密失败。
        """
        encrypt = self._extract_encrypt(body)
        if not encrypt:
            raise WeComError("企微 XML body 缺少 Encrypt 字段")

        plaintext = self._decrypt(encrypt)
        inner = self._parse_xml(plaintext)

        # URL 验证流程 — 内部 XML 包含 EchoStr
        if "EchoStr" in inner:
            timestamp = header_get(headers, "timestamp") or ""
            nonce = header_get(headers, "nonce") or ""
            encrypted_echo = self._encrypt(inner["EchoStr"])
            # 计算响应签名
            sig_parts = sorted([self.token, timestamp, nonce, encrypted_echo])
            sig = hashlib.sha1("".join(sig_parts).encode("utf-8")).hexdigest()
            response_xml = (
                f"<xml><Encrypt><![CDATA[{encrypted_echo}]]></Encrypt>"
                f"<MsgSignature><![CDATA[{sig}]]></MsgSignature>"
                f"<TimeStamp>{timestamp}</TimeStamp>"
                f"<Nonce><![CDATA[{nonce}]]></Nonce></xml>"
            )
            raise WeComURLVerification(response_xml)

        # 普通消息
        from_user = inner.get("FromUserName", "")
        msg_id = inner.get("MsgId", "")
        content = inner.get("Content", "")
        create_time = inner.get("CreateTime", "")

        # 群聊消息内容以 ":" 开头 — 剥离
        if content.startswith(":"):
            content = content[1:].strip()

        # chat_id: 使用 corp_id + from_user 复合键
        # ponytail: 简单复合键。天花板：群聊场景需独立 ChatId；
        # 升级路径：从内部 XML 的 ChatId 字段提取（群聊事件携带）。
        chat_id = f"{self.corp_id}:{from_user}"

        return IncomingMessage(
            channel=ChannelType.WECOM,
            platform_message_id=msg_id,
            user_id=from_user,
            chat_id=chat_id,
            content=content,
            raw_event=inner,
            timestamp=create_time,
        )

    # ------------------------------------------------------------------
    # 消息发送
    # ------------------------------------------------------------------

    async def send_message(self, message: OutgoingMessage) -> bool:
        """向企微发送文本消息。

        Returns:
            True 表示 HTTP 200 且 ``errcode == 0``。
        """
        try:
            token = await self._get_access_token()
            if not token:
                return False

            client = self._get_client()
            payload = {
                "touser": message.chat_id,
                "msgtype": "text",
                "agentid": self.agent_id,
                "text": {"content": message.content},
            }
            resp = await client.post(
                _SEND_MESSAGE_URL,
                params={"access_token": token},
                json=payload,
            )
            if resp.status_code != 200:
                logger.error("企微 send_message HTTP %d: %s", resp.status_code, resp.text[:200])
                return False
            data = resp.json()
            if data.get("errcode") != 0:
                logger.error(
                    "企微 send_message 业务失败 errcode=%s errmsg=%s",
                    data.get("errcode"),
                    data.get("errmsg", "")[:200],
                )
                return False
            return True
        except httpx.HTTPError as exc:
            logger.error("企微 send_message 网络错误: %s", exc)
            return False

    async def _get_access_token(self) -> str | None:
        """获取并缓存企微 ``access_token``。"""
        # 命中缓存
        if self._token_cache is not None:
            token, expiry = self._token_cache
            if time.monotonic() < expiry:
                return token

        try:
            client = self._get_client()
            resp = await client.get(
                _GET_TOKEN_URL,
                params={"corpid": self.corp_id, "corpsecret": self.corp_secret},
            )
            if resp.status_code != 200:
                logger.error("企微 access_token HTTP %d: %s", resp.status_code, resp.text[:200])
                return None
            data = resp.json()
            if data.get("errcode") != 0:
                logger.error(
                    "企微 access_token 业务失败 errcode=%s errmsg=%s",
                    data.get("errcode"),
                    data.get("errmsg", "")[:200],
                )
                return None
            token = data.get("access_token", "")
            if not token:
                return None
            self._token_cache = (token, time.monotonic() + _TOKEN_CACHE_TTL)
            return token
        except httpx.HTTPError as exc:
            logger.error("企微 access_token 网络错误: %s", exc)
            return None
