# =============================================================================
# 企微IT智能服务台 — 企微 API 封装服务
# =============================================================================
# 说明：封装所有与企微服务器的交互逻辑，包括：
#   1. access_token 管理（Redis 缓存 + 自动刷新）
#   2. 发送消息（文本/图片/文件）
#   3. 获取员工信息（通讯录 API）
#   4. 上传临时素材
#   5. OAuth2 授权换算用户身份
# =============================================================================

import json
import logging
from typing import Any, Dict, List, Optional

import httpx
import redis.asyncio as aioredis

from app.config import settings

logger = logging.getLogger(__name__)


class WecomService:
    """企微 API 调用服务。

    封装所有与企微服务器的 HTTP 交互，提供异步方法。
    access_token 通过 Redis 缓存管理，避免频繁调用获取接口。

    Attributes:
        redis: Redis 异步客户端（用于缓存 access_token）
        client: httpx 异步 HTTP 客户端
    """

    def __init__(self, redis_client: Optional[aioredis.Redis] = None):
        """初始化企微服务。

        Args:
            redis_client: Redis 异步客户端实例（可为 None，本地开发时 Redis 不可用）
        """
        self.redis = redis_client
        # 创建 httpx 异步客户端
        # timeout: 连接超时5秒，读取超时10秒
        self.client = httpx.AsyncClient(
            timeout=httpx.Timeout(connect=5.0, read=10.0, write=10.0, pool=5.0)
        )
        # 内存缓存（Redis 不可用时的降级方案）
        self._token_cache: Optional[str] = None

    # --------------------------------------------------------------------------
    # access_token 管理
    # --------------------------------------------------------------------------
    async def get_access_token(self) -> str:
        """获取企微 access_token。

        优先从 Redis 缓存获取，如果缓存不存在或即将过期则重新获取。
        access_token 有效期 7200 秒，缓存 TTL 设为 6900 秒（提前 300 秒刷新）。

        对应企微API:
            GET https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET

        Returns:
            str: access_token 字符串

        Raises:
            Exception: 获取 access_token 失败
        """
        # Redis 缓存 key
        cache_key = "wecom:access_token"

        # 1. 尝试从 Redis 缓存获取
        if self.redis:
            try:
                cached_token = await self.redis.get(cache_key)
                if cached_token:
                    logger.debug("从缓存获取 access_token")
                    return cached_token.decode("utf-8")
            except Exception as e:
                logger.warning(f"Redis 读取失败（降级）: {e}")

        # 1b. 尝试从内存缓存获取
        if self._token_cache:
            logger.debug("从内存缓存获取 access_token")
            return self._token_cache

        # 2. 缓存未命中，调用企微 API 获取
        logger.info("缓存未命中，调用企微API获取 access_token")
        url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
        params = {
            "corpid": settings.wecom_corp_id,
            "corpsecret": settings.wecom_secret,
        }

        try:
            response = await self.client.get(url, params=params)
            result = response.json()

            # 检查企微API返回码
            if result.get("errcode") != 0:
                error_msg = result.get("errmsg", "未知错误")
                logger.error(f"获取 access_token 失败: errcode={result.get('errcode')}, errmsg={error_msg}")
                raise Exception(f"企微API错误: {error_msg}")

            access_token = result["access_token"]
            expires_in = result.get("expires_in", 7200)

            # 3. 缓存到 Redis，TTL = 有效期 - 300秒（提前刷新）
            buffer_seconds = 300
            cache_ttl = max(expires_in - buffer_seconds, 60)  # 至少缓存 60 秒
            if self.redis:
                try:
                    await self.redis.setex(cache_key, cache_ttl, access_token)
                except Exception as e:
                    logger.warning(f"Redis 写入失败（降级）: {e}")

            # 3b. 同时缓存到内存
            self._token_cache = access_token

            logger.info(f"access_token 获取成功，缓存 TTL={cache_ttl}秒")
            return access_token

        except httpx.HTTPError as e:
            logger.error(f"获取 access_token 网络错误: {e}")
            raise Exception(f"企微API网络错误: {e}") from e

    # --------------------------------------------------------------------------
    # 发送文本消息
    # --------------------------------------------------------------------------
    async def send_text_message(
        self, user_id: str, content: str
    ) -> Dict[str, Any]:
        """向员工发送文本消息。

        对应企微API:
            POST https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=TOKEN

        请求体:
            {
                "touser": "UserID",
                "msgtype": "text",
                "agentid": 1000002,
                "text": {"content": "消息内容"}
            }

        Args:
            user_id: 员工的企微 UserID
            content: 消息内容（纯文本）

        Returns:
            Dict[str, Any]: 企微API返回结果
        """
        access_token = await self.get_access_token()
        url = f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}"

        payload = {
            "touser": user_id,
            "msgtype": "text",
            "agentid": int(settings.wecom_agent_id),
            "text": {"content": content},
        }

        try:
            response = await self.client.post(url, json=payload)
            result = response.json()

            if result.get("errcode") != 0:
                logger.error(
                    f"发送文本消息失败: user_id={user_id}, "
                    f"errcode={result.get('errcode')}, errmsg={result.get('errmsg')}"
                )
            else:
                logger.info(f"发送文本消息成功: user_id={user_id}")

            return result

        except httpx.HTTPError as e:
            logger.error(f"发送文本消息网络错误: user_id={user_id}, error={e}")
            raise Exception(f"发送消息网络错误: {e}") from e

    # --------------------------------------------------------------------------
    # 发送卡片消息
    # --------------------------------------------------------------------------
    async def send_card_message(
        self,
        user_id: str,
        title: str,
        description: str,
        url: str = "",
        btntxt: str = "详情",
    ) -> Dict[str, Any]:
        """向员工发送文本卡片消息。

        对应企微API:
            POST https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=TOKEN

        请求体:
            {
                "touser": "UserID",
                "msgtype": "textcard",
                "agentid": 1000002,
                "textcard": {
                    "title": "标题",
                    "description": "描述",
                    "url": "链接",
                    "btntxt": "按钮文字"
                }
            }

        Args:
            user_id: 员工的企微 UserID
            title: 卡片标题
            description: 卡片描述
            url: 卡片点击跳转链接
            btntxt: 按钮文字（默认"详情"）

        Returns:
            Dict[str, Any]: 企微API返回结果
        """
        access_token = await self.get_access_token()
        url_api = f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}"

        payload = {
            "touser": user_id,
            "msgtype": "textcard",
            "agentid": int(settings.wecom_agent_id),
            "textcard": {
                "title": title,
                "description": description,
                "url": url,
                "btntxt": btntxt,
            },
        }

        try:
            response = await self.client.post(url_api, json=payload)
            result = response.json()

            if result.get("errcode") != 0:
                logger.error(
                    f"发送卡片消息失败: user_id={user_id}, "
                    f"errcode={result.get('errcode')}, errmsg={result.get('errmsg')}"
                )
            else:
                logger.info(f"发送卡片消息成功: user_id={user_id}")

            return result

        except httpx.HTTPError as e:
            logger.error(f"发送卡片消息网络错误: user_id={user_id}, error={e}")
            raise Exception(f"发送消息网络错误: {e}") from e

    # --------------------------------------------------------------------------
    # 发送图片消息
    # --------------------------------------------------------------------------
    async def send_image_message(
        self, user_id: str, media_id: str
    ) -> Dict[str, Any]:
        """向员工发送图片消息。

        对应企微API:
            POST https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=TOKEN

        请求体:
            {
                "touser": "UserID",
                "msgtype": "image",
                "agentid": 1000002,
                "image": {"media_id": "MEDIA_ID"}
            }

        注意：发送图片前需要先通过 upload_temp_media 上传图片获取 media_id。

        Args:
            user_id: 员工的企微 UserID
            media_id: 图片媒体ID（通过上传临时素材获取）

        Returns:
            Dict[str, Any]: 企微API返回结果
        """
        access_token = await self.get_access_token()
        url = f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}"

        payload = {
            "touser": user_id,
            "msgtype": "image",
            "agentid": int(settings.wecom_agent_id),
            "image": {"media_id": media_id},
        }

        try:
            response = await self.client.post(url, json=payload)
            result = response.json()

            if result.get("errcode") != 0:
                logger.error(
                    f"发送图片消息失败: user_id={user_id}, "
                    f"errcode={result.get('errcode')}, errmsg={result.get('errmsg')}"
                )
            else:
                logger.info(f"发送图片消息成功: user_id={user_id}")

            return result

        except httpx.HTTPError as e:
            logger.error(f"发送图片消息网络错误: user_id={user_id}, error={e}")
            raise Exception(f"发送消息网络错误: {e}") from e

    # --------------------------------------------------------------------------
    # 发送文件消息
    # --------------------------------------------------------------------------
    async def send_file_message(
        self, user_id: str, media_id: str
    ) -> Dict[str, Any]:
        """向员工发送文件消息。

        对应企微API:
            POST https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=TOKEN

        请求体:
            {
                "touser": "UserID",
                "msgtype": "file",
                "agentid": 1000002,
                "file": {"media_id": "MEDIA_ID"}
            }

        注意：发送文件前需要先通过 upload_temp_media 上传文件获取 media_id。

        Args:
            user_id: 员工的企微 UserID
            media_id: 文件媒体ID（通过上传临时素材获取）

        Returns:
            Dict[str, Any]: 企微API返回结果
        """
        access_token = await self.get_access_token()
        url = f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}"

        payload = {
            "touser": user_id,
            "msgtype": "file",
            "agentid": int(settings.wecom_agent_id),
            "file": {"media_id": media_id},
        }

        try:
            response = await self.client.post(url, json=payload)
            result = response.json()

            if result.get("errcode") != 0:
                logger.error(
                    f"发送文件消息失败: user_id={user_id}, "
                    f"errcode={result.get('errcode')}, errmsg={result.get('errmsg')}"
                )
            else:
                logger.info(f"发送文件消息成功: user_id={user_id}")

            return result

        except httpx.HTTPError as e:
            logger.error(f"发送文件消息网络错误: user_id={user_id}, error={e}")
            raise Exception(f"发送消息网络错误: {e}") from e

    # --------------------------------------------------------------------------
    # 获取员工通讯录信息
    # --------------------------------------------------------------------------
    async def get_user_info(self, user_id: str) -> Dict[str, Any]:
        """获取员工通讯录详细信息（用于 VIP 判断）。

        对应企微API:
            GET https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=TOKEN&userid=USERID

        返回数据包含:
            - userid: 员工UserID
            - name: 员工姓名
            - department: 部门ID列表
            - position: 岗位
            - mobile: 手机号
            - email: 邮箱
            - status: 激活状态

        需要企微通讯录只读权限。

        Args:
            user_id: 员工的企微 UserID

        Returns:
            Dict[str, Any]: 员工信息字典

        Raises:
            Exception: 获取失败
        """
        access_token = await self.get_access_token()
        url = "https://qyapi.weixin.qq.com/cgi-bin/user/get"
        params = {
            "access_token": access_token,
            "userid": user_id,
        }

        try:
            response = await self.client.get(url, params=params)
            result = response.json()

            if result.get("errcode", 0) != 0:
                logger.error(
                    f"获取员工信息失败: user_id={user_id}, "
                    f"errcode={result.get('errcode')}, errmsg={result.get('errmsg')}"
                )
                raise Exception(f"获取员工信息失败: {result.get('errmsg')}")

            logger.info(f"获取员工信息成功: user_id={user_id}, name={result.get('name', '')}")
            return result

        except httpx.HTTPError as e:
            logger.error(f"获取员工信息网络错误: user_id={user_id}, error={e}")
            raise Exception(f"获取员工信息网络错误: {e}") from e

    # --------------------------------------------------------------------------
    # 获取部门成员列表
    # --------------------------------------------------------------------------
    async def get_department_members(
        self, department_id: int = 1, fetch_child: int = 1
    ) -> List[Dict[str, Any]]:
        """获取部门成员列表。

        对应企微API:
            GET https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token=TOKEN&department_id=ID&fetch_child=1

        Args:
            department_id: 部门ID（默认1为根部门）
            fetch_child: 是否递归获取子部门（1=是, 0=否）

        Returns:
            List[Dict[str, Any]]: 部门成员列表

        Raises:
            Exception: 获取失败
        """
        access_token = await self.get_access_token()
        url = "https://qyapi.weixin.qq.com/cgi-bin/user/list"
        params = {
            "access_token": access_token,
            "department_id": department_id,
            "fetch_child": fetch_child,
        }

        try:
            response = await self.client.get(url, params=params)
            result = response.json()

            if result.get("errcode", 0) != 0:
                logger.error(
                    f"获取部门成员失败: dept_id={department_id}, "
                    f"errcode={result.get('errcode')}, errmsg={result.get('errmsg')}"
                )
                raise Exception(f"获取部门成员失败: {result.get('errmsg')}")

            userlist = result.get("userlist", [])
            logger.info(f"获取部门成员成功: dept_id={department_id}, count={len(userlist)}")
            return userlist

        except httpx.HTTPError as e:
            logger.error(f"获取部门成员网络错误: dept_id={department_id}, error={e}")
            raise Exception(f"获取部门成员网络错误: {e}") from e

    # --------------------------------------------------------------------------
    # 上传临时素材
    # --------------------------------------------------------------------------
    async def upload_temp_media(
        self, media_type: str, file_data: bytes, filename: str = "upload"
    ) -> str:
        """上传临时素材（图片/文件/语音），获取 media_id。

        对应企微API:
            POST https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=TOKEN&type=TYPE

        临时素材有效期 3 天，适用于发送图片/文件消息。

        Args:
            media_type: 媒体类型（image/file/voice）
            file_data: 文件二进制数据
            filename: 文件名

        Returns:
            str: media_id（用于发送图片/文件消息时引用）

        Raises:
            Exception: 上传失败
        """
        access_token = await self.get_access_token()
        url = f"https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={access_token}&type={media_type}"

        try:
            # 使用 multipart 上传文件
            files = {"media": (filename, file_data)}
            response = await self.client.post(url, files=files)
            result = response.json()

            if result.get("errcode", 0) != 0:
                logger.error(
                    f"上传临时素材失败: type={media_type}, "
                    f"errcode={result.get('errcode')}, errmsg={result.get('errmsg')}"
                )
                raise Exception(f"上传临时素材失败: {result.get('errmsg')}")

            media_id = result.get("media_id", "")
            logger.info(f"上传临时素材成功: type={media_type}, media_id={media_id}")
            return media_id

        except httpx.HTTPError as e:
            logger.error(f"上传临时素材网络错误: type={media_type}, error={e}")
            raise Exception(f"上传临时素材网络错误: {e}") from e

    # --------------------------------------------------------------------------
    # OAuth2 授权换算用户身份
    # --------------------------------------------------------------------------
    async def get_oauth_user_info(self, code: str) -> Dict[str, str]:
        """通过 OAuth2 授权码换取员工身份信息。

        对应企微API:
            GET https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=TOKEN&code=CODE

        H5 页面通过企微 OAuth2 静默授权获取 code，后端用 code 换取员工 UserID。
        适用于 H5 用户端身份识别。

        Args:
            code: 企微 OAuth2 授权码

        Returns:
            Dict[str, str]: 包含 userid 和 user_ticket 的字典

        Raises:
            Exception: 换取失败
        """
        access_token = await self.get_access_token()
        url = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo"
        params = {
            "access_token": access_token,
            "code": code,
        }

        try:
            response = await self.client.get(url, params=params)
            result = response.json()

            if result.get("errcode", 0) != 0:
                logger.error(
                    f"OAuth2换取用户身份失败: code={code}, "
                    f"errcode={result.get('errcode')}, errmsg={result.get('errmsg')}"
                )
                raise Exception(f"OAuth2换取用户身份失败: {result.get('errmsg')}")

            user_id = result.get("userid", "")
            logger.info(f"OAuth2换取用户身份成功: userid={user_id}")
            return {
                "userid": user_id,
                "user_ticket": result.get("user_ticket", ""),
            }

        except httpx.HTTPError as e:
            logger.error(f"OAuth2换取用户身份网络错误: code={code}, error={e}")
            raise Exception(f"OAuth2换取用户身份网络错误: {e}") from e

    # --------------------------------------------------------------------------
    # 关闭客户端
    # --------------------------------------------------------------------------
    async def close(self) -> None:
        """关闭 HTTP 客户端连接池。

        应用关闭时调用，释放资源。
        """
        await self.client.aclose()
        logger.info("WecomService HTTP 客户端已关闭")
