"""Permission resolution for document access control.

Uses a unified ``acl_ids`` field with prefixed identifiers:
  A_ = area, D_ = department, O_ = office, R_ = role, U_ = user.

User tokens are built from JWT claims; the ES filter is a single ``terms``
query on the ``acl_ids`` keyword field.

文档访问权限解析模块。
基于统一前缀标识符（A_=区域, D_=部门, O_=科室, R_=角色, U_=用户）
构建用户的 ACL 令牌集合，并生成 ES 的 terms 过滤条件。
OA 系统已在每个文档的 acl_ids 中存储了完整的层级祖先 ID，
因此无需在此处进行层级展开。
"""

from __future__ import annotations

from typing import Any

from app.api.deps import UserContext
from app.infrastructure.redis_client import RedisClient
from app.utils.logger import get_logger

logger = get_logger(__name__)


class PermissionContext:
    """Resolved permission tokens for ES query filtering.

    ``acl_tokens`` is a flat list of prefixed IDs that the user is allowed
    to match against.  For example::

        ["U_zhang_san", "O_17", "D_05", "A_01", "R_03"]

    已解析的权限上下文，包含用户的全部 ACL 令牌。
    用于构建 ES 查询中的权限过滤条件。
    """

    __slots__ = ("user_id", "acl_tokens")

    def __init__(self, user_id: str, acl_tokens: list[str]):
        self.user_id = user_id
        self.acl_tokens = acl_tokens

    @staticmethod
    def public_es_filter() -> dict[str, Any]:
        """Build the ES filter that matches only public documents."""
        return {
            "bool": {
                "must_not": [{"exists": {"field": "acl_ids"}}],
            }
        }

    def build_es_filter(self) -> dict[str, Any]:
        """Build the ES permission filter.

        A document is visible if:
          1. It is public (no ``acl_ids`` field), OR
          2. Any of the user's tokens appears in the document's ``acl_ids``.
        """
        return {
            "bool": {
                "should": [
                    # Public document: acl_ids field does not exist / is empty
                    self.public_es_filter(),
                    # Token match
                    {"terms": {"acl_ids": self.acl_tokens}},
                ],
                "minimum_should_match": 1,
            }
        }

    def has_acl_access(self, doc_acl_ids: list[str] | None) -> bool:
        """Check whether the current permission context can access a document."""
        if not doc_acl_ids:
            return True
        return bool(set(doc_acl_ids) & set(self.acl_tokens))

    def __repr__(self) -> str:
        return (
            f"PermissionContext(user_id={self.user_id!r}, "
            f"tokens={len(self.acl_tokens)})"
        )


class PermissionService:
    """Resolves a :class:`UserContext` into a :class:`PermissionContext`.

    权限解析服务，将 JWT 中的用户信息转换为可用于 ES 查询过滤的 ACL 令牌集合。
    支持 Redis 缓存以避免每次请求都重新构建令牌。
    """

    def __init__(self, redis_client: RedisClient | None = None):
        self._redis = redis_client

    async def resolve(self, user: UserContext) -> PermissionContext:
        """Build the full ACL token set for the user.

        1. Check Redis cache for previously resolved tokens.
        2. On miss, build tokens from JWT claims and cache them.
        """
        # Try cache first
        if self._redis:
            cached = await self._redis.get_user_permissions(user.user_id)
            if cached and "acl_tokens" in cached:
                logger.debug(
                    "perm_cache_hit",
                    user_id=user.user_id,
                    tokens=len(cached["acl_tokens"]),
                )
                return PermissionContext(
                    user_id=user.user_id,
                    acl_tokens=cached["acl_tokens"],
                )

        # Build tokens from JWT fields
        tokens = self._build_tokens(user)

        # 缓存权限令牌，TTL 由 settings.redis_permissions_ttl 控制（默认 300 秒）
        # Cache tokens with TTL controlled by settings.redis_permissions_ttl (default 300s)
        if self._redis:
            await self._redis.set_user_permissions(
                user.user_id,
                {"acl_tokens": tokens},
            )
            logger.debug(
                "perm_cache_set",
                user_id=user.user_id,
                tokens=len(tokens),
            )

        return PermissionContext(user_id=user.user_id, acl_tokens=tokens)

    @staticmethod
    def _build_tokens(user: UserContext) -> list[str]:
        """Derive ACL tokens from a UserContext.

        Tokens are the user's direct identifiers at every hierarchy level,
        each prefixed by its type.  No hierarchy expansion is needed because
        the OA system already stores all ancestor IDs on each document's ACL.

        所有 ID 在 SSO/Mock 入口处已添加类型前缀（U_、O_、D_、A_、R_），
        此处直接使用，不再重复添加。
        """
        tokens: list[str] = [user.user_id]

        if user.office_id:
            tokens.append(user.office_id)
        if user.dept_id:
            tokens.append(user.dept_id)
        if user.area_id:
            tokens.append(user.area_id)
        for role_id in user.role_ids:
            tokens.append(role_id)

        return tokens
