# 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, Literal, override

from django.core.exceptions import PermissionDenied
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.client.models import RuntimeParameter
from debusine.db.context import context
from debusine.db.models import Group, WorkRequest, WorkflowTemplate, Workspace
from debusine.db.models.permissions import anonymous_access
from debusine.db.models.work_requests import WorkflowTemplateQuerySet
from debusine.server.exceptions import DebusineAPIException
from debusine.server.serializers import (
    CreateWorkflowRequestSerializer,
    WorkRequestSerializer,
    WorkflowTemplateSerializer,
)
from debusine.server.views.base import (
    BaseAPIView,
    CanDisplayFilterBackend,
    GenericViewSetBase,
)
from debusine.server.views.rest import IsUserTokenAuthenticated

logger = logging.getLogger(__name__)


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

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

    @override
    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"])

    @override
    def get_queryset(self) -> WorkflowTemplateQuerySet[Any]:
        """Get the query set for this view."""
        self.enforce(anonymous_access)  # Filtered to can_display
        return (
            WorkflowTemplate.objects.can_display(context.pc)
            .filter(workspace=context.require_workspace())
            .order_by("name")
        )

    @override
    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)
        assert isinstance(obj, WorkflowTemplate)
        # May raise a permission denied
        self.check_object_permissions(self.request, obj)
        self.enforce(obj.can_display)
        return obj

    def _check_permissions(
        self, serializer: BaseSerializer[WorkflowTemplate]
    ) -> None:
        """Check permissions for various special cases."""
        # Only users who can create workspaces in this scope may
        # create/update workflow templates for create_child_workspace, since
        # that amounts to delegating that authority.
        task_name = serializer.validated_data.get("task_name")
        if task_name == "create_child_workspace":
            self.enforce(context.require_scope().can_create_workspace)

        # 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"
                )

        # Ensure extra groups can be granted
        extra_groups = serializer.validated_data.get("extra_groups", ())
        for group in extra_groups:
            if not group.can_grant(context.pc):
                raise DRFPermissionDenied(
                    f"You are not allowed to grant extra group {group}"
                )

    def _ensure_unique_name(
        self, workspace: Workspace, name: str, *, title: str
    ) -> None:
        if WorkflowTemplate.objects.filter(
            workspace=workspace,
            name=name,
        ).exists():
            raise DebusineAPIException(
                title=title,
                detail=f"A workflow template called {name!r} already exists",
                status_code=status.HTTP_400_BAD_REQUEST,
            )

    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

        self._ensure_unique_name(
            instance.workspace,
            name,
            title="New name conflicts with an existing workflow template",
        )
        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_static_parameters(
        self, instance: WorkflowTemplate, value: dict[str, Any]
    ) -> bool:
        """
        Set the static parameters.

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

    def set_runtime_parameters(
        self,
        instance: WorkflowTemplate,
        value: dict[str, Any] | Literal[RuntimeParameter.ANY],
    ) -> bool:
        """
        Set the runtime parameters.

        :return: True if the value has been changed
        """
        if value == instance.runtime_parameters:
            return False
        # TODO: Like static_parameters above, we should perform full validation.
        instance.runtime_parameters = value
        return True

    def set_extra_groups(
        self, instance: WorkflowTemplate, groups: list[Group]
    ) -> bool:
        """
        Set extra groups.

        :return: True if the value has been changed
        """
        if sorted(
            instance.extra_groups.all().values_list("pk", flat=True)
        ) == sorted(g.pk for g in groups):
            return False
        instance.extra_groups.set(groups)
        return True

    def set_restricted(self, instance: WorkflowTemplate, value: bool) -> bool:
        """
        Set the restricted flag.

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

    @override
    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)
        name = serializer.validated_data["name"]
        self._ensure_unique_name(
            workspace,
            name,
            title="Workflow template already exists",
        )
        serializer.save(workspace=workspace)

    @override
    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)

        # TODO: currently we are enforcing WorkflowTemplate.can_configure,
        # which is true for workspace owners. This would potentially allow a
        # workspace owner to change a workflow template that grants extra
        # groups that they cannot access. See
        # https://salsa.debian.org/freexian-team/debusine/-/work_items/1480
        # for a discussion

        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 "static_parameters" in serializer.validated_data:
            changed = (
                self.set_static_parameters(
                    instance, serializer.validated_data["static_parameters"]
                )
                or changed
            )
        if "runtime_parameters" in serializer.validated_data:
            changed = (
                self.set_runtime_parameters(
                    instance, serializer.validated_data["runtime_parameters"]
                )
                or changed
            )
        if "extra_groups" in serializer.validated_data:
            changed = (
                self.set_extra_groups(
                    instance, serializer.validated_data["extra_groups"]
                )
                or changed
            )
        if "restricted" in serializer.validated_data:
            changed = (
                self.set_restricted(
                    instance, serializer.validated_data["restricted"]
                )
                or changed
            )

        if changed:
            instance.save()


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

    permission_classes = [IsUserTokenAuthenticated]
    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)
        # TODO: Replace with can_create_workflows, when it exists
        self.enforce(workspace.can_create_work_requests)
        # WorkRequest.objects.create_workflow enforces some more detailed
        # permissions depending on its arguments.

        template_name = serializer.validated_data["template_name"]
        task_name = serializer.validated_data["task_name"]

        if template_name:
            try:
                template = WorkflowTemplate.objects.get(
                    name=template_name, workspace=workspace
                )
            except WorkflowTemplate.DoesNotExist:
                raise DebusineAPIException(
                    title="Workflow template not found",
                    status_code=status.HTTP_404_NOT_FOUND,
                )
            assert not task_name
            task_name = None
        else:
            template = None
            assert task_name

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

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