"""JWT（JSON Web Token）的签发与校验。

access token 有效期较短，用于访问接口；refresh token 有效期较长，只用于换新令牌。
"""

import hashlib
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from typing import Any

import jwt

from zbt.core.config import Settings
from zbt.core.errors import AppError
from zbt.core.identifiers import new_ulid


@dataclass(frozen=True, slots=True)
class TokenPair:
    """一次登录签发的一对令牌及其有效期。"""

    access_token: str
    refresh_token: str
    access_expires_in: int
    refresh_expires_in: int
    refresh_jti: str


class JwtService:
    """封装 PyJWT，统一项目中的令牌字段、密钥和校验规则。"""

    def __init__(self, settings: Settings) -> None:
        self._settings = settings

    def issue_pair(
        self,
        *,
        subject_id: str,
        subject_type: str,
        audience: str,
        session_id: str,
        now: datetime | None = None,
    ) -> TokenPair:
        """为一个用户会话签发 access token 和 refresh token。"""
        issued_at = now or datetime.now(UTC)
        access_seconds = self._settings.access_token_minutes * 60
        refresh_seconds = self._settings.refresh_token_days * 24 * 60 * 60
        refresh_jti = new_ulid()
        # 两种令牌共有的标准/自定义声明；sub 是用户，sid 是服务端会话。
        common = {
            "sub": subject_id,
            "subject_type": subject_type,
            "aud": audience,
            "iss": "zhibaotong-s2",
            "sid": session_id,
            "iat": issued_at,
        }
        access = jwt.encode(
            {
                # ** 会把 common 字典的键值展开并合并到新字典。
                **common,
                "typ": "access",
                "exp": issued_at + timedelta(seconds=access_seconds),
            },
            self._settings.jwt_access_secret,
            algorithm="HS256",
        )
        refresh = jwt.encode(
            {
                **common,
                "typ": "refresh",
                "jti": refresh_jti,
                "exp": issued_at + timedelta(seconds=refresh_seconds),
            },
            self._settings.jwt_refresh_secret,
            algorithm="HS256",
        )
        return TokenPair(
            access_token=access,
            refresh_token=refresh,
            access_expires_in=access_seconds,
            refresh_expires_in=refresh_seconds,
            refresh_jti=refresh_jti,
        )

    def decode_access(self, token: str, *, audience: str) -> dict[str, Any]:
        """校验并解码 access token。"""
        return self._decode(
            token,
            secret=self._settings.jwt_access_secret,
            audience=audience,
            token_type="access",
        )

    def decode_refresh(self, token: str, *, audience: str) -> dict[str, Any]:
        """校验并解码 refresh token。"""
        return self._decode(
            token,
            secret=self._settings.jwt_refresh_secret,
            audience=audience,
            token_type="refresh",
        )

    @staticmethod
    def hash_token_identifier(value: str) -> str:
        return hashlib.sha256(value.encode("utf-8")).hexdigest()

    @staticmethod
    def _decode(
        token: str,
        *,
        secret: str,
        audience: str,
        token_type: str,
    ) -> dict[str, Any]:
        """执行公共解码逻辑，并把第三方库异常转换成本项目的业务异常。"""
        try:
            payload = jwt.decode(
                token,
                secret,
                algorithms=["HS256"],
                audience=audience,
                issuer="zhibaotong-s2",
            )
        except jwt.PyJWTError as exc:
            raise AppError("AUTH_REQUIRED", "登录凭证无效或已过期", 401) from exc
        if payload.get("typ") != token_type:
            raise AppError("AUTH_REQUIRED", "登录凭证类型无效", 401)
        return payload
