# Copyright 2023 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.

"""debusine WorkRequest view."""

import functools
from typing import Any, Optional, TYPE_CHECKING

from django.core.exceptions import ValidationError
from django.urls import reverse
from django.views.generic import CreateView, DetailView, ListView

import yaml

from debusine.db.models import Artifact, WorkRequest
from debusine.server.views import IsUserAuthenticated, ValidatePermissionsMixin
from debusine.web.forms import WorkRequestForm
from debusine.web.views.mixins import PaginationMixin

if TYPE_CHECKING:
    CreateViewBase = CreateView
    DetailViewBase = DetailView
    ListViewBase = ListView
else:
    # Django's generic views don't support generic types at run-time yet.
    class _CreateViewBase:
        def __class_getitem__(*args):
            return CreateView

    class _DetailViewBase:
        def __class_getitem__(*args):
            return DetailView

    class _ListViewBase:
        def __class_getitem__(*args):
            return ListView

    CreateViewBase = _CreateViewBase
    DetailViewBase = _DetailViewBase
    ListViewBase = _ListViewBase


class WorkRequestDetailView(DetailViewBase[WorkRequest]):
    """List work requests."""

    model = WorkRequest
    context_object_name = "work_request"
    default_template_name = "web/work_request-detail.html"

    @functools.cached_property
    def _validation_error_message(self) -> Optional[str]:
        """If the work request fails validation, return a suitable message."""
        try:
            self.object.full_clean()
        except ValidationError as e:
            return str(e)
        else:
            return None

    def _current_view_is_specialized(self) -> bool:
        """
        Specialized (based on a plugin) view will be served.

        User did not force the default view, a plugin exists, and the work
        request passes validation.
        """
        use_specialized = self.request.GET.get("view", "default") != "generic"
        plugin_class = WorkRequestPlugin.plugin_for(self.object.task_name)

        return (
            use_specialized
            and plugin_class
            and self._validation_error_message is None
        )

    def get_context_data(self, **kwargs):
        """
        Add context to the default DetailView context.

        Add the artifacts related to the work request.
        """
        context = super().get_context_data(**kwargs)

        plugin_view = WorkRequestPlugin.plugin_for(self.object.task_name)

        if self._current_view_is_specialized():
            return {**context, **plugin_view().get_context_data(self.object)}

        if self._validation_error_message is not None:
            context["validation_error"] = self._validation_error_message
        elif plugin_view:
            # plugin_view for WorkRequest.task_name exists, but the
            # response will return the generic WorkRequest view
            context["specialized_view_path"] = self.request.path

        context["artifacts"] = Artifact.objects.filter(
            created_by_work_request=self.object
        ).order_by("category")
        context["task_data"] = yaml.safe_dump(self.object.task_data)
        return context

    def get_template_names(self):
        """Return the plugin's template_name or the default one."""
        if self._current_view_is_specialized():
            return WorkRequestPlugin.plugin_for(
                self.object.task_name
            ).template_name

        return self.default_template_name


class WorkRequestPlugin:
    """
    WorkRequests with specific outputs must subclass it.

    When subclassing, the subclass:
    - Is automatically used by the /work-request/ID/ endpoints
    - Must define the "template_name" and "task_name"
    - Must implement "get_context_data(work_request)"
    """

    _work_request_plugins: dict[str, "WorkRequestPlugin"] = {}

    def __init_subclass__(cls, **kwargs):  # noqa: U100
        """Register the plugin."""
        cls._work_request_plugins[cls.task_name] = cls

    @classmethod
    def plugin_for(cls, task_name: str) -> Optional["WorkRequestPlugin"]:
        """Return WorkRequestPlugin for task_name or None."""
        return cls._work_request_plugins.get(task_name)

    def get_context_data(self, work_request: WorkRequest):
        """Must be implemented by subclasses."""
        raise NotImplementedError()


class WorkRequestListView(PaginationMixin, ListViewBase[WorkRequest]):
    """List work requests."""

    model = WorkRequest
    template_name = "web/work_request-list.html"
    context_object_name = "work_request_list"
    paginate_by = 50

    def get_queryset(self):
        """Filter work requests displayed by the workspace GET parameter."""
        queryset = super().get_queryset()

        workspace_name = self.request.GET.get("workspace")
        if workspace_name is not None:
            queryset = queryset.filter(workspace__name=workspace_name)

        if not self.request.user.is_authenticated:
            # Non-authenticated users can only list WorkRequests in
            # a public workspace
            queryset = queryset.filter(workspace__public=True)

        return queryset

    def get_ordering(self):
        """Return field used for sorting."""
        order = self.request.GET.get("order")
        if order in ("id", "created_at", "status", "result"):
            if self.request.GET["asc"] == "0":
                return "-" + order
            else:
                return order

        return "-created_at"

    def get_context_data(self, **kwargs):
        """Add context to the default ListView data."""
        context = super().get_context_data(**kwargs)

        context["order"] = self.get_ordering().removeprefix("-")
        context["asc"] = self.request.GET.get("asc", "0")

        if workspace := self.request.GET.get("workspace"):
            context["workspace"] = workspace

        return context


class WorkRequestCreateView(
    ValidatePermissionsMixin, CreateViewBase[WorkRequest, WorkRequestForm]
):
    """Form view for creating a work request."""

    model = WorkRequest
    template_name = "web/work_request-create.html"
    form_class = WorkRequestForm

    permission_denied_message = (
        "You need to be authenticated to create a Work Request"
    )
    permission_classes = [IsUserAuthenticated]

    def get_form_kwargs(self) -> dict[str, Any]:
        """Extend the default kwarg arguments: add "user"."""
        kwargs = super().get_form_kwargs()
        kwargs["user"] = self.request.user
        return kwargs

    def get_success_url(self):
        """Redirect to work_requests:detail for the created WorkRequest."""
        return reverse("work_requests:detail", kwargs={"pk": self.object.id})
