"""Caching wrapper for IntrospectionTokenVerifier.

Reduces Keycloak round-trips by caching introspection results in-memory.
TTL for active tokens: min(token.expires_at - now, INTROSPECTION_CACHE_TTL_SECONDS).
TTL for inactive/failed tokens: INTROSPECTION_NEGATIVE_CACHE_TTL_SECONDS (1 min).
"""

from __future__ import annotations

import asyncio
import hashlib
import time
from typing import TYPE_CHECKING

from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier

from ims_mcp.constants import (
    INTROSPECTION_CACHE_TTL_SECONDS,
    INTROSPECTION_NEGATIVE_CACHE_TTL_SECONDS,
)

if TYPE_CHECKING:
    from mcp.server.auth.provider import AccessToken


def _token_hash(token: str) -> str:
    return hashlib.sha256(token.encode()).hexdigest()


class CachedIntrospectionTokenVerifier(IntrospectionTokenVerifier):
    """IntrospectionTokenVerifier with short-TTL in-memory cache.

    Keycloak introspection is called at most once per token per TTL window,
    eliminating per-request HTTP round-trips to Keycloak.
    """

    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)
        # {token_hash: (result, expires_at_unix)}
        self._cache: dict[str, tuple[AccessToken | None, float]] = {}
        self._lock = asyncio.Lock()

    async def verify_token(self, token: str) -> AccessToken | None:
        key = _token_hash(token)
        now = time.time()

        async with self._lock:
            entry = self._cache.get(key)
            if entry is not None:
                result, cache_expires_at = entry
                if now < cache_expires_at:
                    return result
                # Expired cache entry — remove it
                del self._cache[key]

        result = await super().verify_token(token)

        async with self._lock:
            if result is not None and result.expires_at:
                # Active token: cache until min(token expiry, 15 min from now)
                token_ttl = result.expires_at - now
                ttl = min(token_ttl, INTROSPECTION_CACHE_TTL_SECONDS)
            else:
                # Inactive or unknown expiry: short negative cache
                ttl = INTROSPECTION_NEGATIVE_CACHE_TTL_SECONDS

            if ttl > 0:
                self._cache[key] = (result, now + ttl)

        return result

    def clear_cache(self) -> None:
        """Evict all cache entries (e.g., after logout/revocation)."""
        self._cache.clear()
