# 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 list view paginators."""

from collections import defaultdict
from typing import Any, override

from django.db.models import Model, Q, QuerySet
from django_cte import CTE, with_cte

from debusine.artifacts.models import TaskTypes
from debusine.db.models import (
    Artifact,
    CollectionItem,
    User,
    WorkRequest,
    Worker,
    WorkerPool,
    WorkflowTemplate,
)
from debusine.db.models.auth import GroupAuditLog, GroupMembership
from debusine.db.models.work_requests import WorkRequestQuerySet
from debusine.web.forms import WorkflowFilterForm
from debusine.web.views.table import (
    Column,
    DateTimeColumn,
    FilterField,
    FilterSelectOne,
    FilterToggle,
    NumberColumn,
    Ordering,
    Paginator,
    StringColumn,
    Table,
)


class GroupMembershipTable(Table[GroupMembership]):
    """Table set up for GroupMembership."""

    name = StringColumn(
        "Name",
        ordering=Ordering(
            asc=["user__username", "role"], desc=["-user__username", "role"]
        ),
    )
    display_name = StringColumn("Display name")
    role = StringColumn(
        "Role",
        ordering=Ordering(
            asc=["role", "user__username"], desc=["-role", "user__username"]
        ),
    )

    template_name = "web/_group_membership-list.html"
    default_order = "role"
    related_fields = ("user",)


class GroupMembershipAdminTable(GroupMembershipTable):
    """GroupMembership table with an Actions column."""

    actions = Column("Actions")


class GroupAuditLogTable(Table[GroupAuditLog]):
    """Table set up for GroupAuditLog."""

    date = DateTimeColumn("Date", ordering="created_at")
    actor = StringColumn(
        "User",
        ordering=Ordering(
            asc=["actor__username", "-created_at"],
            desc=["-actor__username", "-created_at"],
        ),
    )
    message = StringColumn("Message")

    template_name = "web/_group_audit_log-list.html"
    default_order = "-date"


class WorkerTable(Table[Worker]):
    """Table set up for Worker."""

    type_ = StringColumn("Type", ordering="worker_type", name="type")
    name = StringColumn("Name", ordering="name")
    pool = StringColumn("Pool", ordering="worker_pool")
    last_seen = DateTimeColumn("Last seen", ordering="token__last_seen_at")
    status = StringColumn("Status")

    filter_name = FilterField("Name")
    filter_pool = FilterSelectOne("Worker pool")
    filter_status = FilterSelectOne("Status")

    template_name = "web/_worker-list.html"
    default_order = "name"

    @override
    def init(self) -> None:
        """Add filter options based on database contents."""
        super().init()

        # Add options to worker pool filter
        self.filters["pool"].add_option(
            "_none", "Static workers", Q(worker_pool__isnull=True)
        )
        for pk, name in (
            WorkerPool.objects.filter(worker__in=self.queryset)
            .order_by("name")
            .distinct()
            .values_list("id", "name")
        ):
            self.filters["pool"].add_option(
                name=str(pk), label=name, query=Q(worker_pool__id=pk)
            )

        # Add options to status filter
        connected = Worker.objects.connected()
        self.filters["status"].add_option(
            "disabled",
            "Disabled",
            Q(token__isnull=True) | Q(token__enabled=False),
        )
        self.filters["status"].add_option(
            "disconnected",
            "Disconnected",
            Q(token__enabled=True) & ~Q(pk__in=connected),
        )
        self.filters["status"].add_option(
            "running",
            "Running",
            Q(
                token__enabled=True,
                pk__in=connected,
                assigned_work_requests__status=(WorkRequest.Statuses.RUNNING),
            ),
        )
        self.filters["status"].add_option(
            "idle",
            "Idle",
            Q(token__enabled=True, pk__in=connected)
            & ~Q(
                assigned_work_requests__status=(WorkRequest.Statuses.RUNNING),
            ),
        )


class WorkerPoolTable(Table[WorkerPool]):
    """Table set up for WorkerPool."""

    name = StringColumn("Name", ordering="name")
    enabled = StringColumn("Enabled", ordering="enabled")
    instance_wide = StringColumn("Instance-wide", ordering="instance_wide")
    ephemeral = StringColumn("Ephemeral", ordering="ephemeral")
    workers = StringColumn("Workers", ordering="workers")

    template_name = "web/_worker_pool-list.html"
    default_order = "name"


class WorkRequestTable(Table[WorkRequest]):
    """Table set up for WorkRequest."""

    id_ = NumberColumn("ID", ordering="pk", name="id")
    created_at = DateTimeColumn("Created", ordering="created_at")
    task_type = StringColumn("Task type", ordering="task_type")
    task_name = StringColumn("Task", ordering="task_name")
    status = StringColumn("Status", ordering="status")
    result = StringColumn("Result", ordering="result")

    template_name = "web/_work_request-list.html"
    default_order = "-created_at"
    related_fields = (
        "workspace__scope",
        "aborted_by",
    )


class FilterStatuses(FilterField):
    """Custom queryset filtering for workflow statuses."""

    @override
    def filter_queryset[M: Model](
        self, queryset: QuerySet[M, M], value: Any
    ) -> QuerySet[M, M]:
        assert isinstance(value, list)
        q_objects = Q()
        for status in value:
            if status == "running__any":
                q_objects |= Q(status=WorkRequest.Statuses.RUNNING)
            elif status.startswith("running__"):
                q_objects |= Q(
                    status=WorkRequest.Statuses.RUNNING,
                    workflow_runtime_status=status[9:],
                )
            else:
                q_objects |= Q(status=status)
        return queryset.filter(q_objects)


class FilterFailedWorkRequests(FilterToggle):
    """Keep only workflows with failed work requests."""

    def __init__(self, label: str) -> None:
        """Pass an empty Q, since we ignore it for filtering."""
        super().__init__(label, q=Q())

    @override
    def filter_queryset[M: Model](
        self, queryset: QuerySet[M, M], value: Any
    ) -> QuerySet[M, M]:
        if not value:
            return queryset
        assert isinstance(queryset, WorkRequestQuerySet)

        # Since we're comparing id to workflow_root_id (which is much more
        # efficient than having to compute the ancestor tree on the fly),
        # this can only be used to filter top-level workflows.
        assert not queryset.filter(parent__isnull=False).exists()

        workflows = CTE(queryset, name="workflows")
        filtered = with_cte(
            workflows,
            select=workflows.join(WorkRequest, id=workflows.col.id).filter(
                id__in=(
                    workflows.join(
                        WorkRequest, workflow_root_id=workflows.col.id
                    )
                    .exclude(task_type=TaskTypes.WORKFLOW)
                    .exclude(
                        workflow_data_json__contains={"allow_failure": True}
                    )
                    .visible_in_workflow()
                    .filter(result=WorkRequest.Results.FAILURE)
                    .values("workflow_root_id")
                    .order_by("workflow_root_id")
                    .distinct("workflow_root_id")
                )
            ),
        )
        assert isinstance(filtered, QuerySet)
        return filtered


class WorkflowPaginator(Paginator[WorkRequest]):
    """Paginator for workflow WorkRequests."""

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

        object_list = self.page_obj.object_list
        assert isinstance(object_list, WorkRequestQuerySet)
        workflows_flattened: dict[int, list[WorkRequest]] = defaultdict(list)
        work_request_status_counts: dict[
            int, dict[WorkRequest.Statuses, int]
        ] = defaultdict(lambda: defaultdict(int))
        work_request_result_counts: dict[
            int, dict[WorkRequest.Results, int]
        ] = defaultdict(lambda: defaultdict(int))
        if object_list.exists():
            for work_request in object_list.workflow_flattened():
                workflows_flattened[work_request.ancestor_id].append(
                    work_request
                )
                work_request_status_counts[work_request.ancestor_id][
                    work_request.status
                ] += 1
                if work_request.result:
                    # Work requests that fail but are allowed
                    # to fail intentionally disappear from
                    # the result counts. Other approaches
                    # (like counting as success, counting as
                    # failure with tricky UI coloring, or
                    # adding new badge types) added too much
                    # confusion or UI noise.
                    if (
                        work_request.result == WorkRequest.Results.FAILURE
                        and getattr(
                            work_request.workflow_data, "allow_failure", False
                        )
                    ):
                        continue
                    work_request_result_counts[work_request.ancestor_id][
                        work_request.result
                    ] += 1
        ctx["workflows_flattened"] = workflows_flattened
        ctx["work_request_status_counts"] = work_request_status_counts
        ctx["work_request_result_counts"] = work_request_result_counts

        return ctx


class WorkflowTable(Table[WorkRequest]):
    """Table set up for workflow WorkRequests."""

    id_ = NumberColumn("ID", ordering="pk", name="id")
    workflow_template = StringColumn("Template", ordering="task_name")
    status = StringColumn(
        "Status", ordering=("status", "workflow_runtime_status")
    )
    result = StringColumn("Result", ordering="result")
    started_at = DateTimeColumn("Started", ordering="started_at")
    completed_at = DateTimeColumn("Completed", ordering="completed_at")
    wr_count = Column("Count of work requests")
    last_activity = DateTimeColumn(
        "Last activity",
        ordering="workflow_last_activity_at",
        help_text="Latest activity recorded for workflow’s work request",
    )
    started_by = StringColumn("Started by", ordering="created_by__username")

    filter_statuses = FilterStatuses()
    filter_results = FilterField(q_field="result__in")
    filter_workflow_templates = FilterSelectOne("Workflow template")
    filter_started_by = FilterSelectOne("Started by")
    filter_with_failed_work_requests = FilterFailedWorkRequests(
        "With failed work requests"
    )

    template_name = "web/_workflow-list.html"
    default_order = "id.desc"
    filter_form_class = WorkflowFilterForm
    related_fields = ("workspace__scope", "created_by")

    @override
    def init_filters(self) -> None:
        """Add filter options based on database contents."""
        super().init_filters()
        for wt in (
            WorkflowTemplate.objects.filter(
                pk__in=(
                    self.queryset.values("workflow_template")
                    .order_by("workflow_template")
                    .distinct("workflow_template")
                )
            )
            .order_by("name")
            .distinct()
        ):
            self.filters["workflow_templates"].add_option(
                wt.name, wt.name, query=Q(workflow_template=wt)
            )

        for user in (
            User.objects.filter(
                pk__in=(
                    self.queryset.values("created_by")
                    .order_by("created_by")
                    .distinct("created_by")
                )
            )
            .order_by("first_name", "last_name")
            .distinct()
        ):
            self.filters["started_by"].add_option(
                user.username, user.get_full_name(), query=Q(created_by=user)
            )

    @override
    def get_paginator(
        self,
        per_page: int,
        *,
        orphans: int = 0,
        allow_empty_first_page: bool = True,
    ) -> WorkflowPaginator:
        return WorkflowPaginator(
            self,
            per_page=per_page,
            orphans=orphans,
            allow_empty_first_page=allow_empty_first_page,
        )


class CollectionItemTable(Table[CollectionItem]):
    """Table set up for CollectionItem."""

    category = StringColumn("Category", ordering="category")
    name = StringColumn("Name", ordering="name")
    created_at = DateTimeColumn("Created at", ordering="created_at")
    details = Column("Details")

    template_name = "web/_collection_item-list.html"
    default_order = "category"


class ArtifactTable(Table[Artifact]):
    """Table set up for Artifact."""

    category = StringColumn(
        "Type",
        ordering=Ordering(asc=["category", "id"], desc=["-category", "id"]),
    )
    name = StringColumn("Name")
    links = Column("")

    template_name = "web/_artifact-list.html"
    default_order = "category"
