"""Command-line interface for auto-mcp."""

from __future__ import annotations

import fnmatch
import importlib
import importlib.util
import inspect as inspect_module
import json
import sys
from pathlib import Path
from types import ModuleType
from typing import Any, Literal

import click
from rich.console import Console
from rich.panel import Panel
from rich.syntax import Syntax
from rich.table import Table
from rich.tree import Tree

from auto_mcp.cache import PromptCache
from auto_mcp.config import Settings, get_settings
from auto_mcp.core.analyzer import MethodMetadata, ModuleAnalyzer
from auto_mcp.core.generator import GeneratorConfig, MCPGenerator
from auto_mcp.core.package import PackageAnalyzer
from auto_mcp.llm import LLMProvider, create_provider
from auto_mcp.types import FunctionWrapper, get_default_registry
from auto_mcp.watcher import HotReloadServer

console = Console()
error_console = Console(stderr=True)


def load_module_from_path(module_path: Path) -> ModuleType:
    """Load a Python module from a file path.

    Args:
        module_path: Path to the Python file

    Returns:
        The loaded module

    Raises:
        click.ClickException: If the module cannot be loaded
    """
    if not module_path.exists():
        raise click.ClickException(f"Module not found: {module_path}")

    if not module_path.suffix == ".py":
        raise click.ClickException(f"Not a Python file: {module_path}")

    module_name = module_path.stem

    spec = importlib.util.spec_from_file_location(module_name, module_path)
    if spec is None or spec.loader is None:
        raise click.ClickException(f"Cannot load module: {module_path}")

    module = importlib.util.module_from_spec(spec)
    sys.modules[module_name] = module

    try:
        spec.loader.exec_module(module)
    except Exception as e:
        raise click.ClickException(f"Error loading module: {e}") from e

    return module


def load_modules(module_paths: tuple[str, ...]) -> list[ModuleType]:
    """Load multiple modules from paths.

    Args:
        module_paths: Tuple of module path strings

    Returns:
        List of loaded modules
    """
    modules = []
    for path_str in module_paths:
        path = Path(path_str).resolve()
        module = load_module_from_path(path)
        modules.append(module)
    return modules


def get_llm_provider(
    provider: str | None,
    model: str | None,
    settings: Settings,
) -> LLMProvider | None:
    """Get an LLM provider based on settings.

    Args:
        provider: Provider name override
        model: Model name override
        settings: Application settings

    Returns:
        LLM provider instance or None
    """
    provider_name = provider or settings.llm_provider
    model_name = model or settings.llm_model

    # Get API keys from settings
    api_key = None
    if provider_name == "openai":
        api_key = settings.openai_api_key
    elif provider_name == "anthropic":
        api_key = settings.anthropic_api_key

    try:
        return create_provider(
            provider_name,  # type: ignore[arg-type]
            model=model_name,
            api_key=api_key,
        )
    except Exception as e:
        error_console.print(f"[yellow]Warning: Could not create LLM provider: {e}[/yellow]")
        return None


@click.group()
@click.version_option(package_name="auto-mcp")
@click.pass_context
def cli(ctx: click.Context) -> None:
    """auto-mcp: Automatically generate MCP servers from Python modules.

    Use this tool to analyze Python modules and generate MCP-compatible
    servers with tools, resources, and prompts.
    """
    ctx.ensure_object(dict)
    ctx.obj["settings"] = get_settings()


@cli.command()
@click.argument("modules", nargs=-1, required=True, type=click.Path(exists=True))
@click.option(
    "-o",
    "--output",
    type=click.Path(),
    help="Output path for generated file or directory",
)
@click.option(
    "--package",
    type=str,
    help="Generate as a package with this name (instead of standalone file)",
)
@click.option(
    "--name",
    type=str,
    default="auto-mcp-server",
    help="Name for the generated server",
)
@click.option(
    "--llm-provider",
    type=click.Choice(["ollama", "openai", "anthropic"]),
    help="LLM provider for description generation",
)
@click.option(
    "--llm-model",
    type=str,
    help="Model name for description generation",
)
@click.option(
    "--no-llm",
    is_flag=True,
    help="Disable LLM description generation (use docstrings only)",
)
@click.option(
    "--no-cache",
    is_flag=True,
    help="Disable caching of generated descriptions",
)
@click.option(
    "--include-private",
    is_flag=True,
    help="Include private methods (starting with _)",
)
@click.option(
    "--no-resources",
    is_flag=True,
    help="Don't generate MCP resources",
)
@click.option(
    "--no-prompts",
    is_flag=True,
    help="Don't generate MCP prompts",
)
@click.option(
    "--context",
    type=str,
    help="Additional context for LLM description generation",
)
@click.option(
    "--enable-sessions",
    is_flag=True,
    help="Enable session lifecycle support (create_session/close_session tools)",
)
@click.option(
    "--session-ttl",
    type=int,
    default=3600,
    help="Session TTL in seconds (default: 3600)",
)
@click.option(
    "--max-sessions",
    type=int,
    default=100,
    help="Maximum number of concurrent sessions (default: 100)",
)
@click.pass_context
def generate(
    ctx: click.Context,
    modules: tuple[str, ...],
    output: str | None,
    package: str | None,
    name: str,
    llm_provider: str | None,
    llm_model: str | None,
    no_llm: bool,
    no_cache: bool,
    include_private: bool,
    no_resources: bool,
    no_prompts: bool,
    context: str | None,
    enable_sessions: bool,
    session_ttl: int,
    max_sessions: int,
) -> None:
    """Generate an MCP server from Python modules.

    MODULES: One or more Python files to analyze and expose as MCP tools.

    Examples:

        # Generate standalone server file
        auto-mcp generate mymodule.py -o server.py

        # Generate with custom server name
        auto-mcp generate mymodule.py -o server.py --name "My MCP Server"

        # Generate as a package
        auto-mcp generate mymodule.py --package my-server -o ./dist

        # Generate without LLM (use docstrings only)
        auto-mcp generate mymodule.py -o server.py --no-llm

        # Generate with session lifecycle support
        auto-mcp generate mymodule.py -o server.py --enable-sessions

        # Generate with custom session settings
        auto-mcp generate mymodule.py -o server.py --enable-sessions --session-ttl 7200
    """
    settings: Settings = ctx.obj["settings"]

    # Load modules
    with console.status("[bold blue]Loading modules..."):
        loaded_modules = load_modules(modules)
    console.print(f"[green]✓[/green] Loaded {len(loaded_modules)} module(s)")

    # Create LLM provider if enabled
    llm = None
    if not no_llm:
        with console.status("[bold blue]Initializing LLM provider..."):
            llm = get_llm_provider(llm_provider, llm_model, settings)
        if llm:
            console.print(f"[green]✓[/green] Using LLM: {llm.model_name}")
        else:
            console.print("[yellow]![/yellow] LLM disabled, using docstrings only")

    # Create cache
    cache = PromptCache() if not no_cache else PromptCache(cache_dir=None)

    # Create generator config
    config = GeneratorConfig(
        server_name=name,
        include_private=include_private,
        generate_resources=not no_resources,
        generate_prompts=not no_prompts,
        use_cache=not no_cache,
        use_llm=not no_llm and llm is not None,
        enable_sessions=enable_sessions,
        session_ttl=session_ttl,
        max_sessions=max_sessions,
    )

    # Create generator
    generator = MCPGenerator(llm=llm, cache=cache, config=config)

    if enable_sessions:
        console.print("[green]✓[/green] Session lifecycle enabled")

    # Generate output
    if package:
        # Generate package
        output_dir = Path(output) if output else Path.cwd()
        with console.status(f"[bold blue]Generating package '{package}'..."):
            result = generator.generate_package(
                loaded_modules,
                output_dir,
                package,
                context=context,
            )
        console.print(f"[green]✓[/green] Generated package at: {result}")
        console.print(f"\n[dim]To install: pip install {result}[/dim]")
    else:
        # Generate standalone file
        output_path = Path(output) if output else Path("server.py")
        with console.status("[bold blue]Generating standalone server..."):
            result = generator.generate_standalone(
                loaded_modules,
                output_path,
                context=context,
            )
        console.print(f"[green]✓[/green] Generated server at: {result}")
        console.print(f"\n[dim]To run: python {result}[/dim]")

    # Save cache if enabled
    if not no_cache:
        for module in loaded_modules:
            cache.save(module.__name__)


@cli.command()
@click.argument("modules", nargs=-1, required=True, type=click.Path(exists=True))
@click.option(
    "--name",
    type=str,
    default="auto-mcp-server",
    help="Name for the server",
)
@click.option(
    "--llm-provider",
    type=click.Choice(["ollama", "openai", "anthropic"]),
    help="LLM provider for description generation",
)
@click.option(
    "--llm-model",
    type=str,
    help="Model name for description generation",
)
@click.option(
    "--no-llm",
    is_flag=True,
    help="Disable LLM description generation",
)
@click.option(
    "--no-cache",
    is_flag=True,
    help="Disable caching of generated descriptions",
)
@click.option(
    "--include-private",
    is_flag=True,
    help="Include private methods",
)
@click.option(
    "--transport",
    type=click.Choice(["stdio", "sse"]),
    default="stdio",
    help="MCP transport to use",
)
@click.option(
    "--watch",
    is_flag=True,
    help="Enable hot-reload on source file changes",
)
@click.option(
    "--enable-sessions",
    is_flag=True,
    help="Enable session lifecycle support (create_session/close_session tools)",
)
@click.option(
    "--session-ttl",
    type=int,
    default=3600,
    help="Session TTL in seconds (default: 3600)",
)
@click.option(
    "--max-sessions",
    type=int,
    default=100,
    help="Maximum number of concurrent sessions (default: 100)",
)
@click.pass_context
def serve(
    ctx: click.Context,
    modules: tuple[str, ...],
    name: str,
    llm_provider: str | None,
    llm_model: str | None,
    no_llm: bool,
    no_cache: bool,
    include_private: bool,
    transport: Literal["stdio", "sse"],
    watch: bool,
    enable_sessions: bool,
    session_ttl: int,
    max_sessions: int,
) -> None:
    """Run an MCP server from Python modules.

    MODULES: One or more Python files to expose as MCP tools.

    Examples:

        # Run server with stdio transport
        auto-mcp serve mymodule.py

        # Run with custom name
        auto-mcp serve mymodule.py --name "My Server"

        # Run with SSE transport
        auto-mcp serve mymodule.py --transport sse

        # Run with hot-reload enabled
        auto-mcp serve mymodule.py --watch

        # Run with session lifecycle support
        auto-mcp serve mymodule.py --enable-sessions

        # Run with custom session settings
        auto-mcp serve mymodule.py --enable-sessions --session-ttl 7200 --max-sessions 50
    """
    settings: Settings = ctx.obj["settings"]

    # Load modules
    console.print("[bold blue]Loading modules...[/bold blue]")
    loaded_modules = load_modules(modules)
    console.print(f"[green]✓[/green] Loaded {len(loaded_modules)} module(s)")

    # Create LLM provider if enabled
    llm = None
    if not no_llm:
        llm = get_llm_provider(llm_provider, llm_model, settings)
        if llm:
            console.print(f"[green]✓[/green] Using LLM: {llm.model_name}")

    # Create cache
    cache = PromptCache() if not no_cache else PromptCache(cache_dir=None)

    # Create generator config
    config = GeneratorConfig(
        server_name=name,
        include_private=include_private,
        use_cache=not no_cache,
        use_llm=not no_llm and llm is not None,
        enable_sessions=enable_sessions,
        session_ttl=session_ttl,
        max_sessions=max_sessions,
    )

    # Create generator and server
    generator = MCPGenerator(llm=llm, cache=cache, config=config)

    console.print("[bold blue]Creating MCP server...[/bold blue]")

    if enable_sessions:
        console.print("[green]✓[/green] Session lifecycle enabled")

    if watch:
        # Hot-reload mode
        from auto_mcp.api import AutoMCP

        auto = AutoMCP(
            llm_provider=llm_provider or "ollama" if not no_llm else None,  # type: ignore[arg-type]
            llm_model=llm_model,
            use_llm=not no_llm,
            use_cache=not no_cache,
            server_name=name,
            include_private=include_private,
            enable_sessions=enable_sessions,
            session_ttl=session_ttl,
            max_sessions=max_sessions,
        )
        hot_server = HotReloadServer(auto, loaded_modules)

        console.print(f"[green]✓[/green] Server '{name}' ready")
        console.print(f"[dim]Transport: {transport}[/dim]")
        console.print("[yellow]Hot-reload enabled - watching for file changes[/yellow]\n")

        hot_server.run(transport=transport)
    else:
        # Normal mode
        server = generator.create_server(loaded_modules)

        console.print(f"[green]✓[/green] Server '{name}' ready")
        console.print(f"[dim]Transport: {transport}[/dim]\n")

        # Run server
        server.run(transport=transport)


@cli.command()
@click.argument("modules", nargs=-1, required=True, type=click.Path(exists=True))
@click.option(
    "--include-private",
    is_flag=True,
    help="Include private methods",
)
@click.option(
    "--verbose",
    "-v",
    is_flag=True,
    help="Show detailed information about each method",
)
@click.pass_context
def check(
    ctx: click.Context,
    modules: tuple[str, ...],
    include_private: bool,
    verbose: bool,
) -> None:
    """Check modules and show what would be exposed as MCP tools.

    This is a dry-run mode that analyzes modules without generating anything.

    MODULES: One or more Python files to analyze.

    Examples:

        # Check what would be exposed
        auto-mcp check mymodule.py

        # Include private methods in check
        auto-mcp check mymodule.py --include-private

        # Show detailed information
        auto-mcp check mymodule.py -v
    """
    # Load modules
    with console.status("[bold blue]Loading modules..."):
        loaded_modules = load_modules(modules)

    # Create analyzer
    analyzer = ModuleAnalyzer(include_private=include_private)

    total_tools = 0
    total_resources = 0
    total_prompts = 0

    for module in loaded_modules:
        console.print(f"\n[bold]Module: {module.__name__}[/bold]")

        methods = analyzer.analyze_module(module)

        # Categorize methods
        tools = []
        resources = []
        prompts = []

        for method in methods:
            if method.is_resource:
                resources.append(method)
            elif method.is_prompt:
                prompts.append(method)
            else:
                tools.append(method)

        # Display tools
        if tools:
            table = Table(title="Tools", show_header=True)
            table.add_column("Name", style="cyan")
            table.add_column("Async", style="yellow")
            table.add_column("Parameters")
            if verbose:
                table.add_column("Docstring")

            for tool in tools:
                params = ", ".join(p["name"] for p in tool.parameters)
                row = [
                    tool.mcp_metadata.get("tool_name") or tool.name,
                    "✓" if tool.is_async else "",
                    params or "(none)",
                ]
                if verbose:
                    doc = tool.docstring or ""
                    doc_display = doc[:50] + "..." if len(doc) > 50 else doc
                    row.append(doc_display)
                table.add_row(*row)

            console.print(table)
            total_tools += len(tools)

        # Display resources
        if resources:
            table = Table(title="Resources", show_header=True)
            table.add_column("Name", style="cyan")
            table.add_column("URI", style="green")
            if verbose:
                table.add_column("Docstring")

            for resource in resources:
                uri = resource.mcp_metadata.get("resource_uri", f"auto://{resource.name}")
                row = [
                    resource.mcp_metadata.get("resource_name") or resource.name,
                    uri,
                ]
                if verbose:
                    doc = resource.docstring or ""
                    doc_display = doc[:50] + "..." if len(doc) > 50 else doc
                    row.append(doc_display)
                table.add_row(*row)

            console.print(table)
            total_resources += len(resources)

        # Display prompts
        if prompts:
            table = Table(title="Prompts", show_header=True)
            table.add_column("Name", style="cyan")
            table.add_column("Parameters")
            if verbose:
                table.add_column("Docstring")

            for prompt in prompts:
                params = ", ".join(p["name"] for p in prompt.parameters)
                row = [
                    prompt.mcp_metadata.get("prompt_name") or prompt.name,
                    params or "(none)",
                ]
                if verbose:
                    doc = prompt.docstring or ""
                    doc_display = doc[:50] + "..." if len(doc) > 50 else doc
                    row.append(doc_display)
                table.add_row(*row)

            console.print(table)
            total_prompts += len(prompts)

        if not tools and not resources and not prompts:
            console.print("[yellow]No public methods found[/yellow]")

    # Summary
    console.print("\n" + "─" * 40)
    console.print(
        f"[bold]Summary:[/bold] {total_tools} tool(s), "
        f"{total_resources} resource(s), {total_prompts} prompt(s)"
    )


# ─────────────────────────────────────────────────────────────────────────────
# Inspect command helper functions
# ─────────────────────────────────────────────────────────────────────────────

# Description truncation constants for consistent display
_DESC_MAX_WIDTH = 40  # Maximum display width for descriptions
_DESC_TRUNCATE_AT = 37  # Where to truncate (leaves room for "...")
_DESC_TREE_MAX_WIDTH = 60  # Wider limit for tree format


def _format_type_str(type_hint: Any) -> str:
    """Convert a type hint to a readable string."""
    if type_hint is None:
        return "Any"
    if hasattr(type_hint, "__name__"):
        return str(type_hint.__name__)
    return str(type_hint).replace("typing.", "")


def _match_filter(name: str, pattern: str) -> bool:
    """Match name against a glob pattern."""
    return fnmatch.fnmatch(name, pattern)


def _get_schema_for_type(type_hint: Any) -> dict[str, Any]:
    """Get JSON schema for a type hint."""
    if type_hint is None:
        return {"type": "any"}
    try:
        registry = get_default_registry()
        return registry.get_json_schema(type_hint)
    except Exception:
        # Fallback for unsupported types
        return {"type": _format_type_str(type_hint)}


def _build_input_schema_from_metadata(metadata: MethodMetadata) -> dict[str, Any]:
    """Build a fallback input schema from metadata when FunctionWrapper fails.

    Args:
        metadata: MethodMetadata object with parameter information

    Returns:
        JSON schema dictionary for the function's parameters
    """
    properties: dict[str, Any] = {}
    required: list[str] = []

    for param in metadata.parameters:
        param_name = param.get("name", "unknown")
        properties[param_name] = _get_schema_for_type(param.get("type"))
        if not param.get("has_default", False):
            required.append(param_name)

    return {"type": "object", "properties": properties, "required": required}


def _build_inspection_data(
    metadata: MethodMetadata,
    func: Any,
    show_schema: bool = False,
    show_source: bool = False,
) -> dict[str, Any]:
    """Build inspection dictionary from method metadata.

    Args:
        metadata: MethodMetadata object with function information
        func: The actual function/method object (may be None if not found)
        show_schema: Whether to generate JSON schemas for parameters/return
        show_source: Whether to include function source code

    Returns:
        Dictionary with keys: name, qualified_name, description, is_async,
        parameters, return_type, and optionally schemas and source code.
    """
    data: dict[str, Any] = {
        "name": metadata.mcp_metadata.get("tool_name") or metadata.name,
        "qualified_name": metadata.qualified_name,
        "description": metadata.docstring or "",
        "is_async": metadata.is_async,
        "is_tool": metadata.is_tool,
        "is_resource": metadata.is_resource,
        "is_prompt": metadata.is_prompt,
        "decorators": metadata.decorators,
        "parameters": [],
        "return_type": _format_type_str(metadata.return_type),
    }

    # Add MCP-specific metadata
    if metadata.mcp_metadata:
        data["mcp_metadata"] = {
            k: v for k, v in metadata.mcp_metadata.items() if not k.startswith("is_")
        }

    # Build parameter info
    for param in metadata.parameters:
        if "name" not in param:
            continue  # Skip malformed parameters
        param_data: dict[str, Any] = {
            "name": param["name"],
            "type": param.get("type_str", "Any"),
            "required": not param.get("has_default", False),
            "default": param.get("default"),
        }
        if show_schema:
            param_data["schema"] = _get_schema_for_type(param.get("type"))
        data["parameters"].append(param_data)

    # Add schemas if requested
    if show_schema:
        if func is not None:
            try:
                wrapper = FunctionWrapper(func)
                data["input_schema"] = wrapper.get_json_schema()
                data["return_schema"] = wrapper.get_return_schema()
            except Exception:
                # Fall back to metadata-based schema generation
                data["input_schema"] = _build_input_schema_from_metadata(metadata)
                data["return_schema"] = _get_schema_for_type(metadata.return_type)
        else:
            # func not available, use metadata-based schema
            data["input_schema"] = _build_input_schema_from_metadata(metadata)
            data["return_schema"] = _get_schema_for_type(metadata.return_type)

    # Add source if requested
    if show_source:
        data["source"] = metadata.source_code

    return data


def _display_table_format(
    module_name: str,
    tools: list[dict[str, Any]],
    resources: list[dict[str, Any]],
    prompts: list[dict[str, Any]],
    show_schema: bool,
    show_source: bool = False,
) -> None:
    """Display inspection results in table format.

    Args:
        module_name: Name of the module being displayed
        tools: List of tool inspection data dictionaries
        resources: List of resource inspection data dictionaries
        prompts: List of prompt inspection data dictionaries
        show_schema: Whether to show detailed schemas (triggers verbose display)
        show_source: Whether to show source code in detailed view
    """
    console.print(f"\n[bold cyan]Module: {module_name}[/bold cyan]")
    console.print("─" * 50)

    # Display tools
    if tools:
        console.print(f"\n[bold]Tools[/bold] ({len(tools)})")
        if not show_schema:
            # Compact table
            table = Table(show_header=True, header_style="bold")
            table.add_column("Name", style="cyan")
            table.add_column("Description", max_width=40)
            table.add_column("Async", justify="center")
            table.add_column("Parameters")

            for tool in tools:
                params = ", ".join(p["name"] for p in tool["parameters"])
                desc = tool["description"]
                desc_display = (
                    desc[:_DESC_TRUNCATE_AT] + "..." if len(desc) > _DESC_MAX_WIDTH else desc
                )
                table.add_row(
                    tool["name"],
                    desc_display,
                    "[yellow]*[/yellow]" if tool["is_async"] else "",
                    params or "(none)",
                )
            console.print(table)
        else:
            # Detailed view with schemas
            for tool in tools:
                _display_tool_detail(tool, show_source)

    # Display resources
    if resources:
        console.print(f"\n[bold]Resources[/bold] ({len(resources)})")
        table = Table(show_header=True, header_style="bold")
        table.add_column("Name", style="cyan")
        table.add_column("URI", style="green")
        table.add_column("Description", max_width=40)

        for res in resources:
            uri = res.get("mcp_metadata", {}).get("resource_uri", f"auto://{res['name']}")
            desc = res["description"]
            desc_display = desc[:_DESC_TRUNCATE_AT] + "..." if len(desc) > _DESC_MAX_WIDTH else desc
            table.add_row(res["name"], uri, desc_display)
        console.print(table)

    # Display prompts
    if prompts:
        console.print(f"\n[bold]Prompts[/bold] ({len(prompts)})")
        table = Table(show_header=True, header_style="bold")
        table.add_column("Name", style="cyan")
        table.add_column("Parameters")
        table.add_column("Description", max_width=40)

        for prompt in prompts:
            params = ", ".join(p["name"] for p in prompt["parameters"])
            desc = prompt["description"]
            desc_display = desc[:_DESC_TRUNCATE_AT] + "..." if len(desc) > _DESC_MAX_WIDTH else desc
            table.add_row(prompt["name"], params or "(none)", desc_display)
        console.print(table)

    if not tools and not resources and not prompts:
        console.print("[yellow]No components found[/yellow]")


def _display_tool_detail(tool: dict[str, Any], show_source: bool) -> None:
    """Display detailed information for a single tool."""
    console.print()
    console.print(
        Panel(
            f"[bold]{tool['name']}[/bold]",
            subtitle=f"{'async ' if tool['is_async'] else ''}function",
            style="cyan",
        )
    )

    if tool["description"]:
        console.print(f"[dim]{tool['description']}[/dim]")

    # Parameters table
    if tool["parameters"]:
        console.print("\n[bold]Parameters:[/bold]")
        param_table = Table(show_header=True, header_style="bold", box=None)
        param_table.add_column("Name")
        param_table.add_column("Type")
        param_table.add_column("Required")
        param_table.add_column("Default")
        if "schema" in tool["parameters"][0]:
            param_table.add_column("Schema")

        for param in tool["parameters"]:
            row = [
                f"[cyan]{param['name']}[/cyan]",
                param["type"],
                "[green]yes[/green]" if param["required"] else "[dim]no[/dim]",
                str(param["default"]) if param["default"] is not None else "[dim]-[/dim]",
            ]
            if "schema" in param:
                schema_str = json.dumps(param["schema"], separators=(",", ":"))
                if len(schema_str) > 30:
                    schema_str = schema_str[:27] + "..."
                row.append(f"[dim]{schema_str}[/dim]")
            param_table.add_row(*row)
        console.print(param_table)
    else:
        console.print("\n[dim]No parameters[/dim]")

    # Return type
    console.print(f"\n[bold]Returns:[/bold] {tool['return_type']}")
    if "return_schema" in tool and tool["return_schema"]:
        schema_str = json.dumps(tool["return_schema"], indent=2)
        console.print(Syntax(schema_str, "json", theme="monokai", line_numbers=False))

    # Source code
    if show_source and "source" in tool and tool["source"]:
        console.print("\n[bold]Source:[/bold]")
        console.print(Syntax(tool["source"], "python", theme="monokai", line_numbers=True))


def _display_json_format(
    module_name: str,
    tools: list[dict[str, Any]],
    resources: list[dict[str, Any]],
    prompts: list[dict[str, Any]],
) -> None:
    """Display inspection results in JSON format."""
    output = {
        "module": module_name,
        "tools": tools,
        "resources": resources,
        "prompts": prompts,
    }
    console.print(Syntax(json.dumps(output, indent=2, default=str), "json", theme="monokai"))


def _display_tree_format(
    module_name: str,
    tools: list[dict[str, Any]],
    resources: list[dict[str, Any]],
    prompts: list[dict[str, Any]],
) -> None:
    """Display inspection results in tree format."""
    tree = Tree(f"[bold cyan]{module_name}[/bold cyan]")

    # Tools branch
    if tools:
        tools_branch = tree.add(f"[bold]Tools[/bold] ({len(tools)})")
        for tool in tools:
            params = ", ".join(f"{p['name']}: {p['type']}" for p in tool["parameters"])
            sig = f"[cyan]{tool['name']}[/cyan]({params}) -> {tool['return_type']}"
            if tool["is_async"]:
                sig = f"[yellow]async[/yellow] {sig}"
            tool_branch = tools_branch.add(sig)
            if tool["description"]:
                desc = tool["description"]
                desc_display = (
                    desc[: _DESC_TREE_MAX_WIDTH - 3] + "..."
                    if len(desc) > _DESC_TREE_MAX_WIDTH
                    else desc
                )
                tool_branch.add(f"[dim]{desc_display}[/dim]")

    # Resources branch
    if resources:
        res_branch = tree.add(f"[bold]Resources[/bold] ({len(resources)})")
        for res in resources:
            uri = res.get("mcp_metadata", {}).get("resource_uri", f"auto://{res['name']}")
            res_item = res_branch.add(f"[green]{uri}[/green]")
            res_item.add(f"[cyan]{res['name']}[/cyan]")
            if res["description"]:
                desc = res["description"]
                desc_display = (
                    desc[: _DESC_TREE_MAX_WIDTH - 10] + "..."
                    if len(desc) > _DESC_TREE_MAX_WIDTH - 10
                    else desc
                )
                res_item.add(f"[dim]{desc_display}[/dim]")

    # Prompts branch
    if prompts:
        prompts_branch = tree.add(f"[bold]Prompts[/bold] ({len(prompts)})")
        for prompt in prompts:
            params = ", ".join(p["name"] for p in prompt["parameters"])
            prompt_item = prompts_branch.add(f"[magenta]{prompt['name']}[/magenta]({params})")
            if prompt["description"]:
                desc = prompt["description"]
                desc_display = (
                    desc[: _DESC_TREE_MAX_WIDTH - 10] + "..."
                    if len(desc) > _DESC_TREE_MAX_WIDTH - 10
                    else desc
                )
                prompt_item.add(f"[dim]{desc_display}[/dim]")

    if not tools and not resources and not prompts:
        tree.add("[yellow]No components found[/yellow]")

    console.print(tree)


@cli.command()
@click.argument("modules", nargs=-1, required=True, type=click.Path(exists=True))
@click.option(
    "-f",
    "--format",
    "output_format",
    type=click.Choice(["table", "json", "tree"]),
    default="table",
    help="Output format",
)
@click.option(
    "--filter",
    "name_filter",
    type=str,
    help="Filter by name (glob pattern, e.g. 'get_*')",
)
@click.option(
    "-t",
    "--type",
    "component_type",
    type=click.Choice(["tools", "resources", "prompts", "all"]),
    default="all",
    help="Component type to show",
)
@click.option(
    "-s",
    "--show-schema",
    is_flag=True,
    help="Show JSON schemas for parameters and return types",
)
@click.option(
    "--show-source",
    is_flag=True,
    help="Show function source code",
)
@click.option(
    "--include-private",
    is_flag=True,
    help="Include private methods (starting with _)",
)
@click.option(
    "-v",
    "--verbose",
    is_flag=True,
    help="Verbose output (implies --show-schema)",
)
@click.pass_context
def inspect(
    ctx: click.Context,
    modules: tuple[str, ...],
    output_format: str,
    name_filter: str | None,
    component_type: str,
    show_schema: bool,
    show_source: bool,
    include_private: bool,
    verbose: bool,
) -> None:
    """Inspect modules and display detailed MCP component information.

    This command provides deep visibility into MCP servers - view tools,
    schemas, parameter types, and metadata without running the server.

    MODULES: One or more Python files to inspect.

    Examples:

        # Basic inspection
        auto-mcp inspect mymodule.py

        # Verbose with schemas
        auto-mcp inspect mymodule.py -v

        # JSON output
        auto-mcp inspect mymodule.py -f json

        # Tree view
        auto-mcp inspect mymodule.py -f tree

        # Filter by name
        auto-mcp inspect mymodule.py --filter "get_*"

        # Show only tools
        auto-mcp inspect mymodule.py -t tools
    """
    # Verbose implies show_schema
    if verbose:
        show_schema = True

    # Load modules
    with console.status("[bold blue]Loading modules..."):
        loaded_modules = load_modules(modules)

    # Create analyzer
    analyzer = ModuleAnalyzer(include_private=include_private)

    # Accumulate totals across all modules
    total_tools = 0
    total_resources = 0
    total_prompts = 0

    for module in loaded_modules:
        methods = analyzer.analyze_module(module)

        # Get function references for schema generation (only actual functions/methods)
        func_map: dict[str, Any] = {}
        for name in dir(module):
            obj = getattr(module, name, None)
            if inspect_module.isfunction(obj) or inspect_module.ismethod(obj):
                func_map[name] = obj

        # Categorize and build inspection data
        tools: list[dict[str, Any]] = []
        resources: list[dict[str, Any]] = []
        prompts: list[dict[str, Any]] = []

        for method in methods:
            # Get the actual function
            func = func_map.get(method.name)

            # Build inspection data
            data = _build_inspection_data(
                method,
                func,
                show_schema=show_schema,
                show_source=show_source,
            )

            # Apply name filter
            if name_filter and not _match_filter(data["name"], name_filter):
                continue

            # Categorize
            if method.is_resource:
                resources.append(data)
            elif method.is_prompt:
                prompts.append(data)
            else:
                tools.append(data)

        # Apply component type filter
        if component_type == "tools":
            resources, prompts = [], []
        elif component_type == "resources":
            tools, prompts = [], []
        elif component_type == "prompts":
            tools, resources = [], []

        # Accumulate totals
        total_tools += len(tools)
        total_resources += len(resources)
        total_prompts += len(prompts)

        # Display based on format
        if output_format == "json":
            _display_json_format(module.__name__, tools, resources, prompts)
        elif output_format == "tree":
            _display_tree_format(module.__name__, tools, resources, prompts)
        else:
            _display_table_format(
                module.__name__,
                tools,
                resources,
                prompts,
                show_schema,
                show_source,
            )

    # Summary for table format
    if output_format == "table":
        console.print("\n" + "─" * 50)
        console.print(
            f"[bold]Total:[/bold] {total_tools} tool(s), "
            f"{total_resources} resource(s), {total_prompts} prompt(s)"
        )


@cli.group()
def cache() -> None:
    """Manage the description cache."""
    pass


@cache.command(name="clear")
@click.argument("modules", nargs=-1, type=click.Path(exists=True))
@click.option(
    "--all",
    "clear_all",
    is_flag=True,
    help="Clear all cached entries",
)
@click.pass_context
def cache_clear(
    ctx: click.Context,
    modules: tuple[str, ...],
    clear_all: bool,
) -> None:
    """Clear cached descriptions.

    MODULES: Optional module files to clear cache for.

    Examples:

        # Clear cache for specific modules
        auto-mcp cache clear mymodule.py

        # Clear all cache
        auto-mcp cache clear --all
    """
    cache_instance = PromptCache()

    if clear_all:
        count = cache_instance.clear()
        console.print(f"[green]✓[/green] Cleared {count} cached entries")
    elif modules:
        total = 0
        for module_path in modules:
            path = Path(module_path)
            module_name = path.stem
            count = cache_instance.invalidate(module_name)
            total += count
            console.print(f"[green]✓[/green] Cleared {count} entries for {module_name}")
        console.print(f"\n[bold]Total:[/bold] {total} entries cleared")
    else:
        raise click.ClickException("Specify modules to clear or use --all")


@cache.command(name="stats")
@click.pass_context
def cache_stats(ctx: click.Context) -> None:
    """Show cache statistics.

    Examples:

        auto-mcp cache stats
    """
    cache_instance = PromptCache()
    stats = cache_instance.get_stats()

    panel = Panel(
        f"""[cyan]Hits:[/cyan] {stats.hits}
[cyan]Misses:[/cyan] {stats.misses}
[cyan]Hit Rate:[/cyan] {stats.hit_rate:.1%}
[cyan]Total Entries:[/cyan] {stats.total_entries}
[cyan]Invalidations:[/cyan] {stats.invalidations}""",
        title="Cache Statistics",
    )
    console.print(panel)


@cli.group()
def config() -> None:
    """View and manage configuration."""
    pass


@config.command(name="show")
@click.pass_context
def config_show(ctx: click.Context) -> None:
    """Show current configuration.

    Examples:

        auto-mcp config show
    """
    settings: Settings = ctx.obj["settings"]

    table = Table(title="Current Configuration", show_header=True)
    table.add_column("Setting", style="cyan")
    table.add_column("Value")

    table.add_row("LLM Provider", settings.llm_provider)
    table.add_row("LLM Model", settings.llm_model)
    table.add_row("LLM Base URL", settings.llm_base_url or "(default)")
    table.add_row("OpenAI API Key", "***" if settings.openai_api_key else "(not set)")
    table.add_row("Anthropic API Key", "***" if settings.anthropic_api_key else "(not set)")
    table.add_row("Cache Enabled", str(settings.cache_enabled))
    table.add_row("Cache Directory", settings.cache_dir or "(default)")
    table.add_row("Server Name", settings.server_name)
    table.add_row("Transport", settings.transport)
    table.add_row("Include Private", str(settings.include_private))
    table.add_row("Generate Resources", str(settings.generate_resources))
    table.add_row("Generate Prompts", str(settings.generate_prompts))
    table.add_row("Enable Sessions", str(settings.enable_sessions))
    table.add_row("Session TTL", f"{settings.session_ttl}s")
    table.add_row("Max Sessions", str(settings.max_sessions))

    console.print(table)

    console.print(
        "\n[dim]Configuration is loaded from environment variables with AUTO_MCP_ prefix.[/dim]"
    )
    console.print("[dim]Example: AUTO_MCP_LLM_PROVIDER=openai[/dim]")


@config.command(name="env")
@click.pass_context
def config_env(ctx: click.Context) -> None:
    """Show environment variable names for configuration.

    Examples:

        auto-mcp config env
    """
    env_vars = [
        ("AUTO_MCP_LLM_PROVIDER", "LLM provider (ollama, openai, anthropic)"),
        ("AUTO_MCP_LLM_MODEL", "Model name"),
        ("AUTO_MCP_LLM_BASE_URL", "Custom LLM endpoint URL"),
        ("AUTO_MCP_OPENAI_API_KEY", "OpenAI API key"),
        ("AUTO_MCP_ANTHROPIC_API_KEY", "Anthropic API key"),
        ("AUTO_MCP_CACHE_ENABLED", "Enable caching (true/false)"),
        ("AUTO_MCP_CACHE_DIR", "Cache directory path"),
        ("AUTO_MCP_SERVER_NAME", "Default server name"),
        ("AUTO_MCP_TRANSPORT", "MCP transport (stdio, sse)"),
        ("AUTO_MCP_INCLUDE_PRIVATE", "Include private methods (true/false)"),
        ("AUTO_MCP_GENERATE_RESOURCES", "Generate resources (true/false)"),
        ("AUTO_MCP_GENERATE_PROMPTS", "Generate prompts (true/false)"),
        ("AUTO_MCP_ENABLE_SESSIONS", "Enable session lifecycle (true/false)"),
        ("AUTO_MCP_SESSION_TTL", "Session TTL in seconds (default: 3600)"),
        ("AUTO_MCP_MAX_SESSIONS", "Maximum concurrent sessions (default: 100)"),
    ]

    table = Table(title="Environment Variables", show_header=True)
    table.add_column("Variable", style="cyan")
    table.add_column("Description")

    for var, desc in env_vars:
        table.add_row(var, desc)

    console.print(table)


# Package commands for analyzing installed packages
@cli.group()
def package() -> None:
    """Analyze and generate servers from installed Python packages.

    These commands work with installed packages (e.g., requests, json)
    rather than local Python files.
    """
    pass


@package.command(name="check")
@click.argument("package_name", required=True)
@click.option(
    "--max-depth",
    type=int,
    default=None,
    help="Maximum recursion depth for submodule discovery",
)
@click.option(
    "--include-private",
    is_flag=True,
    help="Include private modules and methods",
)
@click.option(
    "--public-api-only",
    is_flag=True,
    help="Only show functions in __all__ (public API)",
)
@click.option(
    "--include",
    "include_patterns",
    multiple=True,
    help="Glob patterns for modules to include (e.g., 'requests.api.*')",
)
@click.option(
    "--exclude",
    "exclude_patterns",
    multiple=True,
    help="Glob patterns for modules to exclude (e.g., 'requests.compat.*')",
)
@click.option(
    "--verbose",
    "-v",
    is_flag=True,
    help="Show detailed information about each method",
)
@click.option(
    "--include-reexports",
    is_flag=True,
    help="Include functions re-exported in __all__ from submodules (e.g., pandas, numpy)",
)
@click.option(
    "--no-isolated",
    is_flag=True,
    help="Force local execution only (fail if package not installed)",
)
@click.option(
    "--version",
    "pkg_version",
    type=str,
    default=None,
    help="Package version to use with uvx (e.g., '2.28.0')",
)
@click.pass_context
def package_check(
    ctx: click.Context,
    package_name: str,
    max_depth: int | None,
    include_private: bool,
    public_api_only: bool,
    include_patterns: tuple[str, ...],
    exclude_patterns: tuple[str, ...],
    verbose: bool,
    include_reexports: bool,
    no_isolated: bool,
    pkg_version: str | None,
) -> None:
    """Analyze an installed package and show what would be exposed.

    PACKAGE_NAME: Name of the installed package (e.g., 'requests', 'json')

    If the package is not installed locally, it will automatically be analyzed
    in an isolated uvx environment. Use --no-isolated to disable this behavior.

    Examples:

        # Check requests package
        auto-mcp package check requests

        # Check with depth limit
        auto-mcp package check boto3 --max-depth 2

        # Check only public API
        auto-mcp package check requests --public-api-only

        # Check with module filtering
        auto-mcp package check requests --include 'requests.api.*'

        # Verbose output
        auto-mcp package check requests -v

        # Check a specific version (uses uvx)
        auto-mcp package check requests --version 2.28.0
        auto-mcp package check requests==2.28.0
    """
    from auto_mcp.isolation import IsolationManager, check_uvx_available
    from auto_mcp.isolation.manager import IsolationConfig, IsolationError, PackageNotFoundError

    # Create isolation manager
    isolation = IsolationManager(
        package_name=package_name,
        version=pkg_version,
        force_local=no_isolated,
    )

    # Determine execution mode
    use_isolation = isolation.should_use_isolation()

    # If version specified, always use isolation
    if pkg_version:
        use_isolation = True

    if use_isolation:
        # Check if uvx is available
        if not check_uvx_available():
            raise click.ClickException(
                f"Package '{package_name}' is not installed locally and uvx is not available.\n"
                "Either install the package or install uv: https://docs.astral.sh/uv/"
            )

        # Run in isolation
        console.print(f"[dim]Package not installed locally, using uvx isolation...[/dim]")

        config = IsolationConfig(
            package_name=isolation.package_name,
            version=isolation.version,
            max_depth=max_depth,
            include_private=include_private,
            include_reexports=include_reexports,
            include_patterns=list(include_patterns) if include_patterns else None,
            exclude_patterns=list(exclude_patterns) if exclude_patterns else None,
            public_api_only=public_api_only,
        )

        with console.status(f"[bold blue]Analyzing package '{isolation.get_package_spec()}' via uvx..."):
            try:
                metadata = isolation.run_check(config)
            except PackageNotFoundError as e:
                raise click.ClickException(str(e)) from None
            except IsolationError as e:
                raise click.ClickException(str(e)) from None

        # For isolated execution, methods are already in metadata
        methods = metadata.methods
    else:
        # Local execution (original flow)
        with console.status(f"[bold blue]Analyzing package '{package_name}'..."):
            try:
                analyzer = PackageAnalyzer(
                    include_private=include_private,
                    max_depth=max_depth,
                    include_reexports=include_reexports,
                )
                metadata = analyzer.analyze_package(
                    package_name,
                    include_patterns=list(include_patterns) if include_patterns else None,
                    exclude_patterns=list(exclude_patterns) if exclude_patterns else None,
                )
            except ImportError as e:
                raise click.ClickException(f"Cannot import package '{package_name}': {e}") from None
            except ValueError as e:
                raise click.ClickException(str(e)) from None

        # Get methods to display (local execution can filter)
        methods = analyzer.get_public_methods(metadata) if public_api_only else metadata.methods

    # Display package overview
    console.print(f"\n[bold]Package: {metadata.name}[/bold]")
    console.print(f"[dim]Modules discovered: {metadata.module_count}[/dim]")
    console.print(f"[dim]Public modules: {metadata.public_module_count}[/dim]")

    # Show module tree
    if verbose:
        console.print("\n[bold]Module Structure:[/bold]")
        tree = Tree(f"[cyan]{metadata.name}[/cyan]")
        _build_module_tree(tree, metadata.name, metadata.module_graph, set())
        console.print(tree)

    # Categorize methods (methods variable already set above)
    tools = []
    resources = []
    prompts = []

    for method in methods:
        if method.is_resource:
            resources.append(method)
        elif method.is_prompt:
            prompts.append(method)
        else:
            tools.append(method)

    # Display tools
    if tools:
        table = Table(title=f"Tools ({len(tools)})", show_header=True)
        table.add_column("Name", style="cyan")
        table.add_column("Module", style="dim")
        table.add_column("Async", style="yellow")
        table.add_column("Parameters")
        if verbose:
            table.add_column("Docstring")

        for tool in tools[:50]:  # Limit display to 50
            params = ", ".join(p["name"] for p in tool.parameters)
            row = [
                tool.name,
                tool.module_name.replace(f"{metadata.name}.", ""),
                "✓" if tool.is_async else "",
                params[:30] + "..." if len(params) > 30 else params or "(none)",
            ]
            if verbose:
                doc = tool.docstring or ""
                doc_display = doc[:40] + "..." if len(doc) > 40 else doc
                row.append(doc_display)
            table.add_row(*row)

        if len(tools) > 50:
            table.add_row("[dim]...[/dim]", f"[dim]+{len(tools) - 50} more[/dim]", "", "")

        console.print(table)

    # Display resources
    if resources:
        table = Table(title=f"Resources ({len(resources)})", show_header=True)
        table.add_column("Name", style="cyan")
        table.add_column("Module", style="dim")
        table.add_column("URI", style="green")

        for resource in resources[:20]:
            uri = resource.mcp_metadata.get("resource_uri", f"auto://{resource.name}")
            table.add_row(
                resource.name,
                resource.module_name.replace(f"{metadata.name}.", ""),
                uri,
            )

        console.print(table)

    # Display prompts
    if prompts:
        table = Table(title=f"Prompts ({len(prompts)})", show_header=True)
        table.add_column("Name", style="cyan")
        table.add_column("Module", style="dim")
        table.add_column("Parameters")

        for prompt in prompts[:20]:
            params = ", ".join(p["name"] for p in prompt.parameters)
            table.add_row(
                prompt.name,
                prompt.module_name.replace(f"{metadata.name}.", ""),
                params or "(none)",
            )

        console.print(table)

    if not tools and not resources and not prompts:
        console.print("[yellow]No public methods found[/yellow]")

    # Summary
    console.print("\n" + "─" * 50)
    console.print(
        f"[bold]Summary:[/bold] {len(tools)} tool(s), "
        f"{len(resources)} resource(s), {len(prompts)} prompt(s)"
    )

    if metadata.public_api:
        console.print(f"[dim]Public API symbols (__all__): {len(metadata.public_api)}[/dim]")


def _build_module_tree(
    tree: Tree,
    module_name: str,
    graph: dict[str, list[str]],
    visited: set[str],
) -> None:
    """Build a rich Tree from the module graph."""
    if module_name in visited:
        return
    visited.add(module_name)

    submodules = graph.get(module_name, [])
    for submodule in sorted(submodules):
        sub_name = submodule.split(".")[-1]
        is_private = sub_name.startswith("_")
        style = "dim" if is_private else "cyan"
        branch = tree.add(f"[{style}]{sub_name}[/{style}]")
        _build_module_tree(branch, submodule, graph, visited)


@package.command(name="generate")
@click.argument("package_name", required=True)
@click.option(
    "-o",
    "--output",
    type=click.Path(),
    required=True,
    help="Output path for generated file",
)
@click.option(
    "--name",
    type=str,
    default=None,
    help="Name for the generated server (defaults to package name)",
)
@click.option(
    "--max-depth",
    type=int,
    default=None,
    help="Maximum recursion depth for submodule discovery",
)
@click.option(
    "--include-private",
    is_flag=True,
    help="Include private modules and methods",
)
@click.option(
    "--public-api-only",
    is_flag=True,
    help="Only expose functions in __all__ (public API)",
)
@click.option(
    "--include",
    "include_patterns",
    multiple=True,
    help="Glob patterns for modules to include",
)
@click.option(
    "--exclude",
    "exclude_patterns",
    multiple=True,
    help="Glob patterns for modules to exclude",
)
@click.option(
    "--llm-provider",
    type=click.Choice(["ollama", "openai", "anthropic"]),
    help="LLM provider for description generation",
)
@click.option(
    "--llm-model",
    type=str,
    help="Model name for description generation",
)
@click.option(
    "--no-llm",
    is_flag=True,
    help="Disable LLM description generation (use docstrings only)",
)
@click.option(
    "--no-cache",
    is_flag=True,
    help="Disable caching of generated descriptions",
)
@click.option(
    "--context",
    type=str,
    help="Additional context for LLM description generation",
)
@click.option(
    "--include-reexports",
    is_flag=True,
    help="Include functions re-exported in __all__ from submodules (e.g., pandas, numpy)",
)
@click.option(
    "--enable-sessions",
    is_flag=True,
    help="Enable session lifecycle support (create_session/close_session tools)",
)
@click.option(
    "--session-ttl",
    type=int,
    default=3600,
    help="Session TTL in seconds (default: 3600)",
)
@click.option(
    "--max-sessions",
    type=int,
    default=100,
    help="Maximum number of concurrent sessions (default: 100)",
)
@click.option(
    "--no-isolated",
    is_flag=True,
    help="Force local execution only (fail if package not installed)",
)
@click.option(
    "--version",
    "pkg_version",
    type=str,
    default=None,
    help="Package version to use with uvx (e.g., '2.28.0')",
)
@click.pass_context
def package_generate(
    ctx: click.Context,
    package_name: str,
    output: str,
    name: str | None,
    max_depth: int | None,
    include_private: bool,
    public_api_only: bool,
    include_patterns: tuple[str, ...],
    exclude_patterns: tuple[str, ...],
    llm_provider: str | None,
    llm_model: str | None,
    no_llm: bool,
    no_cache: bool,
    context: str | None,
    include_reexports: bool,
    enable_sessions: bool,
    session_ttl: int,
    max_sessions: int,
    no_isolated: bool,
    pkg_version: str | None,
) -> None:
    """Generate an MCP server from an installed package.

    PACKAGE_NAME: Name of the installed package (e.g., 'requests', 'json')

    If the package is not installed locally, it will automatically be generated
    in an isolated uvx environment. Note: LLM description generation is disabled
    in isolation mode.

    Examples:

        # Generate server from requests
        auto-mcp package generate requests -o requests_server.py

        # Generate with custom name
        auto-mcp package generate requests -o server.py --name "HTTP Server"

        # Generate with filtering
        auto-mcp package generate requests -o server.py --public-api-only

        # Generate without LLM
        auto-mcp package generate json -o json_server.py --no-llm

        # Generate from a specific version (uses uvx)
        auto-mcp package generate requests==2.28.0 -o server.py
    """
    from auto_mcp.isolation import IsolationManager, check_uvx_available
    from auto_mcp.isolation.manager import IsolationConfig, IsolationError, PackageNotFoundError

    settings: Settings = ctx.obj["settings"]

    # Create isolation manager
    isolation = IsolationManager(
        package_name=package_name,
        version=pkg_version,
        force_local=no_isolated,
    )

    # Determine execution mode
    use_isolation = isolation.should_use_isolation()

    # If version specified, always use isolation
    if pkg_version:
        use_isolation = True

    server_name = name or f"{isolation.package_name}-mcp-server"
    output_path = Path(output)

    if use_isolation:
        # Check if uvx is available
        if not check_uvx_available():
            raise click.ClickException(
                f"Package '{package_name}' is not installed locally and uvx is not available.\n"
                "Either install the package or install uv: https://docs.astral.sh/uv/"
            )

        # Warn about LLM being disabled in isolation
        if not no_llm:
            console.print("[yellow]![/yellow] LLM disabled in isolation mode (using docstrings only)")

        console.print(f"[dim]Package not installed locally, using uvx isolation...[/dim]")

        iso_config = IsolationConfig(
            package_name=isolation.package_name,
            version=isolation.version,
            max_depth=max_depth,
            include_private=include_private,
            include_reexports=include_reexports,
            include_patterns=list(include_patterns) if include_patterns else None,
            exclude_patterns=list(exclude_patterns) if exclude_patterns else None,
            public_api_only=public_api_only,
            server_name=server_name,
            enable_sessions=enable_sessions,
            session_ttl=session_ttl,
            max_sessions=max_sessions,
        )

        if enable_sessions:
            console.print("[green]✓[/green] Session lifecycle enabled")

        with console.status(f"[bold blue]Generating from '{isolation.get_package_spec()}' via uvx..."):
            try:
                result = isolation.run_generate(iso_config, output_path)
            except PackageNotFoundError as e:
                raise click.ClickException(str(e)) from None
            except IsolationError as e:
                raise click.ClickException(str(e)) from None

        console.print(f"[green]✓[/green] Generated server at: {result}")
        console.print(f"\n[dim]To run: python {result}[/dim]")
    else:
        # Local execution (original flow)
        # Create LLM provider if enabled
        llm = None
        if not no_llm:
            with console.status("[bold blue]Initializing LLM provider..."):
                llm = get_llm_provider(llm_provider, llm_model, settings)
            if llm:
                console.print(f"[green]✓[/green] Using LLM: {llm.model_name}")
            else:
                console.print("[yellow]![/yellow] LLM disabled, using docstrings only")

        # Create cache
        cache = PromptCache() if not no_cache else PromptCache(cache_dir=None)

        # Create generator config
        config = GeneratorConfig(
            server_name=server_name,
            include_private=include_private,
            use_cache=not no_cache,
            use_llm=not no_llm and llm is not None,
            max_depth=max_depth,
            public_api_only=public_api_only,
            include_patterns=list(include_patterns) if include_patterns else None,
            exclude_patterns=list(exclude_patterns) if exclude_patterns else None,
            include_reexports=include_reexports,
            enable_sessions=enable_sessions,
            session_ttl=session_ttl,
            max_sessions=max_sessions,
        )

        # Create generator
        generator = MCPGenerator(llm=llm, cache=cache, config=config)

        if enable_sessions:
            console.print("[green]✓[/green] Session lifecycle enabled")

        # Generate
        with console.status(f"[bold blue]Analyzing and generating from '{package_name}'..."):
            try:
                result = generator.generate_standalone_from_package(
                    package_name,
                    output_path,
                    context=context,
                )
            except ImportError as e:
                raise click.ClickException(f"Cannot import package '{package_name}': {e}") from None
            except ValueError as e:
                raise click.ClickException(str(e)) from None

        console.print(f"[green]✓[/green] Generated server at: {result}")
        console.print(f"\n[dim]To run: python {result}[/dim]")

        # Save cache if enabled
        if not no_cache:
            cache.save(package_name)


@package.command(name="serve")
@click.argument("package_name", required=True)
@click.option(
    "--name",
    type=str,
    default=None,
    help="Name for the server (defaults to package name)",
)
@click.option(
    "--max-depth",
    type=int,
    default=None,
    help="Maximum recursion depth for submodule discovery",
)
@click.option(
    "--include-private",
    is_flag=True,
    help="Include private modules and methods",
)
@click.option(
    "--public-api-only",
    is_flag=True,
    default=True,
    help="Only expose functions in __all__ (public API) [default: True]",
)
@click.option(
    "--include",
    "include_patterns",
    multiple=True,
    help="Glob patterns for modules to include",
)
@click.option(
    "--exclude",
    "exclude_patterns",
    multiple=True,
    help="Glob patterns for modules to exclude",
)
@click.option(
    "--llm-provider",
    type=click.Choice(["ollama", "openai", "anthropic"]),
    help="LLM provider for description generation",
)
@click.option(
    "--llm-model",
    type=str,
    help="Model name for description generation",
)
@click.option(
    "--no-llm",
    is_flag=True,
    help="Disable LLM description generation",
)
@click.option(
    "--no-cache",
    is_flag=True,
    help="Disable caching of generated descriptions",
)
@click.option(
    "--transport",
    type=click.Choice(["stdio", "sse"]),
    default="stdio",
    help="MCP transport to use",
)
@click.option(
    "--include-reexports",
    is_flag=True,
    help="Include functions re-exported in __all__ from submodules (e.g., pandas, numpy)",
)
@click.option(
    "--enable-sessions",
    is_flag=True,
    help="Enable session lifecycle support (create_session/close_session tools)",
)
@click.option(
    "--session-ttl",
    type=int,
    default=3600,
    help="Session TTL in seconds (default: 3600)",
)
@click.option(
    "--max-sessions",
    type=int,
    default=100,
    help="Maximum number of concurrent sessions (default: 100)",
)
@click.option(
    "--no-isolated",
    is_flag=True,
    help="Force local execution only (fail if package not installed)",
)
@click.option(
    "--version",
    "pkg_version",
    type=str,
    default=None,
    help="Package version to use with uvx (e.g., '2.28.0')",
)
@click.pass_context
def package_serve(
    ctx: click.Context,
    package_name: str,
    name: str | None,
    max_depth: int | None,
    include_private: bool,
    public_api_only: bool,
    include_patterns: tuple[str, ...],
    exclude_patterns: tuple[str, ...],
    llm_provider: str | None,
    llm_model: str | None,
    no_llm: bool,
    no_cache: bool,
    transport: Literal["stdio", "sse"],
    include_reexports: bool,
    enable_sessions: bool,
    session_ttl: int,
    max_sessions: int,
    no_isolated: bool,
    pkg_version: str | None,
) -> None:
    """Run an MCP server from an installed package.

    PACKAGE_NAME: Name of the installed package (e.g., 'requests', 'json')

    If the package is not installed locally, it will automatically be served
    in an isolated uvx environment.

    Examples:

        # Serve requests package
        auto-mcp package serve requests

        # Serve with custom name
        auto-mcp package serve requests --name "HTTP Server"

        # Serve with filtering
        auto-mcp package serve boto3 --include 'boto3.s3.*' --max-depth 2

        # Serve a specific version (uses uvx)
        auto-mcp package serve requests==2.28.0
    """
    from auto_mcp.isolation import IsolationManager, check_uvx_available
    from auto_mcp.isolation.manager import IsolationConfig, IsolationError

    settings: Settings = ctx.obj["settings"]

    # Create isolation manager
    isolation = IsolationManager(
        package_name=package_name,
        version=pkg_version,
        force_local=no_isolated,
    )

    # Determine execution mode
    use_isolation = isolation.should_use_isolation()

    # If version specified, always use isolation
    if pkg_version:
        use_isolation = True

    server_name = name or f"{isolation.package_name}-mcp-server"

    if use_isolation:
        # Check if uvx is available
        if not check_uvx_available():
            raise click.ClickException(
                f"Package '{package_name}' is not installed locally and uvx is not available.\n"
                "Either install the package or install uv: https://docs.astral.sh/uv/"
            )

        console.print(f"[dim]Package not installed locally, using uvx isolation...[/dim]")
        console.print(f"[bold blue]Starting server for '{isolation.get_package_spec()}'...[/bold blue]")

        if enable_sessions:
            console.print("[green]✓[/green] Session lifecycle enabled")

        console.print(f"[green]✓[/green] Server '{server_name}' ready")
        console.print(f"[dim]Transport: {transport}[/dim]\n")

        iso_config = IsolationConfig(
            package_name=isolation.package_name,
            version=isolation.version,
            max_depth=max_depth,
            include_private=include_private,
            include_reexports=include_reexports,
            include_patterns=list(include_patterns) if include_patterns else None,
            exclude_patterns=list(exclude_patterns) if exclude_patterns else None,
            public_api_only=public_api_only,
            server_name=server_name,
            enable_sessions=enable_sessions,
            session_ttl=session_ttl,
            max_sessions=max_sessions,
            transport=transport,
        )

        # This replaces the current process with uvx
        # Note: run_serve does not return - it exec's into the subprocess
        try:
            isolation.run_serve(iso_config)
        except IsolationError as e:
            raise click.ClickException(str(e)) from None
    else:
        # Local execution (original flow)
        console.print(f"[bold blue]Loading package '{package_name}'...[/bold blue]")

        # Create LLM provider if enabled
        llm = None
        if not no_llm:
            llm = get_llm_provider(llm_provider, llm_model, settings)
            if llm:
                console.print(f"[green]✓[/green] Using LLM: {llm.model_name}")

        # Create cache
        cache = PromptCache() if not no_cache else PromptCache(cache_dir=None)

        # Create generator config
        config = GeneratorConfig(
            server_name=server_name,
            include_private=include_private,
            use_cache=not no_cache,
            use_llm=not no_llm and llm is not None,
            max_depth=max_depth,
            public_api_only=public_api_only,
            include_patterns=list(include_patterns) if include_patterns else None,
            exclude_patterns=list(exclude_patterns) if exclude_patterns else None,
            include_reexports=include_reexports,
            enable_sessions=enable_sessions,
            session_ttl=session_ttl,
            max_sessions=max_sessions,
        )

        # Create generator
        generator = MCPGenerator(llm=llm, cache=cache, config=config)

        console.print("[bold blue]Creating MCP server...[/bold blue]")

        if enable_sessions:
            console.print("[green]✓[/green] Session lifecycle enabled")

        try:
            server = generator.create_server_from_package(package_name)
        except ImportError as e:
            raise click.ClickException(f"Cannot import package '{package_name}': {e}") from None
        except ValueError as e:
            raise click.ClickException(str(e)) from None

        console.print(f"[green]✓[/green] Server '{server_name}' ready")
        console.print(f"[dim]Transport: {transport}[/dim]\n")

        # Run server
        server.run(transport=transport)


# =============================================================================
# Internal Worker Commands (hidden, for uvx subprocess execution)
# =============================================================================


@cli.group(hidden=True)
def internal_worker() -> None:
    """Internal commands for subprocess workers (hidden from help).

    These commands are used by the isolation manager when running
    package analysis in a uvx subprocess.
    """
    pass


@internal_worker.command(name="check")
@click.option("--config", "config_json", required=True, help="JSON configuration")
def internal_worker_check(config_json: str) -> None:
    """Internal: Run package check as worker."""
    from auto_mcp.isolation.worker import worker_check

    worker_check(config_json)


@internal_worker.command(name="generate")
@click.option("--config", "config_json", required=True, help="JSON configuration")
def internal_worker_generate(config_json: str) -> None:
    """Internal: Run package generate as worker."""
    from auto_mcp.isolation.worker import worker_generate

    worker_generate(config_json)


@internal_worker.command(name="serve")
@click.option("--config", "config_json", required=True, help="JSON configuration")
def internal_worker_serve(config_json: str) -> None:
    """Internal: Run package serve as worker."""
    from auto_mcp.isolation.worker import worker_serve

    worker_serve(config_json)


def main() -> None:
    """Main entry point for the CLI."""
    cli()


if __name__ == "__main__":
    main()
