# =============================================================================
# 企微IT智能服务台 — Redis 缓存服务
# =============================================================================
# 说明：封装 Redis 缓存操作，提供：
#   1. 消息去重（基于企微 MsgId）
#   2. 内容去重（基于用户 ID + 内容哈希，防快速重复发送）
#   3. 通用缓存读写（供其他服务复用）
#
# 去重窗口：
#   - MsgId 去重：5 分钟（与企微重试窗口一致）
#   - 内容去重：60 秒（防止用户快速重复发送相同消息）
# =============================================================================

import hashlib
import logging
from typing import Optional

import redis.asyncio as aioredis

logger = logging.getLogger(__name__)

# Redis key 前缀
MSG_DEDUP_PREFIX = "msg:dedup"
CONTENT_DEDUP_PREFIX = "msg:dedup:content"

# 默认 TTL（秒）
DEFAULT_MSG_DEDUP_TTL = 300  # 5 分钟，与企微重试窗口一致
DEFAULT_CONTENT_DEDUP_TTL = 60  # 60 秒，防止快速重复发送


class CacheService:
    """Redis 缓存服务，提供消息去重和通用缓存操作。

    使用 Redis 的 SETNX + EXPIRE 语义实现幂等去重：
    - 首次写入成功 → 返回 False（非重复）
    - 再次写入失败 → 返回 True（重复）

    降级策略：Redis 不可用时，去重检查自动放行（宁可重复处理，不可丢消息）。

    Attributes:
        redis: Redis 异步客户端（可为 None，降级时跳过去重）
    """

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

        Args:
            redis_client: Redis 异步客户端实例。
                为 None 时去重检查自动放行（降级模式）。
        """
        self.redis = redis_client

    # --------------------------------------------------------------------------
    # 消息去重（基于企微 MsgId）
    # --------------------------------------------------------------------------

    async def is_duplicate(
        self,
        msg_id: str,
        ttl: int = DEFAULT_MSG_DEDUP_TTL,
    ) -> bool:
        """基于企微 MsgId 判断消息是否重复。

        利用 Redis SETNX 语义实现幂等检查：
        - key 不存在 → 设置 key（带 TTL）→ 返回 False（非重复）
        - key 已存在 → 跳过写入 → 返回 True（重复）

        降级策略：Redis 不可用时返回 False（放行），记录警告日志。

        Args:
            msg_id: 企微消息唯一 ID（MsgId 字段）
            ttl: 去重窗口（秒），默认 300 秒（5 分钟）

        Returns:
            bool: True=重复消息（应跳过处理），False=首次收到（正常处理）
        """
        if not msg_id:
            logger.warning("msg_id 为空，跳过去重检查")
            return False

        if self.redis is None:
            logger.debug("Redis 不可用，跳过 MsgId 去重检查（降级放行）")
            return False

        key = f"{MSG_DEDUP_PREFIX}:{msg_id}"

        try:
            # SETNX + EXPIRE 原子操作：key 不存在时设置并返回 True，已存在返回 False
            is_new = await self.redis.set(key, "1", nx=True, ex=ttl)

            if is_new:
                logger.debug(f"MsgId 去重: 新消息 msg_id={msg_id}")
                return False
            else:
                logger.info(f"MsgId 去重: 重复消息已过滤 msg_id={msg_id}")
                return True

        except Exception as e:
            logger.warning(f"MsgId 去重检查异常（降级放行）: msg_id={msg_id}, error={e}")
            return False

    # --------------------------------------------------------------------------
    # 内容去重（基于用户 ID + 内容哈希）
    # --------------------------------------------------------------------------

    async def is_duplicate_content(
        self,
        user_id: str,
        content: str,
        ttl: int = DEFAULT_CONTENT_DEDUP_TTL,
    ) -> bool:
        """基于用户 ID + 内容哈希判断是否为快速重复发送。

        场景：用户在短时间内连续发送相同内容的消息（如网络卡顿导致重复点击）。
        与 MsgId 去重不同，这里处理的是不同 MsgId 但内容完全相同的消息。

        使用 SHA256 对 user_id + content 生成哈希作为 Redis key，
        窗口默认 60 秒（防止快速重复发送，但不影响正常重新提问）。

        降级策略：Redis 不可用时返回 False（放行），记录警告日志。

        Args:
            user_id: 发送者企微 UserID
            content: 消息内容
            ttl: 去重窗口（秒），默认 60 秒

        Returns:
            bool: True=重复内容（应跳过处理），False=首次收到（正常处理）
        """
        if not user_id or not content:
            logger.debug("user_id 或 content 为空，跳过内容去重检查")
            return False

        if self.redis is None:
            logger.debug("Redis 不可用，跳过内容去重检查（降级放行）")
            return False

        # 使用 SHA256 生成内容哈希，避免 Redis key 中存储原始内容
        content_hash = hashlib.sha256(f"{user_id}:{content}".encode("utf-8")).hexdigest()[:16]
        key = f"{CONTENT_DEDUP_PREFIX}:{user_id}:{content_hash}"

        try:
            is_new = await self.redis.set(key, "1", nx=True, ex=ttl)

            if is_new:
                logger.debug(f"内容去重: 新消息 user_id={user_id}, hash={content_hash}")
                return False
            else:
                logger.info(f"内容去重: 重复内容已过滤 user_id={user_id}, hash={content_hash}")
                return True

        except Exception as e:
            logger.warning(
                f"内容去重检查异常（降级放行）: user_id={user_id}, hash={content_hash}, error={e}"
            )
            return False

    # --------------------------------------------------------------------------
    # 通用缓存操作
    # --------------------------------------------------------------------------

    async def get(self, key: str) -> Optional[str]:
        """从 Redis 获取缓存值。

        Args:
            key: 缓存 key

        Returns:
            Optional[str]: 缓存值，不存在或 Redis 不可用时返回 None
        """
        if self.redis is None:
            return None

        try:
            value = await self.redis.get(key)
            return value.decode("utf-8") if isinstance(value, bytes) else value
        except Exception as e:
            logger.warning(f"Redis GET 异常: key={key}, error={e}")
            return None

    async def set(
        self,
        key: str,
        value: str,
        ttl: Optional[int] = None,
    ) -> bool:
        """向 Redis 写入缓存值。

        Args:
            key: 缓存 key
            value: 缓存值
            ttl: 过期时间（秒），为 None 时永不过期

        Returns:
            bool: True=写入成功，False=写入失败或 Redis 不可用
        """
        if self.redis is None:
            return False

        try:
            if ttl is not None:
                await self.redis.setex(key, ttl, value)
            else:
                await self.redis.set(key, value)
            return True
        except Exception as e:
            logger.warning(f"Redis SET 异常: key={key}, error={e}")
            return False

    async def delete(self, key: str) -> bool:
        """从 Redis 删除缓存 key。

        Args:
            key: 缓存 key

        Returns:
            bool: True=删除成功，False=删除失败或 Redis 不可用
        """
        if self.redis is None:
            return False

        try:
            await self.redis.delete(key)
            return True
        except Exception as e:
            logger.warning(f"Redis DELETE 异常: key={key}, error={e}")
            return False


# 默认实例：Redis 客户端在应用启动时通过 init_cache_service() 注入
# 为什么：ws.py 等模块需要导入一个 cache_service 实例来读取 Redis
cache_service = CacheService()
