"""
Unified setup commands for claude-mpm CLI.

WHY: Users need a consistent way to set up various services and integrations.

DESIGN DECISIONS:
- Use BaseCommand for consistent CLI patterns
- Unified command structure: claude-mpm setup [services...]
- Support multiple services in one command with service-specific options
- Flags after a service name apply to that service
- Delegate to service-specific handlers
"""

import json
import os
import shutil
import subprocess  # nosec B404
import sys
from pathlib import Path
from typing import Any

from rich.console import Console

from ..constants import GLOBAL_SETUP_FLAGS, VALUE_FLAGS, SetupFlag, SetupService
from ..shared import BaseCommand, CommandResult

console = Console()


def parse_service_args(service_args: list[str]) -> dict[str, Any]:
    """
    Parse service arguments into structured service configs.

    Args:
        service_args: Raw argument list (e.g., ['slack', 'oauth', '--oauth-service', 'gworkspace-mcp'])

    Returns:
        Dict with 'services' list and 'global_options' dict
        Example: {
            'services': [
                {'name': 'slack', 'options': {}},
                {'name': 'oauth', 'options': {'oauth_service': 'gworkspace-mcp'}}
            ],
            'global_options': {'no_launch': True}
        }
    """
    if not service_args:
        return {"services": [], "global_options": {}}

    # Use enum values for valid services
    valid_services = {str(s) for s in SetupService}

    # Global flags from constants
    global_flags = {str(f) for f in GLOBAL_SETUP_FLAGS}

    # Pre-process service_args to split comma-separated values
    expanded_args = []
    for arg in service_args:
        if "," in arg:
            # Split on commas and add each part
            expanded_args.extend(
                [part.strip() for part in arg.split(",") if part.strip()]
            )
        else:
            expanded_args.append(arg)
    service_args = expanded_args

    services = []
    current_service = None
    current_options = {}
    global_options = {}

    i = 0
    while i < len(service_args):
        arg = service_args[i]

        # Check if this is a service name
        if arg in valid_services:
            # Save previous service if exists
            if current_service:
                services.append({"name": current_service, "options": current_options})

            # Start new service
            current_service = arg
            current_options = {}
            i += 1
            continue

        # Check if this is a flag
        if arg.startswith("--"):
            flag_name = arg[2:].replace("-", "_")

            # Check if this is a global flag
            if flag_name in global_flags:
                # Global flag - can be used with or without a service
                global_options[flag_name] = True
                # Also apply to current service if one exists
                if current_service:
                    current_options[flag_name] = True
                i += 1
                continue

            # Non-global flag requires a current service
            if not current_service:
                raise ValueError(
                    f"Flag {arg} found before any service name. "
                    "Flags must come after the service they apply to."
                )

            # Check if flag expects a value (using VALUE_FLAGS enum)
            value_flag_names = {str(f) for f in VALUE_FLAGS}
            if flag_name in value_flag_names:
                # Flag expects a value
                if i + 1 >= len(service_args):
                    raise ValueError(f"Flag {arg} requires a value")
                current_options[flag_name] = service_args[i + 1]
                i += 2
            else:
                # Boolean flag
                current_options[flag_name] = True
                i += 1
            continue

        # Unknown argument - build error message from enums
        service_names = ", ".join(str(s) for s in SetupService)
        flag_names = ", ".join(f.cli_flag for f in SetupFlag)
        raise ValueError(
            f"Unknown argument: {arg}. Expected a service name ({service_names}) or a flag ({flag_names})"
        )

    # Save last service
    if current_service:
        services.append({"name": current_service, "options": current_options})

    return {"services": services, "global_options": global_options}


class SetupCommand(BaseCommand):
    """Unified setup command for all services."""

    def __init__(self):
        super().__init__("setup")

    def validate_args(self, args) -> str | None:
        """Validate command arguments."""
        # Parse service_args if present
        if hasattr(args, "service_args") and args.service_args:
            try:
                parsed = parse_service_args(args.service_args)
                args.parsed_services = parsed["services"]
                args.global_options = parsed["global_options"]

                # Validate OAuth requirements
                for service in parsed["services"]:
                    if service["name"] == str(SetupService.OAUTH):
                        oauth_svc_key = str(SetupFlag.OAUTH_SERVICE)
                        if oauth_svc_key not in service["options"]:
                            return (
                                f"OAuth setup requires {SetupFlag.OAUTH_SERVICE.cli_flag} flag. "
                                f"Example: claude-mpm setup oauth {SetupFlag.OAUTH_SERVICE.cli_flag} {SetupService.GWORKSPACE_MCP}"
                            )

                return None
            except ValueError as e:
                return str(e)

        return None

    def run(self, args) -> CommandResult:
        """Execute the setup command."""
        # Check if --provider was given (it can be used alone, without services)
        provider_key = str(SetupFlag.PROVIDER)
        has_provider_flag = bool(getattr(args, provider_key, None))

        # If no services and no --provider flag, show help
        if not hasattr(args, "parsed_services") or not args.parsed_services:
            if not has_provider_flag:
                self._show_help()
                return CommandResult.success_result("Help displayed")
            # --provider only: skip service processing, fall through to provider section
            services = []
        else:
            services = args.parsed_services

        # Process multiple services in sequence
        results = []
        for service_config in services:
            service_name = service_config["name"]
            service_options = service_config["options"]

            console.print(f"\n[bold cyan]Setting up {service_name}...[/bold cyan]")

            # Create a namespace object with the service options
            from argparse import Namespace

            service_args = Namespace(**service_options)

            if service_name == "slack":
                result = self._setup_slack(service_args)
            elif service_name == "gworkspace-mcp":
                result = self._setup_google_workspace(service_args)
            elif service_name == "slack-mpm":
                result = self._setup_slack_mpm(service_args)
            elif service_name == "notion":
                result = self._setup_notion(service_args)
            elif service_name == "confluence":
                result = self._setup_confluence(service_args)
            elif service_name == "kuzu-memory":
                result = self._setup_kuzu_memory(service_args)
            elif service_name == "mcp-vector-search":
                result = self._setup_mcp_vector_search(service_args)
            elif service_name == "mcp-skillset":
                result = self._setup_mcp_skillset(service_args)
            elif service_name == "mcp-ticketer":
                result = self._setup_mcp_ticketer(service_args)
            elif service_name == "oauth":
                result = self._setup_oauth(service_args)
            elif service_name == "brave-search":
                result = self._setup_brave_search(service_args)
            elif service_name == "tavily":
                result = self._setup_tavily(service_args)
            elif service_name == "firecrawl":
                result = self._setup_firecrawl(service_args)
            else:
                result = CommandResult.error_result(f"Unknown service: {service_name}")

            results.append((service_name, result))

            # Track failure but continue processing remaining services
            if not result.success:
                console.print(
                    f"\n[red]✗ Setup failed for {service_name}[/red]",
                    style="bold",
                )
                # Don't return early - continue processing remaining services
            else:
                console.print(
                    f"[green]✓ {service_name} setup complete![/green]",
                    style="bold",
                )

        # Report results
        successful = [r for r in results if r[1].success]
        failed = [r for r in results if not r[1].success]

        if successful:
            console.print(
                f"\n[green]✓ {len(successful)} service(s) set up successfully[/green]",
                style="bold",
            )

        if failed:
            console.print(
                f"\n[red]✗ {len(failed)} service(s) failed to set up[/red]",
                style="bold",
            )

        # Handle --provider flag: configure API provider as part of setup
        provider_key = str(SetupFlag.PROVIDER)
        provider_name = getattr(args, provider_key, None)
        if not provider_name:
            # Also check global_options (parsed from positional service_args)
            global_options_check = getattr(args, "global_options", {})
            provider_name = global_options_check.get(provider_key)
        if provider_name:
            console.print(
                f"\n[bold cyan]Configuring API provider: {provider_name}...[/bold cyan]"
            )
            from ...config.api_provider import APIBackend, APIProviderConfig

            config_path = self.working_dir / ".claude-mpm" / "configuration.yaml"
            config = APIProviderConfig.load(config_path)
            region_key = str(SetupFlag.REGION)
            model_key = str(SetupFlag.MODEL)
            region = getattr(args, region_key, None)
            model = getattr(args, model_key, None)
            if provider_name == "anthropic":
                config.backend = APIBackend.ANTHROPIC
                if model:
                    config.anthropic.model = model
                try:
                    config.save(config_path)
                    console.print(
                        f"[green]✓ Provider set to Anthropic (model: {config.anthropic.model})[/green]"
                    )
                except Exception as e:
                    console.print(f"[red]✗ Failed to save provider config: {e}[/red]")
            elif provider_name == "bedrock":
                config.backend = APIBackend.BEDROCK
                if region:
                    config.bedrock.region = region
                if model:
                    config.bedrock.model = model
                try:
                    config.save(config_path)
                    console.print(
                        f"[green]✓ Provider set to Bedrock (region: {config.bedrock.region}, model: {config.bedrock.model})[/green]"
                    )
                except Exception as e:
                    console.print(f"[red]✗ Failed to save provider config: {e}[/red]")

        # Launch claude-mpm after all services are set up (unless --no-launch specified)
        # Only launch if at least one service succeeded
        # Check argparse flag first, then global_options, then per-service options
        no_launch_key = str(SetupFlag.NO_LAUNCH)
        no_launch = getattr(args, no_launch_key, False)
        if not no_launch:
            global_options = getattr(args, "global_options", {})
            no_launch = global_options.get(no_launch_key, False)
        # Also check if any service had --no-launch applied to it
        if not no_launch:
            no_launch = any(
                svc.get("options", {}).get(no_launch_key, False) for svc in services
            )
        if not no_launch and len(successful) > 0:
            console.print("\n[cyan]Launching claude-mpm...[/cyan]\n")
            try:
                # Replace current process with claude-mpm
                os.execvp("claude-mpm", ["claude-mpm"])  # nosec B606 B607
            except OSError:
                # If execvp fails (e.g., claude-mpm not in PATH), try subprocess
                subprocess.run(["claude-mpm"], check=False)  # nosec B603 B607
                sys.exit(0)

        # Return failure if all services failed, success if any succeeded
        # (If no services were requested, only --provider was used — that's a success)
        if results and len(failed) == len(results):
            return CommandResult.error_result(
                f"All {len(failed)} service(s) failed to set up"
            )
        if results:
            return CommandResult.success_result(
                f"Set up {len(successful)}/{len(results)} service(s) successfully"
            )
        return CommandResult.success_result("Setup complete")

    def _show_help(self) -> None:
        """Display setup command help."""
        help_text = """
[bold]Setup Command:[/bold]
  setup SERVICE [OPTIONS] [SERVICE [OPTIONS] ...]

[bold]Available Services:[/bold]
  slack                  Set up Slack MPM integration
  gworkspace-mcp         Set up Google Workspace MCP (includes OAuth)
  slack-mpm              Set up Slack MCP server (token-based)
  notion                 Set up Notion integration
  confluence             Set up Confluence integration
  kuzu-memory            Set up kuzu-memory graph-based memory backend
  mcp-vector-search      Set up mcp-vector-search semantic code search
  mcp-skillset           Set up mcp-skillset RAG-powered skills (USER-LEVEL)
  mcp-ticketer           Set up mcp-ticketer ticket management via MCP
  oauth                  Set up OAuth authentication

[bold]Service Options:[/bold]
  --oauth-service NAME   Service name for OAuth (required for 'oauth')
  --no-browser           Don't auto-open browser for authentication (oauth only)
  --no-launch            Don't auto-launch claude-mpm after setup (all services)
  --force                Force credential re-entry (oauth) or reinstall (mcp-vector-search, mcp-skillset)
  --upgrade              Upgrade installed packages to latest version
  --provider PROVIDER    Set API provider: anthropic or bedrock (can be used alone)
  --region REGION        AWS region for Bedrock (used with --provider bedrock)
  --model MODEL          Model ID for the selected provider

[bold]Examples:[/bold]
  # Single service
  claude-mpm setup slack

  # Slack without auto-launch
  claude-mpm setup slack --no-launch

  # Multiple services (space-separated)
  claude-mpm setup slack gworkspace-mcp

  # Multiple services (comma-separated)
  claude-mpm setup slack,gworkspace-mcp,notion

  # Service with options
  claude-mpm setup oauth --oauth-service gworkspace-mcp --no-browser

  # Multiple services with options
  claude-mpm setup slack oauth --oauth-service gworkspace-mcp --no-launch

  # Set up mcp-vector-search
  claude-mpm setup mcp-vector-search

  # With force flag to reinstall
  claude-mpm setup mcp-vector-search --force

  # Set up slack-mpm
  claude-mpm setup slack-mpm

  # Force re-validation
  claude-mpm setup slack-mpm --force

[dim]Note: Flags apply to the service that precedes them.[/dim]
"""
        console.print(help_text)

    def _remove_kuzu_memory_hooks(self) -> bool:
        """Remove kuzu-memory's independent hooks from Claude Code settings.

        This is called after setting up subservient mode to ensure kuzu-memory
        integrates through MPM's hook system instead of running independently.
        """
        settings_path = Path.home() / ".claude" / "settings.local.json"

        if not settings_path.exists():
            # No settings file, nothing to clean
            return True

        try:
            # Load settings
            with open(settings_path) as f:
                settings = json.load(f)

            hooks = settings.get("hooks", {})
            removed = []

            # Find and remove kuzu-memory hooks
            for hook_name, hook_config in list(hooks.items()):
                if isinstance(hook_config, dict):
                    command = hook_config.get("command", "")
                    if "kuzu-memory hooks" in command or "/kuzu-memory" in command:
                        del hooks[hook_name]
                        removed.append(hook_name)

            if removed:
                # Save cleaned settings
                settings["hooks"] = hooks
                with open(settings_path, "w") as f:
                    json.dump(settings, f, indent=2)

                console.print(
                    f"[green]✓ Removed {len(removed)} kuzu-memory hooks: "
                    f"{', '.join(removed)}[/green]"
                )
                console.print(
                    "[dim]kuzu-memory will now integrate through claude-mpm hooks[/dim]"
                )

            return True

        except Exception as e:
            console.print(
                f"[yellow]Warning: Could not remove kuzu-memory hooks: {e}[/yellow]"
            )
            console.print(
                "[dim]You may need to manually remove them from "
                ".claude/settings.local.json[/dim]"
            )
            return False

    def _load_mcp_config(self) -> dict:
        """Load .mcp.json configuration file.

        Returns:
            Dict containing config or empty dict if file doesn't exist
        """
        mcp_config_path = Path.cwd() / ".mcp.json"

        if mcp_config_path.exists():
            try:
                with open(mcp_config_path) as f:
                    return json.load(f)
            except (json.JSONDecodeError, OSError) as e:
                console.print(
                    f"[yellow]Warning: Could not read .mcp.json: {e}[/yellow]"
                )
                return {"mcpServers": {}}

        return {"mcpServers": {}}

    def _save_mcp_config(self, config: dict) -> None:
        """Save .mcp.json configuration file.

        Args:
            config: Configuration dict to save
        """
        mcp_config_path = Path.cwd() / ".mcp.json"

        try:
            with open(mcp_config_path, "w") as f:
                json.dump(config, f, indent=2)
                f.write("\n")  # Add trailing newline
        except OSError as e:
            console.print(f"[yellow]Warning: Could not write .mcp.json: {e}[/yellow]")
            raise

    def _add_gworkspace_to_gitignore(self) -> None:
        """Add .gworkspace-mcp/ to project .gitignore to prevent token commits."""
        gitignore_path = Path.cwd() / ".gitignore"
        gworkspace_entry = ".gworkspace-mcp/"

        # Check if .gitignore exists
        if not gitignore_path.exists():
            console.print(
                f"[dim]No .gitignore found at {gitignore_path}, skipping gitignore update[/dim]"
            )
            return

        try:
            # Read existing content
            with open(gitignore_path) as f:
                content = f.read()

            # Check if already present
            if gworkspace_entry in content:
                console.print("[dim].gworkspace-mcp/ already in .gitignore[/dim]")
                return

            # Append entry
            with open(gitignore_path, "a") as f:
                # Add newline if file doesn't end with one
                if content and not content.endswith("\n"):
                    f.write("\n")
                f.write(f"{gworkspace_entry}\n")

            console.print("[green]✓ Added .gworkspace-mcp/ to .gitignore[/green]")

        except Exception as e:
            console.print(f"[yellow]Warning: Could not update .gitignore: {e}[/yellow]")

    def _configure_slack_mpm_in_mcp_json(
        self, slack_mpm_path: str | None = None
    ) -> None:
        """Configure slack-mpm MCP server in .mcp.json."""
        import json

        mcp_config_path = Path.cwd() / ".mcp.json"

        # Build server config — prefer uv run with directory if we know the path
        if slack_mpm_path:
            server_config = {
                "type": "stdio",
                "command": "uv",
                "args": ["run", "--directory", slack_mpm_path, "slack-mpm", "mcp"],
                "env": {},
            }
        else:
            # Fall back to installed binary
            server_config = {
                "type": "stdio",
                "command": "slack-mpm",
                "args": ["mcp"],
                "env": {},
            }

        # Load or create .mcp.json
        if mcp_config_path.exists():
            try:
                with open(mcp_config_path) as f:
                    config = json.load(f)
            except (json.JSONDecodeError, OSError):
                config = {"mcpServers": {}}
        else:
            config = {"mcpServers": {}}

        if "mcpServers" not in config:
            config["mcpServers"] = {}

        if "slack-mpm" in config["mcpServers"]:
            console.print("[dim]slack-mpm already configured in .mcp.json[/dim]")
            return

        config["mcpServers"]["slack-mpm"] = server_config

        try:
            with open(mcp_config_path, "w") as f:
                json.dump(config, f, indent=2)
                f.write("\n")
            console.print("[green]✓ Added slack-mpm to .mcp.json[/green]")
        except OSError as e:
            console.print(f"[yellow]Warning: Could not write .mcp.json: {e}[/yellow]")

    def _setup_slack_mpm(self, args) -> CommandResult:
        """Set up slack-mpm — validates SLACK_BOT_TOKEN and configures .mcp.json."""
        import shutil
        import subprocess

        console.print(
            "\n[bold]Slack MCP Setup[/bold]\n"
            "Connects Claude to your Slack workspace via the slack-mpm server.\n"
            "Requires a Slack bot token (SLACK_BOT_TOKEN=xoxb-...).\n"
        )

        # --- Step 1: Locate slack-mpm binary or local project ---
        slack_mpm_project = Path.home() / "Projects" / "slack-mpm"
        slack_mpm_binary = shutil.which("slack-mpm")
        using_local = (
            slack_mpm_project.exists()
            and (slack_mpm_project / "pyproject.toml").exists()
        )

        if using_local:
            console.print(
                f"[cyan]Found local slack-mpm project at {slack_mpm_project}[/cyan]"
            )
            run_cmd = ["uv", "run", "--directory", str(slack_mpm_project), "slack-mpm"]
            slack_mpm_path = str(slack_mpm_project)
        elif slack_mpm_binary:
            console.print(
                f"[cyan]Found installed slack-mpm at {slack_mpm_binary}[/cyan]"
            )
            run_cmd = [slack_mpm_binary]
            slack_mpm_path = None
        else:
            console.print(
                "[yellow]slack-mpm not found locally or installed.[/yellow]\n"
                "[dim]Install it:[/dim]\n"
                "  git clone https://github.com/bobmatnyc/slack-mpm ~/Projects/slack-mpm\n"
                "  cd ~/Projects/slack-mpm && uv sync\n"
            )
            return CommandResult.error_result(
                "slack-mpm not found. Clone the project to ~/Projects/slack-mpm."
            )

        # --- Step 2: Check for SLACK_BOT_TOKEN ---
        # Load from .env / .env.local in CWD
        from dotenv import dotenv_values

        env_vars: dict[str, str | None] = {}
        for env_file in [Path.cwd() / ".env", Path.cwd() / ".env.local"]:
            if env_file.exists():
                env_vars.update(dotenv_values(env_file))

        token = env_vars.get("SLACK_BOT_TOKEN") or os.environ.get("SLACK_BOT_TOKEN")

        if not token:
            console.print(
                "\n[yellow]SLACK_BOT_TOKEN not found.[/yellow]\n"
                "[dim]Create a .env.local file with:[/dim]\n"
                "  SLACK_BOT_TOKEN=xoxb-your-token-here\n\n"
                "[dim]Get a bot token at: https://api.slack.com/apps[/dim]\n"
                "[dim]Required bot scopes: channels:read, channels:write, chat:write,\n"
                "  users:read, files:read, reactions:write[/dim]\n"
            )
            return CommandResult.error_result(
                "SLACK_BOT_TOKEN required. Set it in .env.local."
            )

        console.print(f"[dim]Bot token found: {token[:6]}...{token[-4:]}[/dim]")

        # --- Step 3: Validate token via slack-mpm setup ---
        console.print("\n[cyan]Validating Slack token...[/cyan]")
        try:
            env = os.environ.copy()
            env["SLACK_BOT_TOKEN"] = token
            result = subprocess.run(  # nosec B603
                run_cmd + ["setup"],
                check=False,
                env=env,
                capture_output=True,
                text=True,
                timeout=30,
            )
            if result.returncode == 0:
                console.print(result.stdout)
                console.print("[green]✓ Token validated successfully[/green]")
            else:
                console.print(
                    f"[red]Token validation failed:[/red]\n{result.stdout}{result.stderr}"
                )
                return CommandResult.error_result("Slack token validation failed.")
        except subprocess.TimeoutExpired:
            return CommandResult.error_result("Token validation timed out.")
        except Exception as e:
            return CommandResult.error_result(f"Failed to run slack-mpm setup: {e}")

        # --- Step 4: Configure .mcp.json ---
        self._configure_slack_mpm_in_mcp_json(slack_mpm_path=slack_mpm_path)

        # --- Step 5: Register with setup registry ---
        try:
            from claude_mpm.services.setup_registry import SetupRegistry

            registry = SetupRegistry()
            registry.add_service(
                name="slack-mpm",
                service_type="mcp",
                version="0.1.0",
                tools=[
                    "list_channels",
                    "get_channel_info",
                    "create_channel",
                    "send_message",
                    "update_message",
                    "delete_message",
                    "add_reaction",
                    "remove_reaction",
                    "pin_message",
                    "list_users",
                    "get_user_info",
                    "get_user_by_email",
                    "upload_file",
                    "list_files",
                    "get_workspace_info",
                    "auth_test",
                    "schedule_message",
                    "add_reminder",
                ],
            )
            console.print("[green]✓ Registered slack-mpm in setup registry[/green]")
        except Exception:
            pass  # Registry is optional, non-fatal

        console.print(
            "\n[bold green]✓ slack-mpm setup complete![/bold green]\n"
            "[dim]The MCP server is now configured in .mcp.json.\n"
            "Claude Code will load it automatically when you open this project.[/dim]\n"
        )

        return CommandResult.success_result("slack-mpm setup complete")

    def _configure_slack_mpm_server(self) -> None:
        """Configure slack-user-proxy MCP server in .mcp.json after OAuth setup."""
        try:
            import json

            mcp_config_path = Path.cwd() / ".mcp.json"

            # Get the slack-user-proxy configuration from service registry
            try:
                from ...services.mcp_service_registry import MCPServiceRegistry

                service = MCPServiceRegistry.get("slack-user-proxy")
                if not service:
                    console.print(
                        "[yellow]Warning: slack-user-proxy not found in service registry[/yellow]"
                    )
                    return

                # Generate config using console script entry point
                # slack-user-proxy is available once claude-mpm is installed
                server_config = {
                    "type": "stdio",
                    "command": "slack-user-proxy",
                    "args": [],
                    "env": {},
                }
            except ImportError:
                # Fallback if registry not available
                server_config = {
                    "type": "stdio",
                    "command": "slack-user-proxy",
                    "args": [],
                    "env": {},
                }

            # Load or create .mcp.json
            if mcp_config_path.exists():
                try:
                    with open(mcp_config_path) as f:
                        config = json.load(f)
                except (json.JSONDecodeError, OSError) as e:
                    console.print(
                        f"[yellow]Warning: Could not read .mcp.json: {e}[/yellow]"
                    )
                    config = {"mcpServers": {}}
            else:
                config = {"mcpServers": {}}

            # Ensure mcpServers key exists
            if "mcpServers" not in config:
                config["mcpServers"] = {}

            # Check if already configured
            if "slack-user-proxy" in config["mcpServers"]:
                console.print(
                    "[dim]slack-user-proxy already configured in .mcp.json[/dim]"
                )
                return

            # Add slack-user-proxy entry
            config["mcpServers"]["slack-user-proxy"] = server_config

            # Write back
            try:
                with open(mcp_config_path, "w") as f:
                    json.dump(config, f, indent=2)
                    f.write("\n")  # Add trailing newline

                console.print("[green]✓ Added slack-user-proxy to .mcp.json[/green]")
            except OSError as e:
                console.print(
                    f"[yellow]Warning: Could not write .mcp.json: {e}[/yellow]"
                )

        except Exception as e:
            console.print(
                f"[yellow]Warning: Could not configure MCP server: {e}[/yellow]"
            )

    def _setup_slack(self, args) -> CommandResult:
        """Run the Slack setup script."""
        try:
            # Find the setup script in the installed package
            import claude_mpm

            package_dir = Path(claude_mpm.__file__).parent
            script_path = package_dir / "scripts" / "setup" / "setup-slack-app.sh"

            if not script_path.exists():
                return CommandResult.error_result(
                    f"Setup script not found at: {script_path}\n"
                    "This may be due to an older version. "
                    "Try: uv tool upgrade claude-mpm"
                )

            # Make sure script is executable
            script_path.chmod(0o755)

            console.print("[cyan]Running Slack setup wizard...[/cyan]\n")

            # Run the script
            result = subprocess.run(
                ["bash", str(script_path)],
                check=False,
                env=os.environ.copy(),
            )  # nosec B603 B607

            if result.returncode == 0:
                console.print("\n[green]✓ Slack setup complete![/green]")

                # Configure slack-user-proxy MCP server
                self._configure_slack_mpm_server()

                # Also configure slack-mpm if available
                self._configure_slack_mpm_in_mcp_json()

                return CommandResult.success_result("Slack setup completed")

            return CommandResult.error_result(
                f"Setup script exited with code {result.returncode}"
            )

        except KeyboardInterrupt:
            console.print("\n[yellow]Setup cancelled by user[/yellow]")
            return CommandResult.error_result("Setup cancelled")
        except Exception as e:
            return CommandResult.error_result(f"Error running setup: {e}")

    def _configure_notion_mcp_server(self) -> None:
        """Configure notion-mcp MCP server in .mcp.json after credentials setup."""
        try:
            import json

            mcp_config_path = Path.cwd() / ".mcp.json"

            # Generate config using console script entry point
            server_config = {
                "type": "stdio",
                "command": "notion-mcp",
                "args": [],
                "env": {},
            }

            # Load or create .mcp.json
            if mcp_config_path.exists():
                try:
                    with open(mcp_config_path) as f:
                        config = json.load(f)
                except (json.JSONDecodeError, OSError) as e:
                    console.print(
                        f"[yellow]Warning: Could not read .mcp.json: {e}[/yellow]"
                    )
                    config = {"mcpServers": {}}
            else:
                config = {"mcpServers": {}}

            # Ensure mcpServers key exists
            if "mcpServers" not in config:
                config["mcpServers"] = {}

            # Check if already configured
            if "notion-mcp" in config["mcpServers"]:
                console.print("[dim]notion-mcp already configured in .mcp.json[/dim]")
                return

            # Add notion-mcp entry
            config["mcpServers"]["notion-mcp"] = server_config

            # Write back
            try:
                with open(mcp_config_path, "w") as f:
                    json.dump(config, f, indent=2)
                    f.write("\n")  # Add trailing newline

                console.print("[green]✓ Added notion-mcp to .mcp.json[/green]")
            except OSError as e:
                console.print(
                    f"[yellow]Warning: Could not write .mcp.json: {e}[/yellow]"
                )

        except Exception as e:
            console.print(
                f"[yellow]Warning: Could not configure MCP server: {e}[/yellow]"
            )

    def _setup_notion(self, args) -> CommandResult:
        """Set up Notion integration with credential collection."""
        try:
            console.print(
                "\n[bold]Notion Integration Setup[/bold]\n"
                "To use Notion, you need an Integration Token from Notion.\n"
                "Visit: https://www.notion.so/my-integrations\n"
            )

            # Check for existing credentials
            env_local = Path.cwd() / ".env.local"
            api_key_exists = False

            if env_local.exists():
                with open(env_local) as f:
                    if "NOTION_API_KEY" in f.read():
                        api_key_exists = True

            if api_key_exists:
                console.print(
                    "[dim]NOTION_API_KEY already configured in .env.local[/dim]\n"
                )
            else:
                # Prompt for API key
                from rich.prompt import Prompt

                api_key = Prompt.ask(
                    "[cyan]Notion Integration Token (secret_...)[/cyan]",
                    password=True,
                )

                if not api_key.startswith("secret_"):
                    console.print(
                        "[yellow]Warning: Notion tokens usually start with 'secret_'[/yellow]"
                    )

                # Optionally ask for default database ID
                database_id = Prompt.ask(
                    "[cyan]Default Database ID (optional, press Enter to skip)[/cyan]",
                    default="",
                )

                # Save to .env.local
                with open(env_local, "a") as f:
                    f.write(
                        f'\nNOTION_API_KEY="{api_key}"  # pragma: allowlist secret\n'
                    )
                    if database_id:
                        f.write(f'NOTION_DATABASE_ID="{database_id}"\n')

                console.print(f"[green]✓ Credentials saved to {env_local}[/green]")

            # Configure MCP server
            self._configure_notion_mcp_server()

            console.print("\n[green]✓ Notion setup complete![/green]")
            console.print(
                "\n[dim]Next steps:[/dim]\n"
                "  1. Share your database with the Notion integration\n"
                "  2. Use 'claude-mpm tools notion' for bulk operations\n"
                "  3. MCP tools are available in Claude Code\n"
            )

            return CommandResult.success_result("Notion setup completed")

        except KeyboardInterrupt:
            console.print("\n[yellow]Setup cancelled by user[/yellow]")
            return CommandResult.error_result("Setup cancelled")
        except Exception as e:
            return CommandResult.error_result(f"Error during setup: {e}")

    def _configure_confluence_mcp_server(self) -> None:
        """Configure confluence-mcp MCP server in .mcp.json after credentials setup."""
        try:
            import json

            mcp_config_path = Path.cwd() / ".mcp.json"

            # Generate config using console script entry point
            server_config = {
                "type": "stdio",
                "command": "confluence-mcp",
                "args": [],
                "env": {},
            }

            # Load or create .mcp.json
            if mcp_config_path.exists():
                try:
                    with open(mcp_config_path) as f:
                        config = json.load(f)
                except (json.JSONDecodeError, OSError) as e:
                    console.print(
                        f"[yellow]Warning: Could not read .mcp.json: {e}[/yellow]"
                    )
                    config = {"mcpServers": {}}
            else:
                config = {"mcpServers": {}}

            # Ensure mcpServers key exists
            if "mcpServers" not in config:
                config["mcpServers"] = {}

            # Check if already configured
            if "confluence-mcp" in config["mcpServers"]:
                console.print(
                    "[dim]confluence-mcp already configured in .mcp.json[/dim]"
                )
                return

            # Add confluence-mcp entry
            config["mcpServers"]["confluence-mcp"] = server_config

            # Write back
            try:
                with open(mcp_config_path, "w") as f:
                    json.dump(config, f, indent=2)
                    f.write("\n")  # Add trailing newline

                console.print("[green]✓ Added confluence-mcp to .mcp.json[/green]")
            except OSError as e:
                console.print(
                    f"[yellow]Warning: Could not write .mcp.json: {e}[/yellow]"
                )

        except Exception as e:
            console.print(
                f"[yellow]Warning: Could not configure MCP server: {e}[/yellow]"
            )

    def _setup_confluence(self, args) -> CommandResult:
        """Set up Confluence integration with credential collection."""
        try:
            console.print(
                "\n[bold]Confluence Integration Setup[/bold]\n"
                "To use Confluence, you need:\n"
                "  1. Your Confluence site URL (e.g., https://yoursite.atlassian.net)\n"
                "  2. Your email address\n"
                "  3. An API token from https://id.atlassian.com/manage-profile/security/api-tokens\n"
            )

            # Check for existing credentials
            env_local = Path.cwd() / ".env.local"
            url_exists = False

            if env_local.exists():
                with open(env_local) as f:
                    if "CONFLUENCE_URL" in f.read():
                        url_exists = True

            if url_exists:
                console.print(
                    "[dim]Confluence credentials already configured in .env.local[/dim]\n"
                )
            else:
                # Prompt for credentials
                from rich.prompt import Prompt

                url = Prompt.ask(
                    "[cyan]Confluence URL (e.g., https://yoursite.atlassian.net)[/cyan]"
                )

                email = Prompt.ask("[cyan]Your email address[/cyan]")

                api_token = Prompt.ask(
                    "[cyan]API Token[/cyan]",
                    password=True,
                )

                # Save to .env.local
                with open(env_local, "a") as f:
                    f.write(f'\nCONFLUENCE_URL="{url}"\n')
                    f.write(f'CONFLUENCE_EMAIL="{email}"\n')
                    f.write(
                        f'CONFLUENCE_API_TOKEN="{api_token}"  # pragma: allowlist secret\n'
                    )

                console.print(f"[green]✓ Credentials saved to {env_local}[/green]")

            # Configure MCP server
            self._configure_confluence_mcp_server()

            console.print("\n[green]✓ Confluence setup complete![/green]")
            console.print(
                "\n[dim]Next steps:[/dim]\n"
                "  1. Use 'claude-mpm tools confluence' for bulk operations\n"
                "  2. MCP tools are available in Claude Code\n"
            )

            return CommandResult.success_result("Confluence setup completed")

        except KeyboardInterrupt:
            console.print("\n[yellow]Setup cancelled by user[/yellow]")
            return CommandResult.error_result("Setup cancelled")
        except Exception as e:
            return CommandResult.error_result(f"Error during setup: {e}")

    def _setup_brave_search(self, args) -> CommandResult:
        """Set up Brave Search MCP server for web search."""
        console.print("\n[bold cyan]Brave Search MCP Setup[/bold cyan]")
        console.print("This will configure Brave Search for web research.\n")

        # Check for API key
        api_key = os.getenv("BRAVE_API_KEY")
        if not api_key:
            console.print("[yellow]⚠️  BRAVE_API_KEY not found in environment[/yellow]")
            console.print("\nTo use Brave Search:")
            console.print("1. Get an API key from: https://brave.com/search/api/")
            console.print("2. Set environment variable:")
            console.print(
                "   export BRAVE_API_KEY='your-api-key'"  # pragma: allowlist secret
            )
            console.print("3. Run setup again\n")

            from rich.prompt import Prompt

            skip = Prompt.ask(
                "Continue without API key? (will configure but won't work until key added)",
                choices=["y", "n"],
                default="n",
            )
            if skip.lower() != "y":
                return CommandResult.error_result("Brave Search API key required")

        # Configure in .mcp.json
        try:
            mcp_config = self._load_mcp_config()

            # Check if already configured
            if "brave-search" in mcp_config.get("mcpServers", {}):
                console.print("[yellow]⚠️  brave-search already configured[/yellow]")
                from rich.prompt import Prompt

                overwrite = Prompt.ask("Overwrite?", choices=["y", "n"], default="n")
                if overwrite.lower() != "y":
                    return CommandResult.failure_result("Setup cancelled")

            # Add Brave Search configuration
            if "mcpServers" not in mcp_config:
                mcp_config["mcpServers"] = {}

            mcp_config["mcpServers"]["brave-search"] = {
                "type": "stdio",
                "command": "npx",
                "args": ["-y", "@modelcontextprotocol/server-brave-search"],
                "env": {"BRAVE_API_KEY": api_key or "${BRAVE_API_KEY}"},
            }

            self._save_mcp_config(mcp_config)
            console.print("[green]✓ Brave Search configured successfully[/green]")

            return CommandResult.success_result("Brave Search setup complete")

        except Exception as e:
            console.print(f"[red]✗ Error setting up Brave Search: {e}[/red]")
            return CommandResult.failure_result(f"Setup failed: {e}")

    def _setup_tavily(self, args) -> CommandResult:
        """Set up Tavily MCP server for AI-optimized search."""
        console.print("\n[bold cyan]Tavily Search MCP Setup[/bold cyan]")
        console.print("This will configure Tavily for AI-optimized research.\n")

        # Check for API key
        api_key = os.getenv("TAVILY_API_KEY")
        if not api_key:
            console.print("[yellow]⚠️  TAVILY_API_KEY not found in environment[/yellow]")
            console.print("\nTo use Tavily:")
            console.print("1. Get an API key from: https://tavily.com/")
            console.print("2. Set environment variable:")
            console.print(
                "   export TAVILY_API_KEY='your-api-key'"  # pragma: allowlist secret
            )
            console.print("3. Run setup again\n")

            from rich.prompt import Prompt

            skip = Prompt.ask(
                "Continue without API key? (will configure but won't work until key added)",
                choices=["y", "n"],
                default="n",
            )
            if skip.lower() != "y":
                return CommandResult.error_result("Tavily API key required")

        # Configure in .mcp.json
        try:
            mcp_config = self._load_mcp_config()

            # Check if already configured
            if "tavily" in mcp_config.get("mcpServers", {}):
                console.print("[yellow]⚠️  tavily already configured[/yellow]")
                from rich.prompt import Prompt

                overwrite = Prompt.ask("Overwrite?", choices=["y", "n"], default="n")
                if overwrite.lower() != "y":
                    return CommandResult.failure_result("Setup cancelled")

            # Add Tavily configuration
            if "mcpServers" not in mcp_config:
                mcp_config["mcpServers"] = {}

            mcp_config["mcpServers"]["tavily"] = {
                "type": "stdio",
                "command": "npx",
                "args": ["-y", "@modelcontextprotocol/server-tavily"],
                "env": {"TAVILY_API_KEY": api_key or "${TAVILY_API_KEY}"},
            }

            self._save_mcp_config(mcp_config)
            console.print("[green]✓ Tavily configured successfully[/green]")

            return CommandResult.success_result("Tavily setup complete")

        except Exception as e:
            console.print(f"[red]✗ Error setting up Tavily: {e}[/red]")
            return CommandResult.failure_result(f"Setup failed: {e}")

    def _setup_firecrawl(self, args) -> CommandResult:
        """Set up Firecrawl MCP server for web scraping."""
        console.print("\n[bold cyan]Firecrawl MCP Setup[/bold cyan]")
        console.print("This will configure Firecrawl for web scraping.\n")

        # Check for API key
        api_key = os.getenv("FIRECRAWL_API_KEY")
        if not api_key:
            console.print(
                "[yellow]⚠️  FIRECRAWL_API_KEY not found in environment[/yellow]"
            )
            console.print("\nTo use Firecrawl:")
            console.print("1. Get an API key from: https://firecrawl.dev/")
            console.print("2. Set environment variable:")
            console.print(
                "   export FIRECRAWL_API_KEY='your-api-key'"  # pragma: allowlist secret
            )
            console.print("3. Run setup again\n")

            from rich.prompt import Prompt

            skip = Prompt.ask(
                "Continue without API key? (will configure but won't work until key added)",
                choices=["y", "n"],
                default="n",
            )
            if skip.lower() != "y":
                return CommandResult.error_result("Firecrawl API key required")

        # Configure in .mcp.json
        try:
            mcp_config = self._load_mcp_config()

            # Check if already configured
            if "firecrawl" in mcp_config.get("mcpServers", {}):
                console.print("[yellow]⚠️  firecrawl already configured[/yellow]")
                from rich.prompt import Prompt

                overwrite = Prompt.ask("Overwrite?", choices=["y", "n"], default="n")
                if overwrite.lower() != "y":
                    return CommandResult.failure_result("Setup cancelled")

            # Add Firecrawl configuration
            if "mcpServers" not in mcp_config:
                mcp_config["mcpServers"] = {}

            mcp_config["mcpServers"]["firecrawl"] = {
                "type": "stdio",
                "command": "npx",
                "args": ["-y", "@mendable/firecrawl-mcp"],
                "env": {"FIRECRAWL_API_KEY": api_key or "${FIRECRAWL_API_KEY}"},
            }

            self._save_mcp_config(mcp_config)
            console.print("[green]✓ Firecrawl configured successfully[/green]")

            return CommandResult.success_result("Firecrawl setup complete")

        except Exception as e:
            console.print(f"[red]✗ Error setting up Firecrawl: {e}[/red]")
            return CommandResult.failure_result(f"Setup failed: {e}")

    def _setup_kuzu_memory(self, args) -> CommandResult:
        """Set up kuzu-memory graph-based memory backend."""
        try:
            console.print(
                "\n[bold]Kuzu Memory Setup[/bold]\n"
                "This will replace static file-based memory with graph-based kuzu-memory.\n"
                "Kuzu-memory provides semantic search and enhanced context management.\n"
            )

            # Use centralized package installer
            console.print("[cyan]Checking kuzu-memory installation...[/cyan]")

            from ...services.package_installer import (
                InstallAction,
                PackageInstallerService,
                get_spec,
            )

            installer = PackageInstallerService()
            spec = get_spec(SetupService.KUZU_MEMORY)

            force = getattr(args, "force", False)
            upgrade = getattr(args, "upgrade", False)

            # Check if already installed and no flags set
            if installer.is_installed(spec) and not force and not upgrade:
                console.print("[green]✓ kuzu-memory already installed[/green]")
            else:
                console.print("[cyan]Detecting installation method...[/cyan]")
                success, message = installer.install(
                    spec, InstallAction.INSTALL, force=force, upgrade=upgrade
                )
                if success:
                    console.print(f"[green]✓ {message}[/green]")
                else:
                    return CommandResult.error_result(message)

            # Migrate existing static memory files if present
            console.print("\n[cyan]Checking for existing memory files...[/cyan]")
            static_memory_dir = Path.cwd() / ".claude-mpm" / "memories"
            memories_migrated = False

            if static_memory_dir.exists() and list(static_memory_dir.glob("*.md")):
                console.print(
                    "[yellow]Found existing static memory files. Migrating to kuzu-memory...[/yellow]"
                )

                # Count memory files
                memory_files = list(static_memory_dir.glob("*.md"))
                console.print(f"  Found {len(memory_files)} memory file(s)")

                # Migrate each memory file to kuzu-memory
                for memory_file in memory_files:
                    try:
                        content = memory_file.read_text()
                        agent_name = memory_file.stem

                        # Use kuzu-memory CLI to import the memory
                        import_result = subprocess.run(
                            [
                                "kuzu-memory",
                                "memory",
                                "learn",
                                content,
                                "--metadata",
                                f"agent={agent_name}",
                                "--metadata",
                                "source=mpm_static_migration",
                            ],
                            capture_output=True,
                            text=True,
                            check=False,
                        )  # nosec B603 B607

                        if import_result.returncode == 0:
                            console.print(
                                f"  [green]✓ Migrated {memory_file.name}[/green]"
                            )
                            memories_migrated = True
                        else:
                            console.print(
                                f"  [yellow]⚠ Could not migrate {memory_file.name}: {import_result.stderr}[/yellow]"
                            )

                    except Exception as e:
                        console.print(
                            f"  [yellow]⚠ Error migrating {memory_file.name}: {e}[/yellow]"
                        )

                if memories_migrated:
                    console.print(
                        "[green]✓ Memory files migrated to kuzu-memory[/green]"
                    )

                    # Create backup of static memory files
                    backup_dir = static_memory_dir.parent / "memories_backup"
                    backup_dir.mkdir(exist_ok=True)

                    for memory_file in memory_files:
                        shutil.copy2(memory_file, backup_dir / memory_file.name)

                    console.print(f"[green]✓ Backup created at: {backup_dir}[/green]")

                    # Archive original files to prevent re-import
                    console.print("\n[cyan]Archiving original files...[/cyan]")
                    archive_dir = static_memory_dir / ".migrated"
                    archive_dir.mkdir(exist_ok=True)

                    archived_count = 0
                    for memory_file in memory_files:
                        try:
                            dest = archive_dir / memory_file.name
                            memory_file.rename(dest)
                            console.print(f"  [dim]✓ Archived {memory_file.name}[/dim]")
                            archived_count += 1
                        except Exception as e:
                            console.print(
                                f"  [yellow]⚠ Could not archive {memory_file.name}: {e}[/yellow]"
                            )

                    if archived_count > 0:
                        # Create README in archive directory
                        from datetime import datetime, timezone

                        readme_content = f"""# Migrated Memory Files

These static memory files were migrated to kuzu-memory on {datetime.now(timezone.utc).isoformat()}.

**Status**: These files are archived and no longer active. The kuzu-memory graph database now manages all memories.

**Backup**: Backup copies exist in `../memories_backup/` for recovery if needed.

**Recovery**: If you need to restore these files, copy them back to `../` (parent directory).
"""
                        try:
                            (archive_dir / "README.md").write_text(readme_content)
                            console.print(
                                f"[green]✓ Archived {archived_count} file(s) to .migrated/[/green]"
                            )
                        except Exception as e:
                            console.print(
                                f"[yellow]Warning: Could not create archive README: {e}[/yellow]"
                            )
            else:
                console.print("  No existing memory files to migrate")

            # Update PROJECT-LOCAL configuration (not global)
            console.print("\n[cyan]Configuring kuzu-memory backend...[/cyan]")

            # Use project-local config in .claude-mpm/configuration.yaml
            config_path = Path.cwd() / ".claude-mpm" / "configuration.yaml"

            # Load existing config or create new
            import yaml

            if config_path.exists():
                with open(config_path) as f:
                    config = yaml.safe_load(f) or {}
            else:
                config = {}

            # Set memory backend to kuzu
            if "memory" not in config:
                config["memory"] = {}

            config["memory"]["backend"] = "kuzu"

            # Set default kuzu config if not present
            if "kuzu" not in config["memory"]:
                config["memory"]["kuzu"] = {
                    "project_root": str(Path.cwd()),
                    "db_path": str(Path.cwd() / "kuzu-memories"),
                }

            # Save config
            config_path.parent.mkdir(parents=True, exist_ok=True)
            with open(config_path, "w") as f:
                yaml.dump(config, f, default_flow_style=False, sort_keys=False)

            console.print(
                f"[green]✓ Project configuration updated: {config_path}[/green]"
            )

            # Create database directory
            db_path = Path(config["memory"]["kuzu"].get("db_path", "./kuzu-memories"))
            db_path.mkdir(parents=True, exist_ok=True)
            console.print(f"[green]✓ Created database directory: {db_path}[/green]")

            # Create .kuzu-memory-config for subservient mode (v1.6.33+)
            kuzu_config_path = Path.cwd() / ".kuzu-memory-config"
            kuzu_config = {
                "mode": "subservient",
                "managed_by": "claude-mpm",
                "version": "1.0",
            }

            try:
                with open(kuzu_config_path, "w") as f:
                    yaml.dump(kuzu_config, f, default_flow_style=False)
                console.print(
                    f"[green]✓ Created subservient mode config: {kuzu_config_path}[/green]"
                )
            except Exception as e:
                console.print(
                    f"[yellow]Warning: Could not create .kuzu-memory-config: {e}[/yellow]"
                )

            # Remove kuzu-memory's independent hooks
            console.print("\n[cyan]Cleaning up kuzu-memory hooks...[/cyan]")
            self._remove_kuzu_memory_hooks()

            console.print("\n[green]✓ Kuzu Memory setup complete![/green]")

            # Build what changed message
            what_changed = [
                "  1. kuzu-memory v1.6.33+ installed (or re-used if already installed)",
                "  2. Project-local configuration created (.claude-mpm/configuration.yaml)",
                "  3. Memory backend set to 'kuzu' for THIS PROJECT ONLY",
                "  4. Database directory created",
                "  5. Subservient mode enabled (MPM controls hooks, project-only)",
            ]

            if memories_migrated:
                what_changed.append(
                    "  6. Existing static memory files migrated and backed up"
                )

            console.print("\n[dim]What changed:[/dim]")
            console.print("\n".join(what_changed))

            console.print(
                "\n[dim]Next steps:[/dim]\n"
                "  1. Start Claude MPM to use graph-based memory\n"
                "  2. Your memories will be stored in the graph database\n"
                "  3. Use semantic search for better context retrieval\n"
                "\n[dim]Important:[/dim]\n"
                "  • Configuration is PROJECT-LOCAL (not global)\n"
                "  • Hooks are PROJECT-ONLY (not system-wide)\n"
                "  • kuzu-memory operates in subservient mode\n"
                "  • Each project can have its own memory backend configuration\n"
            )

            return CommandResult.success_result("Kuzu Memory setup completed")

        except KeyboardInterrupt:
            console.print("\n[yellow]Setup cancelled by user[/yellow]")
            return CommandResult.error_result("Setup cancelled")
        except Exception as e:
            return CommandResult.error_result(f"Error during setup: {e}")

    def _setup_mcp_vector_search(self, args) -> CommandResult:
        """Set up mcp-vector-search semantic code search.

        Delegates to the native mcp-vector-search setup command for indexing
        and configuration, following the pattern used by mcp-ticketer.
        """
        try:
            console.print(
                "\n[bold]MCP Vector Search Setup[/bold]\n"
                "This will set up semantic code search with vector embeddings.\n"
            )

            # Use centralized package installer
            console.print("[cyan]Checking mcp-vector-search installation...[/cyan]")

            from ...services.package_installer import (
                InstallAction,
                PackageInstallerService,
                get_spec,
            )

            installer = PackageInstallerService()
            spec = get_spec(SetupService.MCP_VECTOR_SEARCH)

            force = getattr(args, "force", False)
            upgrade = getattr(args, "upgrade", False)

            # Check if binary exists (skip module import check for tool packages)
            if shutil.which("mcp-vector-search") and not force and not upgrade:
                console.print("[green]✓ mcp-vector-search already installed[/green]")
            else:
                console.print("[cyan]Detecting installation method...[/cyan]")
                success, message = installer.install(
                    spec, InstallAction.INSTALL, force=force, upgrade=upgrade
                )
                if success:
                    console.print(f"[green]✓ {message}[/green]")
                else:
                    return CommandResult.error_result(message)

            # Run the two non-interactive mcp-vector-search sub-commands.
            # WHY: `mcp-vector-search setup` calls setup_llm_api_keys(interactive=True)
            # which issues a Rich console.input() prompt.  With capture_output=True the
            # subprocess's stdin is /dev/null so the prompt blocks forever.
            # `index` and `install mcp` do the same work without interactive prompts.

            console.print("\n[cyan]Indexing codebase with mcp-vector-search...[/cyan]")
            index_args = ["mcp-vector-search", "index"]
            if force:
                index_args.append("--force")

            result = subprocess.run(
                index_args,
                capture_output=True,
                text=True,
                check=False,
            )  # nosec B603 B607

            if result.returncode != 0:
                console.print("[red]✗ mcp-vector-search index failed:[/red]")
                if result.stderr:
                    console.print(result.stderr)
                return CommandResult.error_result("mcp-vector-search index failed")

            console.print("[green]✓ Codebase indexed[/green]")
            console.print("\n[cyan]Configuring MCP integration...[/cyan]")

            install_result = subprocess.run(
                ["mcp-vector-search", "install", "mcp"],
                capture_output=True,
                text=True,
                check=False,
            )  # nosec B603 B607

            if install_result.returncode != 0:
                console.print("[red]✗ mcp-vector-search install mcp failed:[/red]")
                if install_result.stderr:
                    console.print(install_result.stderr)
                return CommandResult.error_result(
                    "mcp-vector-search MCP install failed"
                )

            console.print("\n[green]✓ MCP Vector Search setup complete![/green]")
            console.print(
                "\n[dim]What changed:[/dim]\n"
                "  1. mcp-vector-search installed (or re-used if already installed)\n"
                "  2. Codebase indexed for semantic search\n"
                "  3. MCP configuration updated in .mcp.json\n"
                "  4. MCP server available in Claude Code\n"
            )
            console.print(
                "\n[dim]Next steps:[/dim]\n"
                "  1. Restart Claude Code to load the MCP server\n"
                "  2. Use semantic search tools for better context retrieval\n"
            )

            return CommandResult.success_result("MCP Vector Search setup completed")

        except KeyboardInterrupt:
            console.print("\n[yellow]Setup cancelled by user[/yellow]")
            return CommandResult.error_result("Setup cancelled")
        except Exception as e:
            return CommandResult.error_result(f"Error during setup: {e}")

    def _setup_mcp_skillset(self, args) -> CommandResult:
        """Setup mcp-skillset as a USER-LEVEL MCP server.

        This configures mcp-skillset in Claude Desktop config (~/.claude-mpm/ or
        ~/Library/Application Support/Claude/claude_desktop_config.json),
        NOT in project .mcp.json.

        Args:
            args: Setup options (force flag supported)

        Returns:
            CommandResult indicating success or failure
        """
        console.print(
            "\n[bold cyan]Setting up mcp-skillset (USER-LEVEL)...[/bold cyan]"
        )
        console.print(
            "[dim]This will install mcp-skillset for ALL projects (not project-specific)[/dim]\n"
        )

        try:
            # Use centralized package installer
            console.print("[cyan]Checking mcp-skillset installation...[/cyan]")

            from ...services.package_installer import (
                InstallAction,
                PackageInstallerService,
                get_spec,
            )

            installer = PackageInstallerService()
            spec = get_spec(SetupService.MCP_SKILLSET)

            force = getattr(args, "force", False)
            upgrade = getattr(args, "upgrade", False)

            # Check if already installed and no flags set
            if installer.is_installed(spec) and not force and not upgrade:
                console.print("[green]✓ mcp-skillset already installed[/green]")
            else:
                console.print("[cyan]Detecting installation method...[/cyan]")
                success, message = installer.install(
                    spec, InstallAction.INSTALL, force=force, upgrade=upgrade
                )
                if success:
                    console.print(f"[green]✓ {message}[/green]")
                else:
                    return CommandResult.error_result(message)

            # Configure in USER-LEVEL Claude Desktop config
            console.print(
                "\n[cyan]Configuring in Claude Desktop (user-level)...[/cyan]"
            )

            config_path = self._get_claude_desktop_config_path()
            if not config_path:
                return CommandResult.error_result(
                    "Could not determine Claude Desktop config path"
                )

            console.print(f"  Config: {config_path}")

            # Load or create config
            import json

            if config_path.exists():
                try:
                    with open(config_path) as f:
                        config = json.load(f)
                except (json.JSONDecodeError, OSError) as e:
                    console.print(
                        f"[yellow]Warning: Could not read config: {e}[/yellow]"
                    )
                    config = {"mcpServers": {}}
            else:
                config = {"mcpServers": {}}

            # Ensure mcpServers exists
            if "mcpServers" not in config:
                config["mcpServers"] = {}

            # Check if already configured
            if "mcp-skillset" in config["mcpServers"] and not force:
                console.print("[dim]mcp-skillset already configured[/dim]")
                return CommandResult.success_result("mcp-skillset already configured")

            # Add mcp-skillset configuration
            config["mcpServers"]["mcp-skillset"] = {
                "type": "stdio",
                "command": "mcp-skillset",
                "args": ["mcp"],
                "env": {},
            }

            # Save config
            try:
                config_path.parent.mkdir(parents=True, exist_ok=True)
                with open(config_path, "w") as f:
                    json.dump(config, f, indent=2)
                    f.write("\n")

                console.print(
                    "[green]✓ Added mcp-skillset to Claude Desktop config[/green]"
                )
            except OSError as e:
                return CommandResult.error_result(f"Could not save config: {e}")

            console.print("\n[green]✓ mcp-skillset setup complete![/green]")
            console.print(
                "\n[dim]What changed:[/dim]\n"
                "  1. mcp-skillset installed (or re-used if already installed)\n"
                "  2. Configuration added to Claude Desktop config (USER-LEVEL)\n"
                "  3. MCP tools available across ALL projects\n"
                "  4. Skills optimization can now query mcp-skillset for recommendations\n"
            )
            console.print(
                "\n[dim]Next steps:[/dim]\n"
                "  1. Restart Claude Code to load mcp-skillset\n"
                "  2. Use: claude-mpm skills optimize --use-mcp-skillset\n"
                "  3. MCP tools will enhance skill recommendations\n"
            )

            return CommandResult.success_result("mcp-skillset setup completed")

        except KeyboardInterrupt:
            console.print("\n[yellow]Setup cancelled by user[/yellow]")
            return CommandResult.error_result("Setup cancelled")
        except Exception as e:
            console.print(f"[red]✗ Failed to setup mcp-skillset: {e}[/red]")
            import traceback

            traceback.print_exc()
            return CommandResult.error_result(f"Failed to setup mcp-skillset: {e}")

    def _get_claude_desktop_config_path(self) -> Path | None:
        """Get Claude Desktop configuration path.

        Returns:
            Path to claude_desktop_config.json or None if not found
        """
        import platform

        possible_paths = [
            Path.home()
            / "Library"
            / "Application Support"
            / "Claude"
            / "claude_desktop_config.json",  # macOS
            Path.home() / ".config" / "Claude" / "claude_desktop_config.json",  # Linux
            Path.home()
            / "AppData"
            / "Roaming"
            / "Claude"
            / "claude_desktop_config.json",  # Windows
            Path.home() / ".claude" / "claude_desktop_config.json",  # Alternative
        ]

        for path in possible_paths:
            if path.exists():
                return path

        # Return platform-appropriate default
        system = platform.system()
        if system == "Darwin":  # macOS
            return (
                Path.home()
                / "Library"
                / "Application Support"
                / "Claude"
                / "claude_desktop_config.json"
            )
        if system == "Windows":
            return (
                Path.home()
                / "AppData"
                / "Roaming"
                / "Claude"
                / "claude_desktop_config.json"
            )
        # Linux and others
        return Path.home() / ".config" / "Claude" / "claude_desktop_config.json"

    def _setup_mcp_ticketer(self, args) -> CommandResult:
        """Setup mcp-ticketer with MPM hook integration.

        Args:
            args: Setup options (force, upgrade flags supported)

        Returns:
            CommandResult indicating success or failure
        """
        console.print("\n[bold cyan]Setting up mcp-ticketer...[/bold cyan]")

        try:
            # Use centralized package installer
            console.print("[cyan]Checking mcp-ticketer installation...[/cyan]")

            from ...services.package_installer import (
                InstallAction,
                PackageInstallerService,
                get_spec,
            )

            installer = PackageInstallerService()
            spec = get_spec(SetupService.MCP_TICKETER)

            force = getattr(args, "force", False)
            upgrade = getattr(args, "upgrade", False)

            # Check if already installed and no flags set
            if installer.is_installed(spec) and not force and not upgrade:
                console.print("[green]✓ mcp-ticketer already installed[/green]")
            else:
                console.print("[cyan]Detecting installation method...[/cyan]")
                success, message = installer.install(
                    spec, InstallAction.INSTALL, force=force, upgrade=upgrade
                )
                if success:
                    console.print(f"[green]✓ {message}[/green]")
                else:
                    return CommandResult.error_result(message)

            # Run mcp-ticketer setup with auto mode
            # This integrates with MPM's hook system automatically
            console.print("\n[cyan]Running mcp-ticketer setup...[/cyan]")
            result = subprocess.run(
                ["mcp-ticketer", "setup"],
                capture_output=True,
                text=True,
                check=False,
            )  # nosec B603 B607

            if result.returncode == 0:
                console.print("[green]✓ mcp-ticketer configured successfully[/green]")
                console.print("  [dim]Hooks integrated with MPM hook system[/dim]")
                return CommandResult.success_result("mcp-ticketer setup completed")

            console.print(
                "[yellow]mcp-ticketer setup completed with warnings:[/yellow]"
            )
            console.print(f"  {result.stderr.strip()}")
            return CommandResult.success_result(
                "mcp-ticketer setup completed with warnings"
            )

        except FileNotFoundError:
            console.print("[red]mcp-ticketer not found. Install with:[/red]")
            console.print("  pip install mcp-ticketer")
            return CommandResult.error_result("mcp-ticketer not installed")
        except Exception as e:
            console.print(f"[red]Failed to setup mcp-ticketer: {e}[/red]")
            return CommandResult.error_result(f"Failed to setup mcp-ticketer: {e}")

    def _setup_google_workspace(self, args) -> CommandResult:
        """Set up Google Workspace MCP (delegates to OAuth setup)."""
        console.print(
            "This will configure OAuth authentication for Google Workspace.\n"
        )

        # Use centralized package installer
        console.print("[cyan]Checking gworkspace-mcp installation...[/cyan]")

        from ...services.package_installer import (
            InstallAction,
            PackageInstallerService,
            get_spec,
        )

        installer = PackageInstallerService()
        spec = get_spec(SetupService.GWORKSPACE_MCP)

        force = getattr(args, "force", False)
        upgrade = getattr(args, "upgrade", False)

        # Check if already installed and no flags set
        if installer.is_installed(spec) and not force and not upgrade:
            console.print("[green]✓ gworkspace-mcp already installed[/green]")
        else:
            console.print("[cyan]Detecting installation method...[/cyan]")
            success, message = installer.install(
                spec, InstallAction.INSTALL, force=force, upgrade=upgrade
            )
            if success:
                console.print(f"[green]✓ {message}[/green]\n")
            else:
                return CommandResult.error_result(message)

        # Check if tokens already exist - skip setup if authenticated
        tokens_path = Path.home() / ".gworkspace-mcp" / "tokens.json"
        if tokens_path.exists() and tokens_path.stat().st_size > 10 and not force:
            console.print("[green]✓ Already authenticated (tokens exist)[/green]")
            exit_code = 0
        else:
            # Detect credentials from environment or .env files
            from .oauth import _detect_google_credentials

            client_id, client_secret, source = _detect_google_credentials()

            if client_id and client_secret:
                console.print(f"[dim]Using credentials from {source}[/dim]")
                # Set environment variables for the setup command
                env = os.environ.copy()
                env["GOOGLE_OAUTH_CLIENT_ID"] = client_id
                env["GOOGLE_OAUTH_CLIENT_SECRET"] = client_secret

                console.print("[cyan]Running gworkspace-mcp setup...[/cyan]\n")
                try:
                    setup_result = subprocess.run(  # nosec B603 B607
                        ["gworkspace-mcp", "setup"],
                        check=False,
                        env=env,
                    )
                    exit_code = setup_result.returncode
                except Exception as e:
                    console.print(f"[red]Failed to run setup: {e}[/red]")
                    exit_code = 1
            else:
                console.print(
                    "[yellow]No OAuth credentials found.[/yellow]\n"
                    "[dim]Set GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET in:[/dim]\n"
                    "  - Environment variables, or\n"
                    "  - .env.local file, or\n"
                    "  - .env file\n"
                )
                return CommandResult.error_result(
                    "OAuth credentials required. See above for details."
                )

        # Configure MCP server in .mcp.json
        from .oauth import _ensure_mcp_configured

        _ensure_mcp_configured("gworkspace-mcp", Path.cwd())

        # Add .gworkspace-mcp/ to .gitignore if not present
        if exit_code == 0:
            self._add_gworkspace_to_gitignore()

        # Register service in setup registry on success
        if exit_code == 0:
            try:
                from claude_mpm.services.setup_registry import SetupRegistry

                registry = SetupRegistry()

                # Get CLI help for the tool
                cli_help = ""
                try:
                    help_result = subprocess.run(  # nosec B603 B607
                        ["gworkspace-mcp", "--help"],
                        check=False,
                        capture_output=True,
                        text=True,
                        timeout=5,
                    )
                    if help_result.returncode == 0:
                        cli_help = help_result.stdout
                except Exception:  # nosec B110
                    pass  # Help text is optional, failure is non-fatal

                # Register with known tools
                registry.add_service(
                    name="gworkspace-mcp",
                    service_type="mcp",
                    version="0.1.2",  # TODO: Get from package
                    tools=[
                        "search_gmail_messages",
                        "get_gmail_message_content",
                        "list_calendar_events",
                        "get_calendar_event",
                        "search_drive_files",
                        "get_drive_file_content",
                    ],
                    cli_help=cli_help,
                    config_location="user",
                )
            except Exception as e:
                console.print(
                    f"[dim]Warning: Could not update setup registry: {e}[/dim]"
                )

        return CommandResult(
            success=exit_code == 0,
            exit_code=exit_code,
            message="Google Workspace MCP setup",
        )

    def _setup_oauth(self, args) -> CommandResult:
        """Set up OAuth for a service (delegates to OAuth command)."""
        # Get service name from arguments
        service_name = getattr(args, "oauth_service", None)
        if not service_name:
            return CommandResult.error_result(
                "OAuth setup requires --oauth-service flag. "
                "Example: claude-mpm setup oauth --oauth-service gworkspace-mcp"
            )

        # Delegate to OAuth setup
        from argparse import Namespace

        from .oauth import manage_oauth

        oauth_args = Namespace(
            oauth_command="setup",
            service_name=service_name,
            no_browser=getattr(args, "no_browser", False),
            no_launch=getattr(args, "no_launch", False),
            force=getattr(args, "force", False),
        )

        exit_code = manage_oauth(oauth_args)
        return CommandResult(
            success=exit_code == 0,
            exit_code=exit_code,
            message=f"OAuth setup for {service_name}",
        )


def manage_setup(args) -> int:
    """Main entry point for setup commands.

    Args:
        args: Parsed command line arguments

    Returns:
        Exit code (0 for success, non-zero for errors)
    """
    command = SetupCommand()
    result = command.execute(args)

    # Print error message if command failed
    if not result.success and result.message:
        console.print(f"\n[red]Error:[/red] {result.message}\n", style="bold")

    return result.exit_code
