from typing import Any, TypeAlias, TYPE_CHECKING

from django.db.models import Model
from django.http import Http404

from rest_framework import status
from rest_framework import exceptions
from rest_framework.request import clone_request
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import BaseSerializer
from rest_framework.settings import api_settings
from rest_framework.viewsets import ModelViewSet

if TYPE_CHECKING:
    AllowPUTAsCreateMixinBase: TypeAlias = ModelViewSet
else:
    AllowPUTAsCreateMixinBase = object


class AllowPUTAsCreateMixin(AllowPUTAsCreateMixinBase):
    """
    Taken from https://gist.github.com/tomchristie/a2ace4577eff2c603b1b

    The following mixin class may be used in order to support PUT-as-create
    behavior for incoming requests.
    """

    def update(self, request: Request, *args: Any, **kwargs: Any) -> Response:
        partial = kwargs.pop('partial', False)
        instance = self.get_object_or_none()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)

        if instance is None:
            lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
            lookup_value = self.kwargs[lookup_url_kwarg]
            extra_kwargs = {self.lookup_field: lookup_value}
            self.perform_create(serializer, **extra_kwargs)
            headers = self.get_success_headers(serializer.data)
            return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

        self.perform_update(serializer)
        if hasattr(instance, '_prefetched_objects_cache'):
            # If 'prefetch_related' has been applied to a queryset, we need to
            # forcibly invalidate the prefetch cache on the instance.
            instance._prefetched_objects_cache = {}

        return Response(serializer.data)

    def perform_create(self, serializer: BaseSerializer, **kwargs: Any) -> None:
        serializer.save(**kwargs)

    def perform_update(self, serializer: BaseSerializer) -> None:
        serializer.save()

    def partial_update(self, request: Request, *args: Any, **kwargs: Any) -> Response:
        kwargs['partial'] = True
        return self.update(request, *args, **kwargs)

    def get_object_or_none(self) -> Model | None:
        try:
            return self.get_object()
        except (exceptions.NotFound, Http404):
            if self.request.method == 'PUT':
                # For PUT-as-create operation, we need to ensure that we have
                # relevant permissions, as if this was a POST request.  This
                # will either raise a PermissionDenied exception, or simply
                # return None.
                self.check_permissions(clone_request(self.request, 'POST'))
            else:
                # PATCH requests where the object does not exist should still
                # return a 404 response.
                raise

        return None

    def get_success_headers(self, data: Any) -> dict:
        try:
            return {'Location': str(data[api_settings.URL_FIELD_NAME])}
        except (TypeError, KeyError):
            return {}


__all__ = [
    'AllowPUTAsCreateMixin',
]
