"""Redis 客户端（DataScope 缓存 + CDC 幂等去重 + 预警去重位图）.

降级策略：redis_url 未配置或不可达时，所有缓存查询跳过（cache miss），
直查 ClickHouse / iam gRPC，不阻塞主流程.
"""

from typing import Any

import structlog

from ..config import settings

logger = structlog.get_logger(__name__)

_client: Any | None = None
_client_initialized: bool = False


async def get_client() -> Any | None:
    """获取 Redis 客户端（惰性初始化，失败返回 None 降级）."""
    global _client, _client_initialized

    if not settings.redis_url:
        return None

    if _client_initialized:
        return _client

    _client_initialized = True
    try:
        import redis.asyncio as aioredis

        _client = aioredis.from_url(
            settings.redis_url,
            max_connections=settings.redis_pool_size,
            socket_timeout=settings.redis_socket_timeout_ms / 1000,
            decode_responses=True,
        )
        logger.info("redis_client_initialized", url=settings.redis_url)
    except Exception as exc:  # noqa: BLE001
        logger.warning("redis_client_init_failed_degraded", error=str(exc))
        _client = None

    return _client


async def close_client() -> None:
    """关闭客户端."""
    global _client, _client_initialized
    if _client is not None:
        try:
            await _client.close()
        except Exception as exc:  # noqa: BLE001
            logger.warning("redis_client_close_failed", error=str(exc))
        finally:
            _client = None
            _client_initialized = False


async def ping() -> bool:
    """Redis 连通性检查（供 /readyz 使用）."""
    client = await get_client()
    if client is None:
        return False
    try:
        await client.ping()
        return True
    except Exception as exc:  # noqa: BLE001
        logger.warning("redis_ping_failed", error=str(exc))
        return False


async def get_cache(key: str) -> str | None:
    """读取缓存（不可用时返回 None）."""
    client = await get_client()
    if client is None:
        return None
    try:
        return await client.get(key)
    except Exception as exc:  # noqa: BLE001
        logger.warning("redis_get_failed", key=key, error=str(exc))
        return None


async def set_cache(key: str, value: str, ttl_s: int = 300) -> bool:
    """写入缓存（带 TTL）."""
    client = await get_client()
    if client is None:
        return False
    try:
        await client.setex(key, ttl_s, value)
        return True
    except Exception as exc:  # noqa: BLE001
        logger.warning("redis_set_failed", key=key, error=str(exc))
        return False


async def delete_cache(key: str) -> bool:
    """删除缓存."""
    client = await get_client()
    if client is None:
        return False
    try:
        await client.delete(key)
        return True
    except Exception as exc:  # noqa: BLE001
        logger.warning("redis_delete_failed", key=key, error=str(exc))
        return False


async def setnx_dedup(key: str, ttl_s: int = 86400) -> bool:
    """幂等去重（SETNX，成功表示首次，失败表示重复）."""
    client = await get_client()
    if client is None:
        return True  # Redis 不可用时放行（依赖 ClickHouse 去重）
    try:
        result = await client.set(key, "1", ex=ttl_s, nx=True)
        return bool(result)
    except Exception as exc:  # noqa: BLE001
        logger.warning("redis_setnx_failed", key=key, error=str(exc))
        return True  # 放行


# ===== 缓存键命名规范（对齐 02-architecture-design.md §10.2） =====
# data_ana:datascope:{user_id}          5min  DataScope 缓存
# data_ana:exam:{exam_id}               30day ExamCache（P6 多实例共享）
# data_ana:dedup:{event_id}             7day  事件幂等去重
# data_ana:warning:{target_id}:{type}:{date}  25h  预警去重位图
# data_ana:dashboard:{user_id}:{scope}  5min  Dashboard 聚合结果缓存
# data_ana:kp_meta:{knowledge_point_id} 1day  知识点元数据缓存
