"""
Async-first View Executor Module

Wraps DRF view invocations with async support, permission checking, and error handling.
Uses APIRequestFactory to mock requests and asgiref for sync_to_async conversion.
"""

import logging
from typing import Any, Dict, Optional, Type

from asgiref.sync import sync_to_async
from django.contrib.auth.models import AnonymousUser
from rest_framework.exceptions import PermissionDenied
from rest_framework.request import Request
from rest_framework.test import APIRequestFactory
from rest_framework.views import APIView

logger = logging.getLogger(__name__)


class ViewInvoker:
    """
    Executes DRF view methods with async/await support.
    
    Handles:
    - Mocking HTTP requests via APIRequestFactory
    - Permission checking before execution
    - User authentication binding
    - Sync-to-async conversion for non-blocking execution
    """

    def __init__(self):
        """Initialize view invoker with request factory"""
        self.factory = APIRequestFactory()

    async def invoke(
        self,
        view_class: Type[APIView],
        method: str,
        path: str,
        data: Optional[Dict[str, Any]] = None,
        user: Optional[Any] = None,
    ) -> Any:
        """
        Invoke a DRF view method asynchronously.
        
        Args:
            view_class: DRF APIView or ViewSet class
            method: HTTP method name (GET, POST, etc.)
            path: URL path for the request
            data: Request payload (for POST/PUT/PATCH)
            user: Django User object for authentication
            
        Returns:
            Response data from view
            
        Raises:
            PermissionDenied: If user lacks required permissions
            ValueError: If method is invalid
        """
        method = method.upper()
        if method not in ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]:
            raise ValueError(f"Invalid HTTP method: {method}")

        # Create mocked request
        request = self._create_request(method, path, data, user)

        # Run view invocation asynchronously
        response = await sync_to_async(
            self._invoke_view_sync, thread_sensitive=False
        )(view_class, request, method)

        return response

    def _create_request(
        self,
        method: str,
        path: str,
        data: Optional[Dict[str, Any]] = None,
        user: Optional[Any] = None,
    ) -> Request:
        """
        Create a mocked DRF request.
        
        Args:
            method: HTTP method
            path: URL path
            data: Request body
            user: Authenticated user
            
        Returns:
            DRF Request object
        """
        method_lower = method.lower()

        if method in ["GET", "HEAD", "OPTIONS"]:
            drf_request = getattr(self.factory, method_lower)(path)
        else:
            # POST, PUT, PATCH, DELETE
            drf_request = getattr(self.factory, method_lower)(
                path, data or {}, format="json"
            )

        # Bind user to request
        if user:
            drf_request.user = user
        else:
            drf_request.user = AnonymousUser()

        return drf_request

    @staticmethod
    def _invoke_view_sync(
        view_class: Type[APIView],
        drf_request: Request,
        method: str,
    ) -> Any:
        """
        Synchronously invoke a view (runs in thread pool via sync_to_async).
        
        Args:
            view_class: DRF view class
            drf_request: DRF Request object
            method: HTTP method
            
        Returns:
            View response data
            
        Raises:
            PermissionDenied: If permission check fails
        """
        try:
            # Instantiate view
            view_instance = view_class.as_view()

            # Check permissions before calling view
            # Note: This is important for security - we never bypass permission checks
            if hasattr(view_class, "check_permissions"):
                # Create a temporary view to check permissions
                temp_view = view_class()
                temp_view.request = drf_request
                temp_view.format_kwarg = None
                try:
                    temp_view.check_permissions(drf_request)
                except PermissionDenied as e:
                    logger.warning(
                        f"Permission denied for user {drf_request.user}",
                        extra={"view": view_class.__name__, "method": method},
                    )
                    raise

            # Invoke the view
            response = view_instance(drf_request)

            # If response has data, extract it
            if hasattr(response, "data"):
                return response.data
            elif hasattr(response, "content"):
                return response.content
            else:
                return response

        except PermissionDenied:
            raise
        except Exception as e:
            logger.error(
                f"Error invoking view {view_class.__name__}: {e}",
                extra={"method": method},
                exc_info=True,
            )
            raise

    async def invoke_with_permission_check(
        self,
        view_class: Type[APIView],
        method: str,
        path: str,
        data: Optional[Dict[str, Any]] = None,
        user: Optional[Any] = None,
        obj: Optional[Any] = None,
    ) -> Any:
        """
        Invoke view with both view-level and object-level permission checks.
        
        Args:
            view_class: DRF APIView or ViewSet class
            method: HTTP method
            path: URL path
            data: Request payload
            user: Authenticated user
            obj: Object for row-level permission checks (optional)
            
        Returns:
            Response data
            
        Raises:
            PermissionDenied: If any permission check fails
        """
        request = self._create_request(method, path, data, user)

        # Check object-level permissions if provided
        if obj:
            response = await sync_to_async(
                self._invoke_with_object_permissions_sync, thread_sensitive=False
            )(view_class, request, method, obj)
        else:
            response = await sync_to_async(
                self._invoke_view_sync, thread_sensitive=False
            )(view_class, request, method)

        return response

    @staticmethod
    def _invoke_with_object_permissions_sync(
        view_class: Type[APIView],
        drf_request: Request,
        method: str,
        obj: Any,
    ) -> Any:
        """
        Invoke view with object-level permission checks.
        
        Args:
            view_class: DRF view class
            drf_request: DRF Request object
            method: HTTP method
            obj: Object to check permissions against
            
        Returns:
            View response data
        """
        try:
            # Create view instance for permission checks
            temp_view = view_class()
            temp_view.request = drf_request
            temp_view.format_kwarg = None

            # Check view-level permissions
            try:
                temp_view.check_permissions(drf_request)
            except PermissionDenied:
                raise

            # Check object-level permissions
            if hasattr(temp_view, "check_object_permissions"):
                try:
                    temp_view.check_object_permissions(drf_request, obj)
                except PermissionDenied:
                    logger.warning(
                        f"Object-level permission denied for {drf_request.user}",
                        extra={"view": view_class.__name__, "object": str(obj)},
                    )
                    raise

            # Now invoke the view
            view_instance = view_class.as_view()
            response = view_instance(drf_request)

            if hasattr(response, "data"):
                return response.data
            elif hasattr(response, "content"):
                return response.content
            else:
                return response

        except PermissionDenied:
            raise
        except Exception as e:
            logger.error(
                f"Error invoking view with object permissions: {e}",
                exc_info=True,
            )
            raise
