# Copyright 2021-2022 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."""

import binascii
import copy
import logging
import tempfile
from functools import lru_cache
from pathlib import Path
from typing import Any, Generic, Optional, TYPE_CHECKING, TypeVar

from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import Model
from django.http import (
    Http404,
    JsonResponse,
)
from django.utils import timezone

from rest_framework import status, views
from rest_framework.generics import CreateAPIView, DestroyAPIView, ListAPIView
from rest_framework.pagination import CursorPagination
from rest_framework.parsers import BaseParser, JSONParser
from rest_framework.permissions import BasePermission
from rest_framework.response import Response

from debusine.db.models import (
    Artifact,
    ArtifactRelation,
    DEFAULT_WORKSPACE_NAME,
    File,
    FileInArtifact,
    FileInStore,
    FileStore,
    FileUpload,
    NotificationChannel,
    Token,
    User,
    WorkRequest,
    Worker,
    Workspace,
)
from debusine.server.scheduler import schedule
from debusine.server.serializers import (
    ArtifactRelationResponseSerializer,
    ArtifactSerializer,
    ArtifactSerializerResponse,
    WorkRequestCompletedSerializer,
    WorkRequestSerializer,
    WorkerRegisterSerializer,
)
from debusine.tasks import BaseTask
from debusine.utils import parse_content_range_header

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
    CreateAPIViewBase = CreateAPIView
    DestroyAPIViewBase = DestroyAPIView
    ListAPIViewBase = ListAPIView
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

    CreateAPIViewBase = _CreateAPIViewBase
    DestroyAPIViewBase = _DestroyAPIViewBase
    ListAPIViewBase = _ListAPIViewBase


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) -> Optional[Token]:
        """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: Optional[Token]) -> 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 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 RegisterView(views.APIView):
    """View used by workers to register to debusine."""

    def post(self, request):
        """Worker registers (sends token and fqdn)."""
        worker_register = WorkerRegisterSerializer(data=request.data)

        if not worker_register.is_valid():
            return ProblemResponse(
                "Cannot deserialize worker",
                validation_errors=worker_register.errors,
            )

        token_key = worker_register.validated_data['token']
        fqdn = worker_register.validated_data['fqdn']

        token, _ = Token.objects.get_or_create(key=token_key)

        Worker.objects.create_with_fqdn(fqdn, token)

        logger.info('Client registered. Token key: %s', token_key)

        return Response(status=status.HTTP_201_CREATED)


class GetNextWorkRequestView(views.APIView):
    """View used by workers to request a task."""

    permission_classes = [IsWorkerAuthenticated]

    def get(self, request):
        """Return the task to build."""
        token_key = request.headers['token']
        worker = Worker.objects.get_worker_by_token_key_or_none(token_key)

        work_request = WorkRequest.objects.running(worker=worker).first()

        if work_request is None:
            work_request = WorkRequest.objects.pending(worker=worker).first()

        if work_request:
            content = WorkRequestSerializer(work_request).data
            work_request.mark_running()
            status_code = status.HTTP_200_OK
        else:
            # There is no work request available for the worker
            content = None
            status_code = status.HTTP_204_NO_CONTENT

        return Response(content, status=status_code)


class WorkRequestPagination(CursorPagination):
    """Pagination for lists of work requests."""

    ordering = "-created_at"


class WorkRequestView(ListAPIViewBase[WorkRequest]):
    """View used by the debusine client to get information of a WorkRequest."""

    permission_classes = [
        IsTokenUserAuthenticated | IsTokenAuthenticated & IsGet
    ]
    serializer_class = WorkRequestSerializer
    pagination_class = WorkRequestPagination
    queryset = WorkRequest.objects.all()

    def get(self, request, work_request_id: Optional[int] = None):
        """Return status information for WorkRequest or not found."""
        if work_request_id is None:
            return super().get(request)

        try:
            work_request = WorkRequest.objects.get(pk=work_request_id)
        except WorkRequest.DoesNotExist:
            return Response(
                {'detail': 'Work request id not found'},
                status=status.HTTP_404_NOT_FOUND,
            )

        return Response(
            WorkRequestSerializer(work_request).data, status=status.HTTP_200_OK
        )

    def post(self, request):
        """Create a new work request."""
        data = request.data.copy()

        # "workspace" might not be in data, but it is a mandatory field
        # in the serializer. If not sent by the client use the default workspace
        if "workspace" not in data:
            data["workspace"] = DEFAULT_WORKSPACE_NAME

        token = Token.objects.get(key=request.headers["token"])

        data["created_by"] = token.user.id
        work_request_deserialized = WorkRequestSerializer(
            data=data,
            only_fields=['task_name', 'task_data', 'workspace', 'created_by'],
        )

        if not work_request_deserialized.is_valid():
            errors = work_request_deserialized.errors

            logger.debug(
                "Error creating work request (token key: %s). "
                "Could not be deserialized: %s",
                token.key,
                errors,
            )

            return ProblemResponse(
                "Cannot deserialize work request", validation_errors=errors
            )

        if len(non_existing_channels := self._non_existing_channels(data)) > 0:
            return ProblemResponse(
                f"Non-existing channels: {sorted(non_existing_channels)}"
            )

        task_name = data['task_name']

        if not BaseTask.is_valid_task_name(task_name):
            valid_task_names = ', '.join(BaseTask.task_names())
            logger.debug(
                "Error creating work request (token key: %s). "
                "Non-registered task name: %s",
                token.key,
                task_name,
            )
            return ProblemResponse(
                "Cannot create work request: not registered task name",
                detail=f'Task name: "{task_name}". '
                f"Registered task names: {valid_task_names}",
            )

        w = work_request_deserialized.save()

        schedule()

        w.refresh_from_db()

        return Response(
            WorkRequestSerializer(w).data, status=status.HTTP_200_OK
        )

    @staticmethod
    def _non_existing_channels(task_data: dict[str, Any]) -> set[str]:
        """Return non existing channels from task_data."""
        non_existing_channels: set[str] = set()

        for event, notification in (
            task_data.get("task_data", {}).get("notifications", {}).items()
        ):
            for channel in notification:
                channel_name = channel["channel"]
                try:
                    NotificationChannel.objects.get(name=channel_name)
                except NotificationChannel.DoesNotExist:
                    non_existing_channels.add(channel_name)

        return non_existing_channels


class UpdateWorkRequestAsCompletedView(views.APIView):
    """View used by the workers to mark a task as completed."""

    permission_classes = [IsWorkerAuthenticated]

    def put(self, request, work_request_id: int):
        """Mark a work request as completed."""
        token_key = request.headers['token']
        worker = Worker.objects.get_worker_by_token_key_or_none(token_key)

        try:
            work_request = WorkRequest.objects.get(pk=work_request_id)
        except WorkRequest.DoesNotExist:
            return ProblemResponse(
                "Work request not found",
                status_code=status.HTTP_404_NOT_FOUND,
            )

        if work_request.worker == worker:
            work_request_completed_serializer = WorkRequestCompletedSerializer(
                data=request.data
            )

            if work_request_completed_serializer.is_valid():
                work_request.mark_completed(
                    work_request_completed_serializer.validated_data['result']
                )
                content = None
                status_code = status.HTTP_200_OK
            else:
                return ProblemResponse(
                    "Cannot change work request as completed",
                    validation_errors=work_request_completed_serializer.errors,
                )
        else:
            return ProblemResponse(
                "Invalid worker to update the work request",
                status_code=status.HTTP_401_UNAUTHORIZED,
            )

        return Response(content, status=status_code)


class UpdateWorkerDynamicMetadataView(views.APIView):
    """View used by the workers to post dynamic metadata."""

    permission_classes = [IsWorkerAuthenticated]

    def put(self, request):
        """Update Worker dynamic metadata."""
        token_key = request.headers['token']
        worker = Worker.objects.get_worker_by_token_key_or_none(token_key)

        worker.set_dynamic_metadata(request.data)

        return Response(status=status.HTTP_204_NO_CONTENT)


class ArtifactView(views.APIView):
    """View used to get or create artifacts."""

    permission_classes = [IsTokenAuthenticated]

    def get(self, request, artifact_id: int):
        """Return information of artifact_id."""
        try:
            artifact = Artifact.objects.get(id=artifact_id)
        except Artifact.DoesNotExist:
            return ProblemResponse(
                f"Artifact {artifact_id} not found",
                status_code=status.HTTP_404_NOT_FOUND,
            )

        artifact_response = ArtifactSerializerResponse.from_artifact(
            artifact, request
        )

        return Response(artifact_response.data, status=status.HTTP_200_OK)

    @transaction.atomic
    def post(self, request):
        """Create a new artifact."""
        data = request.data.copy()

        # "workspace" might not be in data, but it is a mandatory field
        # in the serializer.
        # This view might change in the future and use a different "workspace"
        # depending on the user/token that is creating the Artifact.
        # Currently, it uses the DEFAULT_WORKSPACE_NAME
        if "workspace" not in data:
            data["workspace"] = DEFAULT_WORKSPACE_NAME

        artifact_deserialized = ArtifactSerializer(data=data)
        if not artifact_deserialized.is_valid():
            return ProblemResponse(
                "Cannot deserialize artifact",
                validation_errors=artifact_deserialized.errors,
            )

        workspace_name = artifact_deserialized["workspace"].value
        try:
            workspace = Workspace.objects.get(name=workspace_name)
        except Workspace.DoesNotExist:
            return ProblemResponse(
                f'Workspace "{workspace_name}" cannot be found'
            )

        work_request_id: Optional[int] = (
            artifact_deserialized.validated_data.get("work_request")
        )
        created_by: Optional[User] = None

        if work_request_id is not None:
            if not IsWorkerAuthenticated().has_permission(request, self):
                return ProblemResponse(
                    "work_request field can only be used by a request with a "
                    "token from a Worker",
                    status_code=status.HTTP_403_FORBIDDEN,
                )

            try:
                WorkRequest.objects.get(id=work_request_id)
            except WorkRequest.DoesNotExist:
                return ProblemResponse(
                    f"WorkRequest {work_request_id} does not exist"
                )
        else:
            if not IsTokenUserAuthenticated().has_permission(request, self):
                return ProblemResponse(
                    "If work_request field is empty the WorkRequest must be "
                    "created using a Token associated to a user",
                    status_code=status.HTTP_403_FORBIDDEN,
                )
            created_by = Token.objects.get(
                key=request.headers.get("token")
            ).user

        artifact = Artifact.objects.create(
            category=artifact_deserialized["category"].value,
            workspace=workspace,
            data=artifact_deserialized["data"].value,
            created_by_work_request_id=work_request_id,
            expiration_delay=artifact_deserialized["expiration_delay"].value,
            created_by=created_by,
        )

        for path, file_deserialized in artifact_deserialized[
            "files"
        ].value.items():
            file_size = file_deserialized["size"]
            hash_digest = binascii.unhexlify(
                file_deserialized["checksums"]["sha256"]
            )

            fileobj, created = File.get_or_create(
                hash_digest=hash_digest,
                size=file_size,
            )

            file_in_artifact = FileInArtifact.objects.create(
                artifact=artifact, file=fileobj, path=path
            )

            if (
                created or not artifact.workspace.is_file_in_workspace(fileobj)
            ) and file_size == 0:
                # Create the file upload and add the file in the store
                temp_empty_file = tempfile.NamedTemporaryFile(
                    prefix="file-uploading-empty-",
                    dir=settings.DEBUSINE_UPLOAD_DIRECTORY,
                    delete=False,
                )
                temp_empty_file.close()
                file_upload = FileUpload.objects.create(
                    file_in_artifact=file_in_artifact,
                    path=Path(temp_empty_file.name).name,
                )
                UploadFileView.create_file_in_storage(file_upload)

        artifact_response = ArtifactSerializerResponse.from_artifact(
            artifact, request
        )
        return Response(artifact_response.data, status=status.HTTP_201_CREATED)


class FileUploadParser(BaseParser):
    """View used to upload files into an artifact."""

    media_type = "*/*"

    @staticmethod
    def _raise_error_if_gap_in_file(content_range, temporary_file_path):
        if content_range is None:
            return

        uploaded_size = Path(temporary_file_path).stat().st_size
        if uploaded_size < content_range["start"]:
            raise ValueError(
                f"Server had received {uploaded_size} bytes of the file. "
                f"New upload starts at {content_range['start']}. "
                f"Please continue from {uploaded_size}"
            )

    def parse(self, stream, media_type=None, parser_context=None):  # noqa: U100
        """Upload a file to an artifact."""
        file_in_artifact = parser_context["request"].file_in_artifact

        range_start = 0
        range_end = None

        content_range = parse_content_range_header(
            parser_context["request"].headers
        )

        if content_range is not None:
            range_start = content_range["start"]
            range_end = content_range["end"]

        if range_start > (file_size := file_in_artifact.file.size):
            raise ValueError(
                f"Range start ({range_start}) is greater than "
                f"file size ({file_size})"
            )

        try:
            file_upload = FileUpload.objects.get(
                file_in_artifact=file_in_artifact
            )
            temporary_file_path = file_upload.absolute_file_path()
            self._raise_error_if_gap_in_file(content_range, temporary_file_path)
            uploads_file = open(temporary_file_path, "r+b")
        except FileUpload.DoesNotExist:
            uploads_file = tempfile.NamedTemporaryFile(
                prefix='file-uploading-',
                dir=settings.DEBUSINE_UPLOAD_DIRECTORY,
                delete=False,
                mode="a+b",
            )
            file_upload = FileUpload.objects.create(
                file_in_artifact=file_in_artifact,
                path=Path(uploads_file.name).name,
            )

        uploads_file.seek(range_start)

        bytes_received = 0
        # DATA_UPLOAD_MAX_MEMORY_SIZE: arbitrary size that should make sense
        # that can be dealt with
        while chunk := stream.read(settings.DATA_UPLOAD_MAX_MEMORY_SIZE):
            bytes_received += len(chunk)

            if range_start + bytes_received > file_size:
                raise ValueError(
                    f"Cannot process range: attempted to write after end "
                    f"of the file. Range start: {range_start} "
                    f"Received: {bytes_received} File size: {file_size}"
                )

            uploads_file.write(chunk)

        if (
            range_end is not None
            and (expected_bytes := range_end - range_start + 1)
            != bytes_received
        ):
            raise ValueError(
                f"Expected {expected_bytes} bytes (based on range header: "
                f"end-start+1). Received: {bytes_received} bytes"
            )

        uploads_file.close()

        file_upload.last_activity_at = timezone.now()
        file_upload.save()

        return file_upload


class UploadFileView(views.APIView):
    """View to upload files."""

    parser_classes = [FileUploadParser]
    permission_classes = [IsTokenAuthenticated]

    @staticmethod
    def _range_header(start, end):
        return {"Range": f"bytes={start}-{end}"}

    @classmethod
    def _file_status_response(cls, file_in_artifact, range_file_size):
        """
        Return response with the status of a file (completed or missing range).

        :param file_in_artifact: FileInArtifact that is being uploaded
        :param range_file_size: if it's "*" returns the total file size,
          if it contains a number and does not match the expected
          the server returns HTTP 400 Bad request
        """
        file_upload = getattr(file_in_artifact, "fileupload", None)

        if file_upload is None:
            return Response(
                status=status.HTTP_206_PARTIAL_CONTENT,
                headers=cls._range_header(0, 0),
            )

        file_in_db_size = file_upload.file_in_artifact.file.size
        if range_file_size != "*" and range_file_size != file_in_db_size:
            # Client reported a size in Content-Range that is not
            # what was expected
            return ProblemResponse(
                "Invalid file size in Content-Range header. "
                f"Expected {file_in_db_size} "
                f"received {range_file_size}"
            )

        current_size = FileUpload.current_size(
            artifact=file_in_artifact.artifact,
            path_in_artifact=file_in_artifact.path,
        )

        if current_size == file_in_db_size:
            # No missing parts: file upload is already completed
            return Response(status=status.HTTP_200_OK)
        else:
            # Return HTTP 206 with the Range: 0-{uploaded_size}
            # so the client can continue the upload
            return Response(
                status=status.HTTP_206_PARTIAL_CONTENT,
                headers=cls._range_header(0, current_size),
            )

    @transaction.atomic
    def put(self, request, artifact_id: int, file_path: str):
        """Receive file to be uploaded or request uploaded content-range."""
        try:
            file_in_artifact = _get_file_in_artifact_or_raise_value_error(
                artifact_id,
                file_path,
            )
        except ValueError as exc:
            return ProblemResponse(
                str(exc), status_code=status.HTTP_404_NOT_FOUND
            )

        try:
            content_range = parse_content_range_header(request.headers)
        except ValueError as exc:
            return ProblemResponse(str(exc))

        artifact = file_in_artifact.artifact

        if content_range is not None and content_range["start"] == "*":
            # Client would like to know the status of a file
            # (which parts have been uploaded)
            return self._file_status_response(
                file_in_artifact, content_range["size"]
            )

        if self._file_already_uploaded(file_in_artifact.file):
            return Response(status=status.HTTP_201_CREATED)

        request.file_in_artifact = file_in_artifact
        # request.data use the FileUploadParser
        try:
            file_upload: FileUpload = request.data
        except ValueError as value_error:
            return ProblemResponse(str(value_error))

        current_size = FileUpload.current_size(
            artifact=artifact, path_in_artifact=file_path
        )

        if current_size != file_upload.file_in_artifact.file.size:
            return Response(
                status=status.HTTP_200_OK,
                headers=self._range_header(0, current_size),
            )
        else:
            # File is uploaded: create the file in the storage
            # Will return HTTP 201 Created (or HTTP 409 Conflict if the
            # expected hash does not match, the client will need to re-upload)
            response = self.create_file_in_storage(file_upload)
            file_upload.delete()
            return response

    @staticmethod
    def create_file_in_storage(file_upload: FileUpload):
        """
        Add file_upload to the artifact's default store.

        Return Response: 201 Created if real hash and length with the
        expected hash and length match, 409 Conflict if no match
        (and deletes the FileUpload): the client need to upload the file again.
        """
        file_path = file_upload.absolute_file_path()

        hash_digest = File.calculate_hash(file_path)

        file_in_artifact = file_upload.file_in_artifact

        expected_file_hash_digest = file_in_artifact.file.hash_digest
        if file_in_artifact.file.hash_digest.hex() != hash_digest.hex():
            return ProblemResponse(
                "Invalid file hash. Expected: "
                f"{expected_file_hash_digest.hex()} "
                f"Received: {hash_digest.hex()}",
                status_code=status.HTTP_409_CONFLICT,
            )

        workspace = file_in_artifact.artifact.workspace
        file_backend = workspace.default_file_store.get_backend_object()

        file_backend.add_file(file_path, fileobj=file_in_artifact.file)

        return Response(status=status.HTTP_201_CREATED)

    @staticmethod
    def _file_already_uploaded(file):
        return FileInStore.objects.filter(
            store=FileStore.default(),
            file=file,
        ).exists()


def _get_file_in_artifact_or_raise_value_error(
    artifact_id: int, file_path: str
) -> FileInArtifact:
    try:
        artifact = Artifact.objects.get(id=artifact_id)
    except Artifact.DoesNotExist:
        raise ValueError(
            f"Artifact {artifact_id} does not exist",
        )

    try:
        file_in_artifact = FileInArtifact.objects.get(
            artifact=artifact, path=file_path
        )
    except FileInArtifact.DoesNotExist:
        raise ValueError(
            f'No file_path "{file_path}" for artifact {artifact.id}'
        )

    return file_in_artifact


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


T = TypeVar("T", bound=Model, covariant=True)


class GetOrCreateAPIView(CreateAPIViewBase[T], Generic[T]):
    """Class to create a new object (return 201 created) or return 200."""

    def create(self, request, *args, **kwargs):
        """
        Return the object with HTTP 201 (created) or HTTP 200 (already existed).

        It is a modification of CreateAPIView.create().

        The default behaviour in rest_framework when an object cannot be
        inserted because it already exists is to return HTTP 400. In
        GetOrCreateApiView it returns HTTP 200 and the client knows that
        the object that wanted to create was already in the database.
        """
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        try:
            obj = serializer.Meta.model.objects.get(**serializer.validated_data)
            response_status = status.HTTP_200_OK
            pk = obj.pk
        except serializer.Meta.model.DoesNotExist:
            self.perform_create(serializer)
            response_status = status.HTTP_201_CREATED
            pk = serializer.instance.pk

        headers = self.get_success_headers(serializer.data)

        response_data = copy.deepcopy(serializer.data)
        response_data["id"] = pk

        return Response(response_data, status=response_status, headers=headers)


class ArtifactRelationsView(
    GetOrCreateAPIView[ArtifactRelation],
    DestroyAPIViewBase[ArtifactRelation],
    ListAPIViewBase[ArtifactRelation],
):
    """Return, create and delete relations between artifacts."""

    serializer_class = ArtifactRelationResponseSerializer
    parser_classes = [JSONParser]
    pagination_class = None
    queryset = ArtifactRelation.objects

    def get(self, request, *args, **kwargs):
        """Return list of ArtifactRelations for artifact or target_artifact."""
        artifact_id = request.GET.get("artifact", None)
        target_artifact_id = request.GET.get("target_artifact", None)

        if artifact_id is not None:
            filter_kwargs = {"artifact_id": artifact_id}
            required_artifact_id = artifact_id
        elif target_artifact_id is not None:
            filter_kwargs = {"target_id": target_artifact_id}
            required_artifact_id = target_artifact_id
        else:
            return ProblemResponse(
                '"artifact" or "target_artifact" parameter are mandatory'
            )
        try:
            Artifact.objects.get(id=required_artifact_id)
        except Artifact.DoesNotExist:
            return ProblemResponse(
                f"Artifact {required_artifact_id} not found",
                status_code=status.HTTP_404_NOT_FOUND,
            )

        self.queryset = ArtifactRelation.objects.filter(**filter_kwargs)

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