# 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 functools import cached_property
from typing import override

from debusine.artifacts.models import ArtifactCategory
from debusine.assets import KeyPurpose
from debusine.client.models import LookupChildType
from debusine.db.models import WorkRequest
from debusine.server.collections.lookup import artifact_ids
from debusine.server.workflows import Workflow, workflow_utils
from debusine.server.workflows.models import (
    MakeSignedSourceWorkflowData,
    MakeSignedSourceWorkflowDynamicData,
    SbuildWorkflowData,
    WorkRequestWorkflowData,
)
from debusine.signing.tasks.models import SignData
from debusine.tasks.models import (
    ActionUpdateCollectionWithArtifacts,
    AssembleSignedSourceData,
    BackendType,
    ExtractForSigningData,
    ExtractForSigningInput,
    InputArtifact,
    InputArtifactMultiple,
    LookupDict,
    LookupMultiple,
    LookupSingle,
    SbuildInput,
)


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

    TASK_NAME = "make_signed_source"

    @override
    def compute_dynamic_data(self) -> MakeSignedSourceWorkflowDynamicData:
        """
        Compute dynamic data.

        :subject: source package names separated by spaces
        :parameter_summary: source package names separated by spaces followed
          by "for ``vendor``:``codename``"
        :binary_artifacts_ids: ids of the binary_artifacts
        :signing_template_artifacts_ids: ids of the signing_template_artifacts
        """
        binaries = self.work_request.lookup_multiple(
            self.data.binary_artifacts,
            expect_type=LookupChildType.ARTIFACT_OR_PROMISE,
        )

        source_package_names = " ".join(
            workflow_utils.get_source_package_names(
                binaries,
                configuration_key="binary_artifacts",
                artifact_expected_categories=(
                    ArtifactCategory.BINARY_PACKAGE,
                    ArtifactCategory.UPLOAD,
                ),
            )
        )

        signing_templates = self.work_request.lookup_multiple(
            self.data.signing_template_artifacts,
            expect_type=LookupChildType.ARTIFACT_OR_PROMISE,
        )

        return MakeSignedSourceWorkflowDynamicData(
            subject=source_package_names,
            parameter_summary=f"{source_package_names} for "
            f"{self.data.vendor}:{self.data.codename}",
            binary_artifacts_ids=artifact_ids(binaries),
            signing_template_artifacts_ids=artifact_ids(signing_templates),
        )

    @override
    def get_input_artifacts(self) -> list[InputArtifact]:
        """Return input artifacts."""
        return [
            InputArtifactMultiple.from_ids(
                lookup=self.data.binary_artifacts,
                label="binary_artifacts",
                artifact_ids=(
                    self.dynamic_data.binary_artifacts_ids
                    if self.dynamic_data is not None
                    else None
                ),
            ),
            InputArtifactMultiple.from_ids(
                lookup=self.data.signing_template_artifacts,
                label="signing_template_artifacts",
                artifact_ids=(
                    self.dynamic_data.signing_template_artifacts_ids
                    if self.dynamic_data is not None
                    else None
                ),
            ),
        ]

    @cached_property
    def architectures(self) -> set[str]:
        """Architectures to run on."""
        return (
            workflow_utils.get_lookup_architectures(
                self, self.data.signing_template_artifacts
            )
            .intersection(
                workflow_utils.get_lookup_architectures(
                    self, self.data.binary_artifacts
                )
            )
            .intersection(self.data.architectures)
        )

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

        signing_templates = self.work_request.lookup_multiple(
            self.data.signing_template_artifacts,
            expect_type=LookupChildType.ARTIFACT_OR_PROMISE,
        )

        for arch in sorted(self.architectures):
            signing_template_lookups_by_name = {
                workflow_utils.lookup_result_binary_package_name(
                    result
                ): result.reconstruct(workflow_root=workflow_root)
                for result in signing_templates
                if workflow_utils.lookup_result_architectures(result) == {arch}
            }

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

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

                extract_for_signing_wr, extracted_lookup = (
                    self._populate_extract_for_signing(
                        template_artifact=signing_template_lookup,
                        binary_artifacts=binary_artifacts_lookups,
                        environment=environment,
                        architecture=arch,
                        template_name=signing_template_name,
                    )
                )

                sign_wr, signed = self._populate_sign(
                    purpose=self.data.purpose,
                    unsigned=extracted_lookup,
                    key=self.data.key,
                    extract_for_signing_wr=extract_for_signing_wr,
                    architecture=arch,
                    template_name=signing_template_name,
                )

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

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

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

    def _populate_extract_for_signing(
        self,
        *,
        template_artifact: LookupSingle,
        binary_artifacts: LookupMultiple,
        environment: str,
        architecture: str,
        template_name: 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_worker(
            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 ({template_name}/{architecture})"
                ),
                step=f"extract-for-signing-{architecture}-{template_name}",
            ),
        )
        self.requires_artifact(wr, template_artifact)
        self.requires_artifact(wr, binary_artifacts)

        wr.add_event_reaction(
            "on_success",
            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},
            ),
        )
        lookup = LookupMultiple.model_validate(
            {
                "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: str,
        extract_for_signing_wr: WorkRequest,
        architecture: str,
        template_name: str,
    ) -> tuple[WorkRequest, LookupMultiple]:
        """
        Create work request for Sign.

        :returns: A tuple of the Sign work request and a lookup of the
          signed artifacts in the collection.
        """
        wr, _ = self.work_request_ensure_child_signing(
            task_name="sign",
            task_data=SignData(purpose=purpose, unsigned=unsigned, key=key),
            workflow_data=WorkRequestWorkflowData(
                display_name=f"Sign ({template_name}/{architecture})",
                step=f"sign-{architecture}-{template_name}",
            ),
        )

        wr.add_dependency(extract_for_signing_wr)

        wr.add_event_reaction(
            "on_success",
            ActionUpdateCollectionWithArtifacts(
                collection="internal@collections",
                name_template=(
                    "signed-{architecture}-{template_name}-"
                    "{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,
                    "template_name": template_name,
                },
                artifact_filters={"category": ArtifactCategory.SIGNING_OUTPUT},
            ),
        )
        lookup = LookupMultiple.model_validate(
            {
                "collection": "internal@collections",
                "child_type": LookupChildType.ARTIFACT,
                "category": ArtifactCategory.SIGNING_OUTPUT,
                "name__startswith": f"signed-{architecture}-{template_name}-",
                "data__architecture": architecture,
            }
        )
        return wr, lookup

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

        :returns: Lookup of the assembled signed artifact
        """
        wr, _ = self.work_request_ensure_child_worker(
            task_name="assemblesignedsource",
            task_data=AssembleSignedSourceData(
                environment=environment, template=template, signed=signed
            ),
            workflow_data=WorkRequestWorkflowData(
                display_name=(
                    f"Assemble signed source ({template_name}/{architecture})"
                ),
                step=f"assemble-signed-source-{architecture}-{template_name}",
            ),
        )

        wr.add_dependency(sign_wr)

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

    def _populate_sbuild_workflow(
        self,
        assembled: LookupSingle,
        extra_binary_artifacts: LookupMultiple,
        target_distribution: str,
        backend: BackendType,
        architectures: list[str],
        architecture: str,
        template_name: str,
    ) -> None:
        """Populate SbuildWorkflow."""
        wr, _ = self.work_request_ensure_child_workflow(
            task_name="sbuild",
            task_data=SbuildWorkflowData(
                prefix=f"signed-source-{architecture}-{template_name}|",
                input=SbuildInput(
                    source_artifact=assembled,
                    extra_binary_artifacts=extra_binary_artifacts,
                ),
                target_distribution=target_distribution,
                backend=backend,
                architectures=architectures,
            ),
            workflow_data=WorkRequestWorkflowData(
                display_name=f"Sbuild ({template_name}/{architecture})",
                step=f"sbuild-{architecture}-{template_name}",
            ),
        )
        self.requires_artifact(wr, assembled)
        self.orchestrate_child(wr)
