"""iam gRPC 客户端（GetEffectiveDataScope + Redis 缓存 + 降级兜底）.

对齐 02-architecture-design.md §7.2 DataScope 解析链路：
  请求 → Redis 缓存命中 → 返回
       ↓ miss
       iam.GetEffectiveDataScope gRPC → 缓存 5min → 返回
       ↓ 不可达
       按 role 映射默认 DataScope + degraded: true（降级兜底）

IAM_GRPC_ENDPOINT 未配置或 gRPC 调用失败时，自动降级到 role-based fallback.
"""

import json
from typing import Any

import structlog

from ..config import settings
from ..shared.permissions import (
    DataScope,
    DataScopeLevel,
    UserContext,
    get_datascope_fallback,
)
from . import redis_client

logger = structlog.get_logger(__name__)

# gRPC 客户端惰性初始化（避免未安装 grpcio 时 import 失败）
_grpc_channel: Any | None = None
_grpc_stub: Any | None = None
_grpc_initialized: bool = False


async def _init_grpc() -> tuple[Any, Any] | None:
    """惰性初始化 gRPC channel + stub（iam_grpc_endpoint 为空时返回 None）."""
    global _grpc_channel, _grpc_stub, _grpc_initialized

    if not settings.iam_grpc_endpoint:
        return None

    if _grpc_initialized:
        return (_grpc_channel, _grpc_stub) if _grpc_stub is not None else None

    _grpc_initialized = True
    try:
        import grpc  # type: ignore[import-not-found]
        from generated_proto import iam_pb2_grpc  # type: ignore[import-not-found]

        _grpc_channel = grpc.aio.insecure_channel(
            settings.iam_grpc_endpoint,
            options=[
                ("grpc.connect_timeout_ms", settings.iam_grpc_timeout_s * 1000),
                ("grpc.keepalive_time_ms", 60000),
            ],
        )
        _grpc_stub = iam_pb2_grpc.IamServiceStub(_grpc_channel)
        logger.info(
            "iam_grpc_initialized",
            endpoint=settings.iam_grpc_endpoint,
        )
        return _grpc_channel, _grpc_stub
    except Exception as exc:  # noqa: BLE001
        logger.warning("iam_grpc_init_failed_degraded", error=str(exc))
        _grpc_stub = None
        return None


async def close_grpc() -> None:
    """关闭 gRPC channel（应用退出时调用）."""
    global _grpc_channel, _grpc_stub, _grpc_initialized
    if _grpc_channel is not None:
        try:
            await _grpc_channel.close()
        except Exception as exc:  # noqa: BLE001
            logger.warning("iam_grpc_close_failed", error=str(exc))
        finally:
            _grpc_channel = None
            _grpc_stub = None
            _grpc_initialized = False


async def _call_grpc(user_id: str) -> DataScope | None:
    """gRPC 调 iam.GetEffectiveDataScope（失败返回 None 触发降级）."""
    stub_bundle = await _init_grpc()
    if stub_bundle is None:
        return None
    _, stub = stub_bundle
    try:
        from generated_proto import iam_pb2  # type: ignore[import-not-found]

        request = iam_pb2.GetEffectiveDataScopeRequest(user_id=user_id)
        response = await stub.GetEffectiveDataScope(  # type: ignore[union-attr]
            request,
            timeout=settings.iam_grpc_timeout_s,
        )
        # 解析 level 字符串到枚举
        try:
            level = DataScopeLevel(response.level)
        except ValueError:
            logger.warning(
                "iam_datascope_unknown_level_fallback",
                level=response.level,
            )
            return None
        scope = DataScope(
            level=level,
            scope_ids=list(response.scope_ids),
            school_id=response.school_id,
        )
        logger.info(
            "iam_datascope_resolved",
            user_id=user_id,
            level=level.value,
            scope_count=len(scope.scope_ids),
        )
        return scope
    except Exception as exc:  # noqa: BLE001
        logger.warning("iam_grpc_call_failed_degraded", user_id=user_id, error=str(exc))
        return None


def _cache_key(user_id: str) -> str:
    """DataScope 缓存键（5min TTL）."""
    return f"data_ana:datascope:{user_id}"


def _serialize_scope(scope: DataScope) -> str:
    """DataScope 序列化（Redis 存储）."""
    return json.dumps(
        {
            "level": scope.level.value,
            "scope_ids": scope.scope_ids,
            "school_id": scope.school_id,
            "degraded": scope.degraded,
            "degraded_reason": scope.degraded_reason,
        },
        ensure_ascii=False,
    )


def _deserialize_scope(raw: str) -> DataScope | None:
    """DataScope 反序列化."""
    try:
        data = json.loads(raw)
        return DataScope(
            level=DataScopeLevel(data["level"]),
            scope_ids=data.get("scope_ids", []),
            school_id=data.get("school_id", ""),
            degraded=data.get("degraded", False),
            degraded_reason=data.get("degraded_reason", ""),
        )
    except Exception as exc:  # noqa: BLE001
        logger.warning("datascope_deserialize_failed", error=str(exc), raw=raw[:200])
        return None


async def get_effective_datascope(user: UserContext) -> DataScope:
    """解析用户 DataScope（Redis 缓存 → iam gRPC → 降级兜底）.

    返回值始终非 None：
    - iam 可用：返回 iam 解析的 DataScope
    - iam 不可用：返回 role-based fallback + degraded=true
    """
    # 1. Redis 缓存
    cached = await redis_client.get_cache(_cache_key(user.user_id))
    if cached is not None:
        scope = _deserialize_scope(cached)
        if scope is not None:
            return scope

    # 2. iam gRPC
    scope = await _call_grpc(user.user_id)
    if scope is not None:
        # 写缓存（5min TTL）
        await redis_client.set_cache(
            _cache_key(user.user_id),
            _serialize_scope(scope),
            ttl_s=settings.datascope_cache_ttl_s,
        )
        return scope

    # 3. 降级兜底
    fallback = get_datascope_fallback(user)
    # 降级兜底也写缓存（更短 TTL，避免 iam 恢复后仍读降级值）
    await redis_client.set_cache(
        _cache_key(user.user_id),
        _serialize_scope(fallback),
        ttl_s=min(60, settings.datascope_cache_ttl_s),
    )
    logger.warning(
        "datascope_fallback_used",
        user_id=user.user_id,
        roles=user.roles,
        level=fallback.level.value,
    )
    return fallback


async def invalidate_datascope(user_id: str) -> bool:
    """失效 DataScope 缓存（用户角色变更时由 iam 触发）."""
    return await redis_client.delete_cache(_cache_key(user_id))


async def ping() -> bool:
    """iam gRPC 连通性检查（供 /readyz 使用，1s 超时）."""
    if not settings.iam_grpc_endpoint:
        return False  # 未配置不算健康（依赖降级兜底）
    stub_bundle = await _init_grpc()
    if stub_bundle is None:
        return False
    _, stub = stub_bundle
    try:
        # 用空 user_id 探活（iam 应返回 INVALID_ARGUMENT 而非超时）
        from generated_proto import iam_pb2  # type: ignore[import-not-found]

        request = iam_pb2.GetEffectiveDataScopeRequest(user_id="")
        await stub.GetEffectiveDataScope(request, timeout=1)  # type: ignore[union-attr]
        return True
    except Exception as exc:  # noqa: BLE001
        # INVALID_ARGUMENT 表示连通，超时/UNAVAILABLE 表示不连通
        err_str = str(exc).lower()
        if "invalid" in err_str or "not found" in err_str:
            return True
        logger.warning("iam_grpc_ping_failed", error=str(exc))
        return False
