"""
Mixins for Django models and DRF ViewSets with declarative caching.

Provides automatic caching for DRF list/retrieve actions and
model-level cache invalidation on save/delete.
"""

from functools import wraps
from logging import getLogger

from django.core.cache import cache
from django.db.models.signals import class_prepared, m2m_changed
from rest_framework.response import Response

from ez_django_common.utils.caching_utils import (
    get_cache_key_with_version,
    invalidate_cache,
)

logger = getLogger(__name__)


# =============================================================================
# MODEL MIXINS
# =============================================================================


class CacheInvalidationMixin:
    """
    Model mixin that automatically invalidates cache version on save(), delete(),
    and M2M field changes.

    This replaces the need for Django signals by directly hooking into
    model lifecycle methods and auto-registering M2M signal handlers.

    Supports both single and multiple cache keys:
    - cache_version_key: str - single cache key (backward compatible)
    - cache_version_keys: list - multiple cache keys (new feature)
    - invalidate_cache_on_m2m: bool - auto-handle M2M changes (default: True)

    Usage:
        # Single key (backward compatible):
        class Category(CacheInvalidationMixin, models.Model):
            cache_version_key = 'category_list'

        # Multiple keys with M2M auto-handling:
        class Product(CacheInvalidationMixin, models.Model):
            cache_version_keys = ['product_list', 'product_detail']
            # invalidate_cache_on_m2m = True  # This is the default

            categories = models.ManyToManyField(...)  # Automatically handled!

        # Disable M2M auto-handling if needed:
        class SomeModel(CacheInvalidationMixin, models.Model):
            cache_version_key = 'some_list'
            invalidate_cache_on_m2m = False  # Opt-out
    """

    # Cache version key to invalidate (must be set in subclass)
    cache_version_key = None
    cache_version_keys = None  # New: support multiple keys
    invalidate_cache_on_m2m = True  # New: auto-handle M2M changes

    # Class variable to track registered models
    _m2m_signals_registered = set()

    def __init_subclass__(cls, **kwargs):
        """
        Automatically register M2M signal handlers when model class is created.
        """
        super().__init_subclass__(**kwargs)

        # Use class_prepared signal to register M2M handlers after model is fully ready

        def register_m2m_signals(sender, **kwargs):
            # Only process our specific class
            if sender is not cls:
                return

            # Check if this model wants M2M cache invalidation
            if not getattr(cls, "invalidate_cache_on_m2m", True):
                return

            # Check if already registered
            if cls in CacheInvalidationMixin._m2m_signals_registered:
                return

            # Get cache keys
            cache_keys = cls._get_cache_keys_static(cls)
            if not cache_keys:
                return  # No cache keys defined

            # Find all M2M fields and register signals
            for field in cls._meta.get_fields():
                if field.many_to_many and not field.auto_created:
                    # Get the through model
                    through_model = getattr(cls, field.name).through

                    # Create a signal handler for this M2M field
                    def make_m2m_handler(keys):
                        def m2m_handler(sender, instance, action, **kwargs):
                            if action in ["post_add", "post_remove", "post_clear"]:
                                for key in keys:
                                    invalidate_cache(key)

                        return m2m_handler

                    # Connect the signal with a unique dispatch_uid
                    dispatch_uid = f"cache_invalidation_{cls.__name__}_{field.name}_m2m"
                    m2m_changed.connect(
                        make_m2m_handler(cache_keys),
                        sender=through_model,
                        dispatch_uid=dispatch_uid,
                    )

            # Mark as registered
            CacheInvalidationMixin._m2m_signals_registered.add(cls)

        # Connect to class_prepared signal
        class_prepared.connect(register_m2m_signals, weak=False)

    @staticmethod
    def _get_cache_keys_static(cls):
        """Get all cache keys to invalidate (static method for class-level access)."""
        keys = []

        # Add multiple keys if defined
        cache_version_keys = getattr(cls, "cache_version_keys", None)
        if cache_version_keys:
            if isinstance(cache_version_keys, (list, tuple)):
                keys.extend(cache_version_keys)
            else:
                keys.append(cache_version_keys)

        # Add single key if defined (and not already in keys)
        cache_version_key = getattr(cls, "cache_version_key", None)
        if cache_version_key and cache_version_key not in keys:
            keys.append(cache_version_key)

        return keys

    def _get_cache_keys(self):
        """Get all cache keys to invalidate."""
        return self._get_cache_keys_static(self.__class__)

    def save(self, *args, **kwargs):
        """Override save to invalidate cache after saving."""
        # Call parent save first
        result = super().save(*args, **kwargs)

        # Invalidate all cache versions
        for key in self._get_cache_keys():
            invalidate_cache(key)

        return result

    def delete(self, *args, **kwargs):
        """Override delete to invalidate cache after deletion."""
        # Invalidate all cache versions first (before object is gone)
        for key in self._get_cache_keys():
            invalidate_cache(key)

        # Call parent delete
        return super().delete(*args, **kwargs)


# =============================================================================
# DRF VIEWSET MIXINS
# =============================================================================


class CachedListMixin:
    """
    Mixin for caching list() responses in DRF viewsets.

    Usage:
        class MyViewSet(CachedListMixin, viewsets.ModelViewSet):
            cache_key_prefix = "my_model"
            cache_version_key = "my_model_list"
            cache_timeout = 60 * 15  # 15 minutes
            cache_hit_message = "Cache hit for list"  # optional
    """

    cache_key_prefix = None
    cache_version_key = None
    cache_timeout = 60 * 15  # 15 minutes default
    cache_hit_message = None

    def list(self, request, *args, **kwargs):
        """Override list to add caching."""
        if not self.cache_key_prefix or not self.cache_version_key:
            # If not configured, fallback to default behavior
            return super().list(request, *args, **kwargs)

        # Build cache key from query params
        query_params = dict(request.query_params)
        cache_key = get_cache_key_with_version(
            base_key=self.cache_key_prefix,
            version_key=self.cache_version_key,
            params=query_params,
        )

        # Try to get from cache
        cached_data = cache.get(cache_key)
        if cached_data is not None:
            if self.cache_hit_message:
                logger.info(self.cache_hit_message)
            return Response(cached_data)

        # Get fresh data
        response = super().list(request, *args, **kwargs)

        # Cache the response data
        if response.status_code == 200:
            cache.set(cache_key, response.data, self.cache_timeout)

        return response


class CachedRetrieveMixin:
    """
    Mixin for caching retrieve() responses in DRF viewsets.

    Usage:
        class MyViewSet(CachedRetrieveMixin, viewsets.ModelViewSet):
            cache_key_prefix = "my_model"
            cache_detail_version_key = "my_model_detail"
            cache_timeout = 60 * 15  # 15 minutes
            cache_hit_message = "Cache hit for detail"  # optional
    """

    cache_key_prefix = None
    cache_detail_version_key = None
    cache_timeout = 60 * 15  # 15 minutes default
    cache_hit_message = None

    def retrieve(self, request, *args, **kwargs):
        """Override retrieve to add caching."""
        if not self.cache_key_prefix or not self.cache_detail_version_key:
            # If not configured, fallback to default behavior
            return super().retrieve(request, *args, **kwargs)

        # Build cache key from pk and query params
        pk = kwargs.get("pk")
        query_params = dict(request.query_params)
        query_params["pk"] = pk

        cache_key = get_cache_key_with_version(
            base_key=f"{self.cache_key_prefix}_detail",
            version_key=self.cache_detail_version_key,
            params=query_params,
        )

        # Try to get from cache
        cached_data = cache.get(cache_key)
        if cached_data is not None:
            if self.cache_hit_message:
                logger.info(self.cache_hit_message)
            return Response(cached_data)

        # Get fresh data
        response = super().retrieve(request, *args, **kwargs)

        # Cache the response data
        if response.status_code == 200:
            cache.set(cache_key, response.data, self.cache_timeout)

        return response


class CachedViewSetMixin(CachedListMixin, CachedRetrieveMixin):
    """
    Combined mixin for caching both list() and retrieve() in DRF viewsets.

    Usage:
        class MyViewSet(CachedViewSetMixin, viewsets.ModelViewSet):
            cache_key_prefix = "my_model"
            cache_list_version_key = "my_model_list"
            cache_detail_version_key = "my_model_detail"
            cache_timeout = 60 * 15  # 15 minutes
            cache_list_hit_message = "Cache hit for list"  # optional
            cache_detail_hit_message = "Cache hit for detail"  # optional
    """

    cache_list_version_key = None
    cache_detail_version_key = None
    cache_list_hit_message = None
    cache_detail_hit_message = None

    @property
    def cache_version_key(self):
        """Provide cache_version_key for CachedListMixin."""
        return self.cache_list_version_key

    def list(self, request, *args, **kwargs):
        """Override list with custom hit message."""
        # Temporarily set cache_hit_message for list
        original_message = getattr(self, "cache_hit_message", None)
        if self.cache_list_hit_message:
            self.cache_hit_message = self.cache_list_hit_message

        result = super().list(request, *args, **kwargs)

        # Restore original message
        self.cache_hit_message = original_message
        return result

    def retrieve(self, request, *args, **kwargs):
        """Override retrieve with custom hit message."""
        # Temporarily set cache_hit_message for retrieve
        original_message = getattr(self, "cache_hit_message", None)
        if self.cache_detail_hit_message:
            self.cache_hit_message = self.cache_detail_hit_message

        result = super().retrieve(request, *args, **kwargs)

        # Restore original message
        self.cache_hit_message = original_message
        return result


def cached_action(version_key, timeout=60 * 15, key_prefix=None):
    """
    Decorator for caching custom DRF actions.

    Usage:
        @cached_action(version_key="my_custom_action", timeout=60*30)
        @action(detail=False, methods=['get'])
        def custom_action(self, request):
            # Your action logic
            return Response(data)
    """

    def decorator(func):
        @wraps(func)
        def wrapper(self, request, *args, **kwargs):
            # Build cache key
            base_key = key_prefix or f"{self.cache_key_prefix}_{func.__name__}"
            query_params = dict(request.query_params)

            cache_key = get_cache_key_with_version(
                base_key=base_key, version_key=version_key, params=query_params
            )

            # Try to get from cache
            cached_data = cache.get(cache_key)
            if cached_data is not None:
                return Response(cached_data)

            # Get fresh data
            response = func(self, request, *args, **kwargs)

            # Cache the response data
            if isinstance(response, Response) and response.status_code == 200:
                cache.set(cache_key, response.data, timeout)

            return response

        return wrapper

    return decorator
