"""Base utilities for documentation generation."""

import re
from collections.abc import Sequence
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from cyclopts.core import App
    from cyclopts.help import HelpPanel

from cyclopts.command_spec import CommandSpec
from cyclopts.help import format_doc, format_usage


def should_show_usage(app: "App") -> bool:
    """Determine if usage should be shown for an app.

    Root apps always show usage (even without default_command, showing "app COMMAND").
    Subcommands only show usage if they have a default_command.
    This skips usage for command groups that can't be invoked directly.

    The determination is made by checking the app_stack depth:
    - Stack length of 1 means root app (just the initial frame)
    - Stack length > 1 means we're in a subcommand context (frames were pushed)

    Parameters
    ----------
    app : App
        The App instance to check.

    Returns
    -------
    bool
        True if usage should be shown.
    """
    # Check if we're in a subcommand context by examining the stack depth
    is_root = len(app.app_stack.stack) == 1

    if is_root:
        # Root app: always show usage
        return True
    else:
        # Subcommand: only show if it has a default_command
        return app.default_command is not None


def should_show_commands_list(app: "App") -> bool:
    """Determine if commands list should be shown for an app.

    Only show commands list for apps with a default_command.
    Command groups (apps without default_command) skip the list
    since their commands will be documented recursively anyway.

    Parameters
    ----------
    app : App
        The App instance to check.

    Returns
    -------
    bool
        True if commands list should be shown.
    """
    return app.default_command is not None


def _is_builtin_flag(app: "App", name: str) -> bool:
    """Check if a flag name is a built-in help or version flag.

    Parameters
    ----------
    app : App
        The App instance to check against.
    name : str
        The flag name to check.

    Returns
    -------
    bool
        True if this is a built-in help or version flag.
    """
    help_flags = set(app.app_stack.resolve("help_flags", fallback=()))
    version_flags = set(app.app_stack.resolve("version_flags", fallback=()))
    builtin_flags = help_flags | version_flags
    return name in builtin_flags


def is_all_builtin_flags(app: "App", names: Sequence[str]) -> bool:
    """Check if all names in the sequence are builtin help or version flags.

    Parameters
    ----------
    app : App
        The App instance to check against.
    names : Sequence[str]
        Sequence of flag names to check.

    Returns
    -------
    bool
        True if all names are builtin flags.
    """
    if not names:
        return False
    return all(_is_builtin_flag(app, name) for name in names)


def normalize_command_filters(
    commands_filter: list[str] | None = None,
    exclude_commands: list[str] | None = None,
) -> tuple[set[str] | None, set[str] | None]:
    """Normalize command filter lists by converting underscores to dashes.

    Parameters
    ----------
    commands_filter : list[str] | None
        List of commands to include.
    exclude_commands : list[str] | None
        List of commands to exclude.

    Returns
    -------
    tuple[set[str] | None, set[str] | None]
        Normalized include and exclude sets for O(1) lookup.
    """
    normalized_include = None
    if commands_filter is not None:
        normalized_include = {cmd.replace("_", "-") for cmd in commands_filter}

    normalized_exclude = None
    if exclude_commands:
        normalized_exclude = {cmd.replace("_", "-") for cmd in exclude_commands}

    return normalized_include, normalized_exclude


def should_include_command(
    name: str,
    parent_path: list[str],
    normalized_commands_filter: set[str] | None,
    normalized_exclude_commands: set[str] | None,
    subapp: "App",
) -> bool:
    """Determine if a command should be included based on filters.

    Parameters
    ----------
    name : str
        The command name.
    parent_path : list[str]
        Path to parent commands.
    normalized_commands_filter : set[str] | None
        Set of commands to include (already normalized).
    normalized_exclude_commands : set[str] | None
        Set of commands to exclude (already normalized).
    subapp : App
        The subcommand App instance.

    Returns
    -------
    bool
        True if the command should be included, False otherwise.
    """
    full_path = ".".join(parent_path + [name]) if parent_path else name

    if normalized_exclude_commands:
        if name in normalized_exclude_commands or full_path in normalized_exclude_commands:
            return False
        for i in range(len(parent_path)):
            parent_segment = ".".join(parent_path[: i + 1])
            if parent_segment in normalized_exclude_commands:
                return False

    if normalized_commands_filter is not None:
        if name in normalized_commands_filter or full_path in normalized_commands_filter:
            return True

        for i in range(len(parent_path)):
            parent_segment = ".".join(parent_path[: i + 1])
            if parent_segment in normalized_commands_filter:
                return True

        if hasattr(subapp, "_commands") and subapp._commands:
            for filter_cmd in normalized_commands_filter:
                if filter_cmd.startswith(full_path + "."):
                    return True

        return False

    return True


def adjust_filters_for_subcommand(
    name: str,
    normalized_commands_filter: set[str] | None,
    normalized_exclude_commands: set[str] | None,
) -> tuple[list[str] | None, list[str] | None]:
    """Adjust filter lists for subcommand context.

    Parameters
    ----------
    name : str
        The current command name.
    normalized_commands_filter : set[str] | None
        Set of commands to include (already normalized).
    normalized_exclude_commands : set[str] | None
        Set of commands to exclude (already normalized).

    Returns
    -------
    tuple[list[str] | None, list[str] | None]
        Adjusted commands_filter and exclude_commands lists (denormalized).
    """
    sub_commands_filter = None
    if normalized_commands_filter is not None:
        sub_commands_filter = []
        for filter_cmd in normalized_commands_filter:
            if filter_cmd.startswith(name + "."):
                sub_filter = filter_cmd[len(name) + 1 :]
                sub_commands_filter.append(sub_filter.replace("-", "_"))
            elif filter_cmd == name:
                sub_commands_filter = None
                break

        if sub_commands_filter is not None and not sub_commands_filter:
            sub_commands_filter = []

    sub_exclude_commands = None
    if normalized_exclude_commands:
        sub_exclude_commands = []
        for exclude_cmd in normalized_exclude_commands:
            if exclude_cmd.startswith(name + "."):
                sub_exclude = exclude_cmd[len(name) + 1 :]
                sub_exclude_commands.append(sub_exclude.replace("-", "_"))
            else:
                sub_exclude_commands.append(exclude_cmd.replace("-", "_"))

    return sub_commands_filter, sub_exclude_commands


def get_app_info(app: "App", command_chain: list[str] | None = None) -> tuple[str, str, str]:
    """Get app name, full command path, and title.

    Parameters
    ----------
    app : App
        The cyclopts App instance.
    command_chain : Optional[List[str]]
        Chain of parent commands leading to this app.

    Returns
    -------
    Tuple[str, str, str]
        (app_name, full_command, title)
    """
    if not command_chain:
        app_name = app.name[0]
        full_command = app_name
        title = app_name
    else:
        app_name = command_chain[0]
        full_command = " ".join(command_chain)
        title = full_command

    return app_name, full_command, title


def build_command_chain(command_chain: list[str] | None, command_name: str, app_name: str) -> list[str]:
    """Build command chain for a subcommand.

    Parameters
    ----------
    command_chain : Optional[List[str]]
        Current command chain.
    command_name : str
        Name of the subcommand.
    app_name : str
        Name of the root app.

    Returns
    -------
    List[str]
        Updated command chain.
    """
    if command_chain:
        return command_chain + [command_name]
    else:
        return [app_name, command_name]


def generate_anchor(command_path: str) -> str:
    """Generate a URL-friendly anchor from a command path.

    Converts spaces to hyphens and lowercases the string to match
    how markdown/HTML processors generate anchors from headings.
    Strips leading dashes to match markdown processor behavior.

    Parameters
    ----------
    command_path : str
        Full command path (e.g., "myapp files cp").

    Returns
    -------
    str
        Anchor string (e.g., "myapp-files-cp").

    Examples
    --------
    >>> generate_anchor("myapp files cp")
    'myapp-files-cp'
    >>> generate_anchor("myapp --install-completion")
    'myapp-install-completion'
    """
    anchor = command_path.lower().replace(" ", "-")
    # Collapse consecutive dashes to single dash (markdown processors do this)
    anchor = re.sub(r"-+", "-", anchor)
    return anchor


def should_skip_command(command_name: str, subapp: "App", parent_app: "App", include_hidden: bool) -> bool:
    """Check if a command should be skipped.

    Parameters
    ----------
    command_name : str
        Name of the command.
    subapp : App
        The subcommand App instance.
    parent_app : App
        The parent App instance.
    include_hidden : bool
        Whether to include hidden commands.

    Returns
    -------
    bool
        True if command should be skipped.
    """
    if _is_builtin_flag(parent_app, command_name):
        return True

    if not isinstance(subapp, type(parent_app)):
        return True

    if not include_hidden and not subapp.show:
        return True

    return False


def filter_help_entries(app: "App", panel: "HelpPanel", include_hidden: bool) -> list[Any]:
    """Filter help panel entries based on visibility settings.

    Parameters
    ----------
    app : App
        The App instance to check against.
    panel : HelpPanel
        The help panel to filter.
    include_hidden : bool
        Whether to include hidden entries.

    Returns
    -------
    List[Any]
        Filtered panel entries.
    """
    if include_hidden:
        return panel.entries

    return [e for e in panel.entries if not (e.names and is_all_builtin_flags(app, e.names))]


def extract_description(app: "App", help_format: str) -> Any | None:
    """Extract app description.

    Parameters
    ----------
    app : App
        The App instance.
    help_format : str
        Help format type.

    Returns
    -------
    Optional[Any]
        The extracted description object, or None.
    """
    description = format_doc(app, help_format)
    return description


def extract_usage(app: "App") -> Any | None:
    """Extract usage string.

    Parameters
    ----------
    app : App
        The App instance.

    Returns
    -------
    Optional[Any]
        The extracted usage object, or None.
    """
    if app.usage is not None:
        return app.usage if app.usage else None

    usage = format_usage(app, [])
    return usage


def format_usage_line(usage_text: str, command_chain: list[str], prefix: str = "") -> str:
    """Format usage line with proper command path.

    Parameters
    ----------
    usage_text : str
        Raw usage text.
    command_chain : List[str]
        Command chain for the app.
    prefix : str
        Optional prefix for the usage line (e.g., "$").

    Returns
    -------
    str
        Formatted usage line.
    """
    if not usage_text:
        return ""

    if "Usage:" in usage_text:
        usage_text = usage_text.replace("Usage:", "").strip()

    full_command = " ".join(command_chain) if command_chain else ""

    parts = usage_text.split(None, 1)
    if len(parts) > 1 and command_chain:
        usage_line = f"{prefix} {full_command} {parts[1]}" if prefix else f"{full_command} {parts[1]}"
    elif command_chain:
        usage_line = f"{prefix} {full_command}" if prefix else full_command
    else:
        usage_line = f"{prefix} {usage_text}" if prefix else usage_text

    return usage_line.strip()


def iterate_commands(app: "App", include_hidden: bool = False, resolve_lazy: bool = True):
    """Iterate through app commands, yielding valid resolved subapps.

    Automatically resolves CommandSpec instances to App instances.
    Each unique subapp is yielded only once (first occurrence wins).

    Parameters
    ----------
    app : App
        The App instance.
    include_hidden : bool
        Whether to include hidden commands.
    resolve_lazy : bool
        If ``True`` (default), resolve lazy commands (import their modules) to
        include them in the output. If ``False``, skip unresolved lazy commands.
        Set to ``True`` when generating static artifacts that need all commands,
        such as documentation or shell completion scripts.

    Yields
    ------
    Tuple[str, App]
        (command_name, resolved_subapp) for each valid command.
    """
    if not app._commands:
        return

    seen: set[int] = set()

    for name, app_or_spec in app._commands.items():
        if _is_builtin_flag(app, name):
            continue

        if isinstance(app_or_spec, CommandSpec):
            if not app_or_spec.is_resolved and not resolve_lazy:
                continue
            subapp = app_or_spec.resolve(app)
        else:
            subapp = app_or_spec

        if not isinstance(subapp, type(app)):
            continue

        if not include_hidden and not subapp.show:
            continue

        # Skip if we've already yielded this app (alias)
        app_id = id(subapp)
        if app_id in seen:
            continue
        seen.add(app_id)

        yield name, subapp
