"""Cache Manager — Provides method-level caching decorators for Django.

Usage::

    from swifty.cache.manager import cache, cache_property

    class MyService:
        @cache(ttl=3600, static_key="my_data")
        def get_data(self):
            return expensive_computation()

        @cache_property
        def computed(self):
            return heavy_calculation()
"""

from typing import Any, Callable, Optional, Tuple
from functools import wraps

from django.conf import settings

from swifty.cache.exceptions import NotInCache
from swifty.cache.constants import SwiftyCacheEvents
from swifty.logging.logger import SwiftyLoggerMixin
from swifty.utils.mapper import getpath
from swifty.utils.importer import import_module_attribute


class SwiftyCacheDescriptor(SwiftyLoggerMixin):
    """Descriptor that wraps a method with caching logic.

    When accessed on an instance, returns cached data if available,
    otherwise calls the method, stores the result, and returns it.
    """

    cache_attribute_pattern: str = "{name}"

    def __init__(
        self,
        method: Callable,
        *,
        ttl: Optional[int] = None,
        static_key: Optional[str] = None,
        vary_by: Tuple[str, ...] = (),
        context_getter: Optional[str] = None,
        as_property: bool = False,
    ):
        self.method = method
        self.ttl = ttl
        self.static_key = static_key
        self.vary_by = vary_by
        self.context_getter = context_getter
        self.as_property = as_property
        self.cache_attribute = self.cache_attribute_pattern.format(
            name=method.__name__
        )
        wraps(method)(self)

    def __set_name__(self, owner, name):
        self.attr_name = name

    def __get__(self, instance, owner=None):
        if instance is None:
            return self
        if self.as_property:
            return self.getter(instance)
        return lambda *args, **kwargs: self.getter(instance, *args, **kwargs)

    def _get_cache_backend(self, instance):
        """Instantiate the appropriate cache backend."""
        backend_cls = get_cache_backend()
        return backend_cls(
            parent=instance,
            attr=self.cache_attribute,
            ttl=self.ttl or getpath(instance, "redis_cache_pattern.ttl", 3600),
            static_key=self.static_key,
            vary_by=(
                self.vary_by or getpath(instance, "redis_cache_pattern.vary", "")
            ),
            context_getter=self.context_getter,
        )

    def getter(self, instance, *args, new_cache: bool = False, **kwargs) -> Any:
        """Return cached value or compute and store it.

        Args:
            instance: The object instance the method belongs to.
            new_cache: If True, bypass cache and force recomputation.

        Returns:
            The cached or freshly computed value.
        """
        cache_instance = self._get_cache_backend(instance)

        try:
            if new_cache:
                self.logger.debug(
                    SwiftyCacheEvents.CREATE_NEW_CACHE,
                    cached_function=self.cache_attribute,
                    args=args,
                    kwargs=kwargs,
                )
                raise NotInCache

            cached_data = cache_instance.get(*args, **kwargs)
            self.logger.debug(
                SwiftyCacheEvents.CACHED_DATA,
                cached_function=self.cache_attribute,
                data=cached_data,
                args=args,
                kwargs=kwargs,
            )
            return cached_data

        except NotInCache:
            self.logger.debug(
                SwiftyCacheEvents.DATA_NOT_IN_CACHE,
                caching_function=self.cache_attribute,
                args=args,
                kwargs=kwargs,
            )
            result = self.method(instance, *args, **kwargs)
            return cache_instance.set(result, *args, **kwargs)


class SwiftyCacheDecorator:
    """Factory for creating cache descriptors as decorators.

    Usage::

        @SwiftyCacheDecorator.as_decorator()(ttl=3600)
        def my_method(self):
            ...
    """

    def __init__(
        self,
        ttl: Optional[int] = None,
        static_key: Optional[str] = None,
        vary_by: Tuple[str, ...] = (),
        context_getter: str = "context",
        is_method: bool = False,
        as_property: bool = False,
    ):
        self.ttl = ttl
        self.static_key = static_key
        self.vary_by = vary_by
        self.context_getter = context_getter
        self.as_property = as_property

    def __call__(self, method: Callable) -> SwiftyCacheDescriptor:
        return SwiftyCacheDescriptor(
            method,
            ttl=self.ttl,
            static_key=self.static_key,
            vary_by=self.vary_by,
            context_getter=self.context_getter,
            as_property=self.as_property,
        )

    @classmethod
    def as_decorator(cls, **defaults):
        """Create a decorator factory with optional defaults.

        Returns:
            A callable that accepts caching parameters and returns a decorator.
        """

        def decorator_factory(**kwargs):
            merged = {**defaults, **kwargs}
            return cls(**merged)

        return decorator_factory


def get_cache_backend():
    """Get the configured cache backend class.

    Checks ``settings.CACHE_BACKEND`` for a custom backend path.
    Falls back to :class:`swifty.redis.cache.RedisCache`.

    Returns:
        The cache backend class.
    """
    if not hasattr(settings, "CACHE_BACKEND"):
        from swifty.redis.cache import RedisCache  # noqa: F811

        return RedisCache

    return import_module_attribute(settings.CACHE_BACKEND)


cache = SwiftyCacheDecorator.as_decorator()
cache_property = SwiftyCacheDecorator.as_decorator(as_property=True)
