"""Service CLI Generator for automatic CRUD command generation.

This module provides ServiceCLIGenerator, which automatically generates
standard Click CLI commands for services implementing CRUDServiceProtocol.

The generator creates commands for:
- list: List all resources
- retrieve: Get a single resource
- create: Create a new resource
- delete: Delete a resource
- exists: Check if a resource exists (if service supports it)

Example:
    >>> from ._service_metadata import ServiceMetadata, CreateParameter
    >>> from ._service_cli_generator import ServiceCLIGenerator
    >>>
    >>> metadata = ServiceMetadata(
    ...     service_name="buckets",
    ...     service_attr="buckets",
    ...     resource_type="Bucket",
    ...     create_params={
    ...         "description": CreateParameter(
    ...             type="str",
    ...             required=False,
    ...             help="Bucket description",
    ...         )
    ...     },
    ... )
    >>>
    >>> generator = ServiceCLIGenerator(metadata)
    >>> buckets_group = generator.register(parent_group)
"""

from enum import Enum
from itertools import islice
from typing import Any, Callable, TypedDict

import click

from ._service_base import (
    ServiceCommandBase,
    common_service_options,
    service_command,
)
from ._service_metadata import ServiceMetadata
from ._service_protocol import has_exists_method, validate_service_protocol
from ._type_registry import get_type


class GeneratorType(str, Enum):
    """Enum for command generator types.

    Attributes:
        AUTO: Auto-generated by ServiceCLIGenerator
        MANUAL: Manually added nested group or custom command
        OVERRIDE: Manual override of auto-generated command
    """

    AUTO = "ServiceCLIGenerator"
    MANUAL = "manual"
    OVERRIDE = "manual_override"


def _optional_decorator(
    condition: bool, decorator: Callable[[Any], Any]
) -> Callable[[Any], Any]:
    """Apply decorator conditionally based on a boolean flag.

    This makes conditional decorator patterns more explicit and readable.

    Args:
        condition: Whether to apply the decorator
        decorator: The decorator function to apply if condition is True

    Returns:
        The decorator if condition is True, otherwise an identity function

    Example:
        >>> @_optional_decorator(
        ...     condition=config.confirmation_required,
        ...     decorator=click.option("--confirm", is_flag=True)
        ... )
        ... def my_command():
        ...     pass
    """
    if condition:
        return decorator
    return lambda f: f


class ProvenanceDict(TypedDict, total=False):
    """Type definition for command provenance tracking.

    Attributes:
        generator: Source of the command (ServiceCLIGenerator, manual, manual_override)
        service_name: Name of the service
        resource_type: Type of resource (e.g., "Bucket")
        command_function: Name of the command function
        type: Type of manual addition (e.g., "nested_group")
        original_generator: Original generator for overridden commands
        overridden_at: Context where override occurred (e.g., "user_custom")
    """

    generator: str
    service_name: str
    resource_type: str
    command_function: str
    type: str
    original_generator: str
    overridden_at: str


class ServiceCLIGenerator:
    """Generates CLI commands for CRUD services.

    This class automatically creates Click commands for services that implement
    the CRUDServiceProtocol. It handles:
    - Command generation from metadata
    - Parameter type resolution
    - Confirmation and dry-run support
    - Output formatting integration
    - Provenance tracking

    Attributes:
        metadata: Service metadata configuration
        _command_names: Set of generated command names
        _provenance: Dict tracking command generation metadata

    Example:
        >>> metadata = ServiceMetadata(
        ...     service_name="buckets",
        ...     service_attr="buckets",
        ...     resource_type="Bucket",
        ... )
        >>> generator = ServiceCLIGenerator(metadata)
        >>> buckets_group = generator.register(parent_group)
    """

    def __init__(self, metadata: ServiceMetadata) -> None:
        """Initialize the generator with service metadata.

        Args:
            metadata: Service metadata configuration

        Raises:
            ValueError: If metadata has invalid types
        """
        self.metadata = metadata
        self._command_names: set[str] = set()
        self._provenance: dict[str, ProvenanceDict] = {}

        self.metadata.validate_types()

    def validate_service(self, client: Any) -> None:
        """Validate that the service implements the required protocol.

        This method should be called early during CLI setup to fail fast
        if the service doesn't implement the required CRUD methods.

        Args:
            client: Client instance with service attributes

        Raises:
            TypeError: If service doesn't implement CRUDServiceProtocol
            AttributeError: If service attribute doesn't exist on client

        Example:
            >>> generator = ServiceCLIGenerator(metadata)
            >>> from uipath import UiPath
            >>> client = UiPath(...)
            >>> generator.validate_service(client)  # Fails early if invalid
            >>> generator.register(cli_group)  # Now safe to register
        """
        service = getattr(client, self.metadata.service_attr)
        validate_service_protocol(service, self.metadata.service_name)

    def register(self, parent_group: click.Group) -> click.Group:
        """Register service commands under a parent Click group.

        This creates a new Click group for the service and adds all
        CRUD commands to it based on the metadata.

        Args:
            parent_group: Parent Click group to add service group to

        Returns:
            The created service Click group

        Raises:
            TypeError: If service doesn't implement CRUDServiceProtocol

        Example:
            >>> @click.group()
            ... def cli():
            ...     pass
            >>> generator = ServiceCLIGenerator(metadata)
            >>> service_group = generator.register(cli)
        """
        service_group = click.Group(
            name=self.metadata.service_name,
            help=f"Manage {self.metadata.resource_plural}",
        )

        self._add_list_command(service_group)
        self._add_retrieve_command(service_group)
        self._add_create_command(service_group)
        self._add_delete_command(service_group)

        self._add_exists_command(service_group)

        parent_group.add_command(service_group)

        return service_group

    def _add_list_command(self, group: click.Group) -> None:
        """Add list command to the group.

        Generates: uipath <service> list [--folder-path PATH] [--folder-key KEY] [--limit N] [--offset N]
        """

        @group.command("list")
        @click.option("--limit", type=int, help="Maximum number of items to return")
        @click.option("--offset", type=int, default=0, help="Number of items to skip")
        @common_service_options
        @service_command
        def list_cmd(
            ctx: click.Context,
            folder_path: str | None,
            folder_key: str | None,
            format: str | None,
            output: str | None,
            limit: int | None,
            offset: int,
        ) -> list[Any]:
            """List resources."""
            client = ServiceCommandBase.get_client(ctx)
            service = getattr(client, self.metadata.service_attr)

            resources = service.list(folder_path=folder_path, folder_key=folder_key)

            if limit is not None:
                return list(islice(resources, offset, offset + limit))
            elif offset > 0:
                return list(islice(resources, offset, None))
            else:
                return list(resources)

        list_cmd.help = f"""List all {self.metadata.resource_plural}.

Examples:
    uipath {self.metadata.service_name} list
    uipath {self.metadata.service_name} list --folder-path Shared
"""

        self._track_command("list", list_cmd)

    def _add_retrieve_command(self, group: click.Group) -> None:
        """Add retrieve command to the group.

        Generates: uipath <service> retrieve <name> [--folder-path PATH]
        """
        identifier = self.metadata.retrieve_identifier

        @group.command("retrieve")
        @click.argument(identifier.upper())
        @common_service_options
        @service_command
        def retrieve_cmd(
            ctx: click.Context,
            folder_path: str | None,
            folder_key: str | None,
            format: str | None,
            output: str | None,
            **kwargs: Any,
        ) -> None:
            """Retrieve a resource."""
            client = ServiceCommandBase.get_client(ctx)
            service = getattr(client, self.metadata.service_attr)

            identifier_value = kwargs.get(identifier)

            resource = service.retrieve(
                identifier_value, folder_path=folder_path, folder_key=folder_key
            )

            return resource

        retrieve_cmd.help = f"""Retrieve a single {self.metadata.resource_type}.

Examples:
    uipath {self.metadata.service_name} retrieve my-resource
    uipath {self.metadata.service_name} retrieve my-resource --folder-path Shared
"""

        self._track_command("retrieve", retrieve_cmd)

    def _add_create_command(self, group: click.Group) -> None:
        """Add create command to the group.

        Generates: uipath <service> create <name> [options] [--folder-path PATH]
        """

        def create_command_decorator(func: Callable[..., Any]) -> Callable[..., Any]:
            """Apply all create parameter decorators to the function."""
            for param_name, param_config in reversed(
                list(self.metadata.create_params.items())
            ):
                option_name = param_config.option_name or param_name
                param_type = get_type(param_config.type)

                # Build Click option
                func = click.option(
                    f"--{option_name.replace('_', '-')}",
                    type=param_type,
                    required=param_config.required,
                    default=param_config.default,
                    help=param_config.help,
                )(func)

            return func

        @group.command("create")
        @click.argument("NAME")
        @create_command_decorator
        @common_service_options
        @service_command
        def create_cmd(
            ctx: click.Context,
            folder_path: str | None,
            folder_key: str | None,
            format: str | None,
            output: str | None,
            name: str,
            **kwargs: Any,
        ) -> None:
            """Create a resource."""
            client = ServiceCommandBase.get_client(ctx)
            service = getattr(client, self.metadata.service_attr)

            create_kwargs = {}
            for param_name in self.metadata.create_params.keys():
                if param_name in kwargs and kwargs[param_name] is not None:
                    create_kwargs[param_name] = kwargs[param_name]

            resource = service.create(
                name, folder_path=folder_path, folder_key=folder_key, **create_kwargs
            )

            click.echo(
                f"Created {self.metadata.resource_type.lower()} '{name}'", err=True
            )
            return resource

        create_cmd.help = f"""Create a new {self.metadata.resource_type}.

Examples:
    uipath {self.metadata.service_name} create my-resource
    uipath {self.metadata.service_name} create my-resource --folder-path Shared
"""

        self._track_command("create", create_cmd)

    def _add_delete_command(self, group: click.Group) -> None:
        """Add delete command to the group.

        Generates: uipath <service> delete <name> [--confirm] [--dry-run] [--folder-path PATH]
        """
        delete_config = self.metadata.delete_cmd

        @group.command("delete")
        @click.argument("NAME")
        @_optional_decorator(
            delete_config.confirmation_required,
            click.option(
                "--confirm",
                is_flag=True,
                help="Confirm deletion without prompting",
            ),
        )
        @_optional_decorator(
            delete_config.dry_run_supported,
            click.option(
                "--dry-run",
                is_flag=True,
                help="Show what would be deleted without actually deleting",
            ),
        )
        @common_service_options
        @service_command
        def delete_cmd(
            ctx: click.Context,
            folder_path: str | None,
            folder_key: str | None,
            format: str | None,
            output: str | None,
            name: str,
            **kwargs: Any,
        ) -> None:
            """Delete a resource."""
            client = ServiceCommandBase.get_client(ctx)
            service = getattr(client, self.metadata.service_attr)

            if delete_config.dry_run_supported:
                dry_run = kwargs.get("dry_run", False)
                if dry_run:
                    click.echo(
                        f"Would delete {self.metadata.resource_type.lower()} '{name}'",
                        err=True,
                    )
                    return

            if delete_config.confirmation_required:
                confirm_flag = kwargs.get("confirm", False)
                if not confirm_flag:
                    if delete_config.confirmation_prompt:
                        prompt = delete_config.confirmation_prompt.format(
                            resource=self.metadata.resource_type, identifier=name
                        )
                    else:
                        prompt = f"Delete {self.metadata.resource_type} '{name}'?"

                    if not click.confirm(prompt):
                        click.echo("Deletion cancelled.")
                        return

            service.delete(name=name, folder_path=folder_path, folder_key=folder_key)

            click.echo(
                f"Deleted {self.metadata.resource_type.lower()} '{name}'", err=True
            )

        delete_cmd.help = f"""Delete a {self.metadata.resource_type}.

Examples:
    uipath {self.metadata.service_name} delete my-resource --confirm
    uipath {self.metadata.service_name} delete my-resource --dry-run
"""

        self._track_command("delete", delete_cmd)

    def _add_exists_command(self, group: click.Group) -> None:
        """Add exists command to the group (if service supports it).

        Generates: uipath <service> exists <name> [--folder-path PATH]

        Note:
            This command is only added to the CLI. The actual validation
            of whether the service implements exists() happens at runtime.
        """
        exists_config = self.metadata.exists_cmd
        identifier = exists_config.identifier_arg_name

        @group.command("exists")
        @click.argument(identifier.upper())
        @common_service_options
        @service_command
        def exists_cmd(
            ctx: click.Context,
            folder_path: str | None,
            folder_key: str | None,
            format: str | None,
            output: str | None,
            **kwargs: Any,
        ) -> bool | dict[str, Any] | None:
            """Check if a resource exists."""
            client = ServiceCommandBase.get_client(ctx)
            service = getattr(client, self.metadata.service_attr)

            if not has_exists_method(service):
                click.echo(
                    f"Error: {self.metadata.resource_type} service does not support exists command",
                    err=True,
                )
                raise click.Abort()

            identifier_value = kwargs.get(identifier)

            exists = service.exists(
                identifier_value, folder_path=folder_path, folder_key=folder_key
            )

            if exists_config.return_format == "bool":
                return exists
            elif exists_config.return_format == "dict":
                return {"exists": exists, identifier: identifier_value}
            else:  # text
                if exists:
                    click.echo(
                        f"{self.metadata.resource_type} '{identifier_value}' exists"
                    )
                else:
                    click.echo(
                        f"{self.metadata.resource_type} '{identifier_value}' does not exist"
                    )
                return None

        exists_cmd.help = f"""Check if a {self.metadata.resource_type} exists.

Examples:
    uipath {self.metadata.service_name} exists my-resource
    uipath {self.metadata.service_name} exists my-resource --folder-path Shared
"""

        self._track_command("exists", exists_cmd)

    def _track_command(self, command_name: str, command: Callable[..., Any]) -> None:
        """Track generated command for provenance.

        Args:
            command_name: Name of the command (e.g., "list", "create")
            command: The Click command function
        """
        self._command_names.add(command_name)

        func_name = command_name
        if hasattr(command, "callback") and hasattr(command.callback, "__name__"):
            func_name = command.callback.__name__
        elif hasattr(command, "__name__"):
            func_name = command.__name__

        provenance: ProvenanceDict = {
            "generator": GeneratorType.AUTO.value,
            "service_name": self.metadata.service_name,
            "resource_type": self.metadata.resource_type,
            "command_function": func_name,
        }

        self._provenance[command_name] = provenance
        setattr(command, "__provenance__", provenance)  # noqa: B010  # Dynamic attribute not in type

    def get_command_names(self) -> set[str]:
        """Get set of generated command names.

        Returns:
            Set of command names (e.g., {"list", "retrieve", "create", "delete", "exists"})
        """
        return self._command_names.copy()

    def get_provenance(self, command_name: str) -> ProvenanceDict | None:
        """Get provenance information for a generated command.

        Args:
            command_name: Name of the command

        Returns:
            Provenance dict or None if command not found
        """
        return self._provenance.get(command_name)

    def add_nested_group(
        self, service_group: click.Group, group_name: str, custom_group: click.Group
    ) -> None:
        """Add a custom nested command group to the service group.

        This allows adding custom operations that don't fit the CRUD pattern,
        such as the buckets file commands (upload, download, list-files, etc.).

        Args:
            service_group: The service group created by register()
            group_name: Name for the nested group (e.g., "file")
            custom_group: The Click group with custom commands

        Raises:
            ValueError: If group_name collides with existing command

        Example:
            >>> @click.group()
            ... def file_group():
            ...     '''File operations'''
            ...     pass
            >>>
            >>> @file_group.command("upload")
            ... def upload_cmd():
            ...     '''Upload a file'''
            ...     pass
            >>>
            >>> generator = ServiceCLIGenerator(metadata)
            >>> service_group = generator.register(parent_group)
            >>> generator.add_nested_group(service_group, "file", file_group)
        """
        if group_name in service_group.commands:
            existing_provenance = self.get_provenance(group_name)
            if existing_provenance:
                raise ValueError(
                    f"Cannot add nested group '{group_name}': "
                    f"conflicts with auto-generated command '{group_name}' "
                    f"(generator: {existing_provenance['generator']})"
                )
            else:
                raise ValueError(
                    f"Cannot add nested group '{group_name}': "
                    f"command name already exists"
                )

        service_group.add_command(custom_group, name=group_name)

        self._command_names.add(group_name)
        provenance: ProvenanceDict = {
            "generator": GeneratorType.MANUAL.value,
            "type": "nested_group",
            "service_name": self.metadata.service_name,
            "command_function": group_name,
        }
        self._provenance[group_name] = provenance
        setattr(custom_group, "__provenance__", provenance)  # noqa: B010  # Dynamic attribute not in type

    def override_command(
        self,
        service_group: click.Group,
        command_name: str,
        custom_command: click.Command,
    ) -> None:
        """Override an auto-generated command with a custom implementation.

        This allows replacing auto-generated CRUD commands with custom logic
        while preserving the same command name and interface.

        Args:
            service_group: The service group created by register()
            command_name: Name of the command to override (e.g., "list", "create")
            custom_command: The custom Click command

        Raises:
            ValueError: If command_name doesn't exist or wasn't auto-generated

        Example:
            >>> @click.command("list")
            ... def custom_list():
            ...     '''Custom list with extra features'''
            ...     pass
            >>>
            >>> generator = ServiceCLIGenerator(metadata)
            >>> service_group = generator.register(parent_group)
            >>> generator.override_command(service_group, "list", custom_list)
        """
        if command_name not in service_group.commands:
            raise ValueError(
                f"Cannot override command '{command_name}': command does not exist"
            )

        provenance = self.get_provenance(command_name)
        if not provenance or provenance.get("generator") != GeneratorType.AUTO.value:
            raise ValueError(
                f"Cannot override command '{command_name}': "
                f"not an auto-generated command (only auto-generated commands can be overridden)"
            )

        del service_group.commands[command_name]
        service_group.add_command(custom_command, name=command_name)

        new_provenance: ProvenanceDict = {
            "generator": GeneratorType.OVERRIDE.value,
            "original_generator": provenance["generator"],
            "service_name": self.metadata.service_name,
            "resource_type": self.metadata.resource_type,
            "command_function": custom_command.callback.__name__
            if hasattr(custom_command, "callback")
            and custom_command.callback is not None
            else command_name,
            "overridden_at": "user_custom",
        }
        self._provenance[command_name] = new_provenance
        setattr(custom_command, "__provenance__", new_provenance)  # noqa: B010  # Dynamic attribute not in type

    def validate_no_collisions(
        self, service_group: click.Group, additional_commands: list[str]
    ) -> list[str]:
        """Validate that additional commands won't collide with generated ones.

        This is useful for checking naming conflicts before adding custom commands.

        Args:
            service_group: The service group created by register()
            additional_commands: List of command names to check

        Returns:
            List of collision errors (empty if no collisions)

        Example:
            >>> generator = ServiceCLIGenerator(metadata)
            >>> service_group = generator.register(parent_group)
            >>> errors = generator.validate_no_collisions(service_group, ["list", "custom"])
            >>> if errors:
            ...     for error in errors:
            ...         print(error)
        """
        collisions = []

        for cmd_name in additional_commands:
            if cmd_name in service_group.commands:
                provenance = self.get_provenance(cmd_name)
                if provenance:
                    collisions.append(
                        f"Command '{cmd_name}' conflicts with auto-generated command "
                        f"(generator: {provenance.get('generator', 'unknown')})"
                    )
                else:
                    collisions.append(
                        f"Command '{cmd_name}' conflicts with existing command"
                    )

        return collisions


__all__ = ["ServiceCLIGenerator"]
