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

from functools import cached_property
from typing import Any, override

from django.contrib import messages
from django.db import transaction
from django.db.models import QuerySet
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.html import format_html
from django_cte import CTE, with_cte

from debusine.artifacts.models import CollectionCategory
from debusine.db.context import context
from debusine.db.models import Collection, User, Workspace
from debusine.db.models.collections import CollectionRelation
from debusine.db.permissioncontext import PermissionContext
from debusine.web.forms import CollectionRelationCreateForm, ModelFormBase
from debusine.web.views.base import CreateViewBase, DeleteViewBase, ListViewBase
from debusine.web.views.collections import CollectionViewMixin
from debusine.web.views.http_errors import HttpError400, catch_http_errors
from debusine.web.views.places import ResourceSubPlace


class CollectionRelationListView(
    CollectionViewMixin,
    ListViewBase[CollectionRelation],
):
    """Show list of relations for a collection."""

    model = CollectionRelation
    template_name = "web/collection_relation-list.html"
    context_object_name = "collection_relations"

    @override
    def get_place(self) -> ResourceSubPlace[Collection]:
        """Return the Place for listing relations of a collection."""
        return self.collection.ui(self.request).place_collection_relation_list

    @cached_property
    def _relation_type_query_parameter(self) -> str:
        """Return CollectionRelation from query parameter or ""."""
        return self.request.GET.get("relation_type", "")

    @cached_property
    def _relation_type_label(self) -> str | None:
        try:
            return CollectionRelation.Types(
                self._relation_type_query_parameter
            ).label
        except ValueError:
            return self._relation_type_query_parameter

    @override
    def get_queryset(self) -> QuerySet[CollectionRelation]:
        """Filter collection relations by kwargs."""
        queryset = super().get_queryset()

        if relation_type := self._relation_type_query_parameter:
            queryset = queryset.filter(type=relation_type)

        return queryset.filter(source=self.collection).order_by(
            "type", "position", "target"
        )

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

        permission_create = self.collection.can_create_collection_relation(
            context.pc
        )

        ctx["show_create"] = permission_create and any(
            _get_allowed_targets_by_type(self.collection).values()
        )

        ctx["show_reorder_requires"] = permission_create and (
            ctx["collection_relations"]
            .filter(type=CollectionRelation.Types.SUITE_REQUIRES)
            .count()
            > 1
        )

        ctx["collection"] = self.collection

        if self._relation_type_query_parameter:
            ctx["relation_type"] = self._relation_type_query_parameter
            ctx["relation_type_label"] = self._relation_type_label

        return ctx


class CollectionRelationReorderRequiresView(
    CollectionViewMixin,
    ListViewBase[CollectionRelation],
):
    """Show list of Requires for a collection. Allow reordering."""

    model = CollectionRelation
    template_name = "web/collection_relation-reorder_requires.html"
    context_object_name = "collection_relations"

    @override
    def init_view(self) -> None:
        """Enforce permissions."""
        super().init_view()
        self.enforce(self.collection.can_create_collection_relation)

    @override
    def get_place(self) -> ResourceSubPlace[Collection]:
        """Return the Place for listing relations of a collection."""
        return self.collection.ui(
            self.request
        ).place_collection_relation_reorder_requires

    @override
    def get_queryset(self) -> QuerySet[CollectionRelation]:
        """Filter collection relations by kwargs."""
        queryset = super().get_queryset()
        return (
            queryset.filter(type=CollectionRelation.Types.SUITE_REQUIRES)
            .filter(source=self.collection)
            .order_by("type", "position")
        )

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

        ctx["collection"] = self.collection
        ctx["save_button"] = ctx["collection_relations"].exists()

        return ctx

    @catch_http_errors
    def post(
        self, request: HttpRequest, *args: Any, **kwargs: Any
    ) -> HttpResponse:
        """Save the new order of Requires relations."""
        assert isinstance(request.user, User)

        order_raw = request.POST.get("order", None)

        if order_raw is None:
            raise HttpError400('Missing "order" POST parameter')

        try:
            ordered_ids = [int(value) for value in order_raw.split(",")]
        except ValueError:
            raise HttpError400('Invalid "order" format')

        current_db_ids = self.collection.relations.filter(
            type=CollectionRelation.Types.SUITE_REQUIRES
        ).values_list("id", flat=True)

        if set(current_db_ids) != set(ordered_ids):
            raise HttpError400("Submitted ids and DB ids do not match")

        with transaction.atomic():
            relations = {
                r.id: r
                for r in CollectionRelation.objects.filter(
                    id__in=ordered_ids,
                    source=self.collection,
                    type=CollectionRelation.Types.SUITE_REQUIRES,
                )
            }

            for position, relation_id in enumerate(ordered_ids, start=1):
                relation = relations[relation_id]
                relation.position = position
                relation.save(updated_by_user=request.user)

        messages.success(request, "Requires relations reordered.")

        return HttpResponseRedirect(
            reverse(
                "workspaces:collections:collection_relations:list",
                kwargs={
                    "wname": self.collection.workspace.name,
                    "ccat": self.collection.category,
                    "cname": self.collection.name,
                },
            )
        )


class CollectionRelationCreateView(
    CollectionViewMixin,
    CreateViewBase[CollectionRelation, ModelFormBase[CollectionRelation]],
):
    """Create a CollectionRelation."""

    object: CollectionRelation
    model = CollectionRelation
    context_object_name = "collection_relation"
    template_name = "web/collection_relation-create.html"
    form_class = CollectionRelationCreateForm

    #: avoid re-calculating it
    _allowed_targets_by_type: (
        dict[CollectionRelation.Types, list[int]] | None
    ) = None

    @override
    def init_view(self) -> None:
        """Enforce permissions."""
        super().init_view()
        self.enforce(self.collection.can_create_collection_relation)

    @override
    def get_place(self) -> ResourceSubPlace[Collection]:
        """Return the Place for this page."""
        return self.collection.ui(self.request).place_collection_relation_create

    @override
    def get_form_kwargs(self) -> dict[str, Any]:
        """Return keyword arguments for instantiating the form."""
        kwargs = super().get_form_kwargs()
        kwargs["source_collection"] = self.collection
        kwargs["allowed_types"] = {
            rel_type
            for rel_type, ids in self._get_allowed_targets_by_type().items()
            if ids != []
        }
        return kwargs

    @override
    def form_valid(
        self, form: ModelFormBase[CollectionRelation]
    ) -> HttpResponse:
        collection_relation = form.instance
        collection_relation.source = self.collection

        assert isinstance(self.request.user, User)
        collection_relation.created_by_user = self.request.user

        if collection_relation.type == CollectionRelation.Types.SUITE_REQUIRES:
            # append to last
            collection_relation.position = (
                CollectionRelation.get_max_position(
                    collection=collection_relation.source,
                    relation_type=CollectionRelation.Types.SUITE_REQUIRES,
                )
                + 1
            )

        result = super().form_valid(form)

        messages.success(
            self.request,
            format_html(
                "Relation <code>{relation}</code> created.",
                relation=self.object,
            ),
        )
        return result

    @override
    def get_success_url(self) -> str:
        return reverse(
            "workspaces:collections:collection_relations:list",
            kwargs={
                "wname": self.kwargs["wname"],
                "ccat": self.kwargs["ccat"],
                "cname": self.kwargs["cname"],
            },
        )

    def _get_allowed_targets_by_type(
        self,
    ) -> dict[CollectionRelation.Types, list[int]]:
        if self._allowed_targets_by_type is not None:
            return self._allowed_targets_by_type

        self._allowed_targets_by_type = _get_allowed_targets_by_type(
            self.collection
        )
        return self._allowed_targets_by_type

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

        allowed_targets_by_type = self._get_allowed_targets_by_type()

        ctx["allowed_targets_by_type"] = allowed_targets_by_type
        ctx["relations_available"] = any(allowed_targets_by_type.values())

        return ctx


class CollectionRelationDeleteView(
    CollectionViewMixin,
    DeleteViewBase[CollectionRelation, ModelFormBase[CollectionRelation]],
):
    """Delete a CollectionRelation."""

    object: CollectionRelation
    model = CollectionRelation
    context_object_name = "collection_relation"
    template_name = "web/collection_relation-confirm_delete.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) -> ResourceSubPlace[CollectionRelation]:
        """Return the Place for this page."""
        return self.object.ui(self.request).place_delete

    @override
    def get_object(
        self,
        queryset: QuerySet[CollectionRelation] | None = None,
    ) -> CollectionRelation:
        """Return the collection relation to delete."""
        queryset = queryset or self.get_queryset()

        collection_relation = get_object_or_404(
            queryset, pk=self.kwargs["pk"], source=self.collection
        )

        self.enforce(collection_relation.can_delete)

        return collection_relation

    @override
    def form_valid(
        self,
        form: ModelFormBase[CollectionRelation],  # noqa: ARG002, U100
    ) -> HttpResponse:
        success_url = self.get_success_url()
        self.object.delete()

        messages.success(
            self.request,
            format_html(
                "Relation <code>{relation}</code> deleted.",
                relation=self.object,
            ),
        )

        return HttpResponseRedirect(success_url)

    @override
    def get_success_url(self) -> str:
        return reverse(
            "workspaces:collections:collection_relations:list",
            kwargs={
                "wname": self.kwargs["wname"],
                "ccat": self.kwargs["ccat"],
                "cname": self.kwargs["cname"],
            },
        )


def _get_allowed_targets_by_type(
    collection: Collection,
) -> dict[CollectionRelation.Types, list[int]]:
    T = CollectionRelation.Types

    allowed_targets_by_type: dict[CollectionRelation.Types, list[int]] = {
        T.SUITE_BASED_ON: [],
        T.SUITE_DEFAULT_QA_RESULTS: [],
        T.SUITE_FORKED_FROM: [],
        T.SUITE_REQUIRES: [],
        T.SUITE_TARGETING: [],
    }

    if collection.category != CollectionCategory.SUITE:
        # Currently, all the CollectionRelations have the source collection
        # as category SUITE. Other categories cannot have relations
        # (defined in the CollectionCategory constraints)
        return allowed_targets_by_type

    workspace_visible = CTE(
        Workspace.objects.can_display(context.pc),
        name="workspace_visible",
    )
    workspace_chain = CTE.recursive(
        context.require_workspace().make_inheritance_chain_cte(
            workspace_visible
        ),
        name="chain",
    )

    available_suite_ids = _get_collection_ids_from_workspace_chain(
        collection,
        workspace_visible,
        workspace_chain,
        category=CollectionCategory.SUITE,
        pc=context.pc,
    )

    used_cardinality_1_types = set(
        CollectionRelation.objects.filter(source=collection)
        .exclude(type=T.SUITE_REQUIRES)
        .values_list("type", flat=True)
    )

    if T.SUITE_BASED_ON not in used_cardinality_1_types:
        allowed_targets_by_type[T.SUITE_BASED_ON] = available_suite_ids

    if T.SUITE_FORKED_FROM not in used_cardinality_1_types:
        allowed_targets_by_type[T.SUITE_FORKED_FROM] = available_suite_ids

    if T.SUITE_TARGETING not in used_cardinality_1_types:
        allowed_targets_by_type[T.SUITE_TARGETING] = available_suite_ids

    if T.SUITE_DEFAULT_QA_RESULTS not in used_cardinality_1_types:
        allowed_targets_by_type[T.SUITE_DEFAULT_QA_RESULTS] = (
            _get_collection_ids_from_workspace_chain(
                collection,
                workspace_visible,
                workspace_chain,
                category=CollectionCategory.QA_RESULTS,
                pc=context.pc,
            )
        )

    # SUITE_REQUIRES is the only one with cardinality > 1. Add them
    # and remove already used collections
    used_collection_ids_suite_requires = set(
        CollectionRelation.objects.filter(
            source=collection,
            type=T.SUITE_REQUIRES,
        ).values_list("target_id", flat=True)
    )

    allowed_targets_by_type[T.SUITE_REQUIRES] = [
        target_id
        for target_id in available_suite_ids
        if target_id not in used_collection_ids_suite_requires
    ]

    return allowed_targets_by_type


def _get_collection_ids_from_workspace_chain(
    collection: Collection,
    workspace_visible: CTE,
    workspace_chain: CTE,
    *,
    category: CollectionCategory,
    pc: PermissionContext,
) -> list[int]:
    return list(
        with_cte(
            workspace_visible,
            workspace_chain,
            select=workspace_chain.join(
                Collection.objects.filter(category=category)
                .exclude(id=collection.id)
                .can_display(pc),
                workspace_id=workspace_chain.col.chain_parent,
            ),
        )
        .distinct()
        .values_list("id", flat=True)
    )
