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

"""Make signed source workflow."""

from debusine.artifacts.models import ArtifactCategory, KeyPurpose
from debusine.client.models import LookupChildType
from debusine.db.models import WorkRequest
from debusine.server.workflows import Workflow
from debusine.server.workflows.models import (
    MakeSignedSourceWorkflowData,
    SbuildWorkflowData,
    WorkRequestWorkflowData,
)
from debusine.signing.tasks.models import SignData
from debusine.tasks.models import (
    ActionUpdateCollectionWithArtifacts,
    AssembleSignedSourceData,
    BackendType,
    BaseDynamicTaskData,
    ExtractForSigningData,
    ExtractForSigningInput,
    LookupDict,
    LookupMultiple,
    LookupSingle,
    SbuildInput,
    TaskTypes,
)


class MakeSignedSourceWorkflow(
    Workflow[MakeSignedSourceWorkflowData, BaseDynamicTaskData]
):
    """Make signed sources."""

    TASK_NAME = "make_signed_source"

    def __init__(self, work_request: WorkRequest):
        """Instantiate a Workflow with its database instance."""
        super().__init__(work_request)

    def populate(self) -> None:
        """Create work requests and sub-workflows."""
        environment = f"{self.data.vendor}/match:codename={self.data.codename}"

        architectures = self.get_architectures(
            self.data.signing_template_artifacts
        ).intersection(self.get_architectures(self.data.binary_artifacts))

        architectures = architectures.intersection(self.data.architectures)

        for arch in architectures:
            signing_templates_lookup = self.filter_artifact_lookup_by_arch(
                self.data.signing_template_artifacts, (arch,)
            )

            binary_artifacts_lookups = self.filter_artifact_lookup_by_arch(
                self.data.binary_artifacts,
                (arch, "all"),
            )

            for signing_template_lookup in signing_templates_lookup:
                # filter_artifact_lookup_by_arch always returns a parsed list of
                # single lookups.
                assert not isinstance(signing_template_lookup, LookupDict)

                extracted_work_request, extracted_lookup = (
                    self._populate_extract_for_signing(
                        template_artifact=signing_template_lookup,
                        binary_artifacts=binary_artifacts_lookups,
                        environment=environment,
                        architecture=arch,
                    )
                )

                signed = self._populate_sign(
                    purpose=self.data.purpose,
                    unsigned=extracted_lookup,
                    key=self.data.key,
                    extracted=extracted_work_request,
                    architecture=arch,
                )

                assembled = self._populate_assemble_signed_source(
                    environment=environment,
                    template=signing_template_lookup,
                    signed=signed,
                    architecture=arch,
                )

                target_distribution = f"{self.data.vendor}:{self.data.codename}"

                sbuild_architectures = sorted(architectures | {"all"})
                self._populate_sbuild_workflow(
                    assembled=assembled,
                    target_distribution=target_distribution,
                    backend=self.data.sbuild_backend,
                    architectures=sbuild_architectures,
                    architecture=arch,
                )

    def _populate_extract_for_signing(
        self,
        *,
        template_artifact: LookupSingle,
        binary_artifacts: LookupMultiple,
        environment: str,
        architecture: str,
    ) -> tuple[WorkRequest, LookupMultiple]:
        """
        Create work request for ExtractForSigning.

        :returns: A tuple of the ExtractForSigning work request and a lookup
          of the extracted artifacts in the collection.
        """
        wr = self.work_request_ensure_child(
            task_name="extractforsigning",
            task_data=ExtractForSigningData(
                environment=environment,
                input=ExtractForSigningInput(
                    template_artifact=template_artifact,
                    binary_artifacts=binary_artifacts,
                ),
            ),
            workflow_data=WorkRequestWorkflowData(
                display_name=f"Extract for signing {architecture}",
                step=f"extract-for-signing-{architecture}",
            ),
        )
        self.requires_artifact(wr, template_artifact)
        self.requires_artifact(wr, binary_artifacts)

        event_reactions = wr.event_reactions
        event_reactions.on_success.append(
            ActionUpdateCollectionWithArtifacts(
                collection="internal@collections",
                name_template=(
                    "extracted-for-signing-{architecture}-{binary_package_name}"
                ),
                variables={
                    # Extract the binary package name from the artifact
                    # data, since the workflow doesn't know it up-front.
                    "$binary_package_name": "binary_package_name",
                    "architecture": architecture,
                },
                artifact_filters={"category": ArtifactCategory.SIGNING_INPUT},
            )
        )
        wr.event_reactions = event_reactions
        wr.save()
        lookup = LookupMultiple.parse_obj(
            {
                "collection": "internal@collections",
                "child_type": LookupChildType.ARTIFACT,
                "category": ArtifactCategory.SIGNING_INPUT,
                "name__startswith": "extracted-for-signing-",
                "data__architecture": architecture,
            }
        )
        return wr, lookup

    def _populate_sign(
        self,
        *,
        purpose: KeyPurpose,
        unsigned: LookupMultiple,
        key: LookupSingle,
        extracted: WorkRequest,
        architecture: str,
    ) -> LookupSingle:
        """
        Create work request for Signing.

        :returns: Lookup of the signed artifact in the collection.
        """
        wr = self.work_request_ensure_child(
            task_type=TaskTypes.SIGNING,
            task_name="sign",
            task_data=SignData(purpose=purpose, unsigned=unsigned, key=key),
            workflow_data=WorkRequestWorkflowData(
                display_name=f"Sign {architecture}",
                step=f"sign-{architecture}",
            ),
        )

        wr.add_dependency(extracted)

        artifact_name = f"signed-{architecture}"
        self.provides_artifact(
            wr, ArtifactCategory.SIGNING_OUTPUT, artifact_name
        )
        return f"internal@collections/name:{artifact_name}"

    def _populate_assemble_signed_source(
        self,
        *,
        environment: str,
        template: LookupSingle,
        signed: LookupSingle,
        architecture: str,
    ) -> LookupSingle:
        """
        Create work request for assembling the signed source.

        :returns: Lookup of the assembled signed artifact
        """
        wr = self.work_request_ensure_child(
            task_name="assemblesignedsource",
            task_data=AssembleSignedSourceData(
                environment=environment,
                template=template,
                signed=LookupMultiple.parse_obj([signed]),
            ),
            workflow_data=WorkRequestWorkflowData(
                display_name=f"Assemble signed source {architecture}",
                step=f"assemble-signed-source-{architecture}",
            ),
        )

        self.requires_artifact(wr, signed)

        artifact_name = f"signed-source-{architecture}"
        self.provides_artifact(
            wr, ArtifactCategory.SOURCE_PACKAGE, artifact_name
        )
        return f"internal@collections/name:{artifact_name}"

    def _populate_sbuild_workflow(
        self,
        assembled: LookupSingle,
        target_distribution: str,
        backend: BackendType,
        architectures: list[str],
        architecture: str,
    ) -> None:
        """Populate SbuildWorkflow."""
        wr = self.work_request_ensure_child(
            task_type=TaskTypes.WORKFLOW,
            task_name="sbuild",
            task_data=SbuildWorkflowData(
                prefix="signed-source/",
                input=SbuildInput(source_artifact=assembled),
                target_distribution=target_distribution,
                backend=backend,
                architectures=architectures,
            ),
            workflow_data=WorkRequestWorkflowData(
                display_name=f"Sbuild {architecture}",
                step=f"sbuild-{architecture}",
            ),
        )

        self.requires_artifact(wr, assembled)

    def get_label(self) -> str:
        """Return the task label."""
        return "run sign source"
