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

"""Workflow to coordinate metadata updates for all archives in a workspace."""

import logging
from typing import override

from django.db.models import Exists, F, Max, Q, QuerySet
from django.db.models.sql.constants import LOUTER
from django_cte import CTE, with_cte

from debusine.artifacts.models import (
    ArtifactCategory,
    CollectionCategory,
    TaskTypes,
)
from debusine.assets import KeyPurpose
from debusine.assets.models import AssetCategory, SigningKeyData
from debusine.db.models import (
    AssetUsage,
    Collection,
    CollectionItem,
    WorkRequest,
    Workspace,
)
from debusine.db.models.permissions import ResourceGrant
from debusine.server.workflows import Workflow
from debusine.server.workflows.models import (
    UpdateSuiteData,
    UpdateSuitesData,
    WorkRequestWorkflowData,
)
from debusine.signing.tasks.models import GenerateKeyData

logger = logging.getLogger(__name__)


class UpdateSuitesWorkflow(Workflow[UpdateSuitesData]):
    """Coordinate metadata updates for all suites in the workspace."""

    TASK_NAME = "update_suites"

    @override
    def get_extra_resource_grants(self) -> set[ResourceGrant]:
        """
        Grant the workspace owner role while running this workflow.

        This workflow is essentially an implementation detail of publishing
        something to a suite in a workspace, which is a workspace
        contributor operation, but it requires using the archive's signing
        keys which are typically only usable by workspace owners.

        It has very limited direct configuration, and while it picks up some
        configuration from collections, those collections can themselves
        only be configured by workspace owners, so it shouldn't be possible
        to confuse it into doing anything else with the extra role.
        """
        return {ResourceGrant(self.workspace, Workspace.Roles.OWNER)}

    def get_archive(self) -> Collection:
        """
        Return the workspace's archive.

        This deliberately doesn't follow the workspace's inheritance chain.
        """
        return self.workspace.collections.get(
            category=CollectionCategory.ARCHIVE, name="_"
        )

    def _find_stale_suites(self) -> QuerySet[Collection]:
        """
        Return all stale suites in this workspace.

        For this purpose, a suite is "stale" if any source or binary
        packages in it have been changed (collection items created or
        removed) since its most recent ``Release`` file was generated.
        """
        suites: QuerySet[Collection]
        if self.data.force_basic_indexes:
            suites = Collection.objects.all()
        else:
            # Use a couple of CTEs to ensure that PostgreSQL uses a
            # reasonable index to find index:Release items, and that it only
            # does the work of figuring out the latest Release creation time
            # once.

            # Build a virtual table of previously-generated Release indexes.
            release_item = CTE(
                CollectionItem.objects.filter(
                    parent_category=CollectionCategory.SUITE,
                    child_type=CollectionItem.Types.ARTIFACT,
                    category=ArtifactCategory.REPOSITORY_INDEX,
                    name="index:Release",
                ),
                name="release_item",
            )
            # Annotate collections with the most recent Release index
            # creation time in each one.
            collection_generated = CTE(
                release_item.join(
                    Collection,
                    id=release_item.col.parent_collection_id,
                    _join_type=LOUTER,
                )
                .values("id")
                .annotate(latest_created=Max(release_item.col.created_at)),
                name="collection_generated",
            )
            # Find suite names where collection items have been created or
            # removed more recently than their most recent Release index.
            suites = with_cte(
                release_item,
                collection_generated,
                select=(
                    collection_generated.join(
                        Collection, id=collection_generated.col.id
                    )
                    .annotate(
                        changed=Exists(
                            CollectionItem.objects.filter(
                                parent_collection=collection_generated.col.id
                            )
                            .annotate(
                                latest_created=(
                                    collection_generated.col.latest_created
                                )
                            )
                            .filter(
                                Q(latest_created__isnull=True)
                                | Q(created_at__gt=F("latest_created"))
                                | Q(removed_at__gt=F("latest_created"))
                            )
                        ),
                    )
                    .filter(changed=True)
                ),
            )

        if self.data.only_suites is not None:
            suites = suites.filter(name__in=self.data.only_suites)

        return suites.filter(
            workspace=self.workspace,
            id__in=Collection.objects.exported_suites(),
        ).order_by("name")

    def _populate_generate_key(self) -> WorkRequest:
        generate_key, _ = self.work_request_ensure_child_signing(
            task_name="generatekey",
            task_data=GenerateKeyData(
                purpose=KeyPurpose.OPENPGP_REPOSITORY,
                description=f"Archive signing key for {self.workspace}",
            ),
            workflow_data=WorkRequestWorkflowData(
                display_name=f"Generate signing key for {self.workspace}",
                step="generate-signing-key",
            ),
        )
        generated_key, _ = self.work_request_ensure_child_internal(
            task_name="workflow",
            workflow_data=WorkRequestWorkflowData(
                display_name="Record generated key", step="generated-key"
            ),
        )
        generated_key.add_dependency(generate_key)
        self.orchestrate_child(generated_key)
        return generated_key

    def _populate_main(self, generated_key: WorkRequest | None) -> None:
        for suite in self._find_stale_suites():
            update_suite, _ = self.work_request_ensure_child_workflow(
                task_name="update_suite",
                task_data=UpdateSuiteData(suite_collection=suite.id),
                workflow_data=WorkRequestWorkflowData(
                    display_name=f"Update {suite.name}",
                    step=f"update-{suite.name}",
                ),
            )
            if generated_key is not None:
                update_suite.add_dependency(generated_key)

        for update_suite in self.work_request.children.filter(
            task_type=TaskTypes.WORKFLOW,
            task_name="update_suite",
            status__in=(
                WorkRequest.Statuses.BLOCKED,
                WorkRequest.Statuses.PENDING,
            ),
        ):
            self.orchestrate_child(update_suite)

    @override
    def populate(self) -> None:
        """Create child work requests for all stale suites."""
        try:
            archive = self.get_archive()
        except Collection.DoesNotExist:
            logger.info(
                "Workspace %s has no archive; not updating suites.",
                self.workspace,
            )
            return

        generated_key: WorkRequest | None = None
        if "signing_keys" not in archive.data:
            generated_key = self._populate_generate_key()
        self._populate_main(generated_key)

    def callback_generated_key(self) -> bool:
        """Configure the generated key."""
        # Configure the archive to use the new key.
        archive = self.get_archive()
        assert "signing_keys" not in archive.data
        signing_key = self.work_request.children.get(
            task_type=TaskTypes.SIGNING, task_name="generatekey"
        ).asset_set.get(category=AssetCategory.SIGNING_KEY)
        AssetUsage.objects.get_or_create(
            asset=signing_key, workspace=self.workspace
        )
        signing_key_data = signing_key.data_model
        assert isinstance(signing_key_data, SigningKeyData)
        archive.data["signing_keys"] = [signing_key_data.fingerprint]
        archive.save()
        return True
