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

"""Package upload workflow."""

from functools import cached_property
from typing import override

from debusine.artifacts.models import ArtifactCategory
from debusine.client.models import LookupChildType
from debusine.db.models import WorkRequest
from debusine.server.collections.lookup import artifact_ids
from debusine.server.tasks.models import PackageUploadData, PackageUploadInput
from debusine.server.tasks.wait.models import ExternalDebsignData
from debusine.server.workflows import (
    Workflow,
    WorkflowValidationError,
    workflow_utils,
)
from debusine.server.workflows.models import (
    PackageUploadWorkflowData,
    PackageUploadWorkflowDynamicData,
    WorkRequestWorkflowData,
)
from debusine.signing.tasks.models import DebsignData
from debusine.tasks.models import (
    InputArtifact,
    InputArtifactMultiple,
    InputArtifactSingle,
    LookupMultiple,
    LookupSingle,
    MakeSourcePackageUploadData,
    MakeSourcePackageUploadInput,
    MergeUploadsData,
    MergeUploadsInput,
)


class PackageUploadWorkflow(
    Workflow[PackageUploadWorkflowData, PackageUploadWorkflowDynamicData]
):
    """Run package uploads for a set of architectures."""

    TASK_NAME = "package_upload"
    _artifacts_pending_upload: list[tuple[LookupSingle, bool]]

    @override
    def post_init(self) -> None:
        super().post_init()
        self._artifacts_pending_upload: list[tuple[LookupSingle, bool]] = []

    @cached_property
    def source_category(self) -> str | None:
        """Category of source artifact or promise."""
        if self.data.source_artifact is None:
            return None

        return workflow_utils.lookup_result_artifact_category(
            self.work_request.lookup_single(
                self.data.source_artifact,
                expect_type=LookupChildType.ARTIFACT_OR_PROMISE,
            )
        )

    @override
    def validate_input(self) -> None:
        """Raise WorkflowValidationError if needed."""
        if (
            self.data.source_artifact is None
            and self.data.binary_artifacts == LookupMultiple(())
        ):
            raise WorkflowValidationError(
                '"source_artifact" or "binary_artifacts" must be set'
            )

        if self.source_category == ArtifactCategory.SOURCE_PACKAGE and (
            self.data.vendor is None or self.data.codename is None
        ):
            # MakeSourcePackageUpload will be used, but self.data.vendor or
            # self.data.codename are not set.
            raise WorkflowValidationError(
                f'"vendor" and "codename" are required when source '
                f'artifact category is {ArtifactCategory.SOURCE_PACKAGE}'
            )

    @override
    def populate(self) -> None:
        """Create package upload work requests."""
        artifacts_pending_upload: list[tuple[LookupSingle, bool]] = []

        if (source_artifact := self.data.source_artifact) is not None:
            since_version = self.data.since_version
            target_distribution = self.data.target_distribution

            if self.source_category == ArtifactCategory.SOURCE_PACKAGE:
                package_upload_source = (
                    self._populate_make_source_package_upload(
                        source_artifact,
                        since_version=since_version,
                        target_distribution=target_distribution,
                    )
                )
                artifacts_pending_upload.append((package_upload_source, False))

            else:
                artifacts_pending_upload.append((source_artifact, False))

        # Upload binary_artifacts
        results = self.work_request.lookup_multiple(
            self.data.binary_artifacts,
            expect_type=LookupChildType.ARTIFACT_OR_PROMISE,
        )

        artifacts_pending_upload += sorted(
            (
                result.reconstruct(
                    workflow_root=self.work_request.workflow_root
                ),
                True,
            )
            for result in results
        )

        self._artifacts_pending_upload = artifacts_pending_upload

        self._populate_package_uploads()

    @override
    def compute_dynamic_data(self) -> PackageUploadWorkflowDynamicData:
        """
        Compute dynamic data for this workflow.

        :subject: source package names (separated by spaces) of
          ``binary_artifacts`` and ``source_artifact``
        :parameter_summary: package names (same as subject) -> target
        :source_artifact_id: id of source_artifact or None
        :binary_artifacts_ids: ids of the binary_artifacts
        """
        binaries = self.work_request.lookup_multiple(
            self.data.binary_artifacts,
            expect_type=LookupChildType.ARTIFACT_OR_PROMISE,
        )
        source_package_names = workflow_utils.get_source_package_names(
            binaries,
            configuration_key="binary_artifacts",
            artifact_expected_categories=(
                ArtifactCategory.BINARY_PACKAGE,
                ArtifactCategory.UPLOAD,
            ),
        )

        source_artifact_id = None
        if self.data.source_artifact is not None:
            source_artifact = self.work_request.lookup_single(
                self.data.source_artifact,
                expect_type=LookupChildType.ARTIFACT_OR_PROMISE,
            )

            if source_artifact.artifact is not None:
                source_artifact_id = source_artifact.artifact.id

            source_artifact_names = workflow_utils.get_source_package_names(
                [source_artifact],
                configuration_key="source_artifact",
                artifact_expected_categories=(
                    ArtifactCategory.SOURCE_PACKAGE,
                    ArtifactCategory.UPLOAD,
                ),
            )
            source_package_names = sorted(
                set(source_package_names) | set(source_artifact_names)
            )

        package_names = " ".join(source_package_names)

        return PackageUploadWorkflowDynamicData(
            subject=package_names,
            parameter_summary=f"{package_names} → {self.data.target}",
            source_artifact_id=source_artifact_id,
            binary_artifacts_ids=artifact_ids(binaries),
        )

    @override
    def get_input_artifacts(self) -> list[InputArtifact]:
        """Return input artifacts."""
        artifacts: list[InputArtifact] = []

        if self.data.source_artifact is not None:
            artifacts.append(
                InputArtifactSingle(
                    lookup=self.data.source_artifact,
                    label="source_artifact",
                    artifact_id=(
                        self.dynamic_data.source_artifact_id
                        if self.dynamic_data is not None
                        else None
                    ),
                )
            )

        artifacts.append(
            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
                ),
            )
        )

        return artifacts

    def _populate_make_source_package_upload(
        self,
        lookup: LookupSingle,
        *,
        since_version: str | None,
        target_distribution: str | None,
    ) -> LookupSingle:
        """
        Create work request for MakeSourcePackageUpload.

        :returns: Lookup of the signed artifact in the collection
        """
        wr, _ = self.work_request_ensure_child_worker(
            task_name="makesourcepackageupload",
            task_data=MakeSourcePackageUploadData(
                input=MakeSourcePackageUploadInput(source_artifact=lookup),
                target_distribution=target_distribution,
                since_version=since_version,
                environment=(
                    f"{self.data.vendor}/match:codename={self.data.codename}"
                ),
                build_architecture=self.data.arch_all_build_architecture,
            ),
            workflow_data=WorkRequestWorkflowData(
                display_name="Make .changes file",
                step="source-package-upload",
            ),
        )
        self.requires_artifact(wr, lookup)

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

    def _populate_signer(
        self,
        signer_identifier: str,
        unsigned_artifact: LookupSingle,
        key: str | None,
        signed_source_artifact: LookupSingle | None,
    ) -> LookupSingle:
        """
        Create a signer: Debsign or ExternalDebsign depending on self.data.

        :param signer_identifier: identifier for the end user of what is going
          to be signed
        :param unsigned_artifact: unsigned artifact for the signer
        :param key: key to be used by the signer. If None, will use
          ExternalDebsign
        :param signed_source_artifact: if not None, signing this artifact
          requires this other signature to have been completed first
        :returns: lookup of the signed artifact
        """
        if key is not None:
            signer, _ = self.work_request_ensure_child_signing(
                task_name="debsign",
                task_data=DebsignData(unsigned=unsigned_artifact, key=key),
                workflow_data=WorkRequestWorkflowData(
                    display_name=f"Sign upload for {signer_identifier}",
                    step=f"debsign-{signer_identifier}",
                ),
            )

        else:
            signer, _ = self.work_request_ensure_child_wait(
                task_name="externaldebsign",
                task_data=ExternalDebsignData(unsigned=unsigned_artifact),
                workflow_data=WorkRequestWorkflowData(
                    display_name=(
                        f"Wait for signature on upload for {signer_identifier}"
                    ),
                    step=f"external-debsign-{signer_identifier}",
                    needs_input=True,
                ),
            )

        self.requires_artifact(signer, unsigned_artifact)
        if signed_source_artifact is not None:
            self.requires_artifact(signer, signed_source_artifact)

        name_signed_artifact = (
            f"package-upload-signed-{unsigned_artifact}".replace(":", "_")
            .replace("@", "_")
            .replace("/", "_")
        )

        self.provides_artifact(
            signer,
            ArtifactCategory.UPLOAD,
            name_signed_artifact,
        )

        return f"internal@collections/name:{name_signed_artifact}"

    def _populate_package_uploads(self) -> None:
        """
        Create work request(s) to upload the artifacts.

        Depending on self.data.merge_uploads will:
        -Create one work request for each self._artifacts_pending_upload
        -Create a single work request of type MergeUpload
        """
        if self.data.merge_uploads:
            wr, _ = self.work_request_ensure_child_worker(
                task_name="mergeuploads",
                task_data=MergeUploadsData(
                    input=MergeUploadsInput(
                        uploads=LookupMultiple.model_validate(
                            [
                                upload
                                for upload, _ in self._artifacts_pending_upload
                            ]
                        )
                    ),
                ),
                workflow_data=WorkRequestWorkflowData(
                    display_name="Merge uploads",
                    step="merge-uploads",
                ),
            )

            any_binaries = False
            for upload, contains_binaries in self._artifacts_pending_upload:
                self.requires_artifact(wr, upload)
                any_binaries = any_binaries or contains_binaries

            merged_artifact_name = (
                f"package-upload-merged-{self.work_request.id}"
            )
            self.provides_artifact(
                wr, ArtifactCategory.UPLOAD, merged_artifact_name
            )

            self._artifacts_pending_upload = [
                (
                    f"internal@collections/name:{merged_artifact_name}",
                    any_binaries,
                )
            ]

        source_upload: LookupSingle | None = None
        source_uploader: WorkRequest | None = None
        for upload, contains_binaries in self._artifacts_pending_upload:
            identifier = str(upload)

            key = self.data.key
            split_binary_signing = False
            if key is None and (
                self.data.binary_key is not None and contains_binaries
            ):
                key = self.data.binary_key
                split_binary_signing = True
            if key is not None or self.data.require_signature:
                upload = self._populate_signer(
                    identifier,
                    upload,
                    key=key,
                    signed_source_artifact=(
                        source_upload if split_binary_signing else None
                    ),
                )
                if not contains_binaries:
                    # There is at most one source artifact to upload.
                    assert source_upload is None
                    source_upload = upload

            uploader, _ = self.work_request_ensure_child_server(
                task_name="packageupload",
                task_data=PackageUploadData(
                    input=PackageUploadInput(upload=upload),
                    target=self.data.target,
                    delayed_days=self.data.delayed_days,
                ),
                workflow_data=WorkRequestWorkflowData(
                    display_name=f"Package upload {identifier}",
                    step=f"package-upload-{identifier}",
                ),
            )
            if not contains_binaries:
                # There is at most one source artifact to upload.
                assert source_uploader is None
                source_uploader = uploader

            self.requires_artifact(uploader, upload)
            if split_binary_signing and source_uploader is not None:
                uploader.add_dependency(source_uploader)
