"""Context-aware cached property decorator.

This module provides `contextually_cached_property`, a descriptor that caches
property values per context variable, making it suitable for use in concurrent
applications where different contexts (e.g., asyncio tasks) need isolated caches.
"""

from __future__ import annotations

from contextvars import ContextVar
from typing import TYPE_CHECKING
from weakref import WeakKeyDictionary

if TYPE_CHECKING:
    from collections.abc import Callable
    from typing import Self, overload


class contextually_cached_property[InstanceT, ValueT]:  # noqa: N801
    """A property decorator that caches values per context variable.

    Unlike `functools.cached_property`, this decorator maintains separate caches
    for each context (e.g., asyncio tasks), using `contextvars.ContextVar`.
    The cache uses weak references to instances, allowing garbage collection
    when instances are no longer referenced elsewhere.

    Example:
        class MyClass:
            @contextually_cached_property
            def expensive_computation(self) -> int:
                return compute_something()

    """

    def __init__(self, fget: Callable[[InstanceT], ValueT]) -> None:
        """Initialize the cached property.

        Args:
            fget: The getter function to cache results from.

        """
        self._fget = fget
        self._cache: ContextVar[WeakKeyDictionary[InstanceT, ValueT]] = ContextVar(
            f"{fget.__module__}.{fget.__qualname__}",
        )

    if TYPE_CHECKING:

        @overload
        def __get__(self, instance: None, owner: type[InstanceT]) -> Self: ...
        @overload
        def __get__(self, instance: InstanceT, owner: type[InstanceT]) -> ValueT: ...

    def __get__(
        self,
        instance: InstanceT | None,
        owner: type[InstanceT],
    ) -> Self | ValueT:
        """Retrieve the cached value or compute and cache it.

        Args:
            instance: The instance to get the property from, or None if
                accessed on the class.
            owner: The class that owns this property.

        Returns:
            The descriptor itself if accessed on the class, otherwise the
            cached/computed value.

        """
        if instance is None:
            return self
        try:
            cache = self._cache.get()
        except LookupError:
            cache = WeakKeyDictionary()
            self._cache.set(cache)
        if instance not in cache:
            cache[instance] = self._fget(instance)
        return cache[instance]

    def __delete__(self, instance: InstanceT) -> None:
        """Delete the cached value for the given instance.

        Args:
            instance: The instance to delete the cached value for.

        """
        try:
            cache = self._cache.get()
        except LookupError:
            return
        if instance in cache:
            del cache[instance]
