"""飞书 IM 适配器 (U11)。

实现 :class:`MessageAdapter` 协议，对接飞书开放平台事件订阅 webhook。

关键设计决策：
- 事件加密使用 AES-256-CBC（飞书官方协议），与 secrets store 的 AES-256-GCM 不同。
- 签名校验 fail-closed：``encrypt_key`` 缺失或签名头缺失一律返回 False。
- ``tenant_access_token`` 简单 TTL 缓存（5 分钟）；过期后重新拉取。
- httpx 客户端懒构造，避免未使用的适配器持有连接池。
"""

from __future__ import annotations

import base64
import hashlib
import hmac
import json
import logging
import re
import time
from datetime import datetime, timezone
from typing import Any

import httpx

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

logger = logging.getLogger(__name__)

# 签名时间戳允许的最大偏移（秒）— 与飞书官方文档保持一致
_SIGNATURE_MAX_AGE_SECONDS = 300
# tenant_access_token 缓存 TTL（秒）— 飞书 token 实际有效期 2h(7200s)，留 5min 余量
_TOKEN_CACHE_TTL = 6900.0

# 飞书开放平台 API 端点
_TENANT_TOKEN_URL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
_SEND_MESSAGE_URL = "https://open.feishu.cn/open-apis/im/v1/messages"

# @用户 提及标记正则 — 飞书文本消息中提及用户格式为 "@_user_N"
_MENTION_RE = re.compile(r"@_user_\d+\s*")


class SignatureVerificationError(Exception):
    """事件 ``verification_token`` 校验失败 — 拒绝处理。"""


class FeishuMessageAdapter(MessageAdapter):
    """飞书 IM 适配器。

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

    Args:
        app_id: 飞书应用 App ID。
        app_secret: 飞书应用 App Secret。
        encrypt_key: 事件订阅加密密钥（可选 — 启用加密订阅时必填）。
        verification_token: 事件订阅 Verification Token（可选 — 用于校验事件来源）。
    """

    def __init__(
        self,
        app_id: str,
        app_secret: str,
        encrypt_key: str | None = None,
        verification_token: str | None = None,
    ) -> None:
        super().__init__()
        self.app_id = app_id
        self.app_secret = app_secret
        self.encrypt_key = encrypt_key
        self.verification_token = verification_token
        # ponytail: 简单 TTL 缓存 (token, expiry)。天花板：单实例内存；
        # 升级路径：Redis 缓存共享给多实例。
        self._token_cache: tuple[str, float] | None = None

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

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

        fail-closed：``encrypt_key`` 缺失或签名头缺失一律返回 False。
        时间戳超过 5 分钟视为重放攻击，拒绝。

        Args:
            headers: HTTP 请求头（键大小写不敏感查找）。
            body: 原始请求体字节。

        Returns:
            True 表示签名校验通过。
        """
        if not self.encrypt_key:
            logger.warning("飞书适配器未配置 encrypt_key — 拒绝所有 webhook 请求")
            return False

        signature = header_get(headers, "X-Lark-Signature")
        if not signature:
            return False

        timestamp_str = header_get(headers, "X-Lark-Request-Timestamp")
        nonce = header_get(headers, "X-Lark-Request-Nonce")
        if not timestamp_str or not nonce:
            return False

        # 时间戳重放保护
        try:
            ts = int(timestamp_str)
        except ValueError:
            return False
        now = datetime.now(timezone.utc).timestamp()
        if abs(now - ts) > _SIGNATURE_MAX_AGE_SECONDS:
            logger.warning("飞书 webhook 时间戳超出 %ds 窗口 — 拒绝", _SIGNATURE_MAX_AGE_SECONDS)
            return False

        # 计算签名：sha256(timestamp + nonce + encrypt_key + body)
        body_str = body.decode("utf-8")
        expected = hashlib.sha256(
            f"{timestamp_str}{nonce}{self.encrypt_key}{body_str}".encode("utf-8")
        ).hexdigest()

        return hmac.compare_digest(signature, expected)

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

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

        Raises:
            URLVerificationChallenge: 事件为 URL 验证请求。
            SignatureVerificationError: ``verification_token`` 不匹配。
            ValueError: 事件结构无法解析。
        """
        try:
            data: dict[str, Any] = json.loads(body)
        except json.JSONDecodeError as exc:
            raise ValueError(f"飞书事件 body 不是合法 JSON: {exc}") from exc

        # URL 验证流程 — 飞书配置 webhook 时发送
        if "url_verification" in data or "challenge" in data:
            raise URLVerificationChallenge(data.get("challenge", ""))

        # 加密事件 — AES-256-CBC 解密
        if "encrypt" in data:
            data = self._decrypt_event(data["encrypt"])

        # 校验 verification_token
        if self.verification_token is not None:
            token = data.get("verification_token") or data.get("header", {}).get("token")
            if not token:
                raise SignatureVerificationError("事件缺少 verification_token 字段")
            if not hmac.compare_digest(token, self.verification_token):
                raise SignatureVerificationError("verification_token 不匹配")

        event_id = data.get("event_id") or data.get("header", {}).get("event_id", "")
        event = data.get("event", {})
        message = event.get("message", {})
        sender = event.get("sender", {}).get("sender_id", {})

        chat_id = message.get("chat_id", "")
        open_id = sender.get("open_id", "")
        create_time = message.get("create_time", "")
        timestamp = str(create_time) if create_time else ""

        content = self._extract_content(message)

        return IncomingMessage(
            channel=ChannelType.FEISHU,
            platform_message_id=event_id,
            user_id=open_id,
            chat_id=chat_id,
            content=content,
            raw_event=data,
            timestamp=timestamp,
        )

    def _decrypt_event(self, encrypt_b64: str) -> dict[str, Any]:
        """AES-256-CBC 解密飞书加密事件。

        飞书协议：
            key = sha256(encrypt_key).digest()[:32]
            ciphertext = IV(16B) + 密文
            plaintext = AES-256-CBC 解密后去除 PKCS7 padding
        """
        if not self.encrypt_key:
            raise SignatureVerificationError("加密事件但未配置 encrypt_key")

        from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
        from cryptography.hazmat.primitives.padding import PKCS7

        key = hashlib.sha256(self.encrypt_key.encode("utf-8")).digest()
        ciphertext = base64.b64decode(encrypt_b64)

        if len(ciphertext) < 17:  # IV(16) + 至少 1 字节密文
            raise ValueError("加密事件密文长度不足")

        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()

        return json.loads(plaintext.decode("utf-8"))

    def _extract_content(self, message: dict[str, Any]) -> str:
        """从飞书 message 字段提取文本内容。

        - text 类型：解析 ``content`` JSON 中的 ``text`` 字段，剥离 @ 提及标记。
        - 其他类型：返回 ``[unsupported message type: {type}]``。
        """
        message_type = message.get("message_type", "")
        content_raw = message.get("content", "{}")

        if message_type == "text":
            try:
                content_obj = (
                    json.loads(content_raw) if isinstance(content_raw, str) else content_raw
                )
                text = content_obj.get("text", "") if isinstance(content_obj, dict) else ""
            except (json.JSONDecodeError, TypeError):
                return ""
            # 剥离 @ 提及标记（如 "@_user_1 hello" → "hello"）
            return _MENTION_RE.sub("", text).strip()

        return f"[unsupported message type: {message_type}]"

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

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

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

            client = self._get_client()
            payload = {
                "receive_id": message.chat_id,
                "msg_type": "text",
                "content": json.dumps({"text": message.content}),
            }
            resp = await client.post(
                _SEND_MESSAGE_URL,
                params={"receive_id_type": "chat_id"},
                json=payload,
                headers={"Authorization": f"Bearer {token}"},
            )
            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("code") != 0:
                logger.error(
                    "飞书 send_message 业务失败 code=%s msg=%s",
                    data.get("code"),
                    data.get("msg", "")[:200],
                )
                return False
            return True
        except httpx.HTTPError as exc:
            logger.error("飞书 send_message 网络错误: %s", exc)
            return False

    async def _get_tenant_access_token(self) -> str | None:
        """获取并缓存 ``tenant_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.post(
                _TENANT_TOKEN_URL,
                json={"app_id": self.app_id, "app_secret": self.app_secret},
            )
            if resp.status_code != 200:
                logger.error("飞书 tenant_token HTTP %d: %s", resp.status_code, resp.text[:200])
                return None
            data = resp.json()
            if data.get("code") != 0:
                logger.error(
                    "飞书 tenant_token 业务失败 code=%s msg=%s",
                    data.get("code"),
                    data.get("msg", "")[:200],
                )
                return None
            token = data.get("tenant_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("飞书 tenant_token 网络错误: %s", exc)
            return None
