# 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 workspace views."""

import json
from functools import cached_property
from typing import Any, override

from django.contrib import messages
from django.core.exceptions import BadRequest
from django.db.models import Count, QuerySet
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse

from debusine.artifacts.models import BareDataCategory, CollectionCategory
from debusine.db.context import context
from debusine.db.models import Collection, WorkflowTemplate, Workspace
from debusine.db.models.workspaces import WorkspaceChain
from debusine.server.collections.debusine_task_configuration import (
    ConfigurationLookup,
    build_configuration,
)
from debusine.web.forms import (
    BaseFormSetBase,
    TaskConfigurationInspectorForm,
    WorkspaceForm,
    WorkspaceInheritanceForm,
    WorkspaceInheritanceFormSet,
)
from debusine.web.urls.url_utils import make_absolute_archives, urlconf_archives
from debusine.web.views.base import (
    DetailViewBase,
    FormViewBase,
    UpdateViewBase,
    WorkspaceView,
)
from debusine.web.views.places import Place
from debusine.web.views.ui.workspaces import WorkspaceUI


class WorkspaceDetailView(WorkspaceView, DetailViewBase[Workspace]):
    """Show a workspace detail."""

    model = Workspace
    template_name = "web/workspace-detail.html"
    context_object_name = "workspace"

    @override
    def get_object(
        self,
        queryset: QuerySet[Workspace] | None = None,  # noqa: ARG002, U100
    ) -> Workspace:
        """Return the workspace object to show."""
        # Enforced by WorkspaceView.init()
        assert context.workspace is not None
        return context.workspace

    @override
    def get_place(self) -> Place:
        return self.object.ui(self.request).place

    def get_deb_archive_url(self) -> str:
        """Return the Debian archive url for the current workspace."""
        with urlconf_archives():
            return make_absolute_archives(
                reverse(
                    "archive-root",
                    kwargs={
                        "scope": self.object.scope.name,
                        "workspace": self.object.name,
                    },
                )
            )

    def get_resource_actions(self) -> list[Place]:
        """Get a list with Places for each action available on the resource."""
        ui = self.object.ui(self.request)

        res: list[Place] = []
        if ui.can_configure:
            res.append(ui.place_update)
            res.append(ui.place_update_inheritance)

        if ui.can_create_collections:
            res.append(ui.place_collection_create)

        return res

    @override
    def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
        ctx = super().get_context_data(*args, **kwargs)

        ctx["resource_actions"] = self.get_resource_actions()

        ctx["parents"] = [
            x.parent
            for x in WorkspaceChain.objects.filter(child=self.object)
            .order_by("order")
            .select_related("parent")
            if x.parent.can_display(context.pc)
        ]
        ctx["workflow_templates"] = (
            WorkflowTemplate.objects.can_display(context.pc)
            .filter(workspace=self.object)
            .order_by("name")
        )

        # Collections grouped by category
        stats = list(
            Collection.objects.filter(workspace=self.object)
            .exclude(category="debusine:workflow-internal")
            .values("category")
            .annotate(count=Count("category"))
            .order_by("category")
        )
        ctx["collection_stats"] = stats
        ctx["collection_count"] = sum(s["count"] for s in stats)
        if Collection.objects.filter(
            workspace=self.object, category=CollectionCategory.ARCHIVE
        ).exists():
            ctx["deb_archive_url"] = self.get_deb_archive_url()

        return ctx


class WorkspaceUpdateView(
    WorkspaceView, UpdateViewBase[Workspace, WorkspaceForm]
):
    """Configure a workspace."""

    form_class = WorkspaceForm
    template_name = "web/workspace-update.html"

    @override
    def init_view(self) -> None:
        super().init_view()
        # Enforced by WorkspaceView.init()
        assert context.workspace is not None
        self.enforce(context.workspace.can_configure)

    @override
    def get_object(
        self,
        queryset: QuerySet[Workspace] | None = None,  # noqa: ARG002, U100
    ) -> Workspace:
        """Return the workspace object to show."""
        # Enforced by WorkspaceView.init()
        assert context.workspace is not None
        return context.workspace

    @override
    def get_place(self) -> Place:
        return context.require_workspace().ui(self.request).place_update


class TaskConfigInspectorView(WorkspaceView, DetailViewBase[Workspace]):
    """Look up task configuration items."""

    model = Workspace
    template_name = "web/task-configuration-inspector.html"
    context_object_name = "workspace"

    @override
    def get_object(
        self,
        queryset: QuerySet[Workspace] | None = None,  # noqa: ARG002, U100
    ) -> Workspace:
        """Return the workspace object to show."""
        # Enforced by WorkspaceView.init()
        assert context.workspace is not None
        return context.workspace

    @override
    def get_place(self) -> Place:
        return (
            context.require_workspace().ui(self.request).place_config_inspector
        )

    def get_collections(self) -> list[Collection]:
        """Return all accessible task-configuration collections."""
        return list(
            self.object.list_accessible_collections(
                context.pc, CollectionCategory.TASK_CONFIGURATION
            )
        )

    def get_lookup(
        self, collection: Collection, form: TaskConfigurationInspectorForm
    ) -> ConfigurationLookup:
        """Instantiate the configuration lookup engine."""
        lookup = ConfigurationLookup(collection)

        task_type, task_name = form.cleaned_data["task"].split(":", 1)
        lookup.set_task_type_name_constraints(task_type, task_name)

        lookup.set_subject_context_constraints(
            form.cleaned_data["subject"] or None,
            form.cleaned_data["context"] or None,
        )

        if value := form.cleaned_data["tags_provided"]:
            lookup.set_tags_provided(set(value.split()))
        if value := form.cleaned_data["tags_required"]:
            lookup.set_tags_required(set(value.split()))

        return lookup

    @override
    def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
        ctx = super().get_context_data(**kwargs)

        collections = self.get_collections()
        ctx["collections"] = collections

        # Pick the collection to inspect, if possible
        collection: Collection | None = None
        if (collection_id := self.request.GET.get("collection")) is None:
            if len(collections) == 1:
                collection = collections[0]
        else:
            # Validate numeric input
            try:
                collection_id_int = int(collection_id)
            except (TypeError, ValueError):
                raise BadRequest("Invalid collection ID")

            collection = get_object_or_404(
                Collection.objects.can_display(context.pc).select_related(
                    "workspace"
                ),
                pk=collection_id_int,
            )

        if collection is not None:
            item_count = collection.child_items.filter(
                category=BareDataCategory.TASK_CONFIGURATION,
            ).count()
            ctx["item_count"] = item_count

            if item_count > 0:
                form = TaskConfigurationInspectorForm(
                    self.request.GET, collection=collection
                )
                if form.is_valid():
                    config_lookup = self.get_lookup(collection, form)
                    ctx["has_result"] = True

                    # Look up debusine:task-configuration items
                    config_items = list(config_lookup.iter_items())
                    ctx["config_items"] = config_items

                    # Turn items into a configuration to apply
                    changes = build_configuration(config_items)
                    ctx["changes"] = changes
                    ctx["provide_tags_formatted"] = (
                        repr(changes.provide_tags)
                        if changes.provide_tags
                        else "none"
                    )
                    ctx["require_tags_formatted"] = (
                        repr(changes.require_tags)
                        if changes.require_tags
                        else "none"
                    )
                ctx["form"] = form

        ctx["collection"] = collection
        return ctx


# This has a couple of type ignore annotations, as Django typing doesn't seem
# to allow a FormView to use a FormSet instead of a Form, although Django
# supports it.
class WorkspaceUpdateInheritanceView(
    WorkspaceView,
    # Work around error: Type argument "BaseFormSet[WorkspaceInheritanceForm]"
    # of "FormView" must be a subtype of "BaseForm"  [type-var]
    FormViewBase[
        BaseFormSetBase[WorkspaceInheritanceForm],  # type: ignore[type-var]
    ],
    DetailViewBase[Workspace],
):
    """Configure a workspace."""

    template_name = "web/workspace-inheritance.html"

    @cached_property
    def helper(self) -> WorkspaceUI:
        """Return the UI helper for the current workspace."""
        return self.object.ui(self.request)

    @override
    def get_form_class(self) -> type[BaseFormSetBase[WorkspaceInheritanceForm]]:
        return WorkspaceInheritanceFormSet.formset_factory(self.helper)

    @override
    # Work around error: Return type "list[dict[str, int]]" of "get_initial"
    # incompatible with return type "dict[str, Any]" in supertype "FormMixin"
    # [override]
    def get_initial(self) -> list[dict[str, int]]:  # type: ignore[override]
        parents = self.object.chain_parents.order_by("order").values_list(
            "parent__pk", flat=True
        )
        return [{"parent": p} for p in parents]

    @override
    def init_view(self) -> None:
        super().init_view()
        # Enforced by WorkspaceView.init()
        assert context.workspace is not None
        self.enforce(context.workspace.can_configure)

    @override
    def get_object(
        self,
        queryset: QuerySet[Workspace] | None = None,  # noqa: ARG002, U100
    ) -> Workspace:
        """Return the workspace object to show."""
        # Enforced by WorkspaceView.init()
        assert context.workspace is not None
        return context.workspace

    @override
    def get_place(self) -> Place:
        return (
            context.require_workspace()
            .ui(self.request)
            .place_update_inheritance
        )

    @override
    def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
        ctx = super().get_context_data(**kwargs)
        ctx["available"] = json.dumps(
            [(ws.pk, str(ws)) for ws in self.helper.candidate_parents]
        )
        ctx["current"] = json.dumps(
            [
                (x[0], f"{x[1]}/{x[2]}")
                for x in self.object.chain_parents.order_by(
                    "order"
                ).values_list(
                    "parent__pk", "parent__scope__name", "parent__name"
                )
            ]
        )
        return ctx

    @override
    def post(
        self, request: HttpRequest, *args: Any, **kwargs: Any
    ) -> HttpResponse:
        self.object = self.get_object()
        return super().post(request, *args, **kwargs)

    @override
    def form_valid(
        self, form: BaseFormSetBase[WorkspaceInheritanceForm]
    ) -> HttpResponse:
        """Validate the submitted chain."""
        # Keep only valid and non-deleted entries.
        # We already know that cleaned_data["parent"] contains a valid
        # candidate ID as it is enforced by the choice field
        parent_ids: list[int] = [
            int(f.cleaned_data["parent"]) for f in form.ordered_forms
        ]

        # Map the IDs to workspaces
        workspace_map = {
            w.pk: w for w in Workspace.objects.filter(pk__in=parent_ids)
        }

        # Update the inheritance chain
        try:
            self.object.set_inheritance([workspace_map[p] for p in parent_ids])
        except ValueError as e:
            messages.add_message(self.request, messages.ERROR, str(e))
        else:
            messages.add_message(
                self.request, messages.SUCCESS, "Inheritance chain saved"
            )
        return super().form_valid(form)

    @override
    def get_success_url(self) -> str:
        """Redirect to work_requests:detail for the created WorkRequest."""
        assert self.object is not None
        if self.request.POST.get("save") == "continue":
            return self.object.get_absolute_url_configure_inheritance()
        else:
            return self.object.get_absolute_url()
