"""اعتبارسنجی JWT مشترک (Keycloak / OIDC) برای همه سرویس‌های backend."""
from __future__ import annotations

import time
from dataclasses import dataclass
from typing import Any

import httpx
import jwt

from shared.exceptions import UnauthorizedError
from shared.security import CurrentUser


@dataclass(frozen=True)
class JWTSettings:
    keycloak_enabled: bool = False
    keycloak_server_url: str = "http://localhost:8080"
    keycloak_realm: str = "superapp"
    jwt_algorithm: str = "RS256"
    jwt_audience: str = "account"
    jwt_verify_signature: bool = False
    # issuer واقعی داخل JWT (ممکن است با keycloak_server_url داخلی Docker متفاوت باشد)
    jwt_issuer: str | None = None

    @property
    def jwks_url(self) -> str:
        return (
            f"{self.keycloak_server_url}/realms/{self.keycloak_realm}"
            "/protocol/openid-connect/certs"
        )

    @property
    def issuer(self) -> str:
        if self.jwt_issuer:
            return self.jwt_issuer
        return f"{self.keycloak_server_url}/realms/{self.keycloak_realm}"

    @property
    def allowed_issuers(self) -> list[str]:
        """Issuerهای مجاز — توکن ممکن است از URL داخلی Docker یا عمومی صادر شده باشد."""
        issuers = [
            self.issuer,
            f"{self.keycloak_server_url}/realms/{self.keycloak_realm}",
        ]
        if self.jwt_issuer:
            issuers.insert(0, self.jwt_issuer)
        return list(dict.fromkeys(i for i in issuers if i))


class JWTValidator:
    """اعتبارسنج JWT با پشتیبانی JWKS و حالت development."""

    def __init__(self, settings: JWTSettings) -> None:
        self.settings = settings
        self._jwks_cache: dict[str, Any] = {"keys": None, "fetched_at": 0.0}
        self._jwks_ttl = 3600

    async def validate(self, token: str) -> CurrentUser:
        if not token:
            raise UnauthorizedError("توکن احراز هویت ارائه نشده است")
        try:
            if self.settings.jwt_verify_signature and self.settings.keycloak_enabled:
                claims = await self._decode_verified(token)
            else:
                claims = self._decode_unverified(token)
        except UnauthorizedError:
            raise
        except Exception as exc:
            raise UnauthorizedError(f"توکن نامعتبر است: {exc}") from exc
        return self._claims_to_user(claims)

    def _decode_unverified(self, token: str) -> dict[str, Any]:
        return jwt.decode(token, options={"verify_signature": False})

    async def _decode_verified(self, token: str) -> dict[str, Any]:
        jwks = await self._get_jwks()
        if jwks is None:
            raise UnauthorizedError("امکان دریافت کلید عمومی برای اعتبارسنجی توکن نبود")
        try:
            signing_key = jwt.PyJWKClient(self.settings.jwks_url).get_signing_key_from_jwt(token)
            claims = jwt.decode(
                token,
                signing_key.key,
                algorithms=[self.settings.jwt_algorithm],
                options={"verify_aud": False, "verify_iss": False},
            )
            token_iss = claims.get("iss")
            if not self._issuer_allowed(token_iss):
                raise UnauthorizedError(
                    f"توکن نامعتبر است: issuer نامعتبر ({token_iss})"
                )
            return claims
        except jwt.PyJWTError as exc:
            raise UnauthorizedError(f"توکن نامعتبر است: {exc}") from exc

    def _issuer_allowed(self, token_iss: str | None) -> bool:
        if not token_iss:
            return False
        if token_iss in self.settings.allowed_issuers:
            return True
        realm_suffix = f"/realms/{self.settings.keycloak_realm}"
        return token_iss.endswith(realm_suffix)

    async def _get_jwks(self) -> dict[str, Any] | None:
        now = time.time()
        if (
            self._jwks_cache["keys"] is not None
            and now - self._jwks_cache["fetched_at"] < self._jwks_ttl
        ):
            return self._jwks_cache["keys"]
        try:
            async with httpx.AsyncClient(timeout=5.0) as client:
                resp = await client.get(self.settings.jwks_url)
                resp.raise_for_status()
                keys = resp.json()
                self._jwks_cache["keys"] = keys
                self._jwks_cache["fetched_at"] = now
                return keys
        except Exception:
            return None

    @staticmethod
    def _claims_to_user(claims: dict[str, Any]) -> CurrentUser:
        realm_access = claims.get("realm_access") or {}
        roles = list(realm_access.get("roles", [])) if isinstance(realm_access, dict) else []
        # نقش‌های client-specific (مثلاً superapp-frontend)
        resource_access = claims.get("resource_access") or {}
        if isinstance(resource_access, dict):
            for client_roles in resource_access.values():
                if isinstance(client_roles, dict):
                    roles.extend(client_roles.get("roles", []))
        roles = list(dict.fromkeys(roles))  # یکتا
        return CurrentUser(
            user_id=str(claims.get("sub", "")),
            username=claims.get("preferred_username"),
            email=claims.get("email"),
            roles=roles,
            tenant_id=claims.get("tenant_id"),
        )
