# Copyright © 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: workflows."""

import logging
from typing import Any

from rest_framework import mixins, status
from rest_framework.exceptions import PermissionDenied as DRFPermissionDenied
from rest_framework.generics import get_object_or_404
from rest_framework.parsers import JSONParser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import BaseSerializer

from debusine.db.context import context
from debusine.db.models import WorkRequest, WorkflowTemplate
from debusine.db.models.work_requests import WorkflowTemplateQuerySet
from debusine.server.exceptions import DebusineAPIException
from debusine.server.serializers import (
    CreateWorkflowRequestSerializer,
    LegacyWorkflowTemplateSerializer,
    WorkRequestSerializer,
    WorkflowTemplateSerializer,
)
from debusine.server.views.base import (
    BaseAPIView,
    CanDisplayFilterBackend,
    CreateAPIViewBase,
    GenericViewSetBase,
)
from debusine.server.views.rest import IsTokenUserAuthenticated

logger = logging.getLogger(__name__)


class WorkflowTemplateView(CreateAPIViewBase[WorkflowTemplate], BaseAPIView):
    """
    Return, create and delete workflow templates.

    This is a legacy view for compatibility with debusine clients up to and
    including the trixie version
    """

    permission_classes = [IsTokenUserAuthenticated]
    serializer_class = LegacyWorkflowTemplateSerializer
    filter_backends = [CanDisplayFilterBackend]
    parser_classes = [JSONParser]
    pagination_class = None

    def _check_permissions(
        self, serializer: BaseSerializer[WorkflowTemplate]
    ) -> None:
        """Only users with appropriate permissions may set priorities."""
        priority = serializer.validated_data.get("priority")
        if priority is not None and priority > 0:
            token = self.request.auth
            # TODO: This should be replaced by self.enforce with some
            # appropriate debusine permission.
            if (
                token is None
                or token.user is None
                or not token.user.has_perm("db.manage_workrequest_priorities")
            ):
                raise DRFPermissionDenied(
                    "You are not permitted to set positive priorities"
                )

    def perform_create(
        self, serializer: BaseSerializer[WorkflowTemplate]
    ) -> None:
        """Create a workflow template."""
        workspace = serializer.validated_data["workspace"]
        self.set_current_workspace(workspace)
        self.enforce(workspace.can_configure)
        self._check_permissions(serializer)
        super().perform_create(serializer)


class WorkflowTemplateViewSet(
    BaseAPIView,
    mixins.CreateModelMixin,
    mixins.RetrieveModelMixin,
    mixins.ListModelMixin,
    mixins.UpdateModelMixin,
    mixins.DestroyModelMixin,
    GenericViewSetBase[WorkflowTemplate],
):
    """Return, create and delete workflow templates."""

    permission_classes = [IsTokenUserAuthenticated]
    serializer_class = WorkflowTemplateSerializer
    filter_backends = [CanDisplayFilterBackend]
    parser_classes = [JSONParser]

    def initial(self, request: Request, *args: Any, **kwargs: Any) -> None:
        """Return the initial request object."""
        super().initial(request, *args, **kwargs)
        self.set_current_workspace(kwargs["workspace"])

    def get_queryset(self) -> WorkflowTemplateQuerySet[Any]:
        """Get the query set for this view."""
        return (
            WorkflowTemplate.objects.can_display(context.user)
            .filter(workspace=context.require_workspace())
            .order_by("name")
        )

    def get_object(self) -> WorkflowTemplate:
        """Return the object the view is displaying."""
        queryset = self.filter_queryset(self.get_queryset())
        pk = self.kwargs["pk"]
        if pk.isdigit():
            obj = get_object_or_404(queryset, pk=int(pk))
        else:
            obj = get_object_or_404(queryset, name=pk)
        # May raise a permission denied
        self.check_object_permissions(self.request, obj)
        return obj

    def _check_permissions(
        self, serializer: BaseSerializer[WorkflowTemplate]
    ) -> None:
        """Only users with appropriate permissions may set priorities."""
        priority = serializer.validated_data.get("priority")
        if priority is not None and priority > 0:
            token = self.request.auth
            # TODO: This should be replaced by self.enforce with some
            # appropriate debusine permission.
            if (
                token is None
                or token.user is None
                or not token.user.has_perm("db.manage_workrequest_priorities")
            ):
                raise DRFPermissionDenied(
                    "You are not permitted to set positive priorities"
                )

    def set_name(self, instance: WorkflowTemplate, name: str) -> bool:
        """
        Rename the workflow template.

        :return: True if the name has been changed
        """
        if name == instance.name:
            return False

        if WorkflowTemplate.objects.filter(
            workspace=instance.workspace,
            name=name,
        ).exists():
            raise DebusineAPIException(
                title="New name conflicts with an existing workflow template",
                detail=f"A workflow template called {name!r} already exists",
                status_code=status.HTTP_400_BAD_REQUEST,
            )

        instance.name = name
        return True

    def set_priority(self, instance: WorkflowTemplate, value: int) -> bool:
        """
        Set the priority.

        :return: True if the value has been changed
        """
        if value == instance.priority:
            return False
        instance.priority = value
        return True

    def set_data(
        self, instance: WorkflowTemplate, value: dict[str, Any]
    ) -> bool:
        """
        Set the task data.

        :return: True if the value has been changed
        """
        if value == instance.task_data:
            return False
        # TODO: The key-value data should be validated, once we can (See #818)
        instance.task_data = value
        return True

    def perform_create(
        self, serializer: BaseSerializer[WorkflowTemplate]
    ) -> None:
        """Create a workflow template."""
        workspace = context.require_workspace()
        self.enforce(workspace.can_configure)
        self._check_permissions(serializer)
        serializer.save(workspace=workspace)

    def perform_update(
        self, serializer: BaseSerializer[WorkflowTemplate]
    ) -> None:
        """Update a workflow template."""
        assert isinstance(serializer, WorkflowTemplateSerializer)
        instance = serializer.instance
        assert isinstance(instance, WorkflowTemplate)
        self.enforce(instance.can_configure)
        self._check_permissions(serializer)

        changed: bool = False
        if "name" in serializer.validated_data:
            changed = (
                self.set_name(instance, serializer.validated_data["name"])
                or changed
            )
        if "task_name" in serializer.validated_data:
            if serializer.validated_data["task_name"] != instance.task_name:
                raise DebusineAPIException(
                    title="The task name of a workflow template"
                    " cannot be changed",
                    status_code=status.HTTP_400_BAD_REQUEST,
                )
        if "priority" in serializer.validated_data:
            changed = (
                self.set_priority(
                    instance,
                    serializer.validated_data["priority"],
                )
                or changed
            )
        if "task_data" in serializer.validated_data:
            changed = (
                self.set_data(instance, serializer.validated_data["task_data"])
                or changed
            )

        if changed:
            instance.save()


class WorkflowView(BaseAPIView):
    """Create workflows from a template."""

    # TODO: This should be replaced by appropriate debusine permissions.
    permission_classes = [IsTokenUserAuthenticated]
    filter_backends = [CanDisplayFilterBackend]

    def post(self, request: Request) -> Response:
        """Create a new workflow from a template."""
        serializer = CreateWorkflowRequestSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        workspace = serializer.validated_data["workspace"]
        self.set_current_workspace(workspace)

        try:
            template = WorkflowTemplate.objects.get(
                name=serializer.validated_data["template_name"],
                workspace=workspace,
            )
        except WorkflowTemplate.DoesNotExist:
            raise DebusineAPIException(
                title="Workflow template not found",
                status_code=status.HTTP_404_NOT_FOUND,
            )

        token = request.auth
        # permission_classes is declared such that we won't get this far
        # unless the request has an enabled token with an associated user.
        assert token is not None
        assert token.user is not None

        try:
            workflow = WorkRequest.objects.create_workflow(
                template=template,
                data=serializer.validated_data.get("task_data", {}),
            )
        except Exception as e:
            raise DebusineAPIException(
                title="Cannot create workflow", detail=str(e)
            )

        return Response(
            WorkRequestSerializer(workflow).data, status=status.HTTP_201_CREATED
        )
