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

from functools import cached_property
from typing import Any, override

from django.db.models import Count, Q, QuerySet
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.urls import reverse

from debusine.db.context import context
from debusine.db.models import Collection, CollectionItem
from debusine.db.models.collections import CollectionRelation
from debusine.web.forms import (
    CollectionCreateForm,
    CollectionSearchForm,
    CollectionUpdateForm,
    ModelFormBase,
)
from debusine.web.views.base import (
    CreateViewBase,
    DeleteViewBase,
    DetailViewBase,
    FormMixinBase,
    ListViewBase,
    UpdateViewBase,
    WorkspaceView,
)
from debusine.web.views.places import Place, ResourcePlace
from debusine.web.views.table import TableMixin
from debusine.web.views.tables import CollectionItemTable
from debusine.web.views.view_utils import highlight_as_yaml


class CollectionListView(WorkspaceView, ListViewBase[Collection]):
    """List collections."""

    model = Collection
    template_name = "web/collection-list.html"
    context_object_name = "collection_list"
    ordering = ["category", "name"]

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

    @override
    def get_queryset(self) -> QuerySet[Collection]:
        """Filter collection by accessible workspace."""
        queryset = super().get_queryset()
        return queryset.filter(workspace=context.workspace).exclude(
            category="debusine:workflow-internal"
        )


class CollectionCategoryListView(WorkspaceView, ListViewBase[Collection]):
    """List collections with a given category."""

    model = Collection
    template_name = "web/collection-category-list.html"
    context_object_name = "collection_list"
    ordering = ["name"]

    @override
    def get_place(self) -> Place:
        workspace = context.require_workspace()
        return ResourcePlace(
            title=f"{workspace.name} {self.kwargs['ccat']} collections",
            url=reverse(
                "workspaces:collections:category_list",
                kwargs={"wname": workspace.name, "ccat": self.kwargs["ccat"]},
            ),
            breadcrumb=self.kwargs["ccat"],
        )

    @override
    def get_queryset(self) -> QuerySet[Collection]:
        """Filter collection by accessible workspace."""
        queryset = super().get_queryset()
        return (
            queryset.filter(workspace=context.workspace)
            .exclude(category="debusine:workflow-internal")
            .filter(category=self.kwargs["ccat"])
        )

    @override
    def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
        """Return context_data with category."""
        context = super().get_context_data(**kwargs)
        context["category"] = self.kwargs["ccat"]
        return context


class CollectionCreateView(
    WorkspaceView, CreateViewBase[Collection, CollectionCreateForm]
):
    """Create a collection."""

    form_class = CollectionCreateForm
    template_name = "web/collection-create.html"

    @override
    def init_view(self) -> None:
        """Enforce permissions."""
        super().init_view()
        self.enforce(context.require_workspace().can_create_collections)

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


class CollectionViewMixin(WorkspaceView):
    """Common functions for collection-specific views."""

    @cached_property
    def collection(self) -> Collection:
        """Collection for this request."""
        try:
            return Collection.objects.can_display(context.pc).get(
                workspace=context.workspace,
                name=self.kwargs["cname"],
                category=self.kwargs["ccat"],
            )
        except Collection.DoesNotExist:
            raise Http404(
                f"{self.kwargs['cname']}@{self.kwargs['ccat']}"
                " collection not found"
            )

    @override
    def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
        """Return context_data with collection."""
        context = super().get_context_data(**kwargs)
        context["collection"] = self.collection
        return context


class CollectionDetailView(CollectionViewMixin, DetailViewBase[Collection]):
    """Show a collection detail."""

    model = Collection
    template_name = "web/collection-detail.html"
    context_object_name = "collection"

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

    @override
    def get_object(
        self,
        queryset: QuerySet[Collection] | None = None,  # noqa: ARG002, U100
    ) -> Collection:
        """Return the collection object to show."""
        return self.collection

    @override
    def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
        """Return context_data with work_request_list and workspace_list."""
        context = super().get_context_data(**kwargs)
        if self.object.data:
            context["data"] = highlight_as_yaml(self.object.data)

        context["collections"] = (
            self.object.child_items.active()
            .filter(collection__isnull=False)
            .order_by("category", "name")
        )

        context["artifacts"] = (
            self.object.child_items.filter(artifact__isnull=False)
            .values("category")
            .annotate(
                count=Count("category", filter=Q(removed_at__isnull=True)),
                count_removed=Count(
                    "category", filter=Q(removed_at__isnull=False)
                ),
            )
            .order_by("category")
        )

        context["bare"] = (
            self.object.child_items.filter(
                collection__isnull=True, artifact__isnull=True
            )
            .values("category")
            .annotate(
                count=Count("category", filter=Q(removed_at__isnull=True)),
                count_removed=Count(
                    "category", filter=Q(removed_at__isnull=False)
                ),
            )
            .order_by("category")
        )

        try:
            context["suite_based_on"] = self.object.relations.get(
                type=CollectionRelation.Types.SUITE_BASED_ON
            ).target
        except CollectionRelation.DoesNotExist:
            pass

        return context


class CollectionUpdateView(
    CollectionViewMixin, UpdateViewBase[Collection, CollectionUpdateForm]
):
    """Edit configuration for a collection."""

    form_class = CollectionUpdateForm
    template_name = "web/collection-update.html"

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

    @override
    def get_object(
        self,
        queryset: QuerySet[Collection] | None = None,  # noqa: ARG002, U100
    ) -> Collection:
        """Return the collection object to update."""
        self.enforce(self.collection.can_configure)
        return self.collection


class CollectionDeleteView(
    CollectionViewMixin, DeleteViewBase[Collection, ModelFormBase[Collection]]
):
    """Delete a collection."""

    object: Collection
    model = Collection
    template_name = "web/collection-confirm_delete.html"

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

    @override
    def get_object(
        self,
        queryset: QuerySet[Collection] | None = None,  # noqa: ARG002, U100
    ) -> Collection:
        """Return the collection object to delete."""
        self.enforce(self.collection.can_delete)
        return self.collection

    @override
    def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
        ctx = super().get_context_data(**kwargs)
        item_stats = list(self.object.get_item_stats())
        ctx["items_total"] = sum(x["count"] for x in item_stats)
        ctx["item_stats"] = item_stats
        return ctx

    @override
    def form_valid(
        self,
        form: ModelFormBase[Collection],  # noqa: U100
    ) -> HttpResponse:
        success_url = self.get_success_url()
        self.object.delete_collection()
        return HttpResponseRedirect(success_url)

    @override
    def get_success_url(self) -> str:
        return context.require_workspace().get_absolute_url()


class CollectionSearchView(
    TableMixin[CollectionItem],
    CollectionViewMixin,
    FormMixinBase[CollectionSearchForm],
    ListViewBase[CollectionItem],
):
    """Search a collection contents."""

    model = CollectionItem
    template_name = "web/collection-search.html"
    context_object_name = "item_list"
    form_class = CollectionSearchForm
    table_class = CollectionItemTable
    paginate_by = 50

    @override
    def get_place(self) -> Place:
        return self.collection.ui(self.request).place_search

    @override
    def get_queryset(self) -> QuerySet[CollectionItem]:
        """Filter items based on the search form."""
        queryset = (
            super().get_queryset().filter(parent_collection=self.collection)
        )
        queryset = queryset.select_related(
            "parent_collection__workspace__scope",
            "collection",
            "artifact__workspace__scope",
        )
        # FIXME: this builds the form twice: if it becomes a problem we can
        # override get_form to cache its result
        form = self.get_form()
        if form.is_valid():
            if name := form.cleaned_data["name"]:
                queryset = queryset.filter(name__contains=name)
            if category := form.cleaned_data["category"]:
                queryset = queryset.filter(category=category)
            if form.cleaned_data["historical"]:
                queryset = queryset.filter(removed_at__isnull=False)
            else:
                queryset = queryset.filter(removed_at__isnull=True)
        else:
            queryset = queryset.none()
        return queryset

    @override
    def get_form_kwargs(self) -> dict[str, Any]:
        kwargs = super().get_form_kwargs()
        kwargs["instance"] = self.collection
        kwargs["data"] = self.request.GET
        return kwargs

    @override
    def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
        """Return context_data with work_request_list and workspace_list."""
        context = super().get_context_data(**kwargs)
        context["collection"] = self.collection
        return context


class CollectionItemDetailView(
    CollectionViewMixin, DetailViewBase[CollectionItem]
):
    """Show a collection item detail."""

    model = CollectionItem
    template_name = "web/collection-item-detail.html"
    context_object_name = "item"

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

    @override
    def get_object(
        self, queryset: QuerySet[CollectionItem] | None = None
    ) -> CollectionItem:
        """Return the collection object to show."""
        assert queryset is None
        queryset = self.get_queryset()
        try:
            return queryset.filter(removed_at__isnull=True).get(
                pk=self.kwargs["iid"], parent_collection=self.collection
            )
        except CollectionItem.DoesNotExist:
            raise Http404(
                f"{self.kwargs['iid']} ({self.kwargs['iname']})"
                " item not found in "
                f"{self.kwargs['cname']}@{self.kwargs['ccat']}"
            )

    @override
    def get_context_data(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
        context = super().get_context_data(**kwargs)
        if self.object.data:
            context["data"] = highlight_as_yaml(self.object.data)
        return context
