"""Lazy-loadable command specification for deferred imports."""

import importlib
from typing import TYPE_CHECKING, Any

from attrs import Factory, define, field

if TYPE_CHECKING:
    from cyclopts.core import App


@define
class CommandSpec:
    """Specification for a command that will be lazily loaded on first access.

    This allows registering commands via import path strings (e.g., "myapp.commands:create")
    without importing them until they're actually used, improving CLI startup time.

    Parameters
    ----------
    import_path : str
        Import path in the format "module.path:attribute_name".
        The attribute should be either a function or an App instance.
    name : str | tuple[str, ...] | None
        CLI command name. If None, will be derived from the attribute name via name_transform.
        For function imports: used as the name of the wrapper App.
        For App imports: must match the App's internal name, or ValueError is raised at resolution.
    app_kwargs : dict
        Keyword arguments to pass to App() if wrapping a function.
        Raises ValueError if used with App imports (Apps should be configured in their own definition).

    Examples
    --------
    >>> from cyclopts import App
    >>> app = App()
    >>> # Lazy load - doesn't import myapp.commands until "create" is executed
    >>> app.command("myapp.commands:create_user", name="create")
    >>> app()
    """

    import_path: str
    name: str | tuple[str, ...] | None = None
    app_kwargs: dict[str, Any] = Factory(dict)

    _resolved: "App | None" = field(init=False, default=None, repr=False)

    def resolve(self, parent_app: "App") -> "App":
        """Import and resolve the command on first access.

        Parameters
        ----------
        parent_app : App
            Parent app to inherit defaults from (help_flags, version_flags, groups).
            Required to match the behavior of direct command registration.

        Returns
        -------
        App
            The resolved App instance, either imported directly or wrapping a function.

        Raises
        ------
        ValueError
            If import_path is not in the correct format "module.path:attribute_name".
        ImportError
            If the module cannot be imported.
        AttributeError
            If the attribute doesn't exist in the module.
        """
        if self._resolved is not None:
            return self._resolved

        # Parse import path
        module_path, _, attr_name = self.import_path.rpartition(":")
        if not module_path or not attr_name:
            raise ValueError(
                f"Invalid import path: {self.import_path!r}. Expected format: 'module.path:attribute_name'"
            )

        # Import the module and get the attribute
        try:
            module = importlib.import_module(module_path)
        except ImportError as e:
            raise ImportError(f"Cannot import module {module_path!r} from {self.import_path!r}") from e

        try:
            target = getattr(module, attr_name)
        except AttributeError as e:
            raise AttributeError(
                f"Module {module_path!r} has no attribute {attr_name!r} (from import path {self.import_path!r})"
            ) from e

        # Wrap in App if needed
        from cyclopts.core import App

        if isinstance(target, App):
            # Validate that no kwargs were provided for App imports
            if self.app_kwargs:
                raise ValueError(
                    f"Cannot apply configuration to imported App. "
                    f"Import path {self.import_path!r} resolves to an App, "
                    f"but kwargs were specified: {self.app_kwargs!r}. "
                    f"Configure the App in its definition instead."
                )

            # Validate that the App's name matches the expected CLI command name
            # The name used for CLI registration is stored in self.name
            if self.name is not None and target.name[0] != self.name:
                raise ValueError(
                    f"Imported App name mismatch. "
                    f"Import path {self.import_path!r} resolves to an App with name={target.name[0]!r}, "
                    f"but it was registered with CLI command name={self.name!r}. "
                    f"Either use app.command('{self.import_path}', name='{target.name[0]}') "
                    f"or change the App's name to match."
                )

            # Copy parent groups if not set (matches direct App registration behavior)
            from cyclopts.core import _apply_parent_defaults_to_app

            _apply_parent_defaults_to_app(target, parent_app)

            self._resolved = target
        else:
            # It's a function - wrap it in an App with parent defaults
            # Match the behavior of direct function registration
            app_kwargs = dict(self.app_kwargs)  # Copy to avoid mutating

            from cyclopts.core import _apply_parent_groups_to_kwargs

            app_kwargs.setdefault("help_flags", parent_app.help_flags)
            app_kwargs.setdefault("version_flags", parent_app.version_flags)
            if "version" not in app_kwargs and parent_app.version is not None:
                app_kwargs["version"] = parent_app.version

            _apply_parent_groups_to_kwargs(app_kwargs, parent_app)

            self._resolved = App(name=self.name, **app_kwargs)
            self._resolved.default(target)

        return self._resolved

    @property
    def is_resolved(self) -> bool:
        """Check if this command has been imported and resolved yet."""
        return self._resolved is not None
