# 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 django.db.models import Exists, F, Max, Q, QuerySet
from django.db.models.sql.constants import LOUTER
from django_cte import CTEQuerySet, With

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.server.workflows import Workflow, WorkflowValidationError
from debusine.server.workflows.models import (
    UpdateSuiteData,
    UpdateSuitesData,
    WorkRequestWorkflowData,
)
from debusine.signing.tasks.models import GenerateKeyData
from debusine.tasks import DefaultDynamicData
from debusine.tasks.models import BaseDynamicTaskData

logger = logging.getLogger(__name__)


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

    TASK_NAME = "update_suites"

    def validate_input(self) -> None:
        """Thorough validation of input data."""
        # Only a workspace owner may run this workflow, since that implies
        # the SIGNER role on newly-created signing keys.
        if not self.workspace.has_role(
            self.work_request.created_by, Workspace.Roles.OWNER
        ):
            raise WorkflowValidationError(
                'Only a workspace owner may run the "update_suites" workflow'
            )

    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.
            collection_queryset = CTEQuerySet(
                Collection, using=Collection.objects._db
            )
            # Build a virtual table of previously-generated Release indexes.
            release_item = With(
                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 = With(
                release_item.join(
                    collection_queryset,
                    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 = (
                collection_generated.join(
                    collection_queryset, id=collection_generated.col.id
                )
                .with_cte(release_item)
                .with_cte(collection_generated)
                .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)

    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
