import functools
import logging
import traceback
import typing
from collections.abc import Callable
from datetime import timedelta
from typing import Literal

from orjson import JSONDecodeError
from pydantic import BaseModel, ValidationError

from panther._utils import is_function_async
from panther.base_request import BaseRequest
from panther.caching import (
    get_response_from_cache,
    set_response_in_cache,
)
from panther.configs import config
from panther.exceptions import (
    APIError,
    AuthorizationAPIError,
    BadRequestAPIError,
    JSONDecodeAPIError,
    MethodNotAllowedAPIError,
    PantherError,
)
from panther.middlewares import HTTPMiddleware
from panther.openapi import OutputSchema
from panther.permissions import BasePermission
from panther.request import Request
from panther.response import Response
from panther.serializer import ModelSerializer
from panther.throttling import Throttle

__all__ = ('API', 'GenericAPI')

logger = logging.getLogger('panther')


class API:
    """
    methods: Specify the allowed methods.
    input_model: The `request.data` will be validated with this attribute, It will raise an
        `panther.exceptions.BadRequestAPIError` or put the validated data in the `request.validated_data`.
    output_schema: This attribute only used in creation of OpenAPI scheme which is available in `panther.openapi.urls`
        You may want to add its `url` to your urls.
    auth: It will authenticate the user with header of its request or raise an
        `panther.exceptions.AuthenticationAPIError`.
    permissions: List of permissions that will be called sequentially after authentication to authorize the user.
    throttling: It will limit the users' request on a specific (time-window, path)
    cache: Specify the duration of the cache (Will be used only in GET requests).
    middlewares: These middlewares have inner priority than global middlewares.
    """

    func: Callable

    def __init__(
        self,
        *,
        methods: list[Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE']] | None = None,
        input_model: type[ModelSerializer] | type[BaseModel] | None = None,
        output_schema: OutputSchema | None = None,
        auth: bool = False,
        permissions: list[type[BasePermission]] | None = None,
        throttling: Throttle | None = None,
        cache: timedelta | None = None,
        middlewares: list[type[HTTPMiddleware]] | None = None,
        **kwargs,
    ):
        self.methods = {m.upper() for m in methods} if methods else {'GET', 'POST', 'PUT', 'PATCH', 'DELETE'}
        self.input_model = input_model
        self.output_schema = output_schema
        self.auth = auth
        self.permissions = permissions or []
        self.throttling = throttling
        self.cache = cache
        self.middlewares: list[[HTTPMiddleware]] | None = middlewares
        self.request: Request | None = None
        if kwargs.pop('output_model', None):
            deprecation_message = (
                traceback.format_stack(limit=2)[0]
                + '\nThe `output_model` argument has been removed in Panther v5 and is no longer available.'
                '\nPlease update your code to use the new approach. More info: '
                'https://pantherpy.github.io/open_api/'
            )
            raise PantherError(deprecation_message)
        if kwargs.pop('cache_exp_time', None):
            deprecation_message = (
                traceback.format_stack(limit=2)[0]
                + '\nThe `cache_exp_time` argument has been removed in Panther v5 and is no longer available.'
                '\nYou may want to use `cache` instead.'
            )
            raise PantherError(deprecation_message)
        # Validate Cache
        if self.cache and not isinstance(self.cache, timedelta):
            deprecation_message = (
                traceback.format_stack(limit=2)[0] + '\nThe `cache` argument has been changed in Panther v5, '
                'it should be an instance of `datetime.timedelta()`.'
            )
            raise PantherError(deprecation_message)
        assert self.cache is None or isinstance(self.cache, timedelta)
        # Validate Permissions
        for perm in self.permissions:
            if is_function_async(perm.authorization) is False:
                msg = f'{perm.__name__}.authorization() should be `async`'
                logger.error(msg)
                raise PantherError(msg)
            if type(perm.authorization).__name__ != 'method':
                msg = f'{perm.__name__}.authorization() should be `@classmethod`'
                logger.error(msg)
                raise PantherError(msg)
        # Check kwargs
        if kwargs:
            msg = f'Unknown kwargs: {kwargs.keys()}'
            logger.error(msg)
            raise PantherError(msg)

    def __call__(self, func):
        self.func = func
        self.is_function_async = is_function_async(self.func)
        self.function_annotations = {
            k: v for k, v in func.__annotations__.items() if v in {BaseRequest, Request, bool, int}
        }

        @functools.wraps(func)
        async def wrapper(request: Request) -> Response:
            chained_func = self.handle_endpoint
            if self.middlewares:
                for middleware in reversed(self.middlewares):
                    chained_func = middleware(chained_func)
            return await chained_func(request=request)

        # Store attributes on the function, so have the same behaviour as class-based (useful in `openapi.view.OpenAPI`)
        wrapper.auth = self.auth
        wrapper.methods = self.methods
        wrapper.permissions = self.permissions
        wrapper.input_model = self.input_model
        wrapper.output_schema = self.output_schema
        return wrapper

    async def handle_endpoint(self, request: Request) -> Response:
        self.request = request

        # 1. Check Method
        if self.request.method not in self.methods:
            raise MethodNotAllowedAPIError

        # 2. Authentication
        if self.auth:
            if not config.AUTHENTICATION:
                logger.critical('"AUTHENTICATION" has not been set in configs')
                raise APIError
            self.request.user = await config.AUTHENTICATION.authentication(self.request)

        # 3. Permissions
        for perm in self.permissions:
            if await perm.authorization(self.request) is False:
                raise AuthorizationAPIError

        # 4. Throttle
        if throttling := self.throttling or config.THROTTLING:
            await throttling.check_and_increment(request=self.request)

        # 5. Validate Input
        if self.input_model and self.request.method in {'POST', 'PUT', 'PATCH'}:
            self.request.validated_data = self.validate_input(model=self.input_model, request=self.request)

        # 6. Get Cached Response
        if self.cache and self.request.method == 'GET':
            if cached := await get_response_from_cache(request=self.request, duration=self.cache):
                return Response(data=cached.data, headers=cached.headers, status_code=cached.status_code)

        # 7. Put PathVariables and Request(If User Wants It) In kwargs
        kwargs = self.request.clean_parameters(self.function_annotations)

        # 8. Call Endpoint
        if self.is_function_async:
            response = await self.func(**kwargs)
        else:
            response = self.func(**kwargs)

        # 9. Clean Response
        if not isinstance(response, Response):
            response = Response(data=response)
        if response.pagination:
            response.data = await response.pagination.template(response.data)

        # 10. Set New Response To Cache
        if self.cache and self.request.method == 'GET':
            await set_response_in_cache(request=self.request, response=response, duration=self.cache)

        return response

    @classmethod
    def validate_input(cls, model, request: Request):
        if isinstance(request.data, bytes):
            raise BadRequestAPIError(detail='Content-Type is not valid')
        if request.data is None:
            raise BadRequestAPIError(detail='Request body is required')
        try:
            # `request` will be ignored in regular `BaseModel`
            return model(**request.data, request=request)
        except ValidationError as validation_error:
            error = {'.'.join(str(loc) for loc in e['loc']): e['msg'] for e in validation_error.errors()}
            raise BadRequestAPIError(detail=error)
        except JSONDecodeError:
            raise JSONDecodeAPIError


class MetaGenericAPI(type):
    def __new__(cls, cls_name: str, bases: tuple[type[typing.Any], ...], namespace: dict[str, typing.Any], **kwargs):
        if cls_name == 'GenericAPI':
            return super().__new__(cls, cls_name, bases, namespace)
        if 'output_model' in namespace:
            deprecation_message = (
                traceback.format_stack(limit=2)[0]
                + '\nThe `output_model` argument has been removed in Panther v5 and is no longer available.'
                '\nPlease update your code to use the new approach. More info: '
                'https://pantherpy.github.io/open_api/'
            )
            raise PantherError(deprecation_message)
        return super().__new__(cls, cls_name, bases, namespace)


class GenericAPI(metaclass=MetaGenericAPI):
    """
    Check out the documentation of `panther.app.API()`.
    """

    input_model: type[ModelSerializer] | type[BaseModel] | None = None
    output_schema: OutputSchema | None = None
    auth: bool = False
    permissions: list[type[BasePermission]] | None = None
    throttling: Throttle | None = None
    cache: timedelta | None = None
    middlewares: list[HTTPMiddleware] | None = None

    def __init_subclass__(cls, **kwargs):
        # Creating API instance to validate the attributes.
        API(
            input_model=cls.input_model,
            output_schema=cls.output_schema,
            auth=cls.auth,
            permissions=cls.permissions,
            throttling=cls.throttling,
            cache=cls.cache,
            middlewares=cls.middlewares,
        )

    async def get(self, *args, **kwargs):
        raise MethodNotAllowedAPIError

    async def post(self, *args, **kwargs):
        raise MethodNotAllowedAPIError

    async def put(self, *args, **kwargs):
        raise MethodNotAllowedAPIError

    async def patch(self, *args, **kwargs):
        raise MethodNotAllowedAPIError

    async def delete(self, *args, **kwargs):
        raise MethodNotAllowedAPIError

    async def call_method(self, request: Request):
        match request.method:
            case 'GET':
                func = self.get
            case 'POST':
                func = self.post
            case 'PUT':
                func = self.put
            case 'PATCH':
                func = self.patch
            case 'DELETE':
                func = self.delete
            case _:
                raise MethodNotAllowedAPIError

        return await API(
            input_model=self.input_model,
            output_schema=self.output_schema,
            auth=self.auth,
            permissions=self.permissions,
            throttling=self.throttling,
            cache=self.cache,
            middlewares=self.middlewares,
        )(func)(request=request)
