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

"""
Debusine command line interface, suite management commands.

These are "porcelain": they implement opinionated operations on
:collection:`debian:suite` collections and their contents, rather than basic
"plumbing" operations.
"""

import argparse
from collections.abc import Generator
from typing import Any, override

from debusine.artifacts.models import CollectionCategory
from debusine.client.commands.base import WorkspaceCommand
from debusine.client.exceptions import DebusineError
from debusine.client.models import (
    CollectionDataNew,
    CollectionRelationType,
    LookupChildType,
    LookupSingle,
    WorkflowTemplateData,
    WorkflowTemplateDataNew,
)


class Create(WorkspaceCommand, group="archive.suite"):
    """Create a suite in an archive."""

    @override
    @classmethod
    def configure(cls, parser: argparse.ArgumentParser) -> None:
        """Configure the ArgumentParser for this subcommand."""
        super().configure(parser)
        parser.add_argument("suite", type=str, help="Suite name")
        parser.add_argument(
            "--component",
            type=str,
            dest="components",
            action="append",
            help=(
                "Component name to add to the new suite; may be given more "
                "than once (default: [main])"
            ),
        )
        # Although this is currently required, it's an option because we
        # might be able to infer it in future (e.g. from a base suite).
        parser.add_argument(
            "--architecture",
            type=str,
            required=True,
            dest="architectures",
            action="append",
            help=(
                "Architecture name to add to the new suite; may be given more "
                "than once"
            ),
        )
        parser.add_argument(
            "--base-workflow-template",
            type=str,
            help=(
                "Create a workflow template to publish to this suite, using "
                "this one as a base; may be from this workspace or a parent"
            ),
        )

    def iter_workspace_names(self) -> Generator[str]:
        """Iterate over names of this workspace and its parents."""
        yield self.workspace
        with self._api_call_or_fail():
            inheritance = self.debusine.get_workspace_inheritance(
                self.workspace
            )
        for parent in inheritance.chain:
            # Always set in API responses.
            assert parent.workspace is not None
            # We currently only support looking for a base workflow template
            # in the current scope, because our client may not be able to
            # access other scopes.
            if parent.scope == self.debusine.scope:
                yield parent.workspace

    def get_base_workflow_template(self) -> WorkflowTemplateData:
        """Find data for the base workflow template."""
        for workspace in self.iter_workspace_names():
            with self._api_call_or_fail():
                try:
                    base_workflow_template = (
                        self.debusine.workflow_template_get(
                            workspace, self.args.base_workflow_template
                        )
                    )
                    break
                except DebusineError:
                    pass
        else:
            self._fail(
                f"Workflow template {self.args.base_workflow_template!r} not "
                f"found in any parent workspace in this scope."
            )
        if base_workflow_template.task_name != "debian_pipeline":
            self._fail(
                f"Workflow template {base_workflow_template.name!r} is not a "
                f"debian_pipeline template."
            )
        return base_workflow_template

    def _set_suite_relations(
        self, relation_type: CollectionRelationType, targets: list[str]
    ) -> None:
        source = f"{self.args.suite}@{CollectionCategory.SUITE}"

        with self._api_call_or_fail():
            self.debusine.collection_relation_set(
                self.workspace,
                source=source,
                relation_type=relation_type,
                targets=targets,
            )

    def adjust_static_parameters(
        self, static_parameters: dict[str, Any]
    ) -> dict[str, Any]:
        """Adjust a workflow template's static parameters for the new suite."""
        static_parameters = dict(static_parameters)
        static_parameters.pop("enable_upload", None)
        static_parameters["suite"] = (
            f"{self.args.suite}@{CollectionCategory.SUITE}"
        )
        architectures = list(self.args.architectures)
        if "all" not in architectures:
            # This doesn't have to be present in the suite's data (if it's
            # missing, architecture-independent packages will be published
            # in the architecture-specific indexes), but omitting it from
            # the allowlist is likely to be confusing.
            architectures.insert(0, "all")
        static_parameters["architectures_allowlist"] = architectures
        return static_parameters

    @override
    def run(self) -> None:
        if self.args.base_workflow_template is not None:
            base_workflow_template = self.get_base_workflow_template()
        else:
            base_workflow_template = None

        collection_data = CollectionDataNew(
            name=self.args.suite,
            category=CollectionCategory.SUITE,
            data={
                "components": self.args.components or ["main"],
                "architectures": self.args.architectures,
            },
        )
        with self._api_call_or_fail():
            collection = self.debusine.collection_create(
                self.workspace, collection_data
            )
        output: dict[str, Any] = {
            "result": "success",
            "suite": self.debusine.webui_url(
                collection.get_link_url("webui_self")
            ),
        }

        if base_workflow_template is not None:
            relations = self._create_relations_based_on_base_template(
                base_workflow_template
            )

            if relations:
                output["relations"] = relations

            workflow_template_data = WorkflowTemplateDataNew(
                name=f"publish-to-{self.args.suite}",
                task_name="debian_pipeline",
                static_parameters=self.adjust_static_parameters(
                    base_workflow_template.static_parameters
                ),
                runtime_parameters=base_workflow_template.runtime_parameters,
                priority=base_workflow_template.priority,
            )
            with self._api_call_or_fail():
                workflow_template = self.debusine.workflow_template_create(
                    self.workspace, workflow_template_data
                )
            output["workflow_template"] = self.debusine.webui_url(
                workflow_template.get_link_url("webui_self")
            )

        self.print_yaml_output(output)

    def _create_relations_based_on_base_template(
        self,
        base_workflow_template: WorkflowTemplateData,
    ) -> dict[CollectionRelationType, str]:
        relations = {
            "qa_suite": CollectionRelationType.SUITE_TARGETING,
            "regression_tracking_qa_results": (
                CollectionRelationType.SUITE_DEFAULT_QA_RESULTS
            ),
        }

        relations_output = {}

        for parameter_name, relation_type in relations.items():
            if (
                collection := base_workflow_template.static_parameters.get(
                    parameter_name
                )
            ) is None:
                continue

            try:
                normalized_collection_name = self._lookup_collection_identifier(
                    collection
                )
            except DebusineError as exc:
                raise DebusineError(
                    f"Cannot find collection in {collection!r} "
                    f"(from {parameter_name!r} field in the base template)"
                ) from exc

            self._set_suite_relations(
                relation_type,
                [normalized_collection_name],
            )
            relations_output[relation_type] = normalized_collection_name

        return relations_output

    def _lookup_collection_identifier(self, collection: LookupSingle) -> str:
        lookup_response = self.debusine.lookup_single(
            lookup=collection,
            workspace=self.workspace,
            expect_type=LookupChildType.COLLECTION,
            default_category=CollectionCategory.SUITE,
        )

        assert lookup_response.collection is not None
        collection_info = self.debusine.collection_get_by_id(
            workspace=self.workspace,
            collection_id=lookup_response.collection,
        )

        return f"{collection_info.name}@{collection_info.category}"
