# 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: work requests."""

import itertools
import logging
from typing import Any

try:
    import pydantic.v1 as pydantic
except ImportError:
    import pydantic as pydantic  # type: ignore

from rest_framework import status
from rest_framework.pagination import CursorPagination
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView

from debusine.db.models import (
    DEFAULT_WORKSPACE_NAME,
    NotificationChannel,
    Token,
    WorkRequest,
)
from debusine.db.models.work_requests import CannotRetry
from debusine.server.serializers import (
    WorkRequestSerializer,
    WorkRequestUpdateSerializer,
)
from debusine.server.views.base import (
    IsGet,
    IsTokenAuthenticated,
    IsTokenUserAuthenticated,
    ListAPIViewBase,
    ProblemResponse,
    UpdateAPIViewBase,
)
from debusine.tasks import BaseTask
from debusine.tasks.models import ActionTypes, TaskTypes

logger = logging.getLogger(__name__)


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

    ordering = "-created_at"


class WorkRequestView(
    ListAPIViewBase[WorkRequest], UpdateAPIViewBase[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()
    lookup_url_kwarg = "work_request_id"

    def get(self, request, work_request_id: int | None = 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

        # only accept send-notification actions
        try:
            data["event_reactions"] = {
                event: [
                    action
                    for action in reactions
                    if action["action"] == ActionTypes.SEND_NOTIFICATION
                ]
                for event, reactions in data.get("event_reactions", {}).items()
            }
        except (TypeError, KeyError, ValueError, AttributeError):
            return ProblemResponse("Invalid event_reactions")

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

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

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

            logger.debug(
                "Error creating work request. Could not be deserialized: %s",
                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']

        task_types = [TaskTypes.WORKER]
        if token.user.is_superuser:
            task_types.append(TaskTypes.SERVER)

        for task_type in task_types:
            if BaseTask.is_valid_task_name(task_type, task_name):
                break
        else:
            valid_task_names = ', '.join(
                itertools.chain(
                    *(
                        BaseTask.task_names(task_type)
                        for task_type in task_types
                    ),
                ),
            )
            logger.debug(
                "Error creating work request: task name is not registered: %s",
                task_name,
            )
            return ProblemResponse(
                "Cannot create work request: task name is not registered",
                detail=f'Task name: "{task_name}". '
                f"Registered task names: {valid_task_names}",
            )

        w = work_request_deserialized.save(task_type=task_type)

        w.refresh_from_db()

        # Force EventReactions parsing
        try:
            w.event_reactions
        except (TypeError, pydantic.ValidationError):
            return ProblemResponse("Invalid event_reactions")

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

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

        for event, reactions in data.get("event_reactions", {}).items():
            for action in reactions:
                assert (
                    action["action"] == ActionTypes.SEND_NOTIFICATION
                )  # filtered above
                channel_name = action["channel"]
                try:
                    NotificationChannel.objects.get(name=channel_name)
                except NotificationChannel.DoesNotExist:
                    non_existing_channels.add(channel_name)

        return non_existing_channels

    # TODO: We can't declare the return type properly here because the
    # return type of UpdateAPIView.patch is rest_framework.response.Response
    # and ProblemResponse isn't a subtype of that.  It works anyway, but
    # needs to be fixed somehow.
    def patch(self, request: Request, *args: Any, **kwargs: Any):
        """Update properties of a work request."""
        # Only a subset of properties may be patched.
        work_request_deserialized = WorkRequestUpdateSerializer(
            data=request.data
        )
        work_request_deserialized.is_valid(raise_exception=True)

        # Only users with appropriate permissions may set priority
        # adjustments.
        priority_adjustment = work_request_deserialized.validated_data.get(
            "priority_adjustment"
        )
        if priority_adjustment is None:
            pass
        else:
            token = IsTokenUserAuthenticated().token(request)
            # 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
            # Users with manage_workrequest_priorities have full access.
            if token.user.has_perm("db.manage_workrequest_priorities"):
                pass
            # Users can set non-positive priority adjustments on their own
            # work requests.
            elif (
                priority_adjustment <= 0
                and token.user == self.get_object().created_by
            ):
                pass
            else:
                return ProblemResponse(
                    "You are not permitted to set priority adjustments",
                    status_code=status.HTTP_403_FORBIDDEN,
                )

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


class WorkRequestRetryView(APIView):
    """View used by the debusine client to get information of a WorkRequest."""

    permission_classes = [IsTokenUserAuthenticated]

    def post(self, *args, **kwargs):
        """Retry a work request."""
        try:
            work_request = WorkRequest.objects.get(
                pk=self.kwargs["work_request_id"]
            )
        except WorkRequest.DoesNotExist:
            return Response(
                {'detail': 'Work request id not found'},
                status=status.HTTP_404_NOT_FOUND,
            )

        try:
            new_work_request = work_request.retry()
        except CannotRetry as e:
            return ProblemResponse(
                "Cannot retry work request",
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=str(e),
            )

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