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

"""debusine WorkRequest view."""

import abc
import datetime as dt
import functools
import itertools
from dataclasses import dataclass
from typing import Any, TYPE_CHECKING, cast, override

from django.contrib import messages
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect, render
from django.template.context import Context
from django.template.defaultfilters import filesizeformat
from django.utils.duration import duration_string
from django.views.generic.base import View
from rest_framework import status

from debusine.artifacts.models import TaskTypes
from debusine.db.context import context
from debusine.db.models import Artifact, TaskDatabase, User, WorkRequest
from debusine.db.models.tasks import DBTask
from debusine.db.models.work_requests import (
    CannotAbort,
    CannotRetry,
    CannotUnblock,
    InternalTaskError,
    WorkRequestQuerySet,
)
from debusine.server.tasks.wait import Confirm
from debusine.server.workflows import Workflow
from debusine.server.workflows.models import WorkRequestManualUnblockAction
from debusine.tasks import TaskConfigError
from debusine.tasks.models import (
    LookupMultiple,
    LookupSingle,
    OutputData,
)
from debusine.web.forms import (
    WorkRequestConfirmForm,
    WorkRequestForm,
    WorkRequestUnblockForm,
)
from debusine.web.views import sidebar, ui_shortcuts
from debusine.web.views.base import (
    CreateViewBase,
    DetailViewBase,
    FormMixinBase,
    ListViewBase,
    SingleObjectMixinBase,
    Widget,
    WorkspaceView,
)
from debusine.web.views.http_errors import HttpError400, catch_http_errors
from debusine.web.views.mixins import RightbarUIMixin, UIShortcutsMixin
from debusine.web.views.places import Place
from debusine.web.views.table import Paginator, TableMixin
from debusine.web.views.tables import ArtifactTable, WorkRequestTable
from debusine.web.views.view_utils import (
    highlight_as_json,
    highlight_as_yaml,
    syntax_highlight,
)

if TYPE_CHECKING:
    from debusine.db.models.artifacts import (
        ArtifactQuerySet,
        ArtifactWithComplete,
    )

    ArtifactWithComplete  # fake usage for vulture


class Tab(Widget):
    """Define a tab."""

    def __init__(
        self, name: str, label: str, context: dict[str, Any] | None = None
    ) -> None:
        """Store name and label."""
        self.name = name
        self.label = label
        # TODO: this is here only to ease transition from previous WorkRequest
        # plugins. Eventually plugins will move their get_context_data to a Tab
        # subclass
        self.context = context or {}

    def get_context_data(self) -> dict[str, Any]:
        """Return extra context for this tab."""
        return self.context

    @override
    def render(self, context: Context) -> str:
        template_name = f"web/work_request-detail-{self.name}.html"
        assert context.template is not None
        template = context.template.engine.get_template(template_name)
        with context.update(self.get_context_data()):
            return template.render(context)


class WorkRequestObjectMixin(WorkspaceView):
    """
    Add permission filtering to a Single/MultipleObjectMixin.

    Since this inherits from WorkspaceView, this also sets the workspace in the
    context and checks that it is accessible.
    """

    def get_queryset(self) -> WorkRequestQuerySet[WorkRequest]:
        """Filter work requests by current workspace."""
        # Both SingleObjectMixin and MultipleObjectsMixin define a get_queryset
        # without a common base, so we cannot inherit from a base class with
        # the right signature.
        qs = cast(
            WorkRequestQuerySet[WorkRequest],
            super().get_queryset(),  # type: ignore[misc]
        )
        return qs.in_current_workspace().can_display(context.user)


@dataclass(frozen=True)
class InputArtifactsRow:
    """Represents a InputArtifact row for the template."""

    input_label: str
    lookup_display: LookupSingle | LookupMultiple
    artifacts: list[Artifact]


class WorkRequestDetailView(
    RightbarUIMixin,
    WorkRequestObjectMixin,
    UIShortcutsMixin,
    DetailViewBase[WorkRequest],
):
    """Show a work request."""

    queryset = WorkRequest.objects.select_related(
        "workspace__scope", "workflow_root"
    )
    context_object_name = "work_request"
    template_name = "web/work_request-detail.html"

    @functools.cached_property
    def _validation_error_message(self) -> str | None:
        """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

    @functools.cached_property
    def plugin(self) -> "WorkRequestPlugin | None":
        """Return the work request plugin, if defined."""
        return WorkRequestPlugin.plugin_for(self.object)

    def get_main_ui_shortcuts(self) -> list[ui_shortcuts.UIShortcut]:
        """Return a list of UI shortcuts for this view."""
        actions = super().get_main_ui_shortcuts()
        if self.request.user.is_authenticated:
            if self.object.verify_retry():
                actions.append(
                    ui_shortcuts.create_work_request_retry(self.object)
                )
            if self.object.verify_abort():
                actions.append(
                    ui_shortcuts.create_work_request_abort(self.object)
                )
        return actions

    def get_sidebar_items(self) -> list[sidebar.SidebarItem]:
        """Return a list of sidebar items."""
        items = super().get_sidebar_items()
        items.append(sidebar.create_work_request(self.object))
        if hasattr(self.object, "superseded"):
            items.append(sidebar.create_work_request_superseded(self.object))
        if self.object.supersedes:
            items.append(sidebar.create_work_request_supersedes(self.object))
        items.append(sidebar.create_workflow(self.object.workflow_root))
        items.append(sidebar.create_work_request_status(self.object))
        items.append(sidebar.create_workspace(self.object.workspace))
        items.append(
            sidebar.create_user(self.object.created_by, context=self.object)
        )
        items.append(sidebar.create_created_at(self.object.created_at))
        items.append(sidebar.create_worker(self.object.worker))
        items.append(sidebar.create_work_request_started_at(self.object))
        items.append(sidebar.create_work_request_duration(self.object))
        items.append(sidebar.create_expire_at(self.object.expire_at))
        return items

    def get_place(self) -> Place:
        """Return the Place for this page."""
        return self.object.ui(self.request).place

    def get_manual_unblock_log(self) -> list[dict[str, Any]]:
        """Get a representation of the manual unblock log."""
        workflow_data = self.object.workflow_data
        if workflow_data.manual_unblock is None:
            return []

        users = {
            user.id: user
            for user in get_user_model().objects.filter(
                id__in={log.user_id for log in workflow_data.manual_unblock.log}
            )
        }
        return [
            {
                "timestamp": log.timestamp,
                "user": users.get(log.user_id, f"Deleted user {log.user_id}"),
                "action": log.action,
                "notes": log.notes,
            }
            for log in workflow_data.manual_unblock.log
        ]

    def get_default_description(self) -> str | None:
        """Return default description for the Task."""
        try:
            Task = DBTask.class_from_name(
                TaskTypes(self.object.task_type), self.object.task_name
            )
        except ValueError:
            return None

        return Task.__doc__

    def get_tabs(self) -> list[Tab]:
        """Return the tabs to show in the view."""
        tabs: list[Tab] = [
            Tab(name="work_request", label="Work request"),
        ]
        if not self.object.is_workflow or self.object.output_data:
            tabs += [
                Tab(name="output", label="Output"),
            ]
        tabs += [
            Tab(name="dependencies", label="Dependencies"),
            Tab(name="internals", label="Internals"),
        ]

        # After the initial tab list is set, allow the plugin to add to it
        if self.plugin is not None:
            self.plugin.add_tabs(tabs)
        return tabs

    @staticmethod
    def _get_workflow_input_artifacts(
        task: Workflow[Any, Any],
    ) -> tuple[list[InputArtifactsRow], list["ArtifactWithComplete"]]:
        """
        Build context data describing the input artifacts of a workflow.

        Return tuple:
        * list of InputArtifactsRows (containing information for rendering)
        * list of all referenced Artifact objects
        """
        input_artifacts = task.get_input_artifacts()

        # Artifact ids for each InputArtifact
        input_artifacts_artifact_ids: list[list[int]] = []

        all_artifact_ids: set[int] = set()

        # Collect all the artifact_ids
        for input_artifact in input_artifacts:
            artifact_ids = input_artifact.resolved_ids()
            input_artifacts_artifact_ids.append(artifact_ids)
            all_artifact_ids.update(artifact_ids)

        # Query all the artifacts
        artifacts_qs = (
            Artifact.objects.filter(pk__in=all_artifact_ids)
            .order_by("pk")
            .annotate_complete()
        )

        all_artifacts = list(artifacts_qs)
        artifacts_by_id: dict[int, Artifact] = {a.id: a for a in all_artifacts}

        input_artifact_rows: list[InputArtifactsRow] = []

        for input_artifact, artifact_ids in zip(
            input_artifacts, input_artifacts_artifact_ids, strict=True
        ):
            artifacts_for_row = [
                artifacts_by_id[artifact_id]
                for artifact_id in sorted(set(artifact_ids))
                if artifact_id in artifacts_by_id
            ]

            match input_artifact.lookup:
                case str() | int():
                    lookup_display = syntax_highlight(
                        "text", str(input_artifact.lookup)
                    )
                case _:
                    lookup_display = highlight_as_yaml(
                        input_artifact.lookup.export()
                    )

            input_artifact_rows.append(
                InputArtifactsRow(
                    input_label=input_artifact.label,
                    lookup_display=lookup_display,
                    artifacts=artifacts_for_row,
                )
            )

        return input_artifact_rows, all_artifacts

    def _get_task_input_artifacts(
        self, task: DBTask[Any, Any] | None
    ) -> tuple[Paginator[Artifact], list["ArtifactWithComplete"]]:
        """
        Build context data describing the input artifacts of a task.

        Return a tuple containing:
        * paginator over the task input artifacts (empty if the task is None).
        * list of all referenced Artifact objects
        """
        input_artifacts: "ArtifactQuerySet[ArtifactWithComplete]" = (
            Artifact.objects.none().annotate_complete()
        )
        all_artifacts: list["ArtifactWithComplete"] = []

        if task is not None and (
            input_artifact_ids := task.get_input_artifacts_ids()
        ):
            input_artifacts = (
                Artifact.objects.filter(pk__in=input_artifact_ids)
                .order_by("pk")
                .annotate_complete()
            )
            all_artifacts.extend(input_artifacts)

        input_artifacts_table = ArtifactTable(
            self.request,
            input_artifacts,
            prefix="input_artifacts",
            fragment="work_request",
        )
        input_artifacts_table_paginator = input_artifacts_table.get_paginator(
            per_page=20
        )

        return input_artifacts_table_paginator, all_artifacts

    @staticmethod
    def _get_workflow_parents(work_request: WorkRequest) -> list[WorkRequest]:
        workflow_parents = []

        parent = work_request.parent
        while parent is not None:
            workflow_parents.append(parent)
            parent = parent.parent

        return list(reversed(workflow_parents))

    def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
        """Return context for this view's tab."""
        ctx = super().get_context_data(**kwargs)

        ctx["tabs"] = self.get_tabs()

        ctx["description_template"] = (
            "web/_work_request-generic-description.html"
        )
        ctx["description_data"] = {
            "default_description": self.get_default_description()
        }

        ctx["manual_unblock_log_entries"] = self.get_manual_unblock_log()
        ctx["manual_unblock_form"] = WorkRequestUnblockForm()

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

        try:
            task = self.object.get_task()

            if self.object.is_workflow:
                assert isinstance(task, Workflow)
        except (TaskConfigError, InternalTaskError):
            task = None

        if self.object.requires_confirmation:
            assert isinstance(task, Confirm)
            ctx["confirm_form"] = WorkRequestConfirmForm()
            ctx["confirm_label"] = task.data.confirm_label
            ctx["deny_label"] = task.data.deny_label

        if isinstance(task, Workflow):
            ctx["input_artifacts_list"], input_artifacts = (
                self._get_workflow_input_artifacts(task)
            )
        else:
            ctx["input_artifacts_table"], input_artifacts = (
                self._get_task_input_artifacts(task)
            )

        built_artifacts_table = ArtifactTable(
            self.request,
            Artifact.objects.filter(
                created_by_work_request=self.object
            ).annotate_complete(),
            prefix="built_artifacts",
            fragment="output",
        )
        built_artifacts_table_paginator = built_artifacts_table.get_paginator(
            per_page=20
        )
        ctx["built_artifacts_table"] = built_artifacts_table_paginator

        # Generate UI shortcuts for artifacts
        for artifact in itertools.chain(
            input_artifacts,
            built_artifacts_table_paginator.page_obj.object_list,
        ):
            self.add_object_ui_shortcuts(
                artifact,
                ui_shortcuts.create_artifact_view(artifact),
                ui_shortcuts.create_artifact_download(artifact),
            )

        ctx["task_data"] = highlight_as_yaml(self.object.task_data)

        if (dynamic_task_data := self.object.dynamic_task_data) is not None:
            ctx["show_task_data_dynamic"] = True
            ctx["task_data_dynamic"] = highlight_as_yaml(dynamic_task_data)
        else:
            ctx["show_task_data_dynamic"] = False

        if (
            configured_task_data := self.object.configured_task_data
        ) is not None:
            ctx["show_task_data_configured"] = True
            ctx["task_data_configured"] = highlight_as_yaml(
                configured_task_data
            )
            ctx["scheduler_tags"] = highlight_as_yaml(
                {
                    "provided": sorted(self.object.scheduler_tags_provided),
                    "required": sorted(self.object.scheduler_tags_required),
                }
            )
        else:
            ctx["show_task_data_configured"] = False

        ctx["work_request_output_data"] = self._format_output_data(
            self.object.output_data
        )
        ctx["work_request_event_reactions_json"] = highlight_as_json(
            self.object.event_reactions_json
        )
        ctx["work_request_output_data_json"] = highlight_as_json(
            self.object.output_data_json
        )
        ctx["work_request_workflow_data_json"] = highlight_as_json(
            self.object.workflow_data_json
        )

        ctx["work_request_dependencies"] = WorkRequestTable(
            self.request, self.object.dependencies.all(), prefix="dependencies"
        ).get_paginator(per_page=10)
        ctx["work_request_reverse_dependencies"] = WorkRequestTable(
            self.request,
            self.object.reverse_dependencies.all(),
            prefix="reverse_dependencies",
        ).get_paginator(per_page=10)

        ctx["workflow_parents"] = self._get_workflow_parents(self.object)

        # Let the plugin override parts of the context, if needed
        if self.plugin:
            ctx.update(self.plugin.get_context_data())

        return ctx

    @staticmethod
    def _format_output_data(
        output_data: OutputData | None,
    ) -> dict[str, Any]:
        """
        Format OutputData into a dictionary with human-readable values.

        - Memory and disk sizes are formatted as MB/GB.
        - Durations are shown in hours, minutes, and seconds.
        """
        if output_data is None:
            return {}

        runtime_statistics = {}

        if output_data.runtime_statistics is not None:
            for key, value in output_data.runtime_statistics:
                if (
                    key
                    in (
                        "memory",
                        "available_memory",
                        "disk_space",
                        "available_disk_space",
                    )
                    and value is not None
                ):
                    value = filesizeformat(value)

                if key in ("duration", "cpu_time") and value is not None:
                    value = duration_string(dt.timedelta(seconds=value))

                key = key.replace("_", " ").capitalize().replace("Cpu ", "CPU ")

                runtime_statistics[key] = value

        errors = []
        if output_data.errors is not None:
            for error in output_data.errors:
                errors.append({"message": error.message, "code": error.code})

        formatted = {
            "Runtime statistics": runtime_statistics,
            "Errors": errors,
        }
        if output_data.skip_reason is not None:
            formatted["Skipped"] = output_data.skip_reason
        return formatted


class WorkRequestPlugin(abc.ABC):
    """
    WorkRequests with specific outputs must subclass it.

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

    model = WorkRequest
    task_type: str
    task_name: str
    task: DBTask[Any, Any]
    template_name: str

    #: Maps (task_type, task_name) tuples to their associated plugin class.
    #: Used to look up the correct plugin for a given work request.
    _work_request_plugins: dict[tuple[str, str], type["WorkRequestPlugin"]] = {}

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

    def __init__(self, work_request: WorkRequest) -> None:
        """Store the work_request to display."""
        self.work_request = work_request
        self.task = self.work_request.get_task()

    @classmethod
    def plugin_for(cls, instance: WorkRequest) -> "WorkRequestPlugin | None":
        """Return WorkRequestPlugin for task_name or None."""
        plugin_class = cls._work_request_plugins.get(
            (instance.task_type, instance.task_name)
        )
        if plugin_class is None:
            return None
        plugin = plugin_class(instance)
        if not plugin.is_enabled():
            return None
        return plugin

    def is_enabled(self) -> bool:
        """Check if the tab should appear."""
        return True

    def add_tabs(self, tabs: list[Tab]) -> None:
        """Add extra tabs to the tab list."""

    def get_description_data(self) -> dict[str, Any]:
        """Return data for the description or {} if dynamic_data is None."""
        if self.task.dynamic_data is None:
            return {}

        return self.do_get_description_data()

    def do_get_description_data(self) -> dict[str, Any]:
        """Return data used for the description."""
        return {}

    def get_context_data(self) -> dict[str, Any]:
        """Must be implemented by subclasses."""
        raise NotImplementedError()


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

    model = WorkRequest
    table_class = WorkRequestTable
    template_name = "web/work_request-list.html"
    context_object_name = "work_request_list"
    title = "List of work requests"
    paginate_by = 50

    def get_place(self) -> Place:
        """Return the Place for this page."""
        place = super().get_place()
        place.parent = context.require_workspace().ui(self.request).place
        return place

    @staticmethod
    def _filter_by_architecture(
        work_requests: WorkRequestQuerySet[WorkRequest], architecture: str
    ) -> WorkRequestQuerySet[WorkRequest]:
        pending_without_build_architecture = work_requests.filter(
            task_data__build_architecture__isnull=True
        )

        # TODO: When WorkRequests have tags: have a build_architecture
        # tag and filter in the database. This will allow:
        # -simplify logic filtering (instead of using
        # task_data__build_architecture and for the ones without this
        # instantiating the Task
        # -more efficient (done in the DB) so we can filter per architecture
        # for any status (currently it's limited for WorkRequests where
        # status == PENDING to avoid instantiating too many tasks

        # Work requests with task_data__build_architecture=arch_param
        # (leave out work requests for which the architecture
        # is defined elsewhere)
        queryset = work_requests.filter(
            task_data__build_architecture=architecture
        )

        # For work requests which task_data__build_architecture is
        # not there: instantiate the work requests and use
        # Task.build_architecture(), add to the query set
        for work_request in pending_without_build_architecture:
            try:
                task = work_request.get_task()
            except TaskConfigError:
                # When the scheduler picks this work request: will
                # mark it
                continue

            if task.build_architecture() == architecture:
                queryset = queryset | WorkRequest.objects.filter(
                    id=work_request.id
                )

        return queryset.distinct()

    def get_queryset(self) -> WorkRequestQuerySet[WorkRequest]:
        """Filter work requests displayed by the workspace GET parameter."""
        queryset = super().get_queryset().exclude(task_type=TaskTypes.INTERNAL)

        arch_param = self.request.GET.get("arch")
        status_param = self.request.GET.get("status")

        status_mapping = {
            "pending": WorkRequest.Statuses.PENDING,
            "running": WorkRequest.Statuses.RUNNING,
            "completed": WorkRequest.Statuses.COMPLETED,
            "aborted": WorkRequest.Statuses.ABORTED,
            "blocked": WorkRequest.Statuses.BLOCKED,
        }

        status_value = (
            None if status_param is None else status_mapping.get(status_param)
        )

        if status_param is not None and status_value is None:
            messages.warning(
                self.request, 'Invalid "status" parameter, ignoring it'
            )

        if status_value:
            queryset = queryset.filter(status=status_value)

        if arch_param:
            if status_value == WorkRequest.Statuses.PENDING:
                # Filtering by architecture is only allowed for PENDING
                # Work Requests. Filtering by architecture, currently,
                # requires instantiating the Task and calling
                # build_architecture()
                queryset = self._filter_by_architecture(queryset, arch_param)
            else:
                messages.warning(
                    self.request,
                    'Filter by architecture is only supported when '
                    'also filtering by "status=pending", ignoring architecture'
                    'filtering',
                )

        return queryset


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

    model = WorkRequest
    template_name = "web/work_request-create.html"
    form_class = WorkRequestForm
    title = "Create work request"

    def init_view(self) -> None:
        """Set the current workspace."""
        super().init_view()
        self.enforce(context.require_workspace().can_create_work_requests)

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

    def get_success_url(self) -> str:
        """Redirect to work_requests:detail for the created WorkRequest."""
        assert self.object is not None
        return self.object.get_absolute_url()

    def form_valid(self, form: WorkRequestForm) -> HttpResponse:
        """Validate the work request."""
        self.object = form.save(commit=False)

        try:
            self.object.get_task().compute_dynamic_data(
                TaskDatabase(self.object)
            )
        except Exception as exc:
            form.add_error("task_data", f"Invalid task data: {exc}")
            return self.form_invalid(form)

        self.object.mark_pending()
        return super().form_valid(form)


class WorkRequestRetryView(
    WorkRequestObjectMixin, SingleObjectMixinBase[WorkRequest], View
):
    """Form view for retrying a work request."""

    model = WorkRequest

    def retry(self, work_request: WorkRequest) -> HttpResponse:
        """Retry the work request."""
        try:
            new_work_request = work_request.retry()
        except CannotRetry as e:
            messages.error(self.request, f"Cannot retry: {e}")
            return redirect(work_request.get_absolute_url())

        return redirect(new_work_request.get_absolute_url())

    def post(
        self,
        request: HttpRequest,  # noqa: ARG002, U100
        *args: Any,
        **kwargs: Any,
    ) -> HttpResponse:
        """Handle POST requests."""
        work_request = self.get_object()
        self.enforce(work_request.can_retry)
        return self.retry(work_request)


class WorkRequestAbortView(
    WorkRequestObjectMixin, SingleObjectMixinBase[WorkRequest], View
):
    """Form view for aborting a work request."""

    model = WorkRequest

    def abort(self, work_request: WorkRequest) -> HttpResponse:
        """Abort the work request."""
        try:
            work_request.abort()
        except CannotAbort as e:
            messages.error(self.request, f"Cannot abort: {e}")

        return redirect(work_request.get_absolute_url())

    def post(self, *args: Any, **kwargs: Any) -> HttpResponse:
        """Handle POST requests."""
        work_request = self.get_object()
        self.enforce(work_request.can_abort)
        WorkRequest.objects.filter(
            id=work_request.id
        ).lock_workflows_for_update()
        return self.abort(work_request)


class WorkRequestUnblockView(
    WorkRequestObjectMixin,
    SingleObjectMixinBase[WorkRequest],
    FormMixinBase[WorkRequestUnblockForm],
    View,
):
    """Form view for reviewing a work request awaiting manual approval."""

    model = WorkRequest
    form_class = WorkRequestUnblockForm

    def get_action(self) -> WorkRequestManualUnblockAction | None:
        """Get the unblock action from query arguments."""
        raw_action = self.request.POST["action"]
        match raw_action:
            case "Accept":
                return WorkRequestManualUnblockAction.ACCEPT
            case "Reject":
                return WorkRequestManualUnblockAction.REJECT
            case "Record notes only":
                return None
            case _:
                raise HttpError400(f"Invalid action parameter: {raw_action!r}")

    def unblock(self, work_request: WorkRequest) -> HttpResponse:
        """Perform the unblock."""
        # This view requires a permission, so the user can't be an
        # AnonymousUser by this point.
        assert isinstance(self.request.user, User)

        form = self.get_form()
        if not form.is_valid():
            # TODO: This is ugly and we should probably try to render
            # work_requests:detail with inline unblock form errors instead.
            # However, since the form is so limited it's very difficult to
            # actually hit this case in practice, so it doesn't seem worth
            # the effort for now.
            return render(
                self.request,
                "400.html",
                context={"error": form.errors.as_json()},
                status=status.HTTP_400_BAD_REQUEST,
            )

        try:
            work_request.review_manual_unblock(
                user=self.request.user,
                notes=form.cleaned_data["notes"],
                action=self.get_action(),
            )
        except CannotUnblock as e:
            raise HttpError400(f"Cannot unblock: {e}")
        return redirect(work_request.get_absolute_url())

    @catch_http_errors
    def post(
        self,
        request: HttpRequest,  # noqa: ARG002, U100
        *args: Any,
        **kwargs: Any,
    ) -> HttpResponse:
        """Handle POST requests."""
        work_request = self.get_object()
        self.enforce(work_request.can_unblock)
        return self.unblock(work_request)


class WorkRequestConfirmView(
    WorkRequestObjectMixin,
    SingleObjectMixinBase[WorkRequest],
    FormMixinBase[WorkRequestConfirmForm],
    View,
):
    """Form view for confirming a work request."""

    model = WorkRequest
    form_class = WorkRequestConfirmForm

    @property
    def should_confirm(self) -> bool:
        """Get whether to confirm the work request from query arguments."""
        action = self.request.POST["action"]
        match action:
            case "confirm":
                return True
            case "deny":
                return False
            case _:
                raise HttpError400(f"Invalid action parameter: {action!r}")

    def confirm_or_deny(self, work_request: WorkRequest) -> HttpResponse:
        """Perform the confirmation or denial."""
        # This view requires a permission, so the user can't be an
        # AnonymousUser by this point.
        assert isinstance(self.request.user, User)

        if (
            work_request.task_type != TaskTypes.WAIT
            or work_request.task_name != "confirm"
        ):
            raise HttpError400(
                f"Expected work request to be Wait/Confirm; got "
                f"{work_request.task_type}/{work_request.task_name}"
            )
        task = work_request.get_task()
        assert isinstance(task, Confirm)

        form = self.get_form()
        if not form.is_valid():
            # TODO: This is ugly and we should probably try to render
            # work_requests:detail with inline confirm form errors instead.
            # However, since the form is so limited it's very difficult to
            # actually hit this case in practice, so it doesn't seem worth
            # the effort for now.
            return render(
                self.request,
                "400.html",
                context={"error": form.errors.as_json()},
                status=status.HTTP_400_BAD_REQUEST,
            )

        task.respond(
            user=self.request.user,
            confirm=self.should_confirm,
            comment=form.cleaned_data["comment"],
        )
        return redirect(work_request.get_absolute_url())

    @catch_http_errors
    def post(
        self,
        request: HttpRequest,  # noqa: ARG002, U100
        *args: Any,
        **kwargs: Any,
    ) -> HttpResponse:
        """Handle POST requests."""
        work_request = self.get_object()
        self.enforce(work_request.can_confirm)
        return self.confirm_or_deny(work_request)
