# 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 update environments."""

import copy
from dataclasses import dataclass
from typing import Any, cast, override

from debusine.artifacts.models import ArtifactCategory, CollectionCategory
from debusine.server.workflows.base import Workflow
from debusine.server.workflows.models import (
    UpdateEnvironmentsWorkflowData,
    WorkRequestWorkflowData,
)
from debusine.tasks.models import (
    ActionUpdateCollectionWithArtifacts,
    BaseDynamicTaskData,
    BaseTaskData,
    MmDebstrapData,
    SystemImageBuildData,
)


@dataclass(frozen=True)
class ActionVariables:
    """Variables to pass to an update-collection-with-artifacts action."""

    codename: str
    variant: str | None
    backend: str | None

    def to_dict(self) -> dict[str, str]:
        """Make a dict for an update-collection-with-artifacts action."""
        variables = {"codename": self.codename}
        if self.variant is not None:
            variables["variant"] = self.variant
        if self.backend is not None:
            variables["backend"] = self.backend
        return variables


@dataclass(frozen=True)
class PlannedEnvironment:
    """Relevant data to build environments with a specific task."""

    task_data_template: dict[str, Any]
    architecture: str
    codenames: list[str]
    backends: list[str | None]
    variants: list[str | None]


class UpdateEnvironmentsWorkflow(
    Workflow[UpdateEnvironmentsWorkflowData, BaseDynamicTaskData]
):
    """Build tarballs/images and add them to an environments collection."""

    TASK_NAME = "update_environments"

    def _make_action_variables(
        self,
        *,
        codenames: list[str],
        variants: list[str | None],
        backends: list[str | None],
    ) -> list[ActionVariables]:
        """
        Return collection update action variables for a child work request.

        Each item in the returned list should be used to build an event
        reaction that adds artifacts to a collection.
        """
        action_variables: list[ActionVariables] = []
        variants_or_none: list[str | None] = list(variants)
        if not variants_or_none:
            variants_or_none.append(None)
        backends_or_none: list[str | None] = list(backends)
        if not backends_or_none:
            backends_or_none.append(None)
        for codename in codenames:
            for variant in variants_or_none:
                for backend in backends_or_none:
                    action_variables += [
                        ActionVariables(
                            codename=codename, variant=variant, backend=backend
                        )
                    ]
        return action_variables

    def _add_child(
        self,
        task_name: str,
        task_data_class: type[BaseTaskData],
        template: dict[str, Any],
        *,
        category: ArtifactCategory,
        display_name_prefix: str,
        vendor: str,
        codenames: list[str],
        architecture: str,
        variants: list[str | None],
        backends: list[str | None],
    ) -> None:
        """Create a child work request."""
        # Prepare variables and compute string representations
        codename = codenames[0]
        backend_string = (
            "|".join(b or "no-backend" for b in backends) or "no-backend"
        )
        variant_string = (
            "|".join(v or "no-variant" for v in variants) or "no-variant"
        )
        step = (
            f"{task_name}/{codename}/{architecture}/"
            f"{backend_string}/{variant_string}"
        )
        display_name = (
            f"{display_name_prefix} for {codename} / "
            f"{architecture} / {backend_string} / {variant_string}"
        )

        task_data = copy.deepcopy(template)
        task_data.setdefault("bootstrap_options", {})["architecture"] = (
            architecture
        )
        for repository in task_data.get("bootstrap_repositories", []):
            repository.setdefault("suite", codename)

        # Create the work request with its event reactions
        wr, _ = self.work_request_ensure_child_worker(
            task_name=task_name,
            task_data=task_data_class(**task_data),
            workflow_data=WorkRequestWorkflowData(
                step=step, display_name=display_name
            ),
        )
        for variables in self._make_action_variables(
            codenames=codenames, variants=variants, backends=backends
        ):
            wr.add_event_reaction(
                "on_success",
                ActionUpdateCollectionWithArtifacts(
                    collection=(f"{vendor}@{CollectionCategory.ENVIRONMENTS}"),
                    variables=variables.to_dict(),
                    artifact_filters={"category": category},
                ),
            )

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

        :subject: ``vendor``
        :parameter_summary: ``vendor`` being updated and number of targets
        """
        tarballs, images = self._plan_work_requests()
        count = len(tarballs) + len(images)
        summary = f"{self.data.vendor} ({count} environments)"

        return BaseDynamicTaskData(
            subject=self.data.vendor, parameter_summary=summary
        )

    def _plan_work_requests(
        self,
    ) -> tuple[list[PlannedEnvironment], list[PlannedEnvironment]]:
        images = []
        tarballs = []
        for target in self.data.targets:
            codenames = (
                target.codenames
                if isinstance(target.codenames, list)
                else [target.codenames]
            )
            variants: list[str | None] = (
                target.variants
                if isinstance(target.variants, list)
                else [target.variants]
            )
            backends: list[str | None] = (
                cast(list[str | None], target.backends)
                if isinstance(target.backends, list)
                else [target.backends]
            )

            for codename in codenames:
                codename_aliases = [
                    codename,
                    *target.codename_aliases.get(codename, []),
                ]

                for architecture in target.architectures:
                    if target.mmdebstrap_template is not None:
                        environment = PlannedEnvironment(
                            task_data_template=target.mmdebstrap_template,
                            architecture=architecture,
                            codenames=codename_aliases,
                            backends=backends,
                            variants=variants,
                        )
                        tarballs.append(environment)

                    if target.simplesystemimagebuild_template is not None:
                        environment = PlannedEnvironment(
                            task_data_template=target.simplesystemimagebuild_template,
                            architecture=architecture,
                            codenames=codename_aliases,
                            backends=backends,
                            variants=variants,
                        )
                        images.append(environment)

        return tarballs, images

    @override
    def populate(self) -> None:
        """Create child work requests for all targets and architectures."""
        tarballs, images = self._plan_work_requests()

        for tarball in tarballs:
            self._add_child(
                "mmdebstrap",
                MmDebstrapData,
                tarball.task_data_template,
                category=ArtifactCategory.SYSTEM_TARBALL,
                display_name_prefix="Build tarball",
                vendor=self.data.vendor,
                codenames=tarball.codenames,
                architecture=tarball.architecture,
                variants=tarball.variants,
                backends=tarball.backends,
            )

        for image in images:
            self._add_child(
                "simplesystemimagebuild",
                SystemImageBuildData,
                image.task_data_template,
                category=ArtifactCategory.SYSTEM_IMAGE,
                display_name_prefix="Build image",
                vendor=self.data.vendor,
                codenames=image.codenames,
                architecture=image.architecture,
                variants=image.variants,
                backends=image.backends,
            )
