# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
"""command.py

Defines the Command class for Falyx CLI.

Commands are callable units representing a menu option or CLI task,
wrapping either a BaseAction or a simple function. They provide:

- Hook lifecycle (before, on_success, on_error, after, on_teardown)
- Execution timing and duration tracking
- Retry logic (single action or recursively through action trees)
- Confirmation prompts and spinner integration
- Result capturing and summary logging
- Rich-based preview for CLI display

Every Command is self-contained, configurable, and plays a critical role
in building robust interactive menus.
"""
from __future__ import annotations

import shlex
from typing import Any, Callable

from prompt_toolkit.formatted_text import FormattedText
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
from rich.console import Console
from rich.tree import Tree

from falyx.action.action import Action, BaseAction
from falyx.context import ExecutionContext
from falyx.debug import register_debug_hooks
from falyx.execution_registry import ExecutionRegistry as er
from falyx.hook_manager import HookManager, HookType
from falyx.logger import logger
from falyx.options_manager import OptionsManager
from falyx.parsers.argparse import CommandArgumentParser
from falyx.parsers.signature import infer_args_from_func
from falyx.prompt_utils import confirm_async, should_prompt_user
from falyx.protocols import ArgParserProtocol
from falyx.retry import RetryPolicy
from falyx.retry_utils import enable_retries_recursively
from falyx.signals import CancelSignal
from falyx.themes import OneColors
from falyx.utils import ensure_async

console = Console(color_system="auto")


class Command(BaseModel):
    """
    Represents a selectable command in a Falyx menu system.

    A Command wraps an executable action (function, coroutine, or BaseAction)
    and enhances it with:

    - Lifecycle hooks (before, success, error, after, teardown)
    - Retry support (single action or recursive for chained/grouped actions)
    - Confirmation prompts for safe execution
    - Spinner visuals during execution
    - Tagging for categorization and filtering
    - Rich-based CLI previews
    - Result tracking and summary reporting

    Commands are built to be flexible yet robust, enabling dynamic CLI workflows
    without sacrificing control or reliability.

    Attributes:
        key (str): Primary trigger key for the command.
        description (str): Short description for the menu display.
        hidden (bool): Toggles visibility in the menu.
        aliases (list[str]): Alternate keys or phrases.
        action (BaseAction | Callable): The executable logic.
        args (tuple): Static positional arguments.
        kwargs (dict): Static keyword arguments.
        help_text (str): Additional help or guidance text.
        style (str): Rich style for description.
        confirm (bool): Whether to require confirmation before executing.
        confirm_message (str): Custom confirmation prompt.
        preview_before_confirm (bool): Whether to preview before confirming.
        spinner (bool): Whether to show a spinner during execution.
        spinner_message (str): Spinner text message.
        spinner_type (str): Spinner style (e.g., dots, line, etc.).
        spinner_style (str): Color or style of the spinner.
        spinner_kwargs (dict): Extra spinner configuration.
        hooks (HookManager): Hook manager for lifecycle events.
        retry (bool): Enable retry on failure.
        retry_all (bool): Enable retry across chained or grouped actions.
        retry_policy (RetryPolicy): Retry behavior configuration.
        tags (list[str]): Organizational tags for the command.
        logging_hooks (bool): Whether to attach logging hooks automatically.
        options_manager (OptionsManager): Manages global command-line options.
        arg_parser (CommandArgumentParser): Parses command arguments.
        custom_parser (ArgParserProtocol | None): Custom argument parser.
        custom_help (Callable[[], str | None] | None): Custom help message generator.
        auto_args (bool): Automatically infer arguments from the action.

    Methods:
        __call__(): Executes the command, respecting hooks and retries.
        preview(): Rich tree preview of the command.
        confirmation_prompt(): Formatted prompt for confirmation.
        result: Property exposing the last result.
        log_summary(): Summarizes execution details to the console.
    """

    key: str
    description: str
    action: BaseAction | Callable[..., Any]
    args: tuple = ()
    kwargs: dict[str, Any] = Field(default_factory=dict)
    hidden: bool = False
    aliases: list[str] = Field(default_factory=list)
    help_text: str = ""
    help_epilogue: str = ""
    style: str = OneColors.WHITE
    confirm: bool = False
    confirm_message: str = "Are you sure?"
    preview_before_confirm: bool = True
    spinner: bool = False
    spinner_message: str = "Processing..."
    spinner_type: str = "dots"
    spinner_style: str = OneColors.CYAN
    spinner_kwargs: dict[str, Any] = Field(default_factory=dict)
    hooks: "HookManager" = Field(default_factory=HookManager)
    retry: bool = False
    retry_all: bool = False
    retry_policy: RetryPolicy = Field(default_factory=RetryPolicy)
    tags: list[str] = Field(default_factory=list)
    logging_hooks: bool = False
    options_manager: OptionsManager = Field(default_factory=OptionsManager)
    arg_parser: CommandArgumentParser = Field(default_factory=CommandArgumentParser)
    arguments: list[dict[str, Any]] = Field(default_factory=list)
    argument_config: Callable[[CommandArgumentParser], None] | None = None
    custom_parser: ArgParserProtocol | None = None
    custom_help: Callable[[], str | None] | None = None
    auto_args: bool = True
    arg_metadata: dict[str, str | dict[str, Any]] = Field(default_factory=dict)

    _context: ExecutionContext | None = PrivateAttr(default=None)

    model_config = ConfigDict(arbitrary_types_allowed=True)

    async def parse_args(
        self, raw_args: list[str] | str, from_validate: bool = False
    ) -> tuple[tuple, dict]:
        if callable(self.custom_parser):
            if isinstance(raw_args, str):
                try:
                    raw_args = shlex.split(raw_args)
                except ValueError:
                    logger.warning(
                        "[Command:%s] Failed to split arguments: %s",
                        self.key,
                        raw_args,
                    )
                    return ((), {})
            return self.custom_parser(raw_args)

        if isinstance(raw_args, str):
            try:
                raw_args = shlex.split(raw_args)
            except ValueError:
                logger.warning(
                    "[Command:%s] Failed to split arguments: %s",
                    self.key,
                    raw_args,
                )
                return ((), {})
        return await self.arg_parser.parse_args_split(
            raw_args, from_validate=from_validate
        )

    @field_validator("action", mode="before")
    @classmethod
    def wrap_callable_as_async(cls, action: Any) -> Any:
        if isinstance(action, BaseAction):
            return action
        elif callable(action):
            return ensure_async(action)
        raise TypeError("Action must be a callable or an instance of BaseAction")

    def get_argument_definitions(self) -> list[dict[str, Any]]:
        if self.arguments:
            return self.arguments
        elif callable(self.argument_config):
            self.argument_config(self.arg_parser)
        elif self.auto_args:
            if isinstance(self.action, BaseAction):
                infer_target, maybe_metadata = self.action.get_infer_target()
                # merge metadata with the action's metadata if not already in self.arg_metadata
                if maybe_metadata:
                    self.arg_metadata = {**maybe_metadata, **self.arg_metadata}
                return infer_args_from_func(infer_target, self.arg_metadata)
            elif callable(self.action):
                return infer_args_from_func(self.action, self.arg_metadata)
        return []

    def model_post_init(self, _: Any) -> None:
        """Post-initialization to set up the action and hooks."""
        if self.retry and isinstance(self.action, Action):
            self.action.enable_retry()
        elif self.retry_policy and isinstance(self.action, Action):
            self.action.set_retry_policy(self.retry_policy)
        elif self.retry:
            logger.warning(
                "[Command:%s] Retry requested, but action is not an Action instance.",
                self.key,
            )
        if self.retry_all and isinstance(self.action, BaseAction):
            self.retry_policy.enabled = True
            enable_retries_recursively(self.action, self.retry_policy)
        elif self.retry_all:
            logger.warning(
                "[Command:%s] Retry all requested, but action is not a BaseAction.",
                self.key,
            )

        if self.logging_hooks and isinstance(self.action, BaseAction):
            register_debug_hooks(self.action.hooks)

        for arg_def in self.get_argument_definitions():
            self.arg_parser.add_argument(*arg_def.pop("flags"), **arg_def)

    def _inject_options_manager(self) -> None:
        """Inject the options manager into the action if applicable."""
        if isinstance(self.action, BaseAction):
            self.action.set_options_manager(self.options_manager)

    async def __call__(self, *args, **kwargs) -> Any:
        """
        Run the action with full hook lifecycle, timing, error handling,
        confirmation prompts, preview, and spinner integration.
        """
        self._inject_options_manager()
        combined_args = args + self.args
        combined_kwargs = {**self.kwargs, **kwargs}
        context = ExecutionContext(
            name=self.description,
            args=combined_args,
            kwargs=combined_kwargs,
            action=self,
        )
        self._context = context

        if should_prompt_user(confirm=self.confirm, options=self.options_manager):
            if self.preview_before_confirm:
                await self.preview()
            if not await confirm_async(self.confirmation_prompt):
                logger.info("[Command:%s] ❌ Cancelled by user.", self.key)
                raise CancelSignal(f"[Command:{self.key}] Cancelled by confirmation.")

        context.start_timer()

        try:
            await self.hooks.trigger(HookType.BEFORE, context)
            if self.spinner:
                with console.status(
                    self.spinner_message,
                    spinner=self.spinner_type,
                    spinner_style=self.spinner_style,
                    **self.spinner_kwargs,
                ):
                    result = await self.action(*combined_args, **combined_kwargs)
            else:
                result = await self.action(*combined_args, **combined_kwargs)

            context.result = result
            await self.hooks.trigger(HookType.ON_SUCCESS, context)
            return context.result
        except Exception as error:
            context.exception = error
            await self.hooks.trigger(HookType.ON_ERROR, context)
            raise error
        finally:
            context.stop_timer()
            await self.hooks.trigger(HookType.AFTER, context)
            await self.hooks.trigger(HookType.ON_TEARDOWN, context)
            er.record(context)

    @property
    def result(self) -> Any:
        """Get the result of the action."""
        return self._context.result if self._context else None

    @property
    def confirmation_prompt(self) -> FormattedText:
        """Generate a styled prompt_toolkit FormattedText confirmation message."""
        if self.confirm_message and self.confirm_message != "Are you sure?":
            return FormattedText([("class:confirm", self.confirm_message)])

        action_name = getattr(self.action, "__name__", None)
        if isinstance(self.action, BaseAction):
            action_name = self.action.name

        prompt = [(OneColors.WHITE, "Confirm execution of ")]

        prompt.append((OneColors.BLUE_b, f"{self.key}"))
        prompt.append((OneColors.BLUE_b, f" — {self.description} "))

        if action_name:
            prompt.append(("class:confirm", f"(calls `{action_name}`) "))

        if self.args or self.kwargs:
            prompt.append(
                (OneColors.DARK_YELLOW, f"with args={self.args}, kwargs={self.kwargs} ")
            )

        return FormattedText(prompt)

    def log_summary(self) -> None:
        if self._context:
            self._context.log_summary()

    def show_help(self) -> bool:
        """Display the help message for the command."""
        if callable(self.custom_help):
            output = self.custom_help()
            if output:
                console.print(output)
            return True
        if isinstance(self.arg_parser, CommandArgumentParser):
            self.arg_parser.render_help()
            return True
        return False

    async def preview(self) -> None:
        label = f"[{OneColors.GREEN_b}]Command:[/] '{self.key}' — {self.description}"

        if hasattr(self.action, "preview") and callable(self.action.preview):
            tree = Tree(label)
            await self.action.preview(parent=tree)
            if self.help_text:
                tree.add(f"[dim]💡 {self.help_text}[/dim]")
            console.print(tree)
        elif callable(self.action) and not isinstance(self.action, BaseAction):
            console.print(f"{label}")
            if self.help_text:
                console.print(f"[dim]💡 {self.help_text}[/dim]")
            console.print(
                f"[{OneColors.LIGHT_RED_b}]→ Would call:[/] {self.action.__name__}"
                f"[dim](args={self.args}, kwargs={self.kwargs})[/dim]"
            )
        else:
            console.print(f"{label}")
            if self.help_text:
                console.print(f"[dim]💡 {self.help_text}[/dim]")
            console.print(
                f"[{OneColors.DARK_RED}]⚠️ No preview available for this action.[/]"
            )

    def __str__(self) -> str:
        return (
            f"Command(key='{self.key}', description='{self.description}' "
            f"action='{self.action}')"
        )
