# 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,
)
from debusine.tasks.server import TaskDatabaseInterface


@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.append(
                        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 build_dynamic_data(
        self,
        task_database: TaskDatabaseInterface,  # noqa: U100
    ) -> 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,
            )
