#  Copyright (c) ZenML GmbH 2021. All Rights Reserved.
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at:
#
#       https://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
#  or implied. See the License for the specific language governing
#  permissions and limitations under the License.
"""Functionality to generate stack component CLI commands."""

import time
from importlib import import_module
from typing import Any, Callable, List, Optional, Tuple, cast
from uuid import UUID

import click
from rich.markdown import Markdown

from zenml.cli import utils as cli_utils
from zenml.cli.annotator import register_annotator_subcommands
from zenml.cli.cli import TagGroup, cli
from zenml.cli.feature import register_feature_store_subcommands
from zenml.cli.model import register_model_registry_subcommands
from zenml.cli.secret import register_secrets_manager_subcommands
from zenml.cli.served_model import register_model_deployer_subcommands
from zenml.cli.utils import (
    _component_display_name,
    fail_secrets_manager_creation,
    list_options,
    print_page_info,
    warn_deprecated_secrets_manager,
)
from zenml.client import Client
from zenml.console import console
from zenml.enums import CliCategories, StackComponentType
from zenml.exceptions import AuthorizationException, IllegalOperationError
from zenml.io import fileio
from zenml.models import ComponentFilterModel, ServiceConnectorResourcesModel
from zenml.utils.analytics_utils import AnalyticsEvent, track


def generate_stack_component_get_command(
    component_type: StackComponentType,
) -> Callable[[], None]:
    """Generates a `get` command for the specific stack component type.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """

    def get_stack_component_command() -> None:
        """Prints the name of the active component."""
        if component_type == StackComponentType.SECRETS_MANAGER:
            warn_deprecated_secrets_manager()

        client = Client()
        display_name = _component_display_name(component_type)

        with console.status(f"Getting the active `{display_name}`...\n"):
            active_stack = client.active_stack_model
            components = active_stack.components.get(component_type, None)

            if components:
                cli_utils.declare(
                    f"Active {display_name}: '{components[0].name}'"
                )
            else:
                cli_utils.warning(
                    f"No {display_name} set for active stack "
                    f"('{active_stack.name}')."
                )

    return get_stack_component_command


def generate_stack_component_describe_command(
    component_type: StackComponentType,
) -> Callable[[str], None]:
    """Generates a `describe` command for the specific stack component type.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """

    @click.argument(
        "name_id_or_prefix",
        type=str,
        required=False,
    )
    def describe_stack_component_command(name_id_or_prefix: str) -> None:
        """Prints details about the active/specified component.

        Args:
            name_id_or_prefix: Name or id of the component to describe.
        """
        if component_type == StackComponentType.SECRETS_MANAGER:
            warn_deprecated_secrets_manager()

        client = Client()
        try:
            component_ = client.get_stack_component(
                name_id_or_prefix=name_id_or_prefix,
                component_type=component_type,
            )
        except KeyError as err:
            cli_utils.error(str(err))

        with console.status(f"Describing component '{component_.name}'..."):
            active_component_id = None
            active_components = client.active_stack_model.components.get(
                component_type, None
            )
            if active_components:
                active_component_id = active_components[0].id

            cli_utils.print_stack_component_configuration(
                component=component_,
                active_status=component_.id == active_component_id,
            )

    return describe_stack_component_command


def generate_stack_component_list_command(
    component_type: StackComponentType,
) -> Callable[[], None]:
    """Generates a `list` command for the specific stack component type.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """

    @list_options(ComponentFilterModel)
    def list_stack_components_command(**kwargs: Any) -> None:
        """Prints a table of stack components.

        Args:
            kwargs: Keyword arguments to filter the components.
        """
        if component_type == StackComponentType.SECRETS_MANAGER:
            warn_deprecated_secrets_manager()

        client = Client()
        with console.status(f"Listing {component_type.plural}..."):
            kwargs["type"] = component_type
            components = client.list_stack_components(**kwargs)
            if not components:
                cli_utils.declare("No components found for the given filters.")
                return

            cli_utils.print_components_table(
                client=client,
                component_type=component_type,
                components=components.items,
            )
            print_page_info(components)

    return list_stack_components_command


def generate_stack_component_register_command(
    component_type: StackComponentType,
) -> Callable[[str, str, bool, List[str]], None]:
    """Generates a `register` command for the specific stack component type.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """
    display_name = _component_display_name(component_type)

    @click.argument(
        "name",
        type=str,
    )
    @click.option(
        "--flavor",
        "-f",
        "flavor",
        help=f"The flavor of the {display_name} to register.",
        required=True,
        type=str,
    )
    @click.option(
        "--label",
        "-l",
        "labels",
        help="Labels to be associated with the component, in the form "
        "-l key1=value1 -l key2=value2.",
        multiple=True,
    )
    @click.option(
        "--share",
        "share",
        is_flag=True,
        help="Use this flag to share this stack component with other users.",
        type=click.BOOL,
    )
    @click.argument("args", nargs=-1, type=click.UNPROCESSED)
    def register_stack_component_command(
        name: str,
        flavor: str,
        share: bool,
        args: List[str],
        labels: Optional[List[str]] = None,
    ) -> None:
        """Registers a stack component.

        Args:
            name: Name of the component to register.
            flavor: Flavor of the component to register.
            share: Share the stack with other users.
            args: Additional arguments to pass to the component.
            labels: Labels to be associated with the component.
        """
        if component_type == StackComponentType.SECRETS_MANAGER:
            fail_secrets_manager_creation()
            return

        client = Client()

        # Parse the given args
        # name is guaranteed to be set by parse_name_and_extra_arguments
        name, parsed_args = cli_utils.parse_name_and_extra_arguments(  # type: ignore[assignment]
            list(args) + [name], expand_args=True
        )

        parsed_labels = cli_utils.get_parsed_labels(labels)

        # click<8.0.0 gives flags a default of None
        if share is None:
            share = False

        with console.status(f"Registering {display_name} '{name}'...\n"):
            # Create a new stack component model
            component = client.create_stack_component(
                name=name,
                flavor=flavor,
                component_type=component_type,
                configuration=parsed_args,
                labels=parsed_labels,
                is_shared=share,
            )

            cli_utils.declare(
                f"Successfully registered {component.type} `{component.name}`."
            )

    return register_stack_component_command


def generate_stack_component_update_command(
    component_type: StackComponentType,
) -> Callable[[str, List[str]], None]:
    """Generates an `update` command for the specific stack component type.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """
    display_name = _component_display_name(component_type)

    @click.argument(
        "name_id_or_prefix",
        type=str,
        required=False,
    )
    @click.option(
        "--label",
        "-l",
        "labels",
        help="Labels to be associated with the component, in the form "
        "-l key1=value1 -l key2=value2.",
        multiple=True,
    )
    @click.argument("args", nargs=-1, type=click.UNPROCESSED)
    def update_stack_component_command(
        name_id_or_prefix: Optional[str],
        args: List[str],
        labels: Optional[List[str]] = None,
    ) -> None:
        """Updates a stack component.

        Args:
            name_id_or_prefix: The name or id of the stack component to update.
            args: Additional arguments to pass to the update command.
            labels: Labels to be associated with the component.
        """
        if component_type == StackComponentType.SECRETS_MANAGER:
            warn_deprecated_secrets_manager()

        client = Client()

        # Parse the given args
        args = list(args)
        if name_id_or_prefix:
            args.append(name_id_or_prefix)

        name_or_id, parsed_args = cli_utils.parse_name_and_extra_arguments(
            args,
            expand_args=True,
            name_mandatory=False,
        )

        parsed_labels = cli_utils.get_parsed_labels(labels)

        with console.status(f"Updating {display_name}...\n"):
            try:
                updated_component = client.update_stack_component(
                    name_id_or_prefix=name_or_id,
                    component_type=component_type,
                    configuration=parsed_args,
                    labels=parsed_labels,
                )
            except KeyError as err:
                cli_utils.error(str(err))

            cli_utils.declare(
                f"Successfully updated {display_name} "
                f"`{updated_component.name}`."
            )

    return update_stack_component_command


def generate_stack_component_share_command(
    component_type: StackComponentType,
) -> Callable[[str], None]:
    """Generates an `share` command for the specific stack component type.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """
    display_name = _component_display_name(component_type)

    @click.argument(
        "name_id_or_prefix",
        type=str,
        required=False,
    )
    def share_stack_component_command(
        name_id_or_prefix: str,
    ) -> None:
        """Shares a stack component.

        Args:
            name_id_or_prefix: The name or id of the stack component to update.
        """
        if component_type == StackComponentType.SECRETS_MANAGER:
            warn_deprecated_secrets_manager()

        client = Client()

        with console.status(
            f"Updating {display_name} '{name_id_or_prefix}'...\n"
        ):
            try:
                client.update_stack_component(
                    name_id_or_prefix=name_id_or_prefix,
                    component_type=component_type,
                    is_shared=True,
                )
            except (KeyError, IllegalOperationError) as err:
                cli_utils.error(str(err))

            cli_utils.declare(
                f"Successfully shared {display_name} "
                f"`{name_id_or_prefix}`."
            )

    return share_stack_component_command


def generate_stack_component_remove_attribute_command(
    component_type: StackComponentType,
) -> Callable[[str, List[str]], None]:
    """Generates `remove_attribute` command for a specific stack component type.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """
    display_name = _component_display_name(component_type)

    @click.argument(
        "name_id_or_prefix",
        type=str,
        required=True,
    )
    @click.option(
        "--label",
        "-l",
        "labels",
        help="Labels to be removed from the component.",
        multiple=True,
    )
    @click.argument("args", nargs=-1, type=click.UNPROCESSED)
    def remove_attribute_stack_component_command(
        name_id_or_prefix: str,
        args: List[str],
        labels: Optional[List[str]] = None,
    ) -> None:
        """Removes one or more attributes from a stack component.

        Args:
            name_id_or_prefix: The name of the stack component to remove the
                attribute from.
            args: Additional arguments to pass to the remove_attribute command.
            labels: Labels to be removed from the component.
        """
        if component_type == StackComponentType.SECRETS_MANAGER:
            warn_deprecated_secrets_manager()

        client = Client()

        with console.status(
            f"Updating {display_name} '{name_id_or_prefix}'...\n"
        ):
            try:
                client.update_stack_component(
                    name_id_or_prefix=name_id_or_prefix,
                    component_type=component_type,
                    configuration={k: None for k in args},
                    labels={k: None for k in labels} if labels else None,
                )
            except (KeyError, IllegalOperationError) as err:
                cli_utils.error(str(err))

            cli_utils.declare(
                f"Successfully updated {display_name} `{name_id_or_prefix}`."
            )

    return remove_attribute_stack_component_command


def generate_stack_component_rename_command(
    component_type: StackComponentType,
) -> Callable[[str, str], None]:
    """Generates a `rename` command for the specific stack component type.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """
    display_name = _component_display_name(component_type)

    @click.argument(
        "name_id_or_prefix",
        type=str,
        required=True,
    )
    @click.argument(
        "new_name",
        type=str,
        required=True,
    )
    def rename_stack_component_command(
        name_id_or_prefix: str, new_name: str
    ) -> None:
        """Rename a stack component.

        Args:
            name_id_or_prefix: The name of the stack component to rename.
            new_name: The new name of the stack component.
        """
        if component_type == StackComponentType.SECRETS_MANAGER:
            warn_deprecated_secrets_manager()

        client = Client()

        with console.status(
            f"Renaming {display_name} '{name_id_or_prefix}'...\n"
        ):
            try:
                client.update_stack_component(
                    name_id_or_prefix=name_id_or_prefix,
                    component_type=component_type,
                    name=new_name,
                )
            except (KeyError, IllegalOperationError) as err:
                cli_utils.error(str(err))

            cli_utils.declare(
                f"Successfully renamed {display_name} `{name_id_or_prefix}` to"
                f" `{new_name}`."
            )

    return rename_stack_component_command


def generate_stack_component_delete_command(
    component_type: StackComponentType,
) -> Callable[[str], None]:
    """Generates a `delete` command for the specific stack component type.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """
    display_name = _component_display_name(component_type)

    @click.argument("name_id_or_prefix", type=str)
    def delete_stack_component_command(name_id_or_prefix: str) -> None:
        """Deletes a stack component.

        Args:
            name_id_or_prefix: The name of the stack component to delete.
        """
        client = Client()

        with console.status(
            f"Deleting {display_name} '{name_id_or_prefix}'...\n"
        ):
            try:
                client.delete_stack_component(
                    name_id_or_prefix=name_id_or_prefix,
                    component_type=component_type,
                )
            except (KeyError, IllegalOperationError) as err:
                cli_utils.error(str(err))
            cli_utils.declare(f"Deleted {display_name}: {name_id_or_prefix}")

    return delete_stack_component_command


def generate_stack_component_copy_command(
    component_type: StackComponentType,
) -> Callable[[str, str], None]:
    """Generates a `copy` command for the specific stack component type.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """
    display_name = _component_display_name(component_type)

    @click.argument(
        "source_component_name_id_or_prefix", type=str, required=True
    )
    @click.argument("target_component", type=str, required=True)
    @track(AnalyticsEvent.COPIED_STACK_COMPONENT)
    def copy_stack_component_command(
        source_component_name_id_or_prefix: str,
        target_component: str,
    ) -> None:
        """Copies a stack component.

        Args:
            source_component_name_id_or_prefix: Name or id prefix of the
                                         component to copy.
            target_component: Name of the copied component.
        """
        if component_type == StackComponentType.SECRETS_MANAGER:
            warn_deprecated_secrets_manager()

        client = Client()

        with console.status(
            f"Copying {display_name} "
            f"`{source_component_name_id_or_prefix}`..\n"
        ):
            try:
                component_to_copy = client.get_stack_component(
                    name_id_or_prefix=source_component_name_id_or_prefix,
                    component_type=component_type,
                )
            except KeyError as err:
                cli_utils.error(str(err))

            client.create_stack_component(
                name=target_component,
                flavor=component_to_copy.flavor,
                component_type=component_to_copy.type,
                configuration=component_to_copy.configuration,
                labels=component_to_copy.labels,
                is_shared=component_to_copy.is_shared,
            )

    return copy_stack_component_command


def generate_stack_component_up_command(
    component_type: StackComponentType,
) -> Callable[[str], None]:
    """Generates a `up` command for the specific stack component type.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """
    display_name = _component_display_name(component_type)

    @click.argument("name_id_or_prefix", type=str, required=False)
    def up_stack_component_command(name_id_or_prefix: str) -> None:
        """Deploys a stack component locally.

        Args:
            name_id_or_prefix: The name or_id of the stack component to deploy.
        """
        client = Client()

        with console.status(
            f"Provisioning {display_name} '{name_id_or_prefix}' locally...\n"
        ):
            try:
                component_model = client.get_stack_component(
                    name_id_or_prefix=name_id_or_prefix,
                    component_type=component_type,
                )
            except KeyError as err:
                cli_utils.error(str(err))

            from zenml.stack import StackComponent

            component = StackComponent.from_model(
                component_model=component_model
            )

            if component.is_running:
                cli_utils.declare(
                    f"Local deployment is already running for {display_name} "
                    f"'{component.name}'."
                )
                return

            if not component.is_provisioned:
                cli_utils.declare(
                    f"Provisioning local resources for {display_name} "
                    f"'{component.name}'."
                )
                try:
                    component.provision()
                except NotImplementedError:
                    cli_utils.error(
                        f"Provisioning local resources not implemented for "
                        f"{display_name} '{component.name}'."
                    )

            if not component.is_running:
                cli_utils.declare(
                    f"Resuming local resources for {display_name} "
                    f"'{component.name}'."
                )
                component.resume()

    return up_stack_component_command


def generate_stack_component_down_command(
    component_type: StackComponentType,
) -> Callable[[str, bool], None]:
    """Generates a `down` command for the specific stack component type.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """
    display_name = _component_display_name(component_type)

    @click.argument("name_id_or_prefix", type=str, required=False)
    @click.option(
        "--force",
        "-f",
        "force",
        is_flag=True,
        help="Deprovisions local resources instead of suspending them.",
    )
    def down_stack_component_command(
        name_id_or_prefix: str,
        force: bool = False,
    ) -> None:
        """Stops/Tears down the local deployment of a stack component.

        Args:
            name_id_or_prefix: The name or id of the component to
                stop/deprovision.
            force: Deprovision local resources instead of suspending them.
        """
        client = Client()

        with console.status(
            f"De-provisioning {display_name} '{name_id_or_prefix}' locally...\n"
        ):
            try:
                component_model = client.get_stack_component(
                    name_id_or_prefix=name_id_or_prefix,
                    component_type=component_type,
                )
            except KeyError as err:
                cli_utils.error(str(err))

            from zenml.stack import StackComponent

            component = StackComponent.from_model(
                component_model=component_model
            )

            if not force:
                if not component.is_suspended:
                    cli_utils.declare(
                        f"Suspending local resources for {display_name} "
                        f"'{component.name}'."
                    )
                    try:
                        component.suspend()
                    except NotImplementedError:
                        cli_utils.error(
                            f"Provisioning local resources not implemented for "
                            f"{display_name} '{component.name}'. If you want "
                            f"to deprovision all resources for this component, "
                            f"use the `--force/-f` flag."
                        )
                else:
                    cli_utils.declare(
                        f"No running resources found for {display_name} "
                        f"'{component.name}'."
                    )
            else:
                if component.is_provisioned:
                    cli_utils.declare(
                        f"Deprovisioning resources for {display_name} "
                        f"'{component.name}'."
                    )
                    component.deprovision()
                else:
                    cli_utils.declare(
                        f"No provisioned resources found for {display_name} "
                        f"'{component.name}'."
                    )

    return down_stack_component_command


def generate_stack_component_logs_command(
    component_type: StackComponentType,
) -> Callable[[str, bool], None]:
    """Generates a `logs` command for the specific stack component type.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """
    display_name = _component_display_name(component_type)

    @click.argument("name_id_or_prefix", type=str, required=False)
    @click.option(
        "--follow",
        "-f",
        is_flag=True,
        help="Follow the log file instead of just displaying the current logs.",
    )
    def stack_component_logs_command(
        name_id_or_prefix: str, follow: bool = False
    ) -> None:
        """Displays stack component logs.

        Args:
            name_id_or_prefix: The name of the stack component to display logs
                for.
            follow: Follow the log file instead of just displaying the current
                logs.
        """
        client = Client()

        with console.status(
            f"Fetching the logs for the {display_name} "
            f"'{name_id_or_prefix}'...\n"
        ):
            try:
                component_model = client.get_stack_component(
                    name_id_or_prefix=name_id_or_prefix,
                    component_type=component_type,
                )
            except KeyError as err:
                cli_utils.error(str(err))

            from zenml.stack import StackComponent

            component = StackComponent.from_model(
                component_model=component_model
            )
            log_file = component.log_file

            if not log_file or not fileio.exists(log_file):
                cli_utils.warning(
                    f"Unable to find log file for {display_name} "
                    f"'{name_id_or_prefix}'."
                )
                return

        if not log_file or not fileio.exists(log_file):
            cli_utils.warning(
                f"Unable to find log file for {display_name} "
                f"'{component.name}'."
            )
            return

        if follow:
            try:
                with open(log_file, "r") as f:
                    # seek to the end of the file
                    f.seek(0, 2)

                    while True:
                        line = f.readline()
                        if not line:
                            time.sleep(0.1)
                            continue
                        line = line.rstrip("\n")
                        click.echo(line)
            except KeyboardInterrupt:
                cli_utils.declare(f"Stopped following {display_name} logs.")
        else:
            with open(log_file, "r") as f:
                click.echo(f.read())

    return stack_component_logs_command


def generate_stack_component_explain_command(
    component_type: StackComponentType,
) -> Callable[[], None]:
    """Generates an `explain` command for the specific stack component type.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """

    def explain_stack_components_command() -> None:
        """Explains the concept of the stack component."""
        if component_type == StackComponentType.SECRETS_MANAGER:
            warn_deprecated_secrets_manager()

        component_module = import_module(f"zenml.{component_type.plural}")

        if component_module.__doc__ is not None:
            md = Markdown(component_module.__doc__)
            console.print(md)
        else:
            console.print(
                "The explain subcommand is yet not available for "
                "this stack component. For more information, you can "
                "visit our docs page: https://docs.zenml.io/ and "
                "stay tuned for future releases."
            )

    return explain_stack_components_command


def generate_stack_component_flavor_list_command(
    component_type: StackComponentType,
) -> Callable[[], None]:
    """Generates a `list` command for the flavors of a stack component.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """
    display_name = _component_display_name(component_type)

    def list_stack_component_flavor_command() -> None:
        """Lists the flavors for a single type of stack component."""
        if component_type == StackComponentType.SECRETS_MANAGER:
            warn_deprecated_secrets_manager()

        client = Client()

        with console.status(f"Listing {display_name} flavors`...\n"):
            flavors = client.get_flavors_by_type(component_type=component_type)

            cli_utils.print_flavor_list(flavors=flavors)
            cli_utils.print_page_info(flavors)

    return list_stack_component_flavor_command


def generate_stack_component_flavor_register_command(
    component_type: StackComponentType,
) -> Callable[[str], None]:
    """Generates a `register` command for the flavors of a stack component.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """
    command_name = component_type.value.replace("_", "-")
    display_name = _component_display_name(component_type)

    @click.argument(
        "source",
        type=str,
        required=True,
    )
    def register_stack_component_flavor_command(source: str) -> None:
        """Adds a flavor for a stack component type.

        Example:
            Let's say you create an artifact store flavor class `MyArtifactStoreFlavor`
            in the file path `flavors/my_flavor.py`. You would register it as:

            ```shell
            zenml artifact-store flavor register flavors.my_flavor.MyArtifactStoreFlavor
            ```

        Args:
            source: The source path of the flavor class in dot notation format.
        """
        if component_type == StackComponentType.SECRETS_MANAGER:
            warn_deprecated_secrets_manager()

        client = Client()

        if not client.root:
            cli_utils.warning(
                f"You're running the `zenml {command_name} flavor register` "
                "command without a ZenML repository. Your current working "
                "directory will be used as the source root relative to which "
                "the `source` argument is expected. To silence this warning, "
                "run `zenml init` at your source code root."
            )

        with console.status(f"Registering a new {display_name} flavor`...\n"):
            try:
                # Register the new model
                new_flavor = client.create_flavor(
                    source=source,
                    component_type=component_type,
                )
            except ValueError as e:
                root_path = Client.find_repository()
                cli_utils.error(
                    f"Flavor registration failed! ZenML tried loading the module `{source}` from path "
                    f"`{root_path}`. If this is not what you expect, then please ensure you have run "
                    f"`zenml init` at the root of your repository.\n\nOriginal exception: {str(e)}"
                )

            cli_utils.declare(
                f"Successfully registered new flavor '{new_flavor.name}' "
                f"for stack component '{new_flavor.type}'."
            )

    return register_stack_component_flavor_command


def generate_stack_component_flavor_describe_command(
    component_type: StackComponentType,
) -> Callable[[str], None]:
    """Generates a `describe` command for a single flavor of a component.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """
    display_name = _component_display_name(component_type)

    @click.argument(
        "name",
        type=str,
        required=True,
    )
    def describe_stack_component_flavor_command(name: str) -> None:
        """Describes a flavor based on its config schema.

        Args:
            name: The name of the flavor.
        """
        if component_type == StackComponentType.SECRETS_MANAGER:
            warn_deprecated_secrets_manager()

        client = Client()

        with console.status(f"Describing {display_name} flavor: {name}`...\n"):
            flavor_model = client.get_flavor_by_name_and_type(
                name=name, component_type=component_type
            )

            cli_utils.describe_pydantic_object(flavor_model.config_schema)
            resources = flavor_model.connector_requirements
            if resources:
                resources_str = f"a '{resources.resource_type}' resource"
                cli_args = f"--resource-type {resources.resource_type}"
                if resources.connector_type:
                    resources_str += (
                        f" provided by a '{resources.connector_type}' "
                        "connector"
                    )
                    cli_args += f"--connector-type {resources.connector_type}"

                cli_utils.declare(
                    f"This flavor supports connecting to external resources "
                    f"with a Service Connector. It requires {resources_str}. "
                    "You can get a list of all available connectors and the "
                    "compatible resources that they can access by running:\n\n"
                    f"'zenml service-connector list-resources {cli_args}'\n"
                    "If no compatible Service Connectors are yet registered, "
                    "you can can register a new one by running:\n\n"
                    f"'zenml service-connector register -i'"
                )
            else:
                cli_utils.declare(
                    "This flavor does not support connecting to external "
                    "resources with a Service Connector."
                )

    return describe_stack_component_flavor_command


def generate_stack_component_flavor_delete_command(
    component_type: StackComponentType,
) -> Callable[[str], None]:
    """Generates a `delete` command for a single flavor of a component.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """
    display_name = _component_display_name(component_type)

    @click.argument(
        "name_or_id",
        type=str,
        required=True,
    )
    def delete_stack_component_flavor_command(name_or_id: str) -> None:
        """Deletes a flavor.

        Args:
            name_or_id: The name of the flavor.
        """
        client = Client()

        with console.status(
            f"Deleting a {display_name} flavor: {name_or_id}`...\n"
        ):
            client.delete_flavor(name_or_id)

            cli_utils.declare(f"Successfully deleted flavor '{name_or_id}'.")

    return delete_stack_component_flavor_command


def generate_stack_component_deploy_command(
    component_type: StackComponentType,
) -> Callable[[str, str, str, List[str]], None]:
    """Generates a `deploy` command for the stack component type.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """
    display_name = _component_display_name(component_type)

    @click.argument(
        "name",
        type=str,
    )
    @click.option(
        "--flavor",
        "-f",
        "flavor",
        help=f"The flavor of the {display_name} to deploy.",
        required=True,
        type=str,
    )
    @click.option(
        "--cloud",
        "-c",
        "cloud",
        type=click.Choice(["aws", "gcp", "k3d"]),
        help="The cloud provider to use to deploy the stack component.",
    )
    @click.argument("args", nargs=-1, type=click.UNPROCESSED)
    def deploy_stack_component_command(
        name: str,
        flavor: str,
        cloud: str,
        args: List[str],
    ) -> None:
        """Deploy a stack component.

        This function also registers the newly-deployed component.

        Args:
            name: Name of the component to register.
            flavor: Flavor of the component to register.
            cloud: Cloud provider to use to deploy the stack component.
            args: Additional arguments to pass to the component.

        Raises:
            error: If the component type is not supported.
        """
        # generate a python dict with the above structure
        # and then use it to check if the flavor is valid
        # for the given component type
        allowed_flavors = {
            "experiment_tracker": ["mlflow"],
            "model_deployer": ["seldon", "kserve"],
            "artifact_store": ["s3", "gcp", "minio"],
            "container_registry": ["gcp", "aws"],
            "orchestrator": [
                "kubernetes",
                "kubeflow",
                "tekton",
                "sagemaker",
                "vertex",
            ],
            "step_operator": ["sagemaker", "vertex"],
        }

        # if the flavor is not allowed for the given component type
        # raise an error
        if flavor not in allowed_flavors[component_type.value]:
            cli_utils.error(
                f"Flavor '{flavor}' is not supported for "
                f"{_component_display_name(component_type, True)}. "
                "Allowed flavors are: "
                f"{', '.join(allowed_flavors[component_type.value])}."
            )

        # for cases like artifact store, secrets manager and container registry
        # the flavor is the same as the cloud
        if flavor in ["s3", "sagemaker", "aws"]:
            cloud = "aws"
        elif flavor in ["vertex", "gcp"]:
            cloud = "gcp"
        elif cloud is None:
            raise cli_utils.error(
                f"Cloud must be specified while deploying {flavor} "
                f"{_component_display_name(component_type, True)}."
                " Allowed clouds are: aws, gcp, k3d."
            )

        client = Client()

        # Parse the given args
        # name is guaranteed to be set by parse_name_and_extra_arguments
        name, parsed_args = cli_utils.parse_name_and_extra_arguments(  # type: ignore[assignment]
            list(args) + [name], expand_args=True
        )

        # if the cloud is gcp, project_id is required for the first time
        if cloud == "gcp":
            if "project_id" not in parsed_args:
                cli_utils.warning(
                    "You should pass your GCP project ID to the deploy command, "
                    "using the `--project_id` flag, if this is the first time that "
                    "you are deploying a component to GCP. Ignore if you "
                    "have already done so."
                )

        from zenml.recipes import GitStackRecipesHandler

        try:
            stack_recipe = GitStackRecipesHandler().get_stack_recipes(
                f"{cloud}-modular"
            )[0]
        except KeyError as e:
            cli_utils.error(str(e))
        else:
            # warn that prerequisites should be met
            metadata = stack_recipe.metadata
            if not cli_utils.confirmation(
                "\nPrerequisites for running this recipe are as follows.\n"
                f"{metadata['Prerequisites']}"
                "\n\n Are all of these conditions met?"
            ):
                cli_utils.error(
                    "Prerequisites are not installed. Please make sure "
                    "they are met and run deploy again."
                )

            client.deploy_stack_component(
                name=name,
                flavor=flavor,
                cloud=cloud,
                configuration=parsed_args,
                component_type=component_type,
                labels={"cloud": cloud, "created_by": "recipe"},
            )

    return deploy_stack_component_command


def generate_stack_component_destroy_command(
    component_type: StackComponentType,
) -> Callable[[str], None]:
    """Generates a `destroy` command for the stack component type.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """
    _component_display_name(component_type)

    @click.argument(
        "name_id_or_prefix",
        type=str,
        required=True,
    )
    def destroy_stack_component_command(
        name_id_or_prefix: str,
    ) -> None:
        """Destroy a stack component.

        Args:
            name_id_or_prefix: Name, ID or prefix of the component to destroy.
        """
        client = Client()
        from zenml.recipes import GitStackRecipesHandler

        try:
            component = client.get_stack_component(
                name_id_or_prefix=name_id_or_prefix,
                component_type=component_type,
                allow_name_prefix_match=False,
            )
        except KeyError:
            cli_utils.error(
                "Could not find a stack component with name or id "
                f"'{name_id_or_prefix}'.",
            )

        # if the component's labels don't have a key created_by
        # equal to 'recipe', then destroy cannot be called on it
        if (
            not component.labels
            or "created_by" not in component.labels
            or component.labels["created_by"] != "recipe"
        ):
            cli_utils.error(
                f"Cannot destroy stack component {component.name}. It "
                "was not created by a recipe.",
            )

        try:
            GitStackRecipesHandler().get_stack_recipes(
                f"{component.labels['cloud']}-modular"
            )[0]
        except KeyError:
            cli_utils.error(
                "The backing recipe for this component is missing. "
                "Please run `zenml stack recipe pull` to fix this."
            )

        try:
            client.destroy_stack_component(
                component=component,
            )
        except (KeyError, IllegalOperationError) as err:
            cli_utils.error(str(err))

    return destroy_stack_component_command


def prompt_select_resource_id(
    resources: ServiceConnectorResourcesModel,
    resource_name: str,
    interactive: bool = True,
) -> str:
    """Prompts the user to select a resource ID from a list of available IDs.

    Args:
        resources: A list of available resource IDs.
        resource_name: The name of the resource type to select.
        interactive: Whether to prompt the user for input or error out if
            user input is required.

    Returns:
        The selected resource ID.
    """
    resource_ids = resources.resource_ids or []
    if len(resource_ids) == 1:
        # Only one resource ID is available, so we can select it
        # without prompting the user
        return resource_ids[0]

    if len(resource_ids) > 1:
        resource_ids_list = "\n - " + "\n - ".join(resource_ids)
        msg = (
            f"Multiple {resource_name} resources are available for the "
            f"selected connector:\n{resource_ids_list}\n"
        )
        # User needs to select a resource ID from the list
        if not interactive:
            cli_utils.error(
                f"{msg}Please use the `--resource-id` command line "
                f"argument to select a {resource_name} resource from the "
                "list."
            )
        resource_id = click.prompt(
            f"{msg}Please select the {resource_name} that you want to use",
            type=click.Choice(resource_ids),
            show_choices=False,
        )

        return cast(str, resource_id)

    # We should never get here, but just in case...
    cli_utils.error(
        "Could not determine which resource to use. Please select a "
        "different connector."
    )


def prompt_select_resource(
    resource_list: List[ServiceConnectorResourcesModel],
) -> Tuple[UUID, str]:
    """Prompts the user to select a resource ID from a list of resources.

    Args:
        resource_list: List of resources to select from.

    Returns:
        The ID of a selected connector and the ID of the selected resource
        instance.
    """
    if len(resource_list) == 1:
        click.echo("Only one connector has compatible resources:")
    else:
        click.echo("The following connectors have compatible resources:")

    cli_utils.print_service_connector_resource_table(resource_list)

    if len(resource_list) == 1:
        connect = click.confirm(
            "Would you like to use this connector?",
            default=True,
        )
        if not connect:
            cli_utils.error("Aborting.")
        resources = resource_list[0]
    else:
        # Prompt the user to select a connector by its name or ID
        while True:
            connector_id = click.prompt(
                "Please enter the name or ID of the connector you want "
                "to use",
                type=click.Choice(
                    [
                        str(connector.id)
                        for connector in resource_list
                        if connector.id is not None
                    ]
                    + [
                        connector.name
                        for connector in resource_list
                        if connector.name is not None
                    ]
                ),
                show_choices=False,
            )
            matches = [
                c
                for c in resource_list
                if str(c.id) == connector_id or c.name == connector_id
            ]
            if len(matches) > 1:
                cli_utils.declare(
                    f"Multiple connectors with name '{connector_id}' "
                    "were found. Please try again."
                )
            else:
                resources = matches[0]
                break

    connector_uuid = resources.id
    assert connector_uuid is not None

    assert resources.resource_type is not None
    resource_name = resources.resource_type
    if not isinstance(resources.connector_type, str):
        resource_type_spec = resources.connector_type.resource_type_dict[
            resources.resource_type
        ]
        resource_name = resource_type_spec.name

    resource_id = prompt_select_resource_id(
        resources, resource_name=resource_name
    )

    return connector_uuid, resource_id


def generate_stack_component_connect_command(
    component_type: StackComponentType,
) -> Callable[[str, str], None]:
    """Generates a `connect` command for the specific stack component type.

    Args:
        component_type: Type of the component to generate the command for.

    Returns:
        A function that can be used as a `click` command.
    """
    display_name = _component_display_name(component_type)

    @click.argument(
        "name_id_or_prefix",
        type=str,
        required=False,
    )
    @click.option(
        "--connector",
        "-c",
        "connector",
        help="The name, ID or prefix of the connector to use.",
        required=False,
        type=str,
    )
    @click.option(
        "--resource-id",
        "-r",
        "resource_id",
        help="The resource ID to use with the connector. Only required for "
        "multi-instance connectors that are not already configured with a "
        "particular resource ID.",
        required=False,
        type=str,
    )
    @click.option(
        "--interactive",
        "-i",
        "interactive",
        is_flag=True,
        default=False,
        help="Configure a service connector resource interactively.",
        type=click.BOOL,
    )
    @click.option(
        "--no-verify",
        "no_verify",
        is_flag=True,
        default=False,
        help="Skip verification of the connector resource.",
        type=click.BOOL,
    )
    @track(AnalyticsEvent.CONNECTED_STACK_COMPONENT)
    def connect_stack_component_command(
        name_id_or_prefix: Optional[str],
        connector: Optional[str] = None,
        resource_id: Optional[str] = None,
        interactive: bool = False,
        no_verify: bool = False,
    ) -> None:
        """Connect the stack component to a resource through a service connector.

        Args:
            name_id_or_prefix: The name of the stack component to connect.
            connector: The name, ID or prefix of the connector to use.
            resource_id: The resource ID to use connect to. Only
                required for multi-instance connectors that are not already
                configured with a particular resource ID.
            interactive: Configure a service connector resource interactively.
            no_verify: Do not verify whether the resource is accessible.
        """
        if component_type == StackComponentType.SECRETS_MANAGER:
            warn_deprecated_secrets_manager()

        if not connector and not interactive:
            cli_utils.error(
                "Please provide either a connector ID or set the interactive "
                "flag."
            )

        if connector and interactive:
            cli_utils.error(
                "Please provide either a connector ID or set the interactive "
                "flag, not both."
            )

        client = Client()

        try:
            component_model = client.get_stack_component(
                name_id_or_prefix=name_id_or_prefix,
                component_type=component_type,
            )
        except KeyError as err:
            cli_utils.error(str(err))

        try:
            flavor_model = client.get_flavor_by_name_and_type(
                name=component_model.flavor, component_type=component_type
            )
        except KeyError as err:
            cli_utils.error(
                f"Could not find flavor '{component_model.flavor}' for "
                f"{display_name} '{name_id_or_prefix}': {str(err)}"
            )

        requirements = flavor_model.connector_requirements

        if not requirements:
            cli_utils.error(
                f"The '{component_model.name}' {display_name} implementation "
                "does not support using a service connector to connect to "
                "resources."
            )

        resource_type = requirements.resource_type
        if requirements.resource_id_attr is not None:
            # Check if an attribute is set in the component configuration
            resource_id = component_model.configuration.get(
                requirements.resource_id_attr
            )

        if interactive:
            # Fetch the list of connectors that have resources compatible with
            # the stack component's flavor's resource requirements
            with console.status(
                "Finding all resources matching the stack component "
                "requirements (this could take a while)...\n"
            ):
                resource_list = client.list_service_connector_resources(
                    connector_type=requirements.connector_type,
                    resource_type=resource_type,
                    resource_id=resource_id,
                )

            resource_list = [
                resource for resource in resource_list if resource.resource_ids
            ]

            error_resource_list = [
                resource
                for resource in resource_list
                if not resource.resource_ids
            ]

            if not resource_list:
                # No compatible resources were found
                additional_info = ""
                if error_resource_list:
                    additional_info = (
                        f"{len(error_resource_list)} connectors can be used "
                        f"to gain access to {resource_type} resources required "
                        "for the stack component, but they are in an error "
                        "state or they didn't list any matching resources. "
                    )
                command_args = ""
                if requirements.connector_type:
                    command_args += (
                        f" --connector-type {requirements.connector_type}"
                    )
                command_args += (
                    f" --resource-type {requirements.resource_type}"
                )
                if resource_id:
                    command_args += f" --resource-id {resource_id}"

                cli_utils.error(
                    f"No compatible valid resources were found for the "
                    f"'{component_model.name}' {display_name} in your "
                    f"workspace. {additional_info}You can create a new "
                    "connector using the 'zenml service-connector register' "
                    "command or list the compatible resources using the "
                    f"'zenml service-connector list-resources{command_args}' "
                    "command."
                )

            # Prompt the user to select a connector and a resource ID, if
            # applicable
            connector_id, resource_id = prompt_select_resource(resource_list)
            no_verify = False
        else:
            # Non-interactive mode: we need to fetch the connector model first

            assert connector is not None
            try:
                connector_model = client.get_service_connector(connector)
            except KeyError as err:
                cli_utils.error(
                    f"Could not find a connector '{connector}': " f"{str(err)}"
                )

            connector_id = connector_model.id

            satisfied, msg = requirements.is_satisfied_by(
                connector_model, component_model
            )
            if not satisfied:
                cli_utils.error(
                    f"The connector with ID {connector_id} does not match the "
                    f"component's connector requirements: {msg}. Please pick "
                    "a connector that is compatible with the component "
                    "flavor and try again, or use the interactive mode to "
                    "select a compatible connector."
                )

            if not resource_id:
                if connector_model.resource_id:
                    resource_id = connector_model.resource_id
                elif connector_model.supports_instances:
                    cli_utils.error(
                        f"Multiple {resource_type} resources are available for "
                        "the selected connector. Please use the "
                        "`--resource-id` command line argument to configure a "
                        f"{resource_type} resource or use the interactive mode "
                        "to select a resource interactively."
                    )

        connector_resources: Optional[ServiceConnectorResourcesModel] = None
        if not no_verify:
            with console.status(
                "Validating service connector resource configuration...\n"
            ):
                try:
                    connector_resources = client.verify_service_connector(
                        connector_id,
                        resource_type=requirements.resource_type,
                        resource_id=resource_id,
                    )
                except (
                    KeyError,
                    ValueError,
                    IllegalOperationError,
                    NotImplementedError,
                    AuthorizationException,
                ) as e:
                    cli_utils.error(
                        f"Access to the resource could not be verified: {e}"
                    )

            if connector_resources.resource_ids:
                if len(connector_resources.resource_ids) > 1:
                    cli_utils.error(
                        f"Multiple {resource_type} resources are available for "
                        "the selected connector. Please use the "
                        "`--resource-id` command line argument to configure a "
                        f"{resource_type} resource or use the interactive mode "
                        "to select a resource interactively."
                    )
                else:
                    resource_id = connector_resources.resource_ids[0]

        with console.status(
            f"Updating {display_name} '{name_id_or_prefix}'...\n"
        ):
            try:
                client.update_stack_component(
                    name_id_or_prefix=name_id_or_prefix,
                    component_type=component_type,
                    connector_id=connector_id,
                    connector_resource_id=resource_id,
                )
            except (KeyError, IllegalOperationError) as err:
                cli_utils.error(str(err))

        if connector_resources is not None:
            cli_utils.declare(
                f"Successfully connected {display_name} "
                f"`{component_model.name}` to the following resources:"
            )

            cli_utils.print_service_connector_resource_table(
                [connector_resources]
            )

        else:
            cli_utils.declare(
                f"Successfully connected {display_name} "
                f"`{component_model.name}` to resource."
            )

    return connect_stack_component_command


def register_single_stack_component_cli_commands(
    component_type: StackComponentType, parent_group: click.Group
) -> None:
    """Registers all basic stack component CLI commands.

    Args:
        component_type: Type of the component to generate the command for.
        parent_group: The parent group to register the commands to.
    """
    command_name = component_type.value.replace("_", "-")
    singular_display_name = _component_display_name(component_type)
    plural_display_name = _component_display_name(component_type, plural=True)

    @parent_group.group(
        command_name,
        cls=TagGroup,
        help=f"Commands to interact with {plural_display_name}.",
        tag=CliCategories.STACK_COMPONENTS,
    )
    def command_group() -> None:
        """Group commands for a single stack component type."""
        cli_utils.print_active_config()
        cli_utils.print_active_stack()

    # zenml stack-component get
    get_command = generate_stack_component_get_command(component_type)
    command_group.command(
        "get", help=f"Get the name of the active {singular_display_name}."
    )(get_command)

    # zenml stack-component describe
    describe_command = generate_stack_component_describe_command(
        component_type
    )
    command_group.command(
        "describe",
        help=f"Show details about the (active) {singular_display_name}.",
    )(describe_command)

    # zenml stack-component list
    list_command = generate_stack_component_list_command(component_type)
    command_group.command(
        "list", help=f"List all registered {plural_display_name}."
    )(list_command)

    # zenml stack-component register
    register_command = generate_stack_component_register_command(
        component_type
    )
    context_settings = {"ignore_unknown_options": True}
    command_group.command(
        "register",
        context_settings=context_settings,
        help=f"Register a new {singular_display_name}.",
    )(register_command)

    # zenml stack-component update
    update_command = generate_stack_component_update_command(component_type)
    context_settings = {"ignore_unknown_options": True}
    command_group.command(
        "update",
        context_settings=context_settings,
        help=f"Update a registered {singular_display_name}.",
    )(update_command)

    # zenml stack-component share
    share_command = generate_stack_component_share_command(component_type)
    context_settings = {"ignore_unknown_options": True}
    command_group.command(
        "share",
        context_settings=context_settings,
        help=f"Share a registered {singular_display_name}.",
    )(share_command)

    # zenml stack-component remove-attribute
    remove_attribute_command = (
        generate_stack_component_remove_attribute_command(component_type)
    )
    context_settings = {"ignore_unknown_options": True}
    command_group.command(
        "remove-attribute",
        context_settings=context_settings,
        help=f"Remove attributes from a registered {singular_display_name}.",
    )(remove_attribute_command)

    # zenml stack-component rename
    rename_command = generate_stack_component_rename_command(component_type)
    command_group.command(
        "rename", help=f"Rename a registered {singular_display_name}."
    )(rename_command)

    # zenml stack-component delete
    delete_command = generate_stack_component_delete_command(component_type)
    command_group.command(
        "delete", help=f"Delete a registered {singular_display_name}."
    )(delete_command)

    # zenml stack-component copy
    copy_command = generate_stack_component_copy_command(component_type)
    command_group.command(
        "copy", help=f"Copy a registered {singular_display_name}."
    )(copy_command)

    # zenml stack-component up
    up_command = generate_stack_component_up_command(component_type)
    command_group.command(
        "up",
        help=f"Provisions or resumes local resources for the "
        f"{singular_display_name} if possible.",
    )(up_command)

    # zenml stack-component down
    down_command = generate_stack_component_down_command(component_type)
    command_group.command(
        "down",
        help=f"Suspends resources of the local {singular_display_name} "
        f"deployment.",
    )(down_command)

    # zenml stack-component logs
    logs_command = generate_stack_component_logs_command(component_type)
    command_group.command(
        "logs", help=f"Display {singular_display_name} logs."
    )(logs_command)

    # zenml stack-component connect
    connect_command = generate_stack_component_connect_command(component_type)
    command_group.command(
        "connect",
        help=f"Connect {singular_display_name} to a service connector.",
    )(connect_command)

    # zenml stack-component explain
    explain_command = generate_stack_component_explain_command(component_type)
    command_group.command(
        "explain", help=f"Explaining the {plural_display_name}."
    )(explain_command)

    # zenml stack-component deploy
    deploy_command = generate_stack_component_deploy_command(component_type)
    context_settings = {"ignore_unknown_options": True}
    command_group.command(
        "deploy",
        context_settings=context_settings,
        help=f"Deploy a new {singular_display_name}.",
    )(deploy_command)

    # zenml stack-component destroy
    destroy_command = generate_stack_component_destroy_command(component_type)
    command_group.command(
        "destroy",
        help=f"Destroy an existing {singular_display_name}.",
    )(destroy_command)

    # zenml stack-component flavor
    @command_group.group(
        "flavor", help=f"Commands to interact with {plural_display_name}."
    )
    def flavor_group() -> None:
        """Group commands to handle flavors for a stack component type."""

    # zenml stack-component flavor register
    register_flavor_command = generate_stack_component_flavor_register_command(
        component_type=component_type
    )
    flavor_group.command(
        "register",
        help=f"Register a new {singular_display_name} flavor.",
    )(register_flavor_command)

    # zenml stack-component flavor list
    list_flavor_command = generate_stack_component_flavor_list_command(
        component_type=component_type
    )
    flavor_group.command(
        "list",
        help=f"List all registered flavors for {plural_display_name}.",
    )(list_flavor_command)

    # zenml stack-component flavor describe
    describe_flavor_command = generate_stack_component_flavor_describe_command(
        component_type=component_type
    )
    flavor_group.command(
        "describe",
        help=f"Describe a {singular_display_name} flavor.",
    )(describe_flavor_command)

    # zenml stack-component flavor delete
    delete_flavor_command = generate_stack_component_flavor_delete_command(
        component_type=component_type
    )
    flavor_group.command(
        "delete",
        help=f"Delete a {plural_display_name} flavor.",
    )(delete_flavor_command)


def register_all_stack_component_cli_commands() -> None:
    """Registers CLI commands for all stack components."""
    for component_type in StackComponentType:
        register_single_stack_component_cli_commands(
            component_type, parent_group=cli
        )


register_all_stack_component_cli_commands()
register_annotator_subcommands()
register_secrets_manager_subcommands()
register_feature_store_subcommands()
register_model_deployer_subcommands()
register_model_registry_subcommands()
