# Copyright 2021-2024 The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""Views for the server application: base infrastructure."""

from functools import lru_cache
from typing import TYPE_CHECKING

from django.core.exceptions import PermissionDenied
from django.http import (
    Http404,
    JsonResponse,
)

from rest_framework import status
from rest_framework.exceptions import (
    PermissionDenied as DRFPermissionDenied,
)
from rest_framework.generics import (
    CreateAPIView,
    DestroyAPIView,
    ListAPIView,
    RetrieveAPIView,
    UpdateAPIView,
)
from rest_framework.permissions import BasePermission, DjangoModelPermissions

from debusine.db.models import (
    Artifact,
    Token,
)

if TYPE_CHECKING:
    CreateAPIViewBase = CreateAPIView
    DestroyAPIViewBase = DestroyAPIView
    ListAPIViewBase = ListAPIView
    RetrieveAPIViewBase = RetrieveAPIView
    UpdateAPIViewBase = UpdateAPIView
else:
    # REST framework's generic views don't support generic types at run-time
    # yet.
    class _CreateAPIViewBase:
        def __class_getitem__(*args):
            return CreateAPIView

    class _DestroyAPIViewBase:
        def __class_getitem__(*args):
            return DestroyAPIView

    class _ListAPIViewBase:
        def __class_getitem__(*args):
            return ListAPIView

    class _RetrieveAPIViewBase:
        def __class_getitem__(*args):
            return RetrieveAPIView

    class _UpdateAPIViewBase:
        def __class_getitem__(*args):
            return UpdateAPIView

    CreateAPIViewBase = _CreateAPIViewBase
    DestroyAPIViewBase = _DestroyAPIViewBase
    ListAPIViewBase = _ListAPIViewBase
    RetrieveAPIViewBase = _RetrieveAPIViewBase
    UpdateAPIViewBase = _UpdateAPIViewBase


class ProblemResponse(JsonResponse):
    """
    Holds a title and other optional fields to return problems to the client.

    Follows RFC7807 (https://www.rfc-editor.org/rfc/rfc7807#section-6.1)
    """

    def __init__(
        self,
        title,
        detail=None,
        validation_errors=None,
        status_code=status.HTTP_400_BAD_REQUEST,
    ):
        """
        Initialize object.

        :param title: included in the response data.
        :param detail: if not None, included in the response data.
        :param validation_errors: if not None, included in the response data.
        :param status_code: HTTP status code for the response.
        """
        data = {"title": title}

        if detail is not None:
            data["detail"] = detail

        if validation_errors is not None:
            data["validation_errors"] = validation_errors

        super().__init__(
            data, status=status_code, content_type="application/problem+json"
        )


class IsTokenAuthenticated(BasePermission):
    """Allow access to requests with a valid token."""

    @lru_cache(1)
    def token(self, request) -> Token | None:
        """Return the token or None (if no token header or not found in DB)."""
        token_key = request.headers.get('token')

        if token_key is None:
            return None

        token = Token.objects.get_token_or_none(token_key=token_key)

        return token

    @staticmethod
    def _enabled_token(token: Token | None) -> bool:
        return token is not None and token.enabled

    def has_permission(self, request, view):  # noqa: U100
        """Return True if the request is authenticated with a Token."""
        token = self.token(request)

        return self._enabled_token(token)


class IsTokenUserAuthenticated(IsTokenAuthenticated):
    """Allow access if the request has an enabled Token with associated user."""

    def has_permission(self, request, view):  # noqa: U100
        """Return True if valid token has a User assigned."""
        token = self.token(request)

        return self._enabled_token(token) and token.user is not None


class IsTokenUserAuthenticatedDjangoModelPermissions(DjangoModelPermissions):
    """
    Allow access if a Token-authenticated user has model permissions.

    This is similar to :py:class:`DjangoModelPermissions`, except that it
    checks Token authentication.
    """

    def has_permission(self, request, view):
        """Return True if the user has the right permissions."""
        token = IsTokenUserAuthenticated().token(request)
        if not token or not token.user:
            return False

        queryset = self._queryset(view)
        perms = self.get_required_permissions(request.method, queryset.model)

        if not token.user.has_perms(perms):
            # Raise this rather than the default NotAuthenticated exception
            # when an authenticator fails; in this case the user is
            # authenticated, but doesn't have the right permissions.
            raise DRFPermissionDenied()

        return True


class IsUserAuthenticated(BasePermission):
    """Allow access to requests with an authenticated user."""

    def has_permission(self, request, view):  # noqa: U100
        """Return True if the request has an authenticated user."""
        return request.user.is_authenticated


class IsWorkerAuthenticated(IsTokenAuthenticated):
    """Allow access to requests with a token assigned to a worker."""

    def has_permission(self, request, view):
        """
        Return True if the request is an authenticated worker.

        The Token must exist in the database and have a Worker.
        """
        if super().has_permission(request, view) is False:
            # No token authenticated: no Worker Authenticated
            return False

        if not hasattr(self.token(request), 'worker'):
            # The Token doesn't have a worker associated
            return False

        return True


class IsGet(BasePermission):
    """Allow access if the request's method is GET."""

    def has_permission(self, request, view):  # noqa: U100
        """Return True if request.method == "GET"."""
        return request.method == "GET"


class ValidatePermissionsMixin:
    """
    Mixin implementing check_permissions() for the Views.

    To use it:
        class YourView(ValidatePermissionsMixin, View):
            permission_denied_message = "Message explaining the problem"
            permission_classes = [IsUserAuthenticated | IsTokenAuthenticated]

    It overrides dispatch() and raise PermissionDenied if the request cannot
    proceed.
    """

    def check_permissions(self):
        """
        Raise PermissionDenied() if one permission does not allow access.

        Same approach as Django REST ApiView methods: if no permissions:
        it is allowed. If any class denies access raises PermissionDenied().

        See rest_framework/views.py ApiView.check_permissions.
        """
        for permission_class in self.permission_classes:
            permission = permission_class()
            if not permission.has_permission(self.request, None):
                message = getattr(self, "permission_denied_message", None)
                raise PermissionDenied(message)

    def dispatch(self, request, *args, **kwargs):
        """Check permissions for any request method (GET, POST, PUT, etc.)."""
        self.check_permissions()

        return super().dispatch(request, *args, **kwargs)


class ArtifactInPublicWorkspace(BasePermission):
    """Allows access for Artifacts with public workspace."""

    def has_permission(self, request, view):  # noqa: U100
        """
        Return True if request's artifact's workspace is public.

        :raise Http404: if the artifact_id does not exist.
        """
        artifact_id = request.resolver_match.kwargs.get("artifact_id")

        try:
            artifact = Artifact.objects.get(id=artifact_id)
        except Artifact.DoesNotExist:
            raise Http404(f"Artifact {artifact_id} does not exist")

        return artifact.workspace.public
