"""Main CLI entry point for EmDash."""

from collections import Counter
from pathlib import Path
from contextlib import contextmanager

import click
from rich.console import Console, Group
from rich.panel import Panel
from rich.text import Text
from rich.live import Live
from rich.spinner import Spinner
from rich.table import Table

from emdash.agent.providers.factory import DEFAULT_MODEL
from emdash.core.config import get_config
from emdash.graph.connection import get_connection
from emdash.graph.schema import SchemaManager
from emdash.ingestion.orchestrator import IngestionOrchestrator
from emdash.analytics.engine import AnalyticsEngine
from emdash.utils.logger import log

console = Console()

# ASCII Art Logo for Em Dash
EMDASH_LOGO = """
[bold cyan]┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓[/bold cyan]
[bold cyan]┃[/bold cyan]                                                            [bold cyan]┃[/bold cyan]
[bold cyan]┃[/bold cyan]  [bold white]███████╗███╗   ███╗[/bold white] [bold orange1]██████╗  █████╗ ███████╗██╗  ██╗[/bold orange1]  [bold cyan]┃[/bold cyan]
[bold cyan]┃[/bold cyan]  [bold white]██╔════╝████╗ ████║[/bold white] [bold orange1]██╔══██╗██╔══██╗██╔════╝██║  ██║[/bold orange1]  [bold cyan]┃[/bold cyan]
[bold cyan]┃[/bold cyan]  [bold white]█████╗  ██╔████╔██║[/bold white] [bold orange1]██║  ██║███████║███████╗███████║[/bold orange1]  [bold cyan]┃[/bold cyan]
[bold cyan]┃[/bold cyan]  [bold white]██╔══╝  ██║╚██╔╝██║[/bold white] [bold orange1]██║  ██║██╔══██║╚════██║██╔══██║[/bold orange1]  [bold cyan]┃[/bold cyan]
[bold cyan]┃[/bold cyan]  [bold white]███████╗██║ ╚═╝ ██║[/bold white] [bold orange1]██████╔╝██║  ██║███████║██║  ██║[/bold orange1]  [bold cyan]┃[/bold cyan]
[bold cyan]┃[/bold cyan]  [bold white]╚══════╝╚═╝     ╚═╝[/bold white] [bold orange1]╚═════╝ ╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝[/bold orange1]  [bold cyan]┃[/bold cyan]
[bold cyan]┃[/bold cyan]                                                            [bold cyan]┃[/bold cyan]
[bold cyan]┃[/bold cyan]  [dim]The 'Tech Lead' Context Engine[/dim]                          [bold cyan]┃[/bold cyan]
[bold cyan]┃[/bold cyan]                                                            [bold cyan]┃[/bold cyan]
[bold cyan]┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛[/bold cyan]
"""

EMDASH_LOGO_SMALL = """[bold cyan]╭─[/bold cyan] [bold white]EM[/bold white][bold orange1]DASH[/bold orange1] [dim]Graph Intelligence[/dim] [bold cyan]─╮[/bold cyan]"""


class IndexingProgress:
    """Tracks and displays indexing progress with a nice UI."""

    def __init__(self, console: Console):
        self.console = console
        self.steps = []
        self.current_step = ""
        self.completed_steps = []
        self.description_progress = None  # (current, total, snippet)

    def _build_display(self) -> Group:
        """Build the display with logo and progress."""
        # Create progress table
        table = Table.grid(padding=(0, 2))
        table.add_column(justify="left", width=3)
        table.add_column(justify="left")

        # Show completed steps
        for step in self.completed_steps:
            table.add_row("[bold green]✓[/bold green]", f"[green]{step}[/green]")

        # Show current step with spinner
        if self.current_step:
            spinner = Spinner("dots", text=f"[bold yellow]{self.current_step}[/bold yellow]")
            table.add_row(spinner, "")

        elements = [
            Text.from_markup(EMDASH_LOGO_SMALL),
            Text(""),
            table,
        ]

        # Show description generation progress if active
        if self.description_progress:
            current, total, snippet = self.description_progress
            pct = int((current / total) * 100) if total > 0 else 0

            # Progress bar
            bar_width = 30
            filled = int(bar_width * current / total) if total > 0 else 0
            bar = "█" * filled + "░" * (bar_width - filled)

            elements.append(Text(""))
            elements.append(Text.from_markup(
                f"  [cyan]Describing communities:[/cyan] [{current}/{total}] {pct}%"
            ))
            elements.append(Text.from_markup(f"  [dim]{bar}[/dim]"))

            if snippet:
                # Truncate snippet to fit nicely
                display_snippet = snippet[:60] + "..." if len(snippet) > 60 else snippet
                elements.append(Text.from_markup(f"  [dim italic]→ {display_snippet}[/dim italic]"))

        return Group(*elements)

    @contextmanager
    def live_progress(self):
        """Context manager for live progress display."""
        with Live(self._build_display(), console=self.console, refresh_per_second=10) as live:
            self._live = live
            yield self

    def update(self, step: str):
        """Update current step."""
        if self.current_step:
            self.completed_steps.append(self.current_step)
        self.current_step = step
        self.description_progress = None
        if hasattr(self, '_live'):
            self._live.update(self._build_display())

    def update_description_progress(self, current: int, total: int, snippet: str = None):
        """Update description generation progress."""
        self.description_progress = (current, total, snippet)
        if hasattr(self, '_live'):
            self._live.update(self._build_display())

    def complete(self, step: str = None):
        """Mark current step as complete."""
        if step:
            self.current_step = step
        if self.current_step:
            self.completed_steps.append(self.current_step)
        self.current_step = ""
        self.description_progress = None
        if hasattr(self, '_live'):
            self._live.update(self._build_display())


def show_logo():
    """Display the Rove logo."""
    console.print(EMDASH_LOGO)


@click.command()
@click.argument("task", required=False)
def start_coding_agent(task: str | None = None):
    """Start the Em Dash coding agent.

    Run 'em' for interactive mode or 'em "task"' for single task.

    Examples:
        em                    # Interactive mode
        em "Fix the login bug"  # Single task
    """
    from emdash.agent.providers.factory import DEFAULT_MODEL

    # Call agent_code function directly with defaults
    agent_code.callback(
        task=task,
        model=DEFAULT_MODEL,
        mode="code",
        no_graph_tools=False,
        quiet=False,
        save=False,
    )


@click.group()
@click.version_option(version="0.1.0")
def cli():
    """EmDash - The 'Senior Engineer' Context Engine.

    Transform your codebase into a living knowledge graph.

    Commands:
        emdash ingest <path>    Index a codebase
        emdash analyze          Run analytics (pagerank, communities)
        emdash agent code       Start the coding agent

    Use 'em' for quick access to the coding agent.
    """
    pass


def _configure_db_for_cwd():
    """Configure database connection for current working directory's git repo.

    Finds the git root and sets up the database in {git_root}/.emdash/index/kuzu_db.
    This ensures each repository has its own isolated database.
    """
    import subprocess
    from emdash.graph.connection import configure_for_repo

    try:
        result = subprocess.run(
            ["git", "rev-parse", "--show-toplevel"],
            capture_output=True,
            text=True,
            check=True,
        )
        repo_root = Path(result.stdout.strip())
        configure_for_repo(repo_root)
        return repo_root
    except subprocess.CalledProcessError:
        # Not in a git repo - use CWD
        configure_for_repo(Path.cwd())
        return Path.cwd()


@cli.group()
def db():
    """Database management commands."""
    pass


@db.command("init")
def db_init():
    """Initialize the Kuzu database schema."""
    _configure_db_for_cwd()
    console.print("[bold blue]Initializing Kuzu database...[/bold blue]")

    try:
        connection = get_connection()
        connection.connect()

        schema_manager = SchemaManager(connection)
        schema_manager.initialize_schema()

        console.print("[bold green]✓[/bold green] Database initialized successfully")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Failed to initialize database: {e}")
        raise click.Abort()


@db.command("clear")
@click.confirmation_option(prompt="Are you sure you want to clear all data?")
def db_clear():
    """Clear all data from the database."""
    _configure_db_for_cwd()
    console.print("[bold yellow]Clearing database...[/bold yellow]")

    try:
        connection = get_connection()
        connection.clear_database()

        console.print("[bold green]✓[/bold green] Database cleared successfully")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Failed to clear database: {e}")
        raise click.Abort()


@db.command("stats")
def db_stats():
    """Show database statistics."""
    _configure_db_for_cwd()
    console.print("[bold blue]Fetching database statistics...[/bold blue]\n")

    try:
        connection = get_connection()
        info = connection.get_database_info()

        console.print(f"[bold]Nodes:[/bold] {info['node_count']:,}")
        console.print(f"[bold]Relationships:[/bold] {info['relationship_count']:,}")

        if info['labels']:
            console.print(f"\n[bold]Node Labels:[/bold]")
            for label in sorted(info['labels']):
                console.print(f"  • {label}")

        if info['relationship_types']:
            console.print(f"\n[bold]Relationship Types:[/bold]")
            for rel_type in sorted(info['relationship_types']):
                console.print(f"  • {rel_type}")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Failed to fetch statistics: {e}")
        raise click.Abort()


@db.command("test")
def db_test():
    """Test the database connection."""
    _configure_db_for_cwd()
    console.print("[bold blue]Testing Kuzu connection...[/bold blue]")

    try:
        config = get_config()
        console.print(f"\n[bold]Configuration:[/bold]")
        console.print(f"  URI: {config.neo4j.uri}")
        console.print(f"  Database: {config.neo4j.database}")
        console.print(f"  Username: {config.neo4j.username}")

        connection = get_connection()
        if connection.verify_connection():
            console.print("\n[bold green]✓[/bold green] Connection successful")
        else:
            console.print("\n[bold red]✗[/bold red] Connection failed")
            raise click.Abort()

    except Exception as e:
        console.print(f"\n[bold red]✗[/bold red] Connection error: {e}")
        raise click.Abort()


@cli.group()
def context():
    """Session context management commands."""
    pass


@context.command("show")
def context_show():
    """Show current session context."""
    from emdash.context import ContextService

    _configure_db_for_cwd()
    service = ContextService()
    terminal_id = ContextService.get_terminal_id()

    console.print(f"\n[bold]Session Context[/bold]")
    console.print(f"  Terminal ID: {terminal_id[:8]}...")

    items = service.get_context_items(terminal_id)

    if not items:
        console.print("\n[dim]No context items yet. Run an agent to build context.[/dim]")
        return

    console.print(f"\n[bold]Context Items ({len(items)}):[/bold]\n")

    from rich.table import Table
    table = Table(show_header=True, header_style="bold")
    table.add_column("Type", style="cyan", width=10)
    table.add_column("Name", style="green")
    table.add_column("Score", justify="right", width=8)
    table.add_column("Touches", justify="right", width=8)
    table.add_column("Neighbors", width=30)

    for item in items:
        neighbors_str = ", ".join(item.neighbors[:3]) if item.neighbors else "-"
        if item.neighbors and len(item.neighbors) > 3:
            neighbors_str += f" (+{len(item.neighbors) - 3})"

        score_color = "green" if item.score > 0.7 else "yellow" if item.score > 0.4 else "red"
        table.add_row(
            item.entity_type,
            item.qualified_name,
            f"[{score_color}]{item.score:.2f}[/{score_color}]",
            str(item.touch_count),
            neighbors_str,
        )

    console.print(table)


@context.command("clear")
def context_clear():
    """Clear session context."""
    from emdash.context import ContextService

    _configure_db_for_cwd()
    service = ContextService()
    terminal_id = ContextService.get_terminal_id()

    service.clear_context(terminal_id)
    console.print("[green]Session context cleared.[/green]")


@context.command("prompt")
def context_prompt():
    """Show the context prompt that would be injected into the system prompt."""
    from emdash.context import ContextService

    _configure_db_for_cwd()
    service = ContextService()
    terminal_id = ContextService.get_terminal_id()

    prompt = service.get_context_prompt(terminal_id)

    if not prompt:
        console.print("[dim]No context prompt yet.[/dim]")
        return

    from rich.markdown import Markdown
    console.print(Markdown(prompt))


@cli.command()
@click.argument("repo_path")
@click.option("--incremental", is_flag=True, help="Perform incremental update (legacy)")
@click.option("--changed", is_flag=True, help="Only index files changed since last index (uses git diff)")
@click.option("--skip-embeddings", is_flag=True, help="Skip generating embeddings after indexing")
@click.option(
    "--index-git",
    is_flag=True,
    help="Index local git history (commits, authors, file changes)",
)
@click.option(
    "--index-github",
    type=int,
    default=None,
    help="Fetch PRs from GitHub. Value is max PRs per state (e.g., --index-github 100 fetches up to 100 open + 100 merged). Implies --index-git.",
)
@click.option(
    "--communities/--skip-communities",
    default=None,
    help="Detect communities after indexing (default: enabled unless AST_ONLY=true)",
)
@click.option(
    "--community-resolution",
    default=1.0,
    help="Resolution parameter for community detection",
)
@click.option(
    "--describe-communities/--no-describe-communities",
    default=False,
    help="Auto-generate LLM descriptions for detected communities",
)
@click.option(
    "--community-model",
    default=DEFAULT_MODEL,
    help="LLM model for community descriptions",
)
@click.option(
    "--community-limit",
    default=20,
    help="Max communities to describe (default: 20, 0 = all)",
)
def index(
    repo_path: str,
    incremental: bool,
    changed: bool,
    skip_embeddings: bool,
    index_git: bool,
    index_github: int,
    communities: bool,
    community_resolution: float,
    describe_communities: bool,
    community_model: str,
    community_limit: int,
):
    """Index a repository into the knowledge graph.

    REPO_PATH: URL or local path to a Git repository

    By default, indexes code and detects communities (no git history or GitHub PRs).

    Examples:
        emdash index .                       # Index code + detect communities
        emdash index . --changed             # Only index files changed since last index
        emdash index . --index-git           # Also index local git history
        emdash index . --index-github 50     # Also fetch up to 50 open + 50 merged PRs
        emdash index . --skip-communities    # Code only, no community detection
    """
    # Show the logo
    show_logo()
    console.print(f"[bold]Target:[/bold] {repo_path}\n")

    # Configure database for this repository (ensures isolation)
    from emdash.graph.connection import configure_for_repo
    from emdash.core.config import get_config
    try:
        repo_root = Path(repo_path).resolve()
    except (FileNotFoundError, OSError) as e:
        console.print(f"[red]Error:[/red] Cannot resolve path '{repo_path}'")
        console.print("[dim]This can happen if the current directory was deleted (e.g., removed git worktree)[/dim]")
        raise SystemExit(1)

    if not repo_root.exists():
        console.print(f"[red]Error:[/red] Path does not exist: {repo_root}")
        raise SystemExit(1)

    configure_for_repo(repo_root)

    # Apply AST_ONLY defaults from config
    config = get_config()
    ast_only = config.ingestion.ast_only

    # If AST_ONLY=true, skip git and communities by default (unless explicitly requested)
    if ast_only:
        if not index_git and index_github is None:
            # User didn't explicitly request git, so skip it
            index_git = False
        if communities is None:
            # User didn't explicitly set communities, so skip them
            communities = False
            console.print("[dim]AST_ONLY mode: skipping git history and communities[/dim]\n")

    # Default communities to True if not set (and not AST_ONLY)
    if communities is None:
        communities = True

    # --index-github implies --index-git
    if index_github is not None:
        index_git = True

    pr_limit = index_github if index_github is not None else 0

    # Build mode description
    mode_parts = []
    if not index_git:
        mode_parts.append("code analysis")
    elif pr_limit > 0:
        mode_parts.append(f"code + git + {pr_limit} PRs/state")
    else:
        mode_parts.append("code + git history")
    if communities:
        mode_parts.append("communities")
    if not skip_embeddings:
        mode_parts.append("embeddings")
    console.print(f"[dim]Mode: {' → '.join(mode_parts)}[/dim]\n")

    progress = IndexingProgress(console)

    try:
        with progress.live_progress():
            # Step 1: Parse codebase
            progress.update("Parsing codebase...")
            orchestrator = IngestionOrchestrator()
            orchestrator.index(
                repo_path,
                incremental=incremental,
                changed_only=changed,
                skip_git=not index_git,
                pr_limit=pr_limit,
            )
            progress.complete("Codebase indexed")

            # Step 2: Generate embeddings
            if not skip_embeddings:
                progress.update("Generating embeddings...")
                try:
                    from emdash.embeddings.indexer import EmbeddingIndexer

                    connection = get_connection()
                    indexer = EmbeddingIndexer(connection)

                    if indexer.embedding_service.is_available:
                        results = {}
                        if pr_limit > 0:
                            results['prs'] = indexer.index_pull_requests()
                        else:
                            results['prs'] = 0
                        results['functions'] = indexer.index_functions()
                        results['classes'] = indexer.index_classes()
                        total = sum(results.values())
                        progress.complete(f"Generated {total} embeddings")
                    else:
                        progress.complete("Embeddings skipped (no API key)")

                except Exception as e:
                    progress.complete(f"Embeddings failed: {e}")

            # Step 3: Detect communities
            if communities or describe_communities:
                progress.update("Detecting communities...")
                try:
                    connection = get_connection()
                    engine = AnalyticsEngine(connection)
                    assignments = engine.detect_communities(
                        resolution=community_resolution,
                        describe=False,
                    )
                    num_communities = len(set(assignments.values()))
                    progress.complete(f"Found {num_communities} communities")

                    if describe_communities:
                        progress.update("Generating community descriptions...")
                        counts = Counter(assignments.values())
                        if community_limit and community_limit > 0:
                            community_ids = [cid for cid, _ in counts.most_common(community_limit)]
                        else:
                            community_ids = list(counts.keys())

                        # Progress callback for description generation
                        def on_description_progress(current, total, cid, description):
                            progress.update_description_progress(current, total, description)

                        generated = engine.generate_community_descriptions(
                            community_ids=community_ids,
                            model=community_model,
                            overwrite=False,
                            progress_callback=on_description_progress,
                        )
                        progress.complete(f"Generated {len(generated)} descriptions")

                except Exception as e:
                    progress.complete(f"Community detection failed: {e}")

        # Final success message
        console.print("\n[bold green]━━━ Indexing Complete! ━━━[/bold green]")
        console.print("[dim]Run 'emdash analyze community' to explore your codebase[/dim]\n")

    except Exception as e:
        console.print(f"\n[bold red]✗ Indexing failed:[/bold red] {e}")
        log.exception("Indexing error:")
        raise click.Abort()


@cli.command("onboard")
@click.option("--skip-auth", is_flag=True, help="Skip GitHub authentication")
@click.option("--skip-projectmd", is_flag=True, help="Skip PROJECT.md generation")
@click.option("--model", default=DEFAULT_MODEL, help="Model for PROJECT.md generation")
def onboard(skip_auth: bool, skip_projectmd: bool, model: str):
    """Onboard a new repository - index, authenticate, and generate docs.

    This command runs three setup steps:
      1. Index the current repository (code analysis + communities)
      2. Authenticate with GitHub (for PR analysis features)
      3. Generate PROJECT.md (AI-generated codebase documentation)

    Examples:
        emdash onboard                    # Run all steps
        emdash onboard --skip-auth        # Skip GitHub auth
        emdash onboard --skip-projectmd   # Skip PROJECT.md generation
    """
    from emdash.graph.connection import configure_for_repo
    from emdash.auth import GitHubAuth, get_auth_status

    # Show welcome
    show_logo()
    console.print("[bold]Welcome to EmDash![/bold]")
    console.print("Let's get your repository set up.\n")

    # Determine step count
    total_steps = 3
    if skip_auth:
        total_steps -= 1
    if skip_projectmd:
        total_steps -= 1
    step_num = 0

    # --- Step 1: Index the repository ---
    step_num += 1
    console.print(f"[bold cyan]Step {step_num}/{total_steps}: Index the repository[/bold cyan]")
    console.print("[dim]Analyzes code structure, generates embeddings, and detects communities[/dim]")
    console.print()

    repo_root = Path.cwd().resolve()

    if not click.confirm("Proceed with indexing?", default=True):
        console.print("[yellow]⚠[/yellow] Indexing skipped\n")
    else:
        console.print()
        try:
            configure_for_repo(repo_root)

            progress = IndexingProgress(console)
            with progress.live_progress():
                # Parse codebase
                progress.update("Parsing codebase...")
                orchestrator = IngestionOrchestrator()
                orchestrator.index(str(repo_root), incremental=False, skip_git=True, pr_limit=0)
                progress.complete("Codebase indexed")

                # Generate embeddings
                progress.update("Generating embeddings...")
                try:
                    from emdash.embeddings.indexer import EmbeddingIndexer

                    connection = get_connection()
                    indexer = EmbeddingIndexer(connection)

                    if indexer.embedding_service.is_available:
                        results = {}
                        results['functions'] = indexer.index_functions()
                        results['classes'] = indexer.index_classes()
                        total = sum(results.values())
                        progress.complete(f"Generated {total} embeddings")
                    else:
                        progress.complete("Embeddings skipped (no API key)")
                except Exception as e:
                    progress.complete(f"Embeddings skipped: {e}")

                # Detect communities
                progress.update("Detecting communities...")
                try:
                    connection = get_connection()
                    engine = AnalyticsEngine(connection)
                    assignments = engine.detect_communities(resolution=1.0, describe=False)
                    num_communities = len(set(assignments.values()))
                    progress.complete(f"Found {num_communities} communities")
                except Exception as e:
                    progress.complete(f"Community detection skipped: {e}")

            console.print("[green]✓[/green] Repository indexed!\n")

        except Exception as e:
            console.print(f"[bold red]✗ Indexing failed:[/bold red] {e}")
            log.exception("Onboard indexing error:")
            raise click.Abort()

    # --- Step 2: GitHub Authentication ---
    if not skip_auth:
        step_num += 1
        console.print(f"[bold cyan]Step {step_num}/{total_steps}: GitHub Authentication[/bold cyan]")
        console.print("[dim]Required for PR analysis and team insights[/dim]")
        console.print()

        status = get_auth_status()
        if status["authenticated"]:
            console.print(f"[green]✓[/green] Already authenticated as @{status.get('username', 'unknown')}\n")
        elif not click.confirm("Proceed with GitHub authentication?", default=True):
            console.print("[yellow]⚠[/yellow] Authentication skipped")
            console.print("[dim]You can authenticate later with 'emdash auth login'[/dim]\n")
        else:
            console.print()
            console.print("Starting GitHub device flow authentication...")
            console.print("[dim]A browser will open - enter the code shown to authenticate[/dim]\n")
            try:
                auth = GitHubAuth()
                config = auth.login(open_browser=True)
                if config:
                    console.print("[green]✓[/green] GitHub authentication complete!\n")
                else:
                    console.print("[yellow]⚠[/yellow] Authentication skipped or failed\n")
            except Exception as e:
                console.print(f"[yellow]⚠[/yellow] Auth failed: {e}")
                console.print("[dim]You can authenticate later with 'emdash auth login'[/dim]\n")

    # --- Step 3: Generate PROJECT.md ---
    if not skip_projectmd:
        step_num += 1
        console.print(f"[bold cyan]Step {step_num}/{total_steps}: Generate PROJECT.md[/bold cyan]")
        console.print("[dim]AI-generated documentation to guide future interactions[/dim]")
        console.print()

        if not click.confirm("Proceed with PROJECT.md generation?", default=True):
            console.print("[yellow]⚠[/yellow] PROJECT.md skipped")
            console.print("[dim]You can generate it later with 'emdash projectmd --save'[/dim]\n")
        else:
            console.print()
            try:
                from emdash.agent.discovery import ProjectDiscoveryAgent

                agent = ProjectDiscoveryAgent(model=model, verbose=True)
                content = agent.discover()

                output_path = repo_root / "PROJECT.md"
                with open(output_path, "w") as f:
                    f.write(content)

                console.print(f"[green]✓[/green] Generated {output_path}\n")

            except ValueError as e:
                console.print(f"[yellow]⚠[/yellow] PROJECT.md skipped: {e}")
                console.print("[dim]You can generate it later with 'emdash projectmd --save'[/dim]\n")
            except Exception as e:
                console.print(f"[yellow]⚠[/yellow] PROJECT.md failed: {e}")
                console.print("[dim]You can generate it later with 'emdash projectmd --save'[/dim]\n")

    # --- Final Summary ---
    console.print("[bold green]━━━ Onboarding Complete! ━━━[/bold green]")
    console.print()
    console.print("[bold]Start coding:[/bold]")
    console.print("  Run [cyan]em[/cyan] to start your coding session")
    console.print()
    console.print("[bold]Quick tips:[/bold]")
    console.print("  • Type [cyan]/[/cyan] to access commands like [cyan]/plan[/cyan], [cyan]/spec[/cyan], and more")
    console.print("  • Type [cyan]@[/cyan] to mention a file for context")
    console.print("  • Type [cyan]/mcp[/cyan] to add MCP servers")
    console.print("  • Type [cyan]/review-pr[/cyan] to review a pull request")
    console.print("  • Type [cyan]/context[/cyan] to view the current context-frame")
    console.print()


@cli.command("update")
def update_emdash():
    """Update emdash to the latest version from GitHub.

    This command:
      1. Pulls latest changes from the remote repository
      2. Reinstalls dependencies
      3. Reinstalls emdash in development mode

    Examples:
        emdash update
    """
    import subprocess
    import sys
    from pathlib import Path

    # Find the emdash installation directory
    emdash_dir = Path(__file__).parent.parent.parent.resolve()

    console.print("[bold cyan]Updating EmDash...[/bold cyan]\n")

    # Check if it's a git repository
    git_dir = emdash_dir / ".git"
    if not git_dir.exists():
        console.print("[red]Error: EmDash directory is not a git repository.[/red]")
        console.print(f"[dim]Location: {emdash_dir}[/dim]")
        console.print("\n[yellow]If you installed via pip, update with:[/yellow]")
        console.print("  pip install --upgrade emdash")
        raise click.Abort()

    try:
        # Step 1: Git pull
        console.print("[bold]Step 1/3:[/bold] Pulling latest changes...")
        result = subprocess.run(
            ["git", "pull"],
            cwd=emdash_dir,
            capture_output=True,
            text=True
        )
        if result.returncode != 0:
            console.print(f"[red]Git pull failed:[/red] {result.stderr}")
            raise click.Abort()
        console.print(f"[green]✓[/green] {result.stdout.strip()}\n")

        # Step 2: Install dependencies
        console.print("[bold]Step 2/3:[/bold] Installing dependencies...")
        result = subprocess.run(
            [sys.executable, "-m", "pip", "install", "-r", "requirements.txt", "-q"],
            cwd=emdash_dir,
            capture_output=True,
            text=True
        )
        if result.returncode != 0:
            console.print(f"[red]Dependency install failed:[/red] {result.stderr}")
            raise click.Abort()
        console.print("[green]✓[/green] Dependencies updated\n")

        # Step 3: Reinstall emdash
        console.print("[bold]Step 3/3:[/bold] Reinstalling emdash...")
        result = subprocess.run(
            [sys.executable, "-m", "pip", "install", "-e", ".", "-q"],
            cwd=emdash_dir,
            capture_output=True,
            text=True
        )
        if result.returncode != 0:
            console.print(f"[red]Reinstall failed:[/red] {result.stderr}")
            raise click.Abort()
        console.print("[green]✓[/green] EmDash reinstalled\n")

        console.print("[bold green]━━━ Update Complete! ━━━[/bold green]")
        console.print("[dim]Restart your terminal or run 'source ~/.bashrc' to use the updated version[/dim]")

    except FileNotFoundError as e:
        console.print(f"[red]Error: Required tool not found:[/red] {e}")
        raise click.Abort()


@cli.group()
def query():
    """Query the knowledge graph."""
    _configure_db_for_cwd()


@query.command("find-class")
@click.argument("name")
def query_find_class(name: str):
    """Find a class by name."""
    console.print(f"[bold blue]Searching for class:[/bold blue] {name}\n")

    try:
        connection = get_connection()
        with connection.session() as session:
            query = """
            MATCH (c:Class)
            WHERE c.name CONTAINS $name OR c.qualified_name CONTAINS $name
            RETURN c.qualified_name as qualified_name,
                   c.name as name,
                   c.file_path as file_path,
                   c.line_start as line_start,
                   c.docstring as docstring,
                   c.is_abstract as is_abstract,
                   c.base_classes as base_classes
            LIMIT 20
            """

            result = session.run(query, name=name)
            records = list(result)

            if not records:
                console.print(f"[yellow]No classes found matching '{name}'[/yellow]")
                return

            console.print(f"Found {len(records)} class(es):\n")

            for record in records:
                console.print(f"[bold cyan]{record['qualified_name']}[/bold cyan]")
                console.print(f"  File: {record['file_path']}:{record['line_start']}")
                if record['is_abstract']:
                    console.print(f"  [yellow](Abstract)[/yellow]")
                if record['base_classes']:
                    console.print(f"  Inherits: {', '.join(record['base_classes'])}")
                if record['docstring']:
                    console.print(f"  {record['docstring'][:100]}...")
                console.print()

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Query failed: {e}")
        raise click.Abort()


@query.command("find-function")
@click.argument("name")
def query_find_function(name: str):
    """Find a function by name."""
    console.print(f"[bold blue]Searching for function:[/bold blue] {name}\n")

    try:
        connection = get_connection()
        with connection.session() as session:
            query = """
            MATCH (f:Function)
            WHERE f.name CONTAINS $name OR f.qualified_name CONTAINS $name
            RETURN f.qualified_name as qualified_name,
                   f.name as name,
                   f.file_path as file_path,
                   f.line_start as line_start,
                   f.parameters as parameters,
                   f.is_method as is_method,
                   f.cyclomatic_complexity as complexity,
                   f.docstring as docstring
            LIMIT 20
            """

            result = session.run(query, name=name)
            records = list(result)

            if not records:
                console.print(f"[yellow]No functions found matching '{name}'[/yellow]")
                return

            console.print(f"Found {len(records)} function(s):\n")

            for record in records:
                func_type = "Method" if record['is_method'] else "Function"
                console.print(f"[bold cyan]{record['qualified_name']}[/bold cyan] [{func_type}]")
                console.print(f"  File: {record['file_path']}:{record['line_start']}")
                console.print(f"  Parameters: ({', '.join(record['parameters'])})")
                console.print(f"  Complexity: {record['complexity']}")
                if record['docstring']:
                    console.print(f"  {record['docstring'][:100]}...")
                console.print()

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Query failed: {e}")
        raise click.Abort()


@query.command("knowledge-silos")
@click.option("--threshold", default=0.0001, help="Minimum importance threshold (default: 0.0001)")
@click.option("--max-authors", default=2, help="Maximum author count to flag (default: 2)")
@click.option("--top", default=20, help="Number of results to show")
def query_knowledge_silos(threshold: float, max_authors: int, top: int):
    """Detect knowledge silos - critical code with few maintainers.

    Identifies high-risk situations where important code (high PageRank)
    has only been touched by 1-2 developers, creating a "bus factor" risk.
    """
    console.print("[bold blue]Detecting Knowledge Silos...[/bold blue]\n")
    console.print(f"[dim]Looking for code with importance ≥ {threshold} and ≤ {max_authors} authors[/dim]\n")

    try:
        connection = get_connection()
        engine = AnalyticsEngine(connection)

        # Detect knowledge silos
        silos = engine.detect_knowledge_silos(
            importance_threshold=threshold,
            max_authors=max_authors
        )

        if not silos:
            console.print("[yellow]No knowledge silos found![/yellow]")
            console.print("[dim]This is good - your critical code has healthy ownership distribution.[/dim]")
            return

        # Display results
        console.print(f"[bold red]⚠ Found {len(silos)} Knowledge Silos[/bold red]\n")
        console.print(f"{'Risk':<8} {'Authors':<10} {'Importance':<12} {'Critical Entity':<40} {'File'}")
        console.print("─" * 120)

        for i, silo in enumerate(silos[:top], 1):
            risk = silo['risk_score']
            author_count = silo['author_count']
            importance = silo['importance']
            entity = silo['critical_entity']
            file_path = silo['file_path']

            # Truncate entity name if too long
            if len(entity) > 38:
                entity = entity[:35] + "..."

            # Truncate file path if too long
            if len(file_path) > 50:
                file_path = "..." + file_path[-47:]

            # Color code risk level
            risk_color = "red" if risk > 0.001 else "yellow" if risk > 0.0005 else "white"

            console.print(
                f"[{risk_color}]{risk:>7.6f}[/{risk_color}] "
                f"{author_count:<10} "
                f"{importance:<12.6f} "
                f"{entity:<40} "
                f"[dim]{file_path}[/dim]"
            )

        # Show recommendations
        console.print("\n[bold yellow]Recommendations:[/bold yellow]")
        console.print("  1. Pair programming or code reviews with other team members")
        console.print("  2. Documentation and knowledge sharing sessions")
        console.print("  3. Rotate maintenance responsibilities")
        console.print("  4. Consider these files for refactoring/simplification")

        # Show top authors for context
        if silos:
            console.print("\n[bold]Example: Top Knowledge Silo Details[/bold]")
            top_silo = silos[0]
            console.print(f"  File: {top_silo['file_path']}")
            console.print(f"  Critical Entity: {top_silo['critical_entity']}")
            console.print(f"  Importance Score: {top_silo['importance']:.6f}")
            console.print(f"  Only {top_silo['author_count']} author(s): {', '.join(top_silo['authors'])}")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Knowledge silo detection failed: {e}")
        log.exception("Knowledge silo detection error:")
        raise click.Abort()


@cli.group()
def analyze():
    """Run graph analytics."""
    _configure_db_for_cwd()


@analyze.command("pagerank")
@click.option("--top", default=20, help="Number of top results to show")
@click.option("--damping", default=0.85, help="Damping factor (default: 0.85)")
def analyze_pagerank(top: int, damping: float):
    """Compute PageRank scores to identify important code entities.

    Uses Function/Class nodes with CALLS relationships to identify
    the most called/referenced functions in your codebase.
    """
    console.print("[bold blue]Computing PageRank...[/bold blue]\n")

    try:
        connection = get_connection()
        engine = AnalyticsEngine(connection)

        # Compute PageRank
        engine.compute_pagerank(damping=damping)

        # Get top results
        console.print(f"[bold green]Top {top} entities by PageRank:[/bold green]\n")

        results = engine.get_top_pagerank(limit=top)

        if not results:
            console.print("[yellow]No results found. Make sure you've indexed a repository first.[/yellow]")
            return

        # Display results in a table
        console.print(f"{'Rank':<6} {'Type':<15} {'Score':<12} {'Entity'}")
        console.print("─" * 90)

        for i, (name, entity_type, score) in enumerate(results, 1):
            etype = str(entity_type)[:14] if entity_type else "unknown"
            console.print(f"{i:<6} {etype:<15} {score:<12.6f} {name}")

        console.print(f"\n[bold green]✓[/bold green] PageRank computation complete!")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error computing PageRank: {e}")
        log.exception("PageRank error:")
        raise click.Abort()


@analyze.command("commit-importance")
@click.option("--top", default=20, help="Number of top results to show")
def analyze_commit_importance(top: int):
    """Compute file importance based on commit activity.

    Scores files by commit frequency and author diversity.
    Files with more commits by more authors are considered more important.
    """
    console.print("[bold blue]Computing commit-based importance...[/bold blue]\n")

    try:
        connection = get_connection()
        engine = AnalyticsEngine(connection)

        # Compute commit importance
        engine.compute_commit_importance()

        # Get top results
        console.print(f"[bold green]Top {top} files by commit importance:[/bold green]\n")

        results = engine.get_top_commit_importance(limit=top)

        if not results:
            console.print("[yellow]No results found. Make sure you've ingested a repository with git history.[/yellow]")
            return

        # Display results in a table
        console.print(f"{'Rank':<6} {'Commits':<10} {'Authors':<10} {'Score':<12} {'File'}")
        console.print("─" * 100)

        for i, (file_path, commits, authors, score) in enumerate(results, 1):
            # Shorten path for display
            display_path = file_path
            if len(display_path) > 55:
                display_path = "..." + file_path[-52:]
            console.print(f"{i:<6} {commits:<10} {authors:<10} {score:<12.2f} {display_path}")

        console.print(f"\n[bold green]✓[/bold green] Commit importance computation complete!")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error computing commit importance: {e}")
        log.exception("Commit importance error:")
        raise click.Abort()


@analyze.command("betweenness")
@click.option("--top", default=20, help="Number of top results to show")
def analyze_betweenness(top: int):
    """Compute Betweenness Centrality to identify bridge entities."""
    console.print("[bold blue]Computing Betweenness Centrality...[/bold blue]\n")

    try:
        connection = get_connection()
        engine = AnalyticsEngine(connection)

        # Compute Betweenness
        engine.compute_betweenness_centrality()

        # Get top results
        console.print(f"[bold green]Top {top} entities by Betweenness Centrality:[/bold green]\n")

        results = engine.get_top_betweenness(limit=top)

        if not results:
            console.print("[yellow]No results found. Make sure you've indexed a repository first.[/yellow]")
            return

        # Display results in a table
        console.print(f"{'Rank':<6} {'Type':<10} {'Score':<12} {'Entity'}")
        console.print("─" * 80)

        for i, (name, entity_type, score) in enumerate(results, 1):
            console.print(f"{i:<6} {entity_type:<10} {score:<12.6f} {name}")

        console.print(f"\n[bold green]✓[/bold green] Betweenness computation complete!")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error computing Betweenness: {e}")
        log.exception("Betweenness error:")
        raise click.Abort()


@analyze.command("community")
@click.option("--resolution", default=1.0, help="Resolution parameter (default: 1.0)")
@click.option("--top", default=20, help="Number of top communities to show (0 = all)")
@click.option("--details", is_flag=True, help="Show full entity names for each community")
@click.option("--id", "community_id", default=None, type=int, help="Show all members of a specific community ID")
@click.option("--description", default=None, help="Save a description for a community ID")
@click.option("--describe/--no-describe", default=True,
              help="Auto-generate LLM descriptions for detected communities")
@click.option("--describe-model", default=DEFAULT_MODEL,
              help="LLM model for community descriptions")
@click.option("--query", "-q", default=None, help="Search to find related communities")
@click.option("--semantic", "-s", is_flag=True, help="Use semantic search on community descriptions (requires embeddings)")
@click.option("--compact", "-c", is_flag=True, help="Compact output (just ID and size)")
def analyze_community(
    resolution: float,
    top: int,
    details: bool,
    community_id: int,
    description: str,
    describe: bool,
    describe_model: str,
    query: str,
    semantic: bool,
    compact: bool,
):
    """Detect code communities/clusters using Louvain algorithm.

    Examples:
        emdash analyze community                        # Detect and list communities
        emdash analyze community --query "auth"         # Find code matching 'auth', show their communities
        emdash analyze community -q "auth" --semantic   # Semantic search on community descriptions
        emdash analyze community -q "database" -c       # Compact output for database-related
        emdash analyze community --id 5                 # Show members of community 5
    """
    try:
        connection = get_connection()
        engine = AnalyticsEngine(connection)

        if description is not None:
            if community_id is None:
                console.print("[bold red]✗[/bold red] --description requires --id")
                raise click.Abort()

            engine.set_community_description(community_id, description)
            members = engine.get_community_members(community_id)
            if members:
                console.print(
                    f"[bold green]✓[/bold green] Saved description for community {community_id} "
                    f"({len(members)} members)."
                )
            else:
                console.print(f"[bold green]✓[/bold green] Saved description for community {community_id}.")
                console.print("[dim]No members found yet for this community ID.[/dim]")
            return

        # If query is provided, search for related communities
        if query:
            if semantic:
                # Direct semantic search on community descriptions
                console.print(f"[bold blue]Semantic search for communities matching '{query}'...[/bold blue]\n")

                communities = engine.search_communities(
                    query=query,
                    limit=top if top > 0 else 20,
                    max_members=5 if not details else 20,
                )

                if not communities:
                    console.print(f"[yellow]No communities found matching '{query}'.[/yellow]")
                    console.print("[dim]Make sure community embeddings are indexed: emdash embed index[/dim]")
                    return

                console.print(f"[bold green]Found {len(communities)} communities matching '{query}':[/bold green]\n")

                if compact:
                    for comm in communities:
                        score = comm.get('score', 0)
                        console.print(f"  Community [bold cyan]{comm['community_id']}[/bold cyan]: {comm['member_count']} members (score: {score:.2f})")
                else:
                    for comm in communities:
                        score = comm.get('score', 0)
                        console.print(f"[bold cyan]Community {comm['community_id']}[/bold cyan] [dim]({comm['member_count']} members, score: {score:.2f})[/dim]")
                        if comm.get("description"):
                            console.print(f"  [dim]{comm['description']}[/dim]")
                        short_names = [m.split('.')[-1] for m in comm.get('sample_members', [])[:5]]
                        if short_names:
                            console.print(f"  [green]{', '.join(short_names)}[/green]")
                        console.print()

                return

            # Default: Search code entities, then find their communities
            from emdash.planning.similarity import SimilaritySearch
            console.print(f"[bold blue]Searching communities related to '{query}'...[/bold blue]\n")

            similarity = SimilaritySearch(connection)
            search_results = similarity.find_similar_code(
                query=query,
                entity_types=["Function", "Class"],
                limit=20,
                min_score=0.3,
            )

            if not search_results:
                console.print(f"[yellow]No code found matching '{query}'.[/yellow]")
                console.print("[dim]Make sure embeddings are indexed: emdash embed index[/dim]")
                return

            qualified_names = [r["qualified_name"] for r in search_results if r.get("qualified_name")]
            console.print(f"[dim]Found {len(qualified_names)} matching code entities:[/dim]")
            for r in search_results[:10]:  # Show first 10
                name = r.get("name", "unknown")
                etype = r.get("type", "?")
                score = r.get("score", 0)
                console.print(f"  [dim]• {etype}: {name} (score: {score:.2f})[/dim]")
            if len(search_results) > 10:
                console.print(f"  [dim]... and {len(search_results) - 10} more[/dim]")
            console.print()

            communities = engine.search_communities(
                qualified_names=qualified_names,
                limit=top if top > 0 else 100,
                max_members=5 if not details else 20,
            )

            if not communities:
                console.print(f"[yellow]No communities found for matching code.[/yellow]")
                console.print("[dim]Make sure communities are detected first: emdash analyze community[/dim]")
                return

            console.print(f"[bold green]Found {len(communities)} communities related to '{query}':[/bold green]\n")

            if compact:
                # Compact: one line per community
                for comm in communities:
                    console.print(f"  Community [bold cyan]{comm['community_id']}[/bold cyan]: {comm['member_count']} members")
            else:
                # Normal output with sample members
                for comm in communities:
                    console.print(f"[bold cyan]Community {comm['community_id']}[/bold cyan] [dim]({comm['member_count']} members)[/dim]")
                    if comm.get("description"):
                        console.print(f"  [dim]{comm['description']}[/dim]")
                    short_names = [m.split('.')[-1] for m in comm.get('sample_members', [])[:5]]
                    if short_names:
                        console.print(f"  [green]{', '.join(short_names)}[/green]")
                    console.print()

            console.print(f"[dim]Matched {len(qualified_names)} code entities[/dim]")
            return

        # If querying specific community, skip detection
        if community_id is not None:
            members = engine.get_community_members(community_id)
            if not members:
                console.print(f"[yellow]No members found for community {community_id}.[/yellow]")
                return

            if compact:
                console.print(f"Community {community_id}: {len(members)} members")
                return

            console.print(f"[bold cyan]Community {community_id}[/bold cyan] ({len(members)} members)\n")
            description_text = engine.get_community_description(community_id)
            if description_text:
                console.print(f"[dim]{description_text}[/dim]\n")
            console.print(f"{'Type':<10} {'Name':<30} {'File'}")
            console.print("─" * 90)

            for member in members:
                name = member['name'][:29] if member['name'] else 'unknown'
                mtype = member['type'][:9] if member['type'] else 'unknown'
                # Extract short file path
                file_path = member.get('file_path', '')
                if file_path and len(file_path) > 45:
                    file_path = '...' + file_path[-42:]
                console.print(f"{mtype:<10} [bold]{name:<30}[/bold] [dim]{file_path}[/dim]")

            return

        console.print("[bold blue]Detecting communities...[/bold blue]\n")

        # Detect communities
        engine.detect_communities(
            resolution=resolution,
            describe=False,
        )

        # Get summary (more members if details requested)
        console.print("[bold green]Community Detection Results:[/bold green]\n")

        max_members = 50 if details else 5
        communities = engine.get_communities_summary(max_members=max_members)

        if not communities:
            console.print("[yellow]No results found. Make sure you've indexed a repository first.[/yellow]")
            return

        total_communities = len(communities)

        # Handle --top 0 to show all
        display_count = total_communities if top == 0 else min(top, total_communities)
        communities_to_show = communities[:display_count] if top > 0 else communities

        if describe:
            community_ids = [comm["community_id"] for comm in communities_to_show]
            engine.generate_community_descriptions(
                community_ids=community_ids,
                model=describe_model,
                overwrite=False,
            )
            description_map = {
                comm["community_id"]: comm.get("description")
                for comm in engine.get_communities_summary(max_members=0)
            }
            for comm in communities_to_show:
                comm["description"] = description_map.get(comm["community_id"])

        if compact:
            # Ultra-compact: one line per community
            for comm in communities_to_show:
                console.print(f"  Community [bold cyan]{comm['community_id']}[/bold cyan]: {comm['member_count']} members")
        elif details:
            # Show detailed view with full entity names
            for comm in communities_to_show:
                comm_id = comm['community_id']
                member_count = comm['member_count']
                console.print(f"[bold cyan]Community {comm_id}[/bold cyan] ({member_count} members)")
                if comm.get("description"):
                    console.print(f"[dim]{comm['description']}[/dim]")
                console.print("─" * 60)
                for member in comm['sample_members']:
                    # Show short name in bold, full path in dim
                    short_name = member.split('.')[-1] if '.' in member else member
                    path = '.'.join(member.split('.')[:-1]) if '.' in member else ''
                    if path:
                        console.print(f"  [bold]{short_name}[/bold] [dim]({path})[/dim]")
                    else:
                        console.print(f"  [bold]{short_name}[/bold]")
                console.print()
        else:
            # Show compact table view with better formatting
            for comm in communities_to_show:
                comm_id = comm['community_id']
                member_count = comm['member_count']
                # Extract short names (last part after the last dot)
                short_names = []
                for member in comm['sample_members'][:5]:
                    short_name = member.split('.')[-1] if '.' in member else member
                    short_names.append(short_name)

                # Format: Community ID and count on one line, entities below
                console.print(f"[bold cyan]Community {comm_id}[/bold cyan] [dim]({member_count} members)[/dim]")
                if comm.get("description"):
                    console.print(f"  [dim]{comm['description']}[/dim]")
                console.print(f"  [green]{', '.join(short_names)}[/green]")
                console.print()

        # Show summary
        if display_count < total_communities:
            console.print(f"[bold green]✓[/bold green] Showing {display_count} of {total_communities} communities (use --top 0 for all)")
        else:
            console.print(f"[bold green]✓[/bold green] Detected {total_communities} communities!")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error detecting communities: {e}")
        log.exception("Community detection error:")
        raise click.Abort()


@analyze.command("areas")
@click.option("--depth", default=2, help="Directory depth for grouping (default: 2)")
@click.option("--days", default=30, help="Time window for recency scoring (default: 30)")
@click.option("--top", default=20, help="Number of results to show")
@click.option("--sort", type=click.Choice(["importance", "focus", "commits", "authors"]), default="importance",
              help="Sort by: importance (default), focus (recent activity %), commits, or authors")
@click.option("--files", is_flag=True, help="Show individual files instead of directories")
def analyze_areas(depth: int, days: int, top: int, sort: str, files: bool):
    """Get importance metrics by directory/area or individual files.

    Aggregates file activity by directory to show which parts of the
    codebase are most active and who works on them.

    Examples:
        emdash analyze areas                      # Sort by importance (default)
        emdash analyze areas --sort focus         # Sort by recent activity (hot spots)
        emdash analyze areas --files              # Show individual files, not directories
        emdash analyze areas --files --sort focus # Hot files (most recent activity)
        emdash analyze areas --depth 3            # More granular directories
        emdash analyze areas --days 7             # 7-day recency window
    """
    sort_labels = {
        "importance": "Importance",
        "focus": "Recent Focus",
        "commits": "Total Commits",
        "authors": "Contributors"
    }

    try:
        connection = get_connection()
        engine = AnalyticsEngine(connection)

        if files:
            # Show individual files
            console.print(f"[bold blue]Computing file importance (last {days} days, sort by {sort})...[/bold blue]\n")

            file_results = engine.compute_file_importance(days=days, limit=200)

            if not file_results:
                console.print("[yellow]No files found with activity.[/yellow]")
                console.print("[dim]Make sure you've indexed a repository with git history.[/dim]")
                return

            # Sort by requested field
            sort_keys = {
                "importance": lambda x: x['importance_score'],
                "focus": lambda x: x['recent_commits'],
                "commits": lambda x: x['commits'],
                "authors": lambda x: x['authors'],
            }
            file_results.sort(key=sort_keys[sort], reverse=True)

            # Filter out zero-recent files if sorting by focus
            if sort == "focus":
                file_results = [f for f in file_results if f['recent_commits'] > 0]

            file_results = file_results[:top]

            console.print(f"[bold green]Top {len(file_results)} Files by {sort_labels[sort]}:[/bold green]\n")

            # Display results
            console.print(f"{'Rank':<6} {'Commits':<9} {'Authors':<9} {'Recent':<8} {'File'}")
            console.print("─" * 90)

            for i, f in enumerate(file_results, 1):
                path = f['file_path']
                # Make path relative (remove common prefix)
                if '/' in path:
                    # Find a reasonable display path
                    parts = path.split('/')
                    # Try to show last 3-4 meaningful parts
                    display_parts = [p for p in parts if p and p not in ['.emdash', 'repos']][-4:]
                    path = '/'.join(display_parts)
                if len(path) > 50:
                    path = "..." + path[-47:]
                console.print(
                    f"{i:<6} "
                    f"{f['commits']:<9} "
                    f"{f['authors']:<9} "
                    f"{f['recent_commits']:<8} "
                    f"{path}"
                )

            console.print(f"\n[bold green]✓[/bold green] File analysis complete!")
            if sort == "focus":
                console.print(f"[dim]Recent = commits in last {days} days[/dim]")

        else:
            # Show directories/areas
            console.print(f"[bold blue]Computing area importance (depth={depth}, last {days} days, sort by {sort})...[/bold blue]\n")

            areas = engine.compute_area_importance(depth=depth, days=days, limit=100)

            if not areas:
                console.print("[yellow]No areas found with activity.[/yellow]")
                console.print("[dim]Make sure you've indexed a repository with git history.[/dim]")
                return

            # Sort by requested field
            sort_keys = {
                "importance": lambda x: x['importance'],
                "focus": lambda x: x['focus_pct'],
                "commits": lambda x: x['total_commits'],
                "authors": lambda x: x['unique_authors'],
            }
            areas.sort(key=sort_keys[sort], reverse=True)

            # Filter out zero-focus areas if sorting by focus
            if sort == "focus":
                areas = [a for a in areas if a['focus_pct'] > 0]

            areas = areas[:top]

            console.print(f"[bold green]Top {len(areas)} Areas by {sort_labels[sort]}:[/bold green]\n")

            # Display results
            console.print(f"{'Rank':<6} {'Commits':<9} {'Files':<7} {'Authors':<9} {'Focus%':<8} {'Area'}")
            console.print("─" * 90)

            for i, area in enumerate(areas, 1):
                path = area['path']
                if len(path) > 35:
                    path = "..." + path[-32:]
                console.print(
                    f"{i:<6} "
                    f"{area['total_commits']:<9} "
                    f"{area['file_count']:<7} "
                    f"{area['unique_authors']:<9} "
                    f"{area['focus_pct']:<8.1f} "
                    f"{path}"
                )

            console.print(f"\n[bold green]✓[/bold green] Area analysis complete!")
            if sort == "focus":
                console.print(f"[dim]Focus% = percentage of recent commits in this area[/dim]")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error computing importance: {e}")
        log.exception("Area/file importance error:")
        raise click.Abort()


@cli.command()
def status():
    """Show indexing status."""
    console.print("[bold blue]Checking status...[/bold blue]")

    # TODO: Implement status check
    console.print("[yellow]⚠[/yellow] Status not yet implemented")


# ============================================================================
# Plan Commands
# ============================================================================


@cli.group()
def plan():
    """Feature planning commands."""
    _configure_db_for_cwd()


@plan.command("context")
@click.argument("description")
@click.option("--similar-prs", default=5, help="Number of similar PRs to show")
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
def plan_context(description: str, similar_prs: int, output_json: bool):
    """Get planning context for a feature.

    DESCRIPTION is the feature description, e.g. "add user notifications"
    """
    console.print(f"[bold blue]Building planning context for:[/bold blue] {description}\n")

    try:
        from emdash.planning.context_builder import ContextBuilder
        import json

        connection = get_connection()
        builder = ContextBuilder(connection)
        context = builder.build_context(description, similar_pr_limit=similar_prs)

        if output_json:
            console.print(json.dumps(context.to_dict(), indent=2, default=str))
            return

        # Similar PRs
        console.print("[bold cyan]Similar PRs:[/bold cyan]")
        if context.similar_prs:
            for pr in context.similar_prs:
                score = pr.get('score', 0)
                title = pr.get('title', 'Unknown')[:60]
                author = pr.get('author', 'unknown')
                console.print(f"  [green]#{pr.get('number')}[/green] {title} [dim](@{author}, score: {score:.2f})[/dim]")
        else:
            console.print("  [dim]No similar PRs found[/dim]")
        console.print()

        # Similar Code
        console.print("[bold cyan]Similar Code:[/bold cyan]")
        if context.similar_code:
            for code in context.similar_code[:5]:
                score = code.get('score', 0)
                console.print(f"  [{code.get('type')}] [green]{code.get('name')}[/green] [dim](score: {score:.2f})[/dim]")
        else:
            console.print("  [dim]No similar code found[/dim]")
        console.print()

        # Domain Experts
        console.print("[bold cyan]Domain Experts:[/bold cyan]")
        if context.domain_experts:
            for expert in context.domain_experts:
                console.print(f"  [green]{expert.get('author_name')}[/green] [dim]({expert.get('commit_count')} commits)[/dim]")
        else:
            console.print("  [dim]No domain experts found[/dim]")
        console.print()

        # Suggested Tasks
        console.print("[bold cyan]Suggested Tasks (from similar PRs):[/bold cyan]")
        if context.suggested_tasks:
            for task in context.suggested_tasks[:10]:
                status = "[green]✓[/green]" if task.get('is_completed') else "[ ]"
                console.print(f"  {status} {task.get('task_description', 'Unknown task')}")
        else:
            console.print("  [dim]No tasks found in similar PRs[/dim]")
        console.print()

        # Related Files
        console.print("[bold cyan]Related Files:[/bold cyan]")
        if context.related_files:
            for fp in context.related_files[:10]:
                console.print(f"  [dim]{fp}[/dim]")
            if len(context.related_files) > 10:
                console.print(f"  [dim]... and {len(context.related_files) - 10} more[/dim]")
        else:
            console.print("  [dim]No related files found[/dim]")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error building context: {e}")
        log.exception("Context building error:")
        raise click.Abort()


@plan.command("similar")
@click.argument("description")
@click.option("--limit", default=10, help="Number of results to show")
def plan_similar(description: str, limit: int):
    """Find similar PRs to a feature description."""
    console.print(f"[bold blue]Finding similar PRs for:[/bold blue] {description}\n")

    try:
        from emdash.planning.similarity import SimilaritySearch

        connection = get_connection()
        search = SimilaritySearch(connection)
        results = search.find_similar_prs(description, limit=limit)

        if not results:
            console.print("[yellow]No similar PRs found.[/yellow]")
            return

        console.print(f"[bold]Found {len(results)} similar PRs:[/bold]\n")

        for pr in results:
            score = pr.get('score', 0)
            title = pr.get('title', 'Unknown')
            author = pr.get('author', 'unknown')
            state = pr.get('state', 'unknown')
            files = pr.get('files_changed', [])

            console.print(f"[bold green]PR #{pr.get('number')}[/bold green] - {title}")
            console.print(f"  [dim]Author: @{author} | State: {state} | Score: {score:.2f}[/dim]")
            if files:
                console.print(f"  [dim]Files: {', '.join(files[:3])}{'...' if len(files) > 3 else ''}[/dim]")
            console.print()

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("Similar search error:")
        raise click.Abort()


# ============================================================================
# Embed Commands
# ============================================================================


@cli.group()
def embed():
    """Embedding management commands."""
    _configure_db_for_cwd()


@embed.command("index")
@click.option("--prs/--no-prs", default=True, help="Index PR embeddings")
@click.option("--functions/--no-functions", default=True, help="Index function embeddings")
@click.option("--classes/--no-classes", default=True, help="Index class embeddings")
@click.option("--reindex", is_flag=True, help="Re-generate all embeddings (use after code changes)")
def embed_index(prs: bool, functions: bool, classes: bool, reindex: bool):
    """Generate embeddings for graph entities.

    By default, only generates embeddings for entities that don't have them.
    Use --reindex to regenerate all embeddings (useful after indexing new code
    or when embedding quality has been improved).
    """
    if reindex:
        console.print("[bold yellow]Re-indexing all embeddings (this may take a while)...[/bold yellow]\n")
    else:
        console.print("[bold blue]Generating embeddings for new entities...[/bold blue]\n")

    try:
        from emdash.embeddings.indexer import EmbeddingIndexer

        connection = get_connection()
        indexer = EmbeddingIndexer(connection)

        if not indexer.embedding_service.is_available:
            console.print("[bold red]✗[/bold red] OpenAI API key not configured.")
            console.print("[dim]Set OPENAI_API_KEY environment variable.[/dim]")
            raise click.Abort()

        results = {}

        if prs:
            console.print("[bold]Indexing PRs...[/bold]")
            results['prs'] = indexer.index_pull_requests()
            console.print(f"  [green]✓[/green] Indexed {results['prs']} PR embeddings")

        if functions:
            console.print("[bold]Indexing functions...[/bold]")
            results['functions'] = indexer.index_functions(reindex=reindex)
            console.print(f"  [green]✓[/green] Indexed {results['functions']} function embeddings")

        if classes:
            console.print("[bold]Indexing classes...[/bold]")
            results['classes'] = indexer.index_classes(reindex=reindex)
            console.print(f"  [green]✓[/green] Indexed {results['classes']} class embeddings")

        total = sum(results.values())
        console.print(f"\n[bold green]✓[/bold green] Total embeddings generated: {total}")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("Embedding error:")
        raise click.Abort()


@embed.command("status")
def embed_status():
    """Show embedding coverage statistics."""
    console.print("[bold blue]Fetching embedding statistics...[/bold blue]\n")

    try:
        from emdash.embeddings.indexer import EmbeddingIndexer

        connection = get_connection()
        indexer = EmbeddingIndexer(connection)
        stats = indexer.get_embedding_stats()

        console.print("[bold]Embedding Coverage:[/bold]\n")

        # PRs
        pr_stats = stats.get('pull_requests', {})
        pr_total = pr_stats.get('total', 0)
        pr_embedded = pr_stats.get('with_embedding', 0)
        pr_pct = (pr_embedded / pr_total * 100) if pr_total > 0 else 0
        console.print(f"  [bold]Pull Requests:[/bold] {pr_embedded}/{pr_total} ({pr_pct:.1f}%)")

        # Functions
        func_stats = stats.get('functions', {})
        func_total = func_stats.get('total', 0)
        func_embedded = func_stats.get('with_embedding', 0)
        func_docstring = func_stats.get('with_docstring', 0)
        func_pct = (func_embedded / func_total * 100) if func_total > 0 else 0
        console.print(f"  [bold]Functions:[/bold] {func_embedded}/{func_total} ({func_pct:.1f}%)")
        console.print(f"    [dim]{func_docstring} have docstrings[/dim]")

        # Classes
        cls_stats = stats.get('classes', {})
        cls_total = cls_stats.get('total', 0)
        cls_embedded = cls_stats.get('with_embedding', 0)
        cls_docstring = cls_stats.get('with_docstring', 0)
        cls_pct = (cls_embedded / cls_total * 100) if cls_total > 0 else 0
        console.print(f"  [bold]Classes:[/bold] {cls_embedded}/{cls_total} ({cls_pct:.1f}%)")
        console.print(f"    [dim]{cls_docstring} have docstrings[/dim]")

        # Show hint if not fully indexed
        if func_embedded < func_total or cls_embedded < cls_total:
            console.print(f"\n[yellow]Tip:[/yellow] Run [bold]emdash embed index[/bold] to generate missing embeddings")
            console.print("[dim]Use --reindex to regenerate all with improved context[/dim]")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("Status error:")
        raise click.Abort()


@embed.command("models")
def embed_models():
    """List all available embedding models."""
    from emdash.embeddings import EmbeddingModel, EmbeddingService

    console.print("[bold blue]Available Embedding Models[/bold blue]\n")

    # Group by provider
    providers = {}
    for model in EmbeddingModel:
        provider = model.provider
        if provider not in providers:
            providers[provider] = []
        providers[provider].append(model)

    # Show available providers
    available = EmbeddingService.list_available_providers()
    console.print(f"[bold]Configured providers:[/bold] {', '.join(available) if available else '[yellow]None[/yellow]'}")
    console.print()

    for provider, models in sorted(providers.items()):
        is_available = provider in available
        status = "[green]✓[/green]" if is_available else "[dim]○[/dim]"
        console.print(f"{status} [bold cyan]{provider.upper()}[/bold cyan]")

        for model in models:
            console.print(f"   [bold]{model.name}[/bold]")
            console.print(f"   [dim]model_id: {model.model_id}[/dim]")
            console.print(f"   [dim]dimensions: {model.dimensions}, max_tokens: {model.spec.max_tokens}[/dim]")
            console.print(f"   {model.spec.description}")
            console.print()

    console.print("[bold]Environment Variables:[/bold]")
    console.print("  OPENAI_API_KEY     - OpenAI API key")
    console.print("  FIREWORKS_API_KEY  - Fireworks AI API key")


@embed.command("test")
@click.option("--model", "-m", default=None, help="Model to test (e.g., 'openai:text-embedding-3-small')")
def embed_test(model: str):
    """Test embedding generation with a sample text."""
    from emdash.embeddings import EmbeddingService, EmbeddingModel

    if model:
        parsed = EmbeddingModel.from_string(model)
        if parsed is None:
            console.print(f"[bold red]✗[/bold red] Unknown model: {model}")
            console.print("\n[dim]Run 'emdash embed models' to see available models[/dim]")
            raise click.Abort()
        service = EmbeddingService(model=parsed)
        console.print(f"[bold blue]Testing model:[/bold blue] {parsed.name}")
    else:
        service = EmbeddingService()
        if service.model:
            console.print(f"[bold blue]Testing default model:[/bold blue] {service.model.name}")
        else:
            console.print("[bold red]✗[/bold red] No embedding provider available")
            console.print("\n[dim]Set OPENAI_API_KEY or FIREWORKS_API_KEY[/dim]")
            raise click.Abort()

    console.print(f"[dim]Provider: {service.model.provider}, Dimensions: {service.dimensions}[/dim]\n")

    test_text = "function to handle user authentication and login"
    console.print(f"[bold]Test text:[/bold] \"{test_text}\"")

    try:
        embedding = service.embed_text(test_text)
        if embedding:
            console.print(f"\n[green]✓[/green] Generated embedding with {len(embedding)} dimensions")
            console.print(f"[dim]First 5 values: {embedding[:5]}[/dim]")
        else:
            console.print(f"\n[red]✗[/red] Failed to generate embedding")
            raise click.Abort()

    except Exception as e:
        console.print(f"\n[bold red]✗[/bold red] Error: {e}")
        raise click.Abort()


# ============================================================================
# Feature Commands
# ============================================================================


@cli.group()
def feature():
    """Feature analysis commands using AST graph."""
    _configure_db_for_cwd()


@feature.command("context")
@click.argument("query")
@click.option("--hops", default=2, help="Max relationship hops to traverse")
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
def feature_context(query: str, hops: int, output_json: bool):
    """Find a feature and expand its AST graph.

    QUERY is a description like "streaming chat responses"

    Example:
        emdash feature context "handle user authentication"
    """
    import json as json_lib

    console.print(f"\n[bold blue]Finding feature: {query}[/bold blue]\n")

    try:
        from emdash.planning.feature_context import FeatureContextBuilder

        connection = get_connection()
        connection.connect()

        builder = FeatureContextBuilder(connection)
        context = builder.build_context(query, max_hops=hops)

        if output_json:
            console.print(json_lib.dumps(context.to_dict(), indent=2, default=str))
            return

        # Display root node
        root = context.root_node
        if not root:
            console.print("[yellow]No relevant code found for this query.[/yellow]")
            return

        console.print(f"[bold]Root Node:[/bold] {root.get('type', 'Unknown')} - {root.get('name', 'Unknown')}")
        console.print(f"[dim]File: {root.get('file_path', 'N/A')}[/dim]")
        if root.get('score'):
            console.print(f"[dim]Similarity: {root['score']:.2f}[/dim]")
        console.print("")

        graph = context.feature_graph

        # Display call graph
        if graph.call_graph:
            console.print("[bold]Call Graph:[/bold]")
            for call in graph.call_graph[:10]:
                console.print(f"  {call['caller']} → {call['callee']}")
            if len(graph.call_graph) > 10:
                console.print(f"  [dim]... and {len(graph.call_graph) - 10} more[/dim]")
            console.print("")

        # Display classes
        if graph.classes:
            console.print("[bold]Classes:[/bold]")
            for cls in graph.classes[:5]:
                console.print(f"  {cls.get('name', 'Unknown')}")
            if len(graph.classes) > 5:
                console.print(f"  [dim]... and {len(graph.classes) - 5} more[/dim]")
            console.print("")

        # Display functions
        if graph.functions:
            console.print("[bold]Functions:[/bold]")
            for func in graph.functions[:8]:
                console.print(f"  {func.get('name', 'Unknown')}")
            if len(graph.functions) > 8:
                console.print(f"  [dim]... and {len(graph.functions) - 8} more[/dim]")
            console.print("")

        # Display files
        if graph.files:
            console.print("[bold]Files:[/bold]")
            for f in graph.files[:8]:
                console.print(f"  {f.get('path', f.get('name', 'Unknown'))}")
            if len(graph.files) > 8:
                console.print(f"  [dim]... and {len(graph.files) - 8} more[/dim]")
            console.print("")

        # Display authors
        if context.authors:
            console.print("[bold]Domain Experts:[/bold]")
            for author in context.authors[:5]:
                console.print(f"  {author['name']} ({author['commit_count']} commits)")
            console.print("")

        # Summary
        console.print(f"[bold green]✓[/bold green] Expanded {len(graph.functions)} functions, {len(graph.classes)} classes across {len(graph.files)} files")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("Feature context error:")
        raise click.Abort()


@feature.command("llm")
@click.argument("query")
@click.option("--hops", default=2, help="Max relationship hops to traverse")
@click.option("--style", default="developer",
              type=click.Choice(["developer", "architect", "onboarding"]),
              help="Explanation style")
def feature_llm(query: str, hops: int, style: str):
    """Explain a feature using LLM based on its AST graph.

    QUERY is a description like "streaming chat responses"

    Example:
        emdash feature llm "handle streaming" --style architect
    """
    from rich.markdown import Markdown

    console.print(f"\n[bold blue]Analyzing: {query}[/bold blue]\n")

    try:
        from emdash.planning.feature_context import FeatureContextBuilder
        from emdash.planning.llm_explainer import LLMExplainer

        connection = get_connection()
        connection.connect()

        # Build context
        builder = FeatureContextBuilder(connection)
        context = builder.build_context(query, max_hops=hops)

        if not context.root_node:
            console.print("[yellow]No relevant code found for this query.[/yellow]")
            return

        console.print(f"[dim]Root: {context.root_node.get('name', 'Unknown')} ({context.root_node.get('type', 'Unknown')})[/dim]")
        console.print(f"[dim]Expanded {len(context.feature_graph.functions)} functions, {len(context.feature_graph.classes)} classes[/dim]\n")

        # Generate explanation
        explainer = LLMExplainer()
        if not explainer.is_available:
            console.print("[bold red]✗[/bold red] OpenAI API key not configured.")
            console.print("[dim]Set OPENAI_API_KEY environment variable.[/dim]")
            raise click.Abort()

        console.print(f"[dim]Generating {style} explanation...[/dim]\n")
        explanation = explainer.explain_feature(context, style=style)

        console.print(Markdown(explanation))

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("Feature LLM error:")
        raise click.Abort()


# ============================================================================
# Agent Commands
# ============================================================================


@cli.group()
def agent():
    """Agent toolkit commands for LLM integration."""
    _configure_db_for_cwd()


@agent.command("list-tools")
@click.option("--format", "output_format", type=click.Choice(["table", "json", "schema"]),
              default="table", help="Output format")
@click.option("--category", type=click.Choice(["search", "traversal", "analytics", "history", "planning"]),
              help="Filter by category")
def agent_list_tools(output_format: str, category: str):
    """List all available agent tools."""
    import json as json_lib
    from emdash.agent import AgentToolkit

    try:
        toolkit = AgentToolkit(enable_session=False)

        if output_format == "schema":
            if category:
                schemas = toolkit.get_schemas_by_category(category)
            else:
                schemas = toolkit.get_all_schemas()
            console.print(json_lib.dumps(schemas, indent=2))

        elif output_format == "json":
            tools_info = toolkit.list_tools()
            if category:
                tools_info = [t for t in tools_info if t["category"] == category]
            console.print(json_lib.dumps(tools_info, indent=2))

        else:
            console.print("[bold]Available Agent Tools:[/bold]\n")
            categories = ["search", "traversal", "analytics", "history", "planning"]
            if category:
                categories = [category]

            for cat in categories:
                tools = toolkit.get_tools_by_category(cat)
                if tools:
                    console.print(f"[bold cyan]{cat.upper()}[/bold cyan]")
                    for tool in tools:
                        console.print(f"  [green]{tool.name}[/green]: {tool.description[:60]}...")
                    console.print()

            console.print(f"[dim]Total: {len(toolkit.list_tools())} tools[/dim]")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("Agent list-tools error:")
        raise click.Abort()


@agent.command("models")
def agent_models():
    """List all available LLM models."""
    from emdash.agent.providers import ChatModel

    console.print("[bold blue]Available LLM Models (via LiteLLM)[/bold blue]\n")

    # Group by provider
    providers = {}
    for model in ChatModel:
        provider = model.provider
        if provider not in providers:
            providers[provider] = []
        providers[provider].append(model)

    for provider, models in sorted(providers.items()):
        console.print(f"[bold cyan]{provider.upper()}[/bold cyan]")

        for model in models:
            tools_icon = "[green]T[/green]" if model.spec.supports_tools else "[dim]-[/dim]"
            vision_icon = "[green]V[/green]" if model.spec.supports_vision else "[dim]-[/dim]"
            console.print(f"   [bold]{model.name}[/bold] {tools_icon}{vision_icon}")
            console.print(f"   [dim]litellm: {model.litellm_model}[/dim]")
            console.print(f"   [dim]context: {model.context_window:,} tokens[/dim]")
            console.print(f"   {model.spec.description}")
            console.print()

    console.print("[bold]Legend:[/bold] [green]T[/green]=Tools [green]V[/green]=Vision")
    console.print()
    console.print("[bold]Environment Variables:[/bold]")
    console.print("  FIREWORKS_API_KEY   - Fireworks AI (default)")
    console.print("  ANTHROPIC_API_KEY   - Anthropic (Claude)")
    console.print("  OPENAI_API_KEY      - OpenAI (GPT)")
    console.print()
    console.print("[bold]Usage:[/bold]")
    console.print("  emdash agent chat --model glm-4p7   # default")
    console.print("  emdash agent chat --model haiku")
    console.print("  emdash agent chat --model gpt-4o-mini")


@agent.command("execute")
@click.argument("tool_name")
@click.option("--params", "-p", type=str, help="JSON params string")
@click.option("--json", "output_json", is_flag=True, help="Output as JSON")
def agent_execute(tool_name: str, params: str, output_json: bool):
    """Execute an agent tool directly.

    TOOL_NAME is the name of the tool to execute.

    Example:
        emdash agent execute semantic_search -p '{"query": "streaming"}'
    """
    import json as json_lib
    from emdash.agent import AgentToolkit

    try:
        toolkit = AgentToolkit()
        parsed_params = json_lib.loads(params) if params else {}

        console.print(f"[dim]Executing {tool_name}...[/dim]\n")
        result = toolkit.execute(tool_name, **parsed_params)

        if output_json:
            console.print(json_lib.dumps(result.to_dict(), indent=2, default=str))
        else:
            if result.success:
                console.print("[bold green]Success[/bold green]\n")

                # Pretty print data
                data = result.data
                if "results" in data:
                    console.print(f"[bold]Results ({data.get('count', len(data['results']))}):[/bold]")
                    for item in data["results"][:10]:
                        if isinstance(item, dict):
                            name = item.get("name", item.get("qualified_name", "Unknown"))
                            score = item.get("score", item.get("pagerank_score"))
                            score_str = f" (score: {score:.2f})" if score else ""
                            console.print(f"  [green]{name}[/green]{score_str}")
                    if len(data.get("results", [])) > 10:
                        console.print(f"  [dim]... and {len(data['results']) - 10} more[/dim]")
                else:
                    console.print(json_lib.dumps(data, indent=2, default=str))

                if result.suggestions:
                    console.print("\n[bold]Suggestions:[/bold]")
                    for s in result.suggestions:
                        console.print(f"  - {s}")
            else:
                console.print(f"[bold red]Error:[/bold red] {result.error}")
                if result.suggestions:
                    console.print("\n[bold]Suggestions:[/bold]")
                    for s in result.suggestions:
                        console.print(f"  - {s}")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("Agent execute error:")
        raise click.Abort()


@agent.command("plan")
@click.argument("goal")
@click.option("--depth", type=click.Choice(["shallow", "medium", "deep"]),
              default="medium", help="Exploration depth")
@click.option("--context", "-c", type=str, help="Additional context")
def agent_plan(goal: str, depth: str, context: str):
    """Generate an exploration plan for a goal.

    GOAL is what you're trying to understand or accomplish.

    Example:
        emdash agent plan "understand how streaming works"
    """
    from emdash.agent import AgentToolkit

    try:
        toolkit = AgentToolkit()

        console.print(f"[dim]Planning exploration...[/dim]\n")
        result = toolkit.plan(goal, exploration_depth=depth, context=context or "")

        if result.success:
            plan = result.data.get("plan", {})
            console.print(f"[bold blue]Exploration Plan: {goal}[/bold blue]\n")
            console.print(f"[bold]Strategy:[/bold] [green]{plan.get('strategy')}[/green]")
            console.print(f"[dim]{plan.get('strategy_description')}[/dim]")
            console.print(f"[bold]Depth:[/bold] {plan.get('exploration_depth')}")
            console.print(f"[bold]Estimated steps:[/bold] {plan.get('estimated_steps')}\n")

            console.print("[bold]Steps:[/bold]")
            for step in plan.get("steps", []):
                console.print(f"  {step['step']}. [cyan]{step['action']}[/cyan]")
                console.print(f"     [dim]{step['rationale']}[/dim]")

            questions = plan.get("key_questions_to_answer", [])
            if questions:
                console.print("\n[bold]Key Questions to Answer:[/bold]")
                for q in questions:
                    console.print(f"  - {q}")

            if result.suggestions:
                console.print("\n[bold]Next:[/bold]")
                for s in result.suggestions:
                    console.print(f"  - {s}")
        else:
            console.print(f"[bold red]Error:[/bold red] {result.error}")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("Agent plan error:")
        raise click.Abort()


@agent.command("session")
def agent_session():
    """Show current agent session context."""
    from emdash.agent import AgentToolkit

    try:
        toolkit = AgentToolkit()
        context = toolkit.get_session_context()

        if context:
            console.print("[bold]Agent Session Context:[/bold]\n")
            console.print(f"  Session ID: {context['session_id']}")
            console.print(f"  Steps taken: {context['steps_taken']}")
            console.print(f"  Entities discovered: {context['entities_discovered']}")
            console.print(f"  Current focus: {context.get('current_focus', 'None')}")

            if context.get('recent_actions'):
                console.print("\n[bold]Recent Actions:[/bold]")
                for action in context['recent_actions']:
                    console.print(f"  - {action['tool']}")
        else:
            console.print("[yellow]No session active[/yellow]")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        raise click.Abort()


@agent.command("chat")
@click.option("--model", default=DEFAULT_MODEL, help="LLM model (claude-* or gpt-*)")
@click.option("--quiet", is_flag=True, help="Hide tool call output")
@click.option("--query", "-q", default=None, help="Single query (non-interactive mode)")
@click.option(
    "--tool-result-max-chars",
    type=int,
    default=None,
    help="Max chars for tool results before truncation (0 disables truncation)",
)
def agent_chat(model: str, quiet: bool, query: str | None, tool_result_max_chars: int | None):
    """Interactive chat with code exploration agent.

    Start an interactive session where you can ask questions about the
    codebase. The agent uses semantic search, graph traversal, and
    analytics to explore and answer questions.

    Example:
        emdash agent chat
        emdash agent chat --model gpt-5
        emdash agent chat -q "How does authentication work?"
    """
    from emdash.agent import AgentRunner
    from rich.markdown import Markdown

    try:
        runner = AgentRunner(
            model=model,
            verbose=not quiet,
            tool_result_max_chars=tool_result_max_chars,
        )

        # Non-interactive mode: single query
        if query:
            response = runner.chat(query)
            # Output raw response for programmatic use
            if quiet:
                print(response)
            else:
                console.print(Markdown(response))
            return

        # Interactive mode
        console.print()
        console.print("[bold cyan]Code Exploration Agent[/bold cyan]")
        console.print(f"[dim]Model: {model} | Type 'exit' or Ctrl+C to quit[/dim]")
        console.print()

        while True:
            try:
                # Get user input
                user_query = console.input("[bold green]You:[/bold green] ").strip()

                if not user_query:
                    continue

                if user_query.lower() in ("exit", "quit", "q"):
                    console.print("\n[dim]Goodbye![/dim]")
                    break

                if user_query.lower() == "reset":
                    runner.reset()
                    console.print("[dim]Session reset.[/dim]\n")
                    continue

                if user_query.lower() == "session":
                    context = runner.get_session_summary()
                    if context:
                        console.print(f"[dim]Session: {context['session_id'][:8]}... "
                                     f"| Steps: {context['steps_taken']} "
                                     f"| Entities: {context['entities_discovered']}[/dim]\n")
                    continue

                # Run the query
                console.print()
                response = runner.chat(user_query)
                console.print()

                # Display response as markdown
                console.print("[bold blue]Agent:[/bold blue]")
                console.print(Markdown(response))
                console.print()

            except KeyboardInterrupt:
                console.print("\n\n[dim]Goodbye![/dim]")
                break

    except ValueError as e:
        console.print(f"[bold red]✗[/bold red] Configuration error: {e}")
        raise click.Abort()
    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("Agent chat error:")
        raise click.Abort()


def handle_mcp_command(cmd: str, repo_root, console) -> None:
    """Handle /mcp slash command.

    Subcommands:
        /mcp or /mcp list  - List all configured servers
        /mcp add <name> <command> [args...]  - Add new server
        /mcp remove <name>  - Remove a server
        /mcp describe <name>  - Show server details and tools
        /mcp enable <name>  - Enable a server
        /mcp disable <name>  - Disable a server
    """
    from pathlib import Path
    from emdash.agent.mcp import (
        MCPServerManager,
        MCPServerConfig,
        get_default_mcp_config_path,
    )

    parts = cmd.split(maxsplit=3)
    subcmd = parts[1] if len(parts) > 1 else "list"

    config_path = get_default_mcp_config_path(repo_root)
    manager = MCPServerManager(config_path=config_path, repo_root=repo_root)

    if subcmd == "list":
        servers = manager.list_servers()
        if not servers:
            console.print("[dim]No MCP servers configured[/dim]")
            console.print(f"[dim]Config file: {config_path}[/dim]")
            console.print()
            console.print("[dim]Add a server with:[/dim]")
            console.print("  /mcp add <name> <command> [args...]")
            console.print()
            return

        console.print()
        console.print("[bold]MCP Servers:[/bold]")
        for srv in servers:
            status = "[green]●[/green]" if srv["running"] else "[dim]○[/dim]"
            enabled = "" if srv["enabled"] else " [yellow](disabled)[/yellow]"
            tools = f" ({srv['tool_count']} tools)" if srv["running"] else ""
            console.print(f"  {status} [cyan]{srv['name']}[/cyan]{enabled}{tools}")
            console.print(f"      [dim]{srv['command']} {' '.join(srv['args'])}[/dim]")
        console.print()

    elif subcmd == "add":
        if len(parts) < 4:
            console.print("[yellow]Usage: /mcp add <name> <command> [args...][/yellow]")
            console.print("[dim]Example: /mcp add filesystem npx -y @modelcontextprotocol/server-filesystem /tmp[/dim]")
            console.print()
            return

        name = parts[2]
        # Parse command and args
        rest = parts[3].split()
        command = rest[0]
        args = rest[1:] if len(rest) > 1 else []

        config = MCPServerConfig(
            name=name,
            command=command,
            args=args,
            enabled=True,
        )
        manager.add_server(config)
        console.print(f"[green]✓[/green] Added MCP server '{name}'")
        console.print()

    elif subcmd == "remove":
        if len(parts) < 3:
            console.print("[yellow]Usage: /mcp remove <name>[/yellow]")
            console.print()
            return

        name = parts[2]
        if manager.remove_server(name):
            console.print(f"[green]✓[/green] Removed MCP server '{name}'")
        else:
            console.print(f"[yellow]Server '{name}' not found[/yellow]")
        console.print()

    elif subcmd == "describe":
        if len(parts) < 3:
            console.print("[yellow]Usage: /mcp describe <name>[/yellow]")
            console.print()
            return

        name = parts[2]
        info = manager.describe_server(name)
        if not info:
            console.print(f"[yellow]Server '{name}' not found[/yellow]")
            console.print()
            return

        console.print()
        console.print(f"[bold cyan]{info['name']}[/bold cyan]")
        console.print(f"  Command: {info['command']} {' '.join(info['args'])}")
        console.print(f"  Enabled: {info['enabled']}")
        console.print(f"  Running: {info['running']}")
        console.print(f"  Timeout: {info['timeout']}s")

        if info.get("env"):
            console.print("  Environment:")
            for k, v in info["env"].items():
                # Mask sensitive values
                display_v = "***" if "token" in k.lower() or "key" in k.lower() else v
                console.print(f"    {k}={display_v}")

        if info["tools"]:
            console.print(f"  Tools ({len(info['tools'])}):")
            for tool in info["tools"]:
                console.print(f"    [cyan]{tool['name']}[/cyan]: {tool['description'][:60]}...")
        console.print()

    elif subcmd == "enable":
        if len(parts) < 3:
            console.print("[yellow]Usage: /mcp enable <name>[/yellow]")
            console.print()
            return

        name = parts[2]
        if manager.enable_server(name):
            console.print(f"[green]✓[/green] Enabled MCP server '{name}'")
        else:
            console.print(f"[yellow]Server '{name}' not found[/yellow]")
        console.print()

    elif subcmd == "disable":
        if len(parts) < 3:
            console.print("[yellow]Usage: /mcp disable <name>[/yellow]")
            console.print()
            return

        name = parts[2]
        if manager.disable_server(name):
            console.print(f"[green]✓[/green] Disabled MCP server '{name}'")
        else:
            console.print(f"[yellow]Server '{name}' not found[/yellow]")
        console.print()

    else:
        console.print(f"[yellow]Unknown MCP subcommand: {subcmd}[/yellow]")
        console.print("[dim]Available: list, add, remove, describe, enable, disable[/dim]")
        console.print()


def handle_create_agent_command(repo_root, console, model: str = DEFAULT_MODEL) -> None:
    """Handle /create-agent slash command - use LLM to create a custom agent."""
    from emdash.agent.agents import ensure_agents_dir, load_agent
    from emdash.agent.providers import get_provider
    from prompt_toolkit import prompt as pt_prompt
    from prompt_toolkit.styles import Style
    from rich.markdown import Markdown

    style = Style.from_dict({"prompt": "cyan"})

    console.print()
    console.print("[bold]Create a New Custom Agent[/bold]")
    console.print("[dim]Describe the agent you want to create and the LLM will generate it[/dim]")
    console.print()

    # Get agent description from user
    try:
        description = pt_prompt("Describe your agent: ", style=style).strip()
        if not description:
            console.print("[yellow]Cancelled - no description provided[/yellow]\n")
            return
    except (KeyboardInterrupt, EOFError):
        console.print("\n[yellow]Cancelled[/yellow]\n")
        return

    # Use LLM to generate the agent
    console.print()
    console.print("[dim]Generating agent...[/dim]")

    provider = get_provider(model)

    system_prompt = """You are an expert at creating AI agent personas. Generate a markdown file that defines a specialized coding agent.

The markdown should include:
1. A title heading (# Title)
2. A clear description of the agent's role and expertise
3. A list of specialties/focus areas
4. A numbered workflow/approach for how the agent handles tasks
5. Guidelines for using tools and asking clarifying questions

Keep it concise but comprehensive. The agent will have access to code exploration tools, file editing, and command execution.

IMPORTANT:
- Output ONLY the markdown content, no explanations or code blocks
- Start with # and the agent title
- Also output the suggested filename (without .md) on the LAST line prefixed with "FILENAME: " """

    user_prompt = f"Create an agent for: {description}"

    try:
        response = provider.chat(
            messages=[{"role": "user", "content": user_prompt}],
            system=system_prompt,
        )
        content = response.content.strip()

        # Extract filename from response
        lines = content.split("\n")
        filename_line = lines[-1] if lines[-1].startswith("FILENAME:") else None

        if filename_line:
            name = filename_line.replace("FILENAME:", "").strip().lower()
            name = name.replace(" ", "-").replace("/", "-")
            content = "\n".join(lines[:-1]).strip()
        else:
            # Extract name from first heading
            first_line = lines[0] if lines else "agent"
            name = first_line.lstrip("#").strip().lower().replace(" ", "-")[:20]

        # Show preview
        console.print()
        console.print("[bold]Preview:[/bold]")
        console.print(Markdown(content))
        console.print()

        # Confirm save
        try:
            confirm = pt_prompt(f"Save as '{name}.md'? (Y/n/rename): ", style=style).strip().lower()
            if confirm == "n":
                console.print("[dim]Cancelled[/dim]\n")
                return
            elif confirm and confirm not in ("y", "yes", ""):
                name = confirm.replace(" ", "-").replace("/", "-")
        except (KeyboardInterrupt, EOFError):
            console.print("\n[yellow]Cancelled[/yellow]\n")
            return

        # Check if agent already exists
        if load_agent(name, repo_root):
            try:
                overwrite = pt_prompt(f"Agent '{name}' exists. Overwrite? (y/N): ", style=style).strip().lower()
                if overwrite != "y":
                    console.print("[dim]Cancelled[/dim]\n")
                    return
            except (KeyboardInterrupt, EOFError):
                console.print("\n[yellow]Cancelled[/yellow]\n")
                return

        # Save the agent
        agents_dir = ensure_agents_dir(repo_root)
        agent_path = agents_dir / f"{name}.md"
        agent_path.write_text(content, encoding="utf-8")

        console.print()
        console.print(f"[green]✓ Created agent '{name}'[/green]")
        console.print(f"[dim]Saved to: {agent_path}[/dim]")
        console.print()
        console.print(f"[dim]Use /agents {name} to switch to this agent[/dim]")
        console.print()

    except Exception as e:
        console.print(f"[red]Failed to generate agent: {e}[/red]\n")


def handle_add_rule_command(
    repo_root,
    console,
    runner,
    model: str = DEFAULT_MODEL,
) -> None:
    """Handle /add-rule slash command - create a rule from conversation context.

    This command uses the recent conversation history to understand what rule
    the user wants to add, generates a markdown rule file, and saves it to
    .emdash/rules/ directory.
    """
    from emdash.agent.rules import ensure_rules_dir, get_rules_dir
    from emdash.agent.providers import get_provider
    from prompt_toolkit import prompt as pt_prompt
    from prompt_toolkit.styles import Style
    from rich.markdown import Markdown

    style = Style.from_dict({"prompt": "cyan"})

    console.print()
    console.print("[bold]Add a Rule from Conversation[/bold]")
    console.print("[dim]The agent will analyze the conversation and suggest a rule[/dim]")
    console.print()

    # Get recent conversation context from runner
    recent_messages = []
    if hasattr(runner, "messages") and runner.messages:
        # Get last 10 messages (excluding system)
        for msg in runner.messages[-10:]:
            role = msg.get("role", "")
            content = msg.get("content", "")
            if role in ("user", "assistant") and content:
                if isinstance(content, list):
                    # Handle structured content
                    text_parts = [
                        block.get("text", "")
                        for block in content
                        if isinstance(block, dict) and block.get("type") == "text"
                    ]
                    content = " ".join(text_parts)
                recent_messages.append({"role": role, "content": content[:500]})

    # Ask user what kind of rule they want to add
    try:
        user_hint = pt_prompt(
            "Describe the rule (or press Enter to extract from conversation): ",
            style=style,
        ).strip()
    except (KeyboardInterrupt, EOFError):
        console.print("\n[yellow]Cancelled[/yellow]\n")
        return

    if not recent_messages and not user_hint:
        console.print("[yellow]No conversation context and no description provided.[/yellow]")
        console.print("[dim]Start a conversation first, or describe the rule you want to add.[/dim]\n")
        return

    # Generate rule using LLM
    console.print()
    console.print("[dim]Generating rule...[/dim]")

    provider = get_provider(model)

    system_prompt = """You are an expert at creating coding rules and guidelines. Generate a markdown file that defines a clear, actionable rule for an AI coding assistant.

The rule should:
1. Have a clear title (# Title)
2. Explain WHEN this rule applies
3. Explain WHAT the assistant should do
4. Provide concrete examples if helpful
5. Be concise but complete

Keep it focused on ONE specific behavior or guideline. The rule will be added to the AI assistant's system prompt.

IMPORTANT:
- Output ONLY the markdown content, no explanations or code blocks
- Start with # and the rule title
- Also output the suggested filename (without .md) on the LAST line prefixed with "FILENAME: "

Example output:
# Always Use Type Hints

When writing Python code, always include type hints for function parameters and return values.

## When This Applies
- Writing new Python functions
- Modifying existing Python functions
- Reviewing Python code

## What To Do
- Add type hints to all function parameters
- Add return type hints to all functions
- Use Optional[] for parameters that can be None

## Examples
```python
# Good
def get_user(user_id: int) -> Optional[User]:
    ...

# Bad
def get_user(user_id):
    ...
```

FILENAME: always-use-type-hints"""

    # Build context from conversation
    context_parts = []
    if recent_messages:
        context_parts.append("Recent conversation:")
        for msg in recent_messages:
            role = msg["role"].upper()
            content = msg["content"]
            context_parts.append(f"{role}: {content}")

    if user_hint:
        context_parts.append(f"\nUser's description of the rule: {user_hint}")

    user_prompt = "\n".join(context_parts) + "\n\nBased on this context, create a rule that captures the lesson learned or guideline discussed."

    try:
        response = provider.chat(
            messages=[{"role": "user", "content": user_prompt}],
            system=system_prompt,
        )
        content = response.content.strip()

        # Extract filename from response
        lines = content.split("\n")
        filename_line = lines[-1] if lines[-1].startswith("FILENAME:") else None

        if filename_line:
            name = filename_line.replace("FILENAME:", "").strip().lower()
            name = name.replace(" ", "-").replace("/", "-")
            content = "\n".join(lines[:-1]).strip()
        else:
            # Extract name from first heading
            first_line = lines[0] if lines else "rule"
            name = first_line.lstrip("#").strip().lower().replace(" ", "-")[:30]

        # Show preview
        console.print()
        console.print("[bold]Preview:[/bold]")
        console.print(Markdown(content))
        console.print()

        # Confirm save
        try:
            confirm = pt_prompt(
                f"Save as '{name}.md'? (Y/n/rename): ",
                style=style,
            ).strip().lower()
            if confirm == "n":
                console.print("[dim]Cancelled[/dim]\n")
                return
            elif confirm and confirm not in ("y", "yes", ""):
                # User wants to rename
                name = confirm.replace(" ", "-").replace("/", "-")
        except (KeyboardInterrupt, EOFError):
            console.print("\n[yellow]Cancelled[/yellow]\n")
            return

        # Ensure .md extension
        if not name.endswith(".md"):
            name = f"{name}.md"

        # Save rule
        rules_dir = ensure_rules_dir(repo_root)
        rule_path = rules_dir / name

        rule_path.write_text(content, encoding="utf-8")

        console.print()
        console.print(f"[green]✓ Created rule '{name}'[/green]")
        console.print(f"[dim]Saved to: {rule_path}[/dim]")
        console.print()
        console.print("[dim]The rule will be applied in future conversations.[/dim]")
        console.print()

    except Exception as e:
        console.print(f"[red]Failed to generate rule: {e}[/red]\n")


def handle_agents_command(cmd: str, repo_root, runner, console) -> None:
    """Handle /agents slash command.

    Subcommands:
        /agents          - List available agents
        /agents <name>   - Switch to named agent
        /agents default  - Return to default agent
    """
    from emdash.agent.agents import list_agents, load_agent

    parts = cmd.split(maxsplit=1)
    agent_name = parts[1].strip() if len(parts) > 1 else None

    if not agent_name:
        # List agents
        agents = list_agents(repo_root)
        current = runner.get_current_agent_name()

        if not agents:
            console.print("[dim]No custom agents found.[/dim]")
            console.print(f"[dim]Create .md files in {repo_root}/.emdash/agents/[/dim]\n")
            return

        console.print()
        console.print("[bold]Available Agents:[/bold]")
        for agent in agents:
            marker = "[green]●[/green] " if agent.name == current else "  "
            console.print(f"  {marker}[cyan]{agent.name}[/cyan] - {agent.description}")
        console.print()
        console.print("[dim]Use /agents <name> to switch, /agents default to return to normal[/dim]")
        console.print()
        return

    if agent_name == "default":
        # Return to default agent
        runner.set_custom_agent(None)
        console.print("[dim]Switched to default agent[/dim]\n")
        return

    # Switch to named agent
    agent = load_agent(agent_name, repo_root)
    if not agent:
        console.print(f"[yellow]Agent '{agent_name}' not found[/yellow]")
        console.print(f"[dim]Create {repo_root}/.emdash/agents/{agent_name}.md[/dim]\n")
        return

    runner.set_custom_agent(agent)
    console.print(f"[dim]Switched to {agent_name} agent[/dim]\n")


@agent.command("code")
@click.argument("task", required=False)
@click.option("--model", "-m", default=DEFAULT_MODEL, help="LLM model to use")
@click.option("--mode", type=click.Choice(["plan", "tasks", "code"]), default="code", help="Starting mode")
@click.option("--no-graph-tools", is_flag=True, help="Skip graph exploration tools")
@click.option("--quiet", "-q", is_flag=True, help="Hide tool call output")
@click.option("--save", is_flag=True, help="Save specs to specs/<feature>/")
def agent_code(task: str | None, model: str, mode: str, no_graph_tools: bool, quiet: bool, save: bool):
    """Start a coding agent session with three modes.

    NOTE: You can also just run 'emdash' without arguments to start this agent.

    MODES:
      plan   - Explore codebase and create specifications
      tasks  - Generate implementation task lists
      code   - Execute code changes (default)

    SLASH COMMANDS (in interactive mode):
      /plan   - Switch to plan mode
      /tasks  - Switch to tasks mode
      /code   - Switch to code mode
      /help   - Show available commands
      /reset  - Reset session

    Examples:
        emdash                                         # Interactive code mode (shortcut!)
        emdash agent code                              # Same as above
        emdash agent code --mode plan                  # Start in plan mode
        emdash agent code "Fix the login bug"          # Single task
    """
    from emdash.agent.coding_agent import CodingAgentRunner
    from emdash.agent.tools.modes import AgentMode
    from emdash.agent.specification import slugify
    from rich.markdown import Markdown
    from rich.prompt import Prompt
    import subprocess

    # Map mode string to enum
    mode_map = {
        "plan": AgentMode.PLAN,
        "tasks": AgentMode.TASKS,
        "code": AgentMode.CODE,
    }
    initial_mode = mode_map[mode]

    # Find git root
    try:
        result = subprocess.run(
            ["git", "rev-parse", "--show-toplevel"],
            capture_output=True,
            text=True,
            check=True,
        )
        git_root = Path(result.stdout.strip())
    except subprocess.CalledProcessError:
        console.print("[bold red]✗[/bold red] Not in a git repository")
        raise click.Abort()

    # Ensure .emdash/mcp.json exists with default config
    from emdash.agent.mcp.config import get_default_mcp_config_path, ensure_mcp_config
    mcp_config_path = get_default_mcp_config_path(git_root)
    ensure_mcp_config(mcp_config_path)

    # Check for project.md and suggest creating one if missing
    project_md_path = git_root / "project.md"
    if not project_md_path.exists():
        console.print()
        console.print("[yellow]💡 Tip:[/yellow] No [bold]project.md[/bold] found in your repository.")
        console.print("[dim]   Create one to give the agent context about your project:[/dim]")
        console.print(f"[dim]   {project_md_path}[/dim]")
        console.print()
        console.print("[dim]   Example content:[/dim]")
        console.print("[dim]   # Project Name[/dim]")
        console.print("[dim]   Brief description of the project, tech stack, conventions.[/dim]")
        console.print()

    # Session management - auto-create worktree if another session is active
    from emdash.swarm.session_manager import SessionManager
    session_manager = SessionManager(git_root)
    repo_root, worktree_branch = session_manager.start_session(
        task_hint=task or "",
    )

    # Show worktree info if isolated
    # Each worktree gets its own Kuzu database to avoid lock conflicts
    is_worktree = worktree_branch is not None
    if is_worktree:
        # Configure separate database for this worktree
        import os
        import shutil
        worktree_db_path = repo_root / ".emdash-task" / "kuzu_db"
        main_db_path = git_root / ".emdash" / "kuzu_db"

        # Copy main database to worktree if it doesn't exist yet
        if not worktree_db_path.exists() and main_db_path.exists():
            worktree_db_path.parent.mkdir(parents=True, exist_ok=True)
            try:
                shutil.copytree(main_db_path, worktree_db_path)
            except Exception as e:
                if not quiet:
                    console.print(f"[yellow]Warning: Could not copy database: {e}[/yellow]")

        os.environ["KUZU_DATABASE_PATH"] = str(worktree_db_path)

        if not quiet:
            console.print()
            console.print(f"[bold yellow]Isolated Session[/bold yellow]")
            console.print(f"[dim]Another emdash instance is active. Working in worktree:[/dim]")
            console.print(f"  Path:   [cyan]{repo_root}[/cyan]")
            console.print(f"  Branch: [cyan]{worktree_branch}[/cyan]")
            console.print(f"[dim]Changes will be on a separate branch. Merge when ready.[/dim]")
            console.print(f"[dim]Graph tools using copied database.[/dim]")

    # Determine save path if --save is used
    save_spec_path = None
    if save and task:
        slug = slugify(task[:50])
        specs_dir = repo_root / "specs" / slug
        save_spec_path = specs_dir / "spec.json"

    try:
        runner = CodingAgentRunner(
            repo_root=repo_root,
            model=model,
            include_graph_tools=not no_graph_tools,
            verbose=not quiet,
            initial_mode=initial_mode,
            save_spec_path=save_spec_path,
        )

        def show_help():
            """Show available commands."""
            console.print()
            console.print("[bold]Available Commands:[/bold]")
            console.print("  [cyan]/plan[/cyan]   - Switch to plan mode (create specifications)")
            console.print("  [cyan]/tasks[/cyan]  - Switch to tasks mode (generate task lists)")
            console.print("  [cyan]/code[/cyan]   - Switch to code mode (execute changes)")
            console.print("  [cyan]/mode[/cyan]   - Show current mode")
            console.print("  [cyan]/spec[/cyan]   - Show current specification")
            console.print("  [cyan]/save[/cyan]   - Save current spec to disk")
            console.print("  [cyan]/mcp[/cyan]    - Manage MCP servers (list, add, remove, describe)")
            console.print("  [cyan]/agents[/cyan] - Switch to custom agent or list available")
            console.print("  [cyan]/create-agent[/cyan] - Create a new custom agent")
            console.print("  [cyan]/kanban[/cyan] - Open Kanban board")
            console.print("  [cyan]/reset[/cyan]  - Reset session")
            console.print("  [cyan]/help[/cyan]   - Show this help")
            console.print("  [cyan]exit[/cyan]    - Quit")
            console.print()
            console.print("[dim]Tip: Type / and press Tab for autocomplete[/dim]")
            console.print()

        # Single task mode
        if task:
            if not quiet:
                console.print()
                console.print(f"[bold cyan]Coding Agent[/bold cyan] [dim]({mode} mode)[/dim]")
                console.print(f"[dim]Task: {task}[/dim]")
                console.print()

            response = runner.run(task)

            # Handle spec approval with interactive menu
            from emdash.cli.prompt import (
                show_spec_approval_menu, SpecApprovalChoice,
                show_tasks_approval_menu, TasksApprovalChoice,
            )
            while response and "Spec Ready for Review" in response:
                if not quiet:
                    console.print()
                    console.print(Markdown(response))

                # Show interactive approval menu
                result = show_spec_approval_menu()

                if result.choice == SpecApprovalChoice.TASKS:
                    # Switch to tasks mode and continue
                    runner.set_mode(AgentMode.TASKS)
                    response = runner.chat("Generate implementation tasks from the approved spec.")
                elif result.choice == SpecApprovalChoice.CODE:
                    # Switch to code mode and start implementing
                    runner.set_mode(AgentMode.CODE)
                    response = runner.chat("Start implementing the approved spec.")
                elif result.choice == SpecApprovalChoice.REFINE:
                    # Send feedback for refinement
                    if result.feedback:
                        response = runner.chat(f"Update the spec based on this feedback. IMPORTANT: Keep ALL existing content intact and only modify the specific parts related to this feedback: {result.feedback}")
                    else:
                        console.print("[yellow]No feedback provided[/yellow]")
                        continue
                else:  # ABORT
                    response = runner.chat("abort")
                    if response == "Task cancelled.":
                        break

            # Handle tasks approval (after task generation)
            task_markers = ["Ready to start implementation", "## Phase", "# Implementation Tasks", "Task 1:"]
            while response and any(marker in response for marker in task_markers) and runner.get_mode() == AgentMode.TASKS:
                if not quiet:
                    console.print()
                    console.print(Markdown(response))

                # Show tasks approval menu
                result = show_tasks_approval_menu()

                if result.choice == TasksApprovalChoice.IMPLEMENT:
                    runner.set_mode(AgentMode.CODE)
                    response = runner.chat("Start implementing the tasks. Begin with Task 1.")
                    break
                elif result.choice == TasksApprovalChoice.REFINE:
                    if result.feedback:
                        response = runner.chat(f"Please modify the tasks: {result.feedback}")
                    else:
                        console.print("[yellow]No feedback provided[/yellow]")
                        continue
                elif result.choice == TasksApprovalChoice.PLAN:
                    runner.set_mode(AgentMode.PLAN)
                    response = runner.chat("Let's revisit the spec.")
                    break
                else:  # ABORT
                    console.print("[yellow]Cancelled[/yellow]")
                    response = ""
                    break

            if response:
                if not quiet:
                    console.print()
                    console.print(Markdown(response))
                else:
                    print(response)
            return

        # Interactive mode with autocomplete
        from emdash.cli.prompt import create_agent_prompt
        from emdash.agent.tools.spec import SpecState

        console.print()
        console.print("[bold cyan]Coding Agent[/bold cyan]")
        console.print(f"[dim]Model: {model} | Repo: {repo_root.name} | Mode: {mode}[/dim]")
        console.print("[dim]Type / for commands, Tab to autocomplete, 'exit' to quit[/dim]")
        console.print()

        # Create interactive prompt with autocomplete
        # Show agent name if custom agent is active, otherwise show mode
        def get_prompt_label():
            agent_name = runner.get_current_agent_name()
            if agent_name:
                return agent_name
            return runner.get_mode().value

        agent_prompt = create_agent_prompt(
            get_mode=get_prompt_label,
        )

        # Track if this is the first message (use run() to init, chat() for follow-ups)
        first_message = True

        while True:
            try:
                prompt_result = agent_prompt.prompt()
                user_input = prompt_result.text
                pending_images = prompt_result.images

                if not user_input or not user_input.strip():
                    continue

                # Exit commands
                if user_input.lower() in ("exit", "quit", "q"):
                    console.print("\n[dim]Goodbye![/dim]")
                    break

                # Slash commands (but not file paths)
                if user_input.startswith("/") and not user_input.startswith("/var") and not user_input.startswith("/tmp") and not user_input.startswith("/Users") and not user_input.startswith("/home"):
                    cmd = user_input[1:].lower().strip()

                    if cmd == "plan":
                        runner.set_mode(AgentMode.PLAN)
                        console.print("[dim]Switched to plan mode[/dim]\n")
                        continue

                    elif cmd == "tasks":
                        runner.set_mode(AgentMode.TASKS)
                        console.print("[dim]Switched to tasks mode[/dim]\n")
                        continue

                    elif cmd == "code":
                        runner.set_mode(AgentMode.CODE)
                        console.print("[dim]Switched to code mode[/dim]\n")
                        continue

                    elif cmd == "mode":
                        current = runner.get_mode()
                        console.print(f"[dim]Current mode: {current.value}[/dim]\n")
                        continue

                    elif cmd == "spec":
                        spec_state = SpecState.get_instance()
                        if spec_state.spec:
                            console.print(Markdown(spec_state.get_markdown()))
                        else:
                            console.print("[dim]No spec created yet[/dim]")
                        console.print()
                        continue

                    elif cmd == "reset":
                        runner.reset()
                        first_message = True
                        console.print("[dim]Session reset (messages + context cleared)[/dim]\n")
                        continue

                    elif cmd == "save":
                        spec_state = SpecState.get_instance()
                        if spec_state.spec and save:
                            slug = slugify(spec_state.spec.title[:50])
                            specs_dir = repo_root / "specs" / slug
                            specs_dir.mkdir(parents=True, exist_ok=True)
                            spec_path = specs_dir / "spec.json"
                            spec_state.configure(save_path=spec_path)
                            spec_state._write_to_disk()
                            console.print(f"[green]✓ Saved to {spec_path}[/green]\n")
                        elif not spec_state.spec:
                            console.print("[yellow]No spec to save[/yellow]\n")
                        else:
                            console.print("[yellow]Use --save flag to enable saving[/yellow]\n")
                        continue

                    elif cmd == "image" or cmd.startswith("image "):
                        # Handle /image command - paste from clipboard or file path
                        from emdash.utils.image import read_clipboard_image, ClipboardImageError, get_image_info
                        from emdash.agent.providers.base import ImageContent

                        # Check if a file path was provided
                        parts = user_input.split(maxsplit=1)
                        if len(parts) > 1:
                            # File path provided
                            file_path = parts[1].strip()
                            try:
                                import os
                                # Handle escaped spaces and quotes
                                file_path = file_path.replace("\\ ", " ").strip("'\"")
                                if os.path.exists(file_path):
                                    with open(file_path, "rb") as f:
                                        image_data = f.read()
                                    info = get_image_info(image_data)
                                    # Determine format from extension
                                    ext = os.path.splitext(file_path)[1].lower()
                                    fmt = {".png": "png", ".jpg": "jpeg", ".jpeg": "jpeg", ".gif": "gif", ".webp": "webp"}.get(ext, "png")
                                    pending_images.append(ImageContent(image_data=image_data, format=fmt))
                                    console.print(f"[green]✓[/green] [image_{len(pending_images)}] attached")
                                else:
                                    console.print(f"[red]File not found:[/red] {file_path}")
                            except Exception as e:
                                console.print(f"[red]Failed to read image:[/red] {e}")
                        else:
                            # Try clipboard
                            try:
                                image_data = read_clipboard_image()
                                if image_data:
                                    info = get_image_info(image_data)
                                    pending_images.append(ImageContent(image_data=image_data, format="png"))
                                    console.print(f"[green]✓[/green] [image_{len(pending_images)}] attached")
                                else:
                                    console.print("[yellow]No image in clipboard[/yellow]")
                            except ClipboardImageError as e:
                                console.print(f"[red]Failed to read clipboard:[/red] {e}")
                        continue

                    elif cmd.startswith("mcp"):
                        # Handle /mcp command - manage MCP servers
                        handle_mcp_command(cmd, repo_root, console)
                        continue

                    elif cmd.startswith("agents"):
                        # Handle /agents command - switch custom agents
                        handle_agents_command(cmd, repo_root, runner, console)
                        continue

                    elif cmd == "create-agent":
                        # Handle /create-agent command - create new custom agent
                        handle_create_agent_command(repo_root, console, model=model)
                        continue

                    elif cmd == "add-rule":
                        # Handle /add-rule command - create rule from conversation
                        handle_add_rule_command(repo_root, console, runner, model=model)
                        continue

                    elif cmd == "kanban":
                        # Handle /kanban command - launch Electron Kanban app
                        from emdash.kanban_app.launcher import run_kanban_electron
                        run_kanban_electron(repo_root=repo_root)
                        continue

                    elif cmd == "kill":
                        # Handle /kill command - kill other emdash processes holding database lock
                        import os
                        import signal
                        import subprocess

                        current_pid = os.getpid()

                        # Find other emdash/em processes
                        try:
                            result = subprocess.run(
                                ["pgrep", "-f", "emdash|em run|em chat"],
                                capture_output=True,
                                text=True,
                            )
                            pids = [int(p) for p in result.stdout.strip().split("\n") if p and int(p) != current_pid]
                        except Exception:
                            pids = []

                        if not pids:
                            console.print("[yellow]No other emdash processes found[/yellow]")
                            # Still close our own connection
                            from emdash.graph.connection import close_connection
                            close_connection()
                            console.print("[dim]Closed local database connection[/dim]")
                            continue

                        console.print(f"[yellow]Found {len(pids)} other emdash process(es): {pids}[/yellow]")
                        confirm = console.input("[bold red]Kill them? (yes/no): [/bold red]")

                        if confirm.lower() in ("yes", "y"):
                            killed = 0
                            for pid in pids:
                                try:
                                    os.kill(pid, signal.SIGTERM)
                                    killed += 1
                                except ProcessLookupError:
                                    pass
                                except PermissionError:
                                    console.print(f"[red]Permission denied to kill PID {pid}[/red]")
                            console.print(f"[green]✓[/green] Killed {killed} process(es)")
                        else:
                            console.print("[dim]Cancelled[/dim]")
                        continue

                    elif cmd in ("help", "?"):
                        show_help()
                        continue

                    else:
                        console.print(f"[yellow]Unknown command: /{cmd}[/yellow]")
                        console.print("[dim]Type /help for available commands[/dim]\n")
                        continue

                # Update save path for this task if --save
                if save:
                    slug = slugify(user_input[:50])
                    specs_dir = repo_root / "specs" / slug
                    runner.save_spec_path = specs_dir / "spec.json"
                    spec_state = SpecState.get_instance()
                    spec_state.configure(save_path=runner.save_spec_path)

                # Run the task with any pending images
                # Use run() for first message to initialize, chat() for follow-ups to preserve history
                console.print()
                if first_message:
                    response = runner.run(user_input, images=pending_images if pending_images else None)
                    first_message = False
                else:
                    response = runner.chat(user_input, images=pending_images if pending_images else None)
                pending_images = []  # Clear after sending

                # Update token counts in prompt
                input_tokens, output_tokens = runner.get_token_usage()
                agent_prompt.update_tokens(input_tokens, output_tokens)

                # Handle spec approval with interactive menu
                from emdash.cli.prompt import (
                    show_spec_approval_menu, SpecApprovalChoice,
                    show_tasks_approval_menu, TasksApprovalChoice,
                )
                while response and "Spec Ready for Review" in response:
                    console.print()
                    console.print(Markdown(response))

                    # Show interactive approval menu
                    result = show_spec_approval_menu()

                    if result.choice == SpecApprovalChoice.TASKS:
                        # Switch to tasks mode and continue
                        runner.set_mode(AgentMode.TASKS)
                        response = runner.chat("Generate implementation tasks from the approved spec.")
                    elif result.choice == SpecApprovalChoice.CODE:
                        # Switch to code mode and start implementing
                        runner.set_mode(AgentMode.CODE)
                        response = runner.chat("Start implementing the approved spec.")
                    elif result.choice == SpecApprovalChoice.REFINE:
                        # Send feedback for refinement
                        if result.feedback:
                            response = runner.chat(f"Update the spec based on this feedback. IMPORTANT: Keep ALL existing content intact and only modify the specific parts related to this feedback: {result.feedback}")
                        else:
                            console.print("[yellow]No feedback provided[/yellow]")
                            continue
                    else:  # ABORT
                        response = runner.chat("abort")
                        if response == "Task cancelled.":
                            break

                # Handle tasks approval (after task generation)
                task_markers = ["Ready to start implementation", "## Phase", "# Implementation Tasks", "Task 1:"]
                while response and any(marker in response for marker in task_markers) and runner.get_mode() == AgentMode.TASKS:
                    console.print()
                    console.print(Markdown(response))

                    # Show tasks approval menu
                    result = show_tasks_approval_menu()

                    if result.choice == TasksApprovalChoice.IMPLEMENT:
                        # Switch to code mode and start implementing
                        runner.set_mode(AgentMode.CODE)
                        response = runner.chat("Start implementing the tasks. Begin with Task 1.")
                        break
                    elif result.choice == TasksApprovalChoice.REFINE:
                        # Send feedback for task refinement
                        if result.feedback:
                            response = runner.chat(f"Please modify the tasks: {result.feedback}")
                        else:
                            console.print("[yellow]No feedback provided[/yellow]")
                            continue
                    elif result.choice == TasksApprovalChoice.PLAN:
                        # Go back to plan mode
                        runner.set_mode(AgentMode.PLAN)
                        response = runner.chat("Let's revisit the spec. What changes should we make?")
                        break
                    else:  # ABORT
                        console.print("[yellow]Cancelled[/yellow]")
                        response = ""
                        break

                if response:
                    console.print()
                    console.print(Markdown(response))
                console.print()

            except KeyboardInterrupt:
                console.print("\n\n[dim]Goodbye![/dim]")
                break
            except EOFError:
                console.print("\n[dim]Goodbye![/dim]")
                break

    except ValueError as e:
        console.print(f"[bold red]✗[/bold red] Configuration error: {e}")
        raise click.Abort()
    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("Coding agent error:")
        raise click.Abort()


@agent.command("build")
@click.argument("agent_name")
@click.option(
    "--purpose", "-p",
    prompt="What should this agent specialize in?",
    help="The purpose/specialization of the agent",
)
@click.option("--model", "-m", default=DEFAULT_MODEL, help="LLM model to use")
@click.option("--max-prs", default=50, help="Maximum PRs to analyze")
@click.option("--no-interactive", is_flag=True, help="Skip interactive MCP tool selection")
@click.option("--output", "-o", type=click.Path(), help="Custom output directory")
def agent_build(
    agent_name: str,
    purpose: str,
    model: str,
    max_prs: int,
    no_interactive: bool,
    output: str | None,
):
    """Build a custom specialized agent.

    Creates an AGENTS.md profile and mcp.json configuration based on
    analysis of the codebase, PRs, and review patterns.

    AGENT_NAME: Name for the new agent (e.g., "pr-reviewer", "feature-planner")

    Examples:
        emdash agent build pr-reviewer -p "Review PRs for code quality"
        emdash agent build feature-designer --purpose "Design new features"
    """
    from pathlib import Path
    from emdash.agent.agent_builder import AgentBuilderAgent

    console.print(f"[bold blue]Building agent: {agent_name}[/bold blue]")
    console.print(f"[dim]Purpose: {purpose}[/dim]\n")

    try:
        builder = AgentBuilderAgent(model=model, verbose=True)

        # Build agent context
        context = builder.build(
            agent_name=agent_name,
            purpose=purpose,
            max_prs=max_prs,
            interactive=not no_interactive,
        )

        # Save files
        output_path = Path(output) if output else None
        saved_dir = builder.save(context, output_path)

        console.print(f"\n[bold green]Agent created![/bold green]")
        console.print(f"  [green]AGENTS.md[/green]: {saved_dir / 'AGENTS.md'}")
        console.print(f"  [green]mcp.json[/green]: {saved_dir / 'mcp.json'}")

        # Summary
        console.print(f"\n[bold]Summary:[/bold]")
        console.print(f"  PRs analyzed: {len(context.related_prs)}")
        console.print(f"  Patterns found: {len(context.review_patterns)}")
        console.print(f"  Anti-patterns: {len(context.things_to_avoid)}")
        console.print(f"  Domain experts: {len(context.domain_experts)}")
        console.print(f"  MCP tools: {len(context.selected_mcp_tools)}")

    except Exception as e:
        console.print(f"[bold red]Error building agent:[/bold red] {e}")
        log.exception("Agent build error:")
        raise click.Abort()


@cli.command("pr")
@click.argument("pr_number", required=False, nargs=-1)
@click.option(
    "--search",
    "--query",
    "-q",
    "search_query",
    default=None,
    help="Search PRs by title/number/body",
)
@click.option("--state", default="all", type=click.Choice(["open", "closed", "all"]))
@click.option("--model", default=DEFAULT_MODEL, help="LLM model (claude-* or gpt-*)")
@click.option("--save", is_flag=True, help="Save review to PRs/review/ directory")
@click.option("--quiet", is_flag=True, help="Hide tool call output")
def pr(
    pr_number: tuple[str, ...] | None,
    search_query: str | None,
    state: str,
    model: str,
    save: bool,
    quiet: bool,
):
    """Generate a narrative PR review report."""
    from rich.markdown import Markdown
    from rich.prompt import Prompt

    from emdash.agent.review import ReviewAgent
    from emdash.core.review_config import load_review_config

    try:
        config = load_review_config()
        agent = ReviewAgent(
            model=model,
            max_files=config.max_files,
            max_tokens=config.max_tokens,
            verbose=not quiet,
        )

        pr_number_value = None
        if pr_number:
            joined = " ".join(pr_number).strip()
            if joined.isdigit():
                pr_number_value = int(joined)
            else:
                if search_query:
                    search_query = f"{search_query} {joined}".strip()
                else:
                    search_query = joined

        pr_number = pr_number_value

        if not pr_number:
            if not search_query:
                console.print("[bold red]✗[/bold red] Provide a PR number or --search query.")
                raise click.Abort()

            prs = agent.search_prs(search_query, state=state)
            if not prs:
                console.print("[yellow]No PRs found for that query.[/yellow]")
                raise click.Abort()

            if len(prs) > 1:
                console.print("[bold]Matching PRs:[/bold]")
                for i, pr in enumerate(prs, 1):
                    console.print(f"  {i}. #{pr.get('number')} - {pr.get('title')}")
                raw = Prompt.ask("Select PR (index or PR number)", default="1").strip()
                if raw.isdigit():
                    selected = int(raw)
                    by_index = 1 <= selected <= len(prs)
                    if by_index:
                        pr_number = prs[selected - 1].get("number")
                    else:
                        match = next((pr for pr in prs if pr.get("number") == selected), None)
                        if match:
                            pr_number = match.get("number")
                        else:
                            console.print("[bold red]✗[/bold red] Invalid selection.")
                            raise click.Abort()
                else:
                    console.print("[bold red]✗[/bold red] Invalid selection.")
                    raise click.Abort()
            else:
                pr_number = prs[0].get("number")

        if not pr_number:
            console.print("[bold red]✗[/bold red] No PR number selected.")
            raise click.Abort()

        report = agent.generate_review(int(pr_number))

        if save:
            reviews_dir = Path.cwd() / "PRs" / "review"
            reviews_dir.mkdir(parents=True, exist_ok=True)
            output_path = reviews_dir / f"pr-{pr_number}.md"
            with open(output_path, "w", encoding="utf-8") as handle:
                handle.write(report)
            console.print(f"[bold green]✓[/bold green] Saved review to {output_path}")

        console.print(Markdown(report))

    except ValueError as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        raise click.Abort()
    except click.Abort:
        raise
    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("PR review error:")
        raise click.Abort()


@cli.command("create-reviewer")
@click.option("--top-reviewers", default=5, help="Number of top reviewers to analyze")
@click.option("--top-contributors", default=5, help="Number of cross-team contributors")
@click.option("--max-prs", default=100, help="Maximum PRs to analyze")
@click.option("--model", default=DEFAULT_MODEL, help="LLM model to use")
@click.option("--output", "-o", default=None, help="Output path (default: .emdash-rules/reviewer.md.template)")
@click.option("--quiet", is_flag=True, help="Hide progress output")
def create_reviewer(
    top_reviewers: int,
    top_contributors: int,
    max_prs: int,
    model: str,
    output: str | None,
    quiet: bool,
):
    """Analyze repository reviewers and create a reviewer profile template.

    This command analyzes the reviewing patterns of top reviewers in the repository
    and generates a profile template that captures their focus areas, feedback patterns,
    and review style. The generated template is used by the 'review' command.

    Examples:
        emdash create-reviewer
        emdash create-reviewer --top-reviewers 10 --max-prs 200
        emdash create-reviewer -o custom-reviewer.md.template
    """
    from pathlib import Path
    from emdash.agent.reviewer_profile import ReviewerProfileAgent

    try:
        agent = ReviewerProfileAgent(model=model, verbose=not quiet)

        if not quiet:
            console.print()
            console.print("[bold cyan]Reviewer Profile Generator[/bold cyan]")
            console.print(f"[dim]Analyzing top {top_reviewers} reviewers from up to {max_prs} PRs...[/dim]")
            console.print()

        profile = agent.analyze(
            top_n_reviewers=top_reviewers,
            top_n_contributors=top_contributors,
            max_prs=max_prs,
        )

        # Save the template
        output_path = Path(output) if output else None
        saved_path = agent.save_template(profile, output_path)

        console.print()
        console.print(f"[bold green]✓[/bold green] Saved reviewer profile to: [cyan]{saved_path}[/cyan]")
        console.print()
        console.print("[dim]Use 'emdash review <pr_number>' to review PRs using this profile.[/dim]")

    except ValueError as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        raise click.Abort()
    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("Create reviewer error:")
        raise click.Abort()


@cli.command("review")
@click.argument("pr_identifier", required=False, nargs=-1)
@click.option(
    "--search",
    "--query",
    "-q",
    "search_query",
    default=None,
    help="Search PRs by title/number/body",
)
@click.option("--state", default="all", type=click.Choice(["open", "closed", "all"]))
@click.option("--model", default=DEFAULT_MODEL, help="LLM model to use")
@click.option("--post", is_flag=True, help="Post review to GitHub (default: output only)")
@click.option(
    "--verdict",
    type=click.Choice(["APPROVE", "REQUEST_CHANGES", "COMMENT"]),
    default=None,
    help="Override verdict (default: LLM decides)",
)
@click.option("--quiet", is_flag=True, help="Hide progress output")
def review(
    pr_identifier: tuple[str, ...] | None,
    search_query: str | None,
    state: str,
    model: str,
    post: bool,
    verdict: str | None,
    quiet: bool,
):
    """Generate a PR review using the learned reviewer profile.

    This command uses the reviewer profile from .emdash-rules/reviewer.md.template
    (created by 'emdash create-reviewer') to generate code review comments
    in the style of your team's top reviewers.

    By default, the review is only displayed in the terminal. Use --post
    to actually submit the review to GitHub.

    Examples:
        emdash review 123                    # Review PR #123
        emdash review "fix auth"             # Search and review matching PR
        emdash review --search "refactor"    # Search PRs by query
        emdash review 123 --post             # Generate and post to GitHub
        emdash review 123 --verdict APPROVE  # Override the verdict
    """
    from rich.prompt import Prompt
    from emdash.agent.code_reviewer import CodeReviewerAgent
    from emdash.agent.review import ReviewAgent

    try:
        agent = CodeReviewerAgent(
            model=model,
            verbose=not quiet,
            enable_posting=post,
        )

        # Parse PR identifier - could be number or search string
        pr_number = None
        if pr_identifier:
            joined = " ".join(pr_identifier).strip()
            if joined.isdigit():
                pr_number = int(joined)
            else:
                # Treat as search query
                if search_query:
                    search_query = f"{search_query} {joined}".strip()
                else:
                    search_query = joined

        # If no PR number, search for it
        if not pr_number:
            if not search_query:
                console.print("[bold red]✗[/bold red] Provide a PR number or search query.")
                console.print("[dim]Examples: emdash review 123, emdash review \"fix bug\"[/dim]")
                raise click.Abort()

            # Use ReviewAgent for search (it has the search_prs method)
            search_agent = ReviewAgent(model=model, verbose=False)
            prs = search_agent.search_prs(search_query, state=state)

            if not prs:
                console.print(f"[yellow]No PRs found for '{search_query}'[/yellow]")
                raise click.Abort()

            if len(prs) > 1:
                console.print("[bold]Matching PRs:[/bold]")
                for i, pr in enumerate(prs[:10], 1):
                    console.print(f"  {i}. #{pr.get('number')} - {pr.get('title', '')[:60]}")
                if len(prs) > 10:
                    console.print(f"  [dim]... and {len(prs) - 10} more[/dim]")

                raw = Prompt.ask("Select PR (index or PR number)", default="1").strip()
                if raw.isdigit():
                    selected = int(raw)
                    if 1 <= selected <= len(prs):
                        pr_number = prs[selected - 1].get("number")
                    else:
                        # Try as PR number
                        match = next((pr for pr in prs if pr.get("number") == selected), None)
                        if match:
                            pr_number = match.get("number")
                        else:
                            console.print("[bold red]✗[/bold red] Invalid selection.")
                            raise click.Abort()
                else:
                    console.print("[bold red]✗[/bold red] Invalid selection.")
                    raise click.Abort()
            else:
                pr_number = prs[0].get("number")
                console.print(f"[dim]Found PR #{pr_number}: {prs[0].get('title', '')}[/dim]")

        if not pr_number:
            console.print("[bold red]✗[/bold red] No PR selected.")
            raise click.Abort()

        # Generate review
        result = agent.review(pr_number)

        # Override verdict if specified
        if verdict:
            result.verdict = verdict

        # Post if requested
        if post:
            success = agent.post_review(pr_number, result)
            if not success:
                console.print("[bold red]✗[/bold red] Failed to post review to GitHub")
                raise click.Abort()
        else:
            # Just display the review
            if not quiet:
                console.print()
                console.print("[dim]Use --post to submit this review to GitHub.[/dim]")

    except ValueError as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        raise click.Abort()
    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("Review error:")
        raise click.Abort()


@cli.command("projectmd")
@click.option("--output", "-o", default="PROJECT.md", help="Output file path")
@click.option("--save/--no-save", default=True, help="Save to file (default: save)")
@click.option("--model", default=DEFAULT_MODEL, help="LLM model (claude-* or gpt-*)")
@click.option("--quiet", is_flag=True, help="Hide exploration progress")
def projectmd(output: str, save: bool, model: str, quiet: bool):
    """Generate PROJECT.md by exploring the codebase.

    Uses AI to analyze the code graph and generate a comprehensive
    project document that describes architecture, patterns, and
    key components. This document guides future LLM interactions.

    Example:
        emdash projectmd
        emdash projectmd -o docs/PROJECT.md
        emdash projectmd --no-save
        emdash projectmd --model gpt-5
    """
    from emdash.agent.discovery import ProjectDiscoveryAgent
    from rich.markdown import Markdown

    try:
        console.print()
        console.print("[bold cyan]Generating PROJECT.md[/bold cyan]")
        if save:
            console.print(f"[dim]Model: {model} | Output: {output}[/dim]")
        else:
            console.print(f"[dim]Model: {model} | Preview only (--no-save)[/dim]")
        console.print()

        agent = ProjectDiscoveryAgent(model=model, verbose=not quiet)
        content = agent.discover()

        # Write to file if --save flag is provided
        if save:
            with open(output, "w") as f:
                f.write(content)
            console.print()
            console.print(f"[bold green]✓[/bold green] Generated {output}")

        console.print()

        # Show preview
        if not quiet:
            console.print("[bold]Preview:[/bold]")
            console.print("─" * 60)
            # Show first 50 lines
            preview_lines = content.split("\n")[:50]
            console.print(Markdown("\n".join(preview_lines)))
            if len(content.split("\n")) > 50:
                console.print(f"\n[dim]... and {len(content.split(chr(10))) - 50} more lines[/dim]")
            console.print("─" * 60)

    except ValueError as e:
        console.print(f"[bold red]✗[/bold red] Configuration error: {e}")
        raise click.Abort()
    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("Project discovery error:")
        raise click.Abort()


@cli.command("spec")
@click.argument("feature", required=True)
@click.option("--save", is_flag=True, help="Save spec to specs/<task-name>/spec.json")
@click.option("--project-md", "-p", default=None, help="Path to PROJECT.md file")
@click.option("--model", default=DEFAULT_MODEL, help="LLM model (claude-* or gpt-*)")
@click.option("--quiet", is_flag=True, help="Hide exploration progress")
@click.option("--verbose", "-v", is_flag=True, help="Show full tool results")
@click.option("--output-format", type=click.Choice(["rich", "json"]), default="rich", help="Output format (rich for terminal, json for programmatic use)")
def spec(feature: str, save: bool, project_md: str, model: str, quiet: bool, verbose: bool, output_format: str):
    """Generate a detailed specification for a feature.

    Uses AI to explore the codebase, ask clarification questions,
    and generate a comprehensive specification document.

    FEATURE is the description of what you want to build.

    Example:
        emdash spec "add user authentication with JWT"
        emdash spec "implement caching layer for API" --save
        emdash spec "add dark mode toggle" --save --project-md ./PROJECT.md
        emdash spec "add caching" --verbose  # Show full tool results
        emdash spec "add feature" --output-format json  # For VS Code extension
    """
    from emdash.agent.specification import SpecificationAgent, slugify
    from emdash.agent.events import AgentEventEmitter
    from emdash.agent.handlers import RichConsoleHandler, JSONStreamHandler
    import os
    import json
    import json

    try:
        # Create emitter and add appropriate handler based on output format
        emitter = AgentEventEmitter(agent_name="SpecificationAgent")
        
        if output_format == "json":
            # JSON output for programmatic consumption (VS Code extension)
            emitter.add_handler(JSONStreamHandler())
        elif not quiet:
            # Rich console output for terminal
            emitter.add_handler(RichConsoleHandler(
                console=console,
                verbose=True,
                show_tool_results=verbose,
            ))
            # Also show header for rich output
            console.print()
            console.print("[bold cyan]Specification Generator[/bold cyan]")
            console.print(f"[dim]Feature: {feature}[/dim]")
            console.print(f"[dim]Model: {model}[/dim]")
            console.print()

        agent = SpecificationAgent(
            model=model,
            verbose=not quiet and output_format != "json",
            project_md_path=project_md,
            show_tool_results=verbose,
            emitter=emitter,
            interactive=output_format != "json",  # Non-interactive in JSON mode
        )
        spec = agent.generate_spec(feature)
        # Convert dataclass to dict (Spec is a dataclass, not Pydantic)
        from dataclasses import asdict
        spec_dict = asdict(spec)

        # Save to file if --save flag is provided
        if save:
            task_slug = slugify(feature)
            spec_dir = os.path.join("specs", task_slug)
            os.makedirs(spec_dir, exist_ok=True)
            spec_path = os.path.join(spec_dir, "spec.json")

            with open(spec_path, "w") as f:
                f.write(json.dumps(spec_dict, indent=2))

            if output_format == "json":
                print(json.dumps({
                    "type": "saved",
                    "data": {"path": spec_path}
                }))
            else:
                console.print()
                console.print(f"[bold green]✓[/bold green] Saved to {spec_path}")

        # Final output
        if output_format == "json":
            # JSON: emit complete event (events already include the response)
            print(json.dumps({
                "type": "complete",
                "data": {"content": spec_dict}
            }))
        elif not quiet:
            # Rich: show the full spec
            console.print()
            console.print("[bold]Generated Specification (JSON):[/bold]")
            console.print("─" * 60)
            try:
                console.print(json.dumps(spec_dict, indent=2))
            except Exception:
                # Fall back to plain text if Markdown rendering fails
                console.print(json.dumps(spec_dict, indent=2))
            console.print("─" * 60)
        else:
            # Quiet mode: just print the content for programmatic use
            print(json.dumps(spec_dict))

    except KeyboardInterrupt:
        if output_format == "json":
            print(json.dumps({"type": "error", "data": {"message": "Cancelled"}}))
        else:
            console.print("\n[yellow]Specification generation cancelled.[/yellow]")
        raise click.Abort()
    except ValueError as e:
        if output_format == "json":
            print(json.dumps({"type": "error", "data": {"message": str(e)}}))
        else:
            console.print(f"[bold red]✗[/bold red] Configuration error: {e}")
        raise click.Abort()
    except Exception as e:
        if output_format == "json":
            print(json.dumps({"type": "error", "data": {"message": str(e)}}))
        else:
            console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("Specification error:")
        raise click.Abort()


@cli.command("tasks")
@click.argument("spec_name", required=False)
@click.option("--save", is_flag=True, help="Save tasks to specs/<name>/tasks.md")
@click.option("--project-md", "-p", default=None, help="Path to PROJECT.md file")
@click.option("--model", default=DEFAULT_MODEL, help="LLM model (claude-* or gpt-*)")
@click.option("--quiet", is_flag=True, help="Hide exploration progress")
@click.option("--verbose", "-v", is_flag=True, help="Show full tool results")
def tasks_command(spec_name: str, save: bool, project_md: str, model: str, quiet: bool, verbose: bool):
    """Generate implementation tasks from a specification.

    Takes a product specification and creates a detailed, actionable
    task list that can be executed by an LLM coding agent.

    SPEC_NAME is optional - if not provided, shows interactive picker.

    Example:
        emdash tasks
        emdash tasks "imageconfig"
        emdash tasks add-imageconfig-support --save
        emdash tasks --verbose  # Show full tool results
    """
    from emdash.agent.implementation import (
        ImplementationPlanAgent,
        list_specs,
        find_spec,
        load_spec,
    )
    from emdash.agent.specification import SpecificationAgent
    from rich.markdown import Markdown
    from rich.prompt import Prompt, Confirm, IntPrompt
    import os

    try:
        # Get available specs
        specs = list_specs()

        if not specs:
            console.print()
            console.print("[yellow]No specifications found in specs/ directory.[/yellow]")
            if Confirm.ask("Would you like to create a specification first?"):
                feature = Prompt.ask("[bold cyan]Feature description[/bold cyan]")
                console.print()
                # Run spec generation
                spec_agent = SpecificationAgent(model=model, verbose=not quiet, show_tool_results=verbose)
                spec_obj = spec_agent.generate_spec(feature)
                spec_dict = spec_obj.model_dump()

                # Save spec
                from emdash.agent.specification import slugify
                task_slug = slugify(feature)
                spec_dir = os.path.join("specs", task_slug)
                os.makedirs(spec_dir, exist_ok=True)
                spec_path = os.path.join(spec_dir, "spec.json")
                with open(spec_path, "w") as f:
                    f.write(json.dumps(spec_dict, indent=2))
                console.print(f"[green]✓[/green] Saved spec to {spec_path}")
                console.print()

                # Continue with implementation plan
                spec_name = task_slug
                specs = [(task_slug, spec_path)]
            else:
                console.print("[dim]Run 'emdash spec \"your feature\"' to create a specification.[/dim]")
                raise click.Abort()

        # Find or select spec
        selected_spec = None
        selected_path = None

        if spec_name:
            # Try to find by name
            result = find_spec(spec_name)
            if result:
                selected_spec, selected_path = result
            else:
                # Check for ambiguous matches
                matches = [(n, p) for n, p in specs if spec_name.lower() in n.lower()]
                if len(matches) > 1:
                    console.print(f"[yellow]Multiple specs match '{spec_name}':[/yellow]")
                    for i, (name, _) in enumerate(matches, 1):
                        console.print(f"  {i}. {name}")
                    choice = IntPrompt.ask("Select", default=1)
                    if 1 <= choice <= len(matches):
                        selected_spec, selected_path = matches[choice - 1]
                elif len(matches) == 0:
                    console.print(f"[yellow]No spec found matching '{spec_name}'[/yellow]")
                    if Confirm.ask("Would you like to create a specification for this feature?"):
                        # Create spec first
                        spec_agent = SpecificationAgent(model=model, verbose=not quiet, show_tool_results=verbose)
                        spec_obj = spec_agent.generate_spec(spec_name)
                        spec_dict = spec_obj.model_dump()

                        from emdash.agent.specification import slugify
                        task_slug = slugify(spec_name)
                        spec_dir = os.path.join("specs", task_slug)
                        os.makedirs(spec_dir, exist_ok=True)
                        spec_path = os.path.join(spec_dir, "spec.json")
                        with open(spec_path, "w") as f:
                            f.write(json.dumps(spec_dict, indent=2))
                        console.print(f"[green]✓[/green] Saved spec to {spec_path}")
                        console.print()
                        selected_spec = task_slug
                        selected_path = spec_path
                    else:
                        raise click.Abort()
        else:
            # Interactive selection
            console.print()
            console.print("[bold]Available Specifications:[/bold]")
            for i, (name, _) in enumerate(specs, 1):
                console.print(f"  [cyan]{i}.[/cyan] {name}")
            console.print()

            choice = IntPrompt.ask("Select spec", default=1)
            if 1 <= choice <= len(specs):
                selected_spec, selected_path = specs[choice - 1]
            else:
                console.print("[red]Invalid selection[/red]")
                raise click.Abort()

        if not selected_spec or not selected_path:
            console.print("[red]No specification selected[/red]")
            raise click.Abort()

        # Load spec content
        spec_content = load_spec(selected_path)

        console.print()
        console.print("[bold cyan]Task Generator[/bold cyan]")
        console.print(f"[dim]Spec: {selected_spec}[/dim]")
        console.print(f"[dim]Model: {model}[/dim]")
        console.print()

        # Generate implementation plan
        agent = ImplementationPlanAgent(
            model=model,
            verbose=not quiet,
            spec_content=spec_content,
            spec_name=selected_spec,
            project_md_path=project_md,
            show_tool_results=verbose,
        )
        plan_content = agent.generate_plan()

        # Check if we got any meaningful content
        # Valid plans should have markdown headers and reasonable length
        content_stripped = plan_content.strip() if plan_content else ""
        has_headers = "##" in content_stripped or content_stripped.startswith("#")
        is_valid_plan = (
            len(content_stripped) >= 200 and
            has_headers and
            ("task" in content_stripped.lower() or "step" in content_stripped.lower())
        )

        if not is_valid_plan:
            reason_bits = []
            if not content_stripped:
                reason_bits.append("No output returned from the model.")
            if not has_headers:
                reason_bits.append("Missing markdown headers.")
            if len(content_stripped) < 200:
                reason_bits.append("Output looks too short or truncated.")
            if "task" not in content_stripped.lower() and "step" not in content_stripped.lower():
                reason_bits.append("No tasks/steps detected in the output.")
            console.print()
            console.print("[bold yellow]⚠[/bold yellow] Could not generate valid tasks.")
            console.print("[dim]The model response was incomplete or malformed.[/dim]")
            if reason_bits:
                console.print(f"[dim]Possible cause: {' '.join(reason_bits)}[/dim]")
            console.print("[dim]Try again, run with --verbose, or switch to a different model (e.g., --model sonnet).[/dim]")
            return

        # Save to file if --save flag is provided
        if save:
            spec_dir = os.path.dirname(selected_path)
            tasks_path = os.path.join(spec_dir, "tasks.md")

            with open(tasks_path, "w") as f:
                f.write(plan_content)

            console.print()
            console.print(f"[bold green]✓[/bold green] Saved to {tasks_path}")

        console.print()

        # Show the plan
        if not quiet:
            console.print("[bold]Generated Tasks:[/bold]")
            console.print("─" * 60)
            console.print(Markdown(plan_content))
            console.print("─" * 60)

    except KeyboardInterrupt:
        console.print("\n[yellow]Task generation cancelled.[/yellow]")
        raise click.Abort()
    except ValueError as e:
        console.print(f"[bold red]✗[/bold red] Configuration error: {e}")
        raise click.Abort()
    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("Task generation error:")
        raise click.Abort()


# =============================================================================
# Rules Commands - Template Management
# =============================================================================

@cli.group()
def rules():
    """Manage .emdash-rules templates for spec, tasks, and PROJECT.md generation."""
    pass


@rules.command("init")
@click.option("--global", "global_", is_flag=True, help="Initialize in ~/.emdash-rules instead of ./.emdash-rules")
@click.option("--force", is_flag=True, help="Overwrite existing templates")
def rules_init(global_: bool, force: bool):
    """Initialize custom templates in .emdash-rules directory.

    Examples:
        emdash rules init              # Create .emdash-rules/ in current directory
        emdash rules init --global     # Create ~/.emdash-rules/ for user-wide defaults
        emdash rules init --force      # Overwrite existing templates
    """
    from pathlib import Path
    from emdash.templates.loader import copy_templates_to_dir, EMDASH_RULES_DIR

    if global_:
        target_dir = Path.home() / EMDASH_RULES_DIR
        location = "user-wide"
    else:
        target_dir = Path.cwd() / EMDASH_RULES_DIR
        location = "project-local"

    console.print(f"[bold blue]Initializing {location} templates...[/bold blue]")

    try:
        copied = copy_templates_to_dir(target_dir, overwrite=force)

        if copied:
            console.print(f"[bold green]✓[/bold green] Created {target_dir}/")
            for filename in copied:
                console.print(f"  [green]✓[/green] {filename}")
        else:
            console.print(f"[yellow]Templates already exist in {target_dir}/[/yellow]")
            console.print("[dim]Use --force to overwrite[/dim]")

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Failed to initialize templates: {e}")
        raise click.Abort()


@rules.command("show")
@click.argument("template", type=click.Choice(["spec", "tasks", "project", "focus", "pr-review"]))
def rules_show(template: str):
    """Show the active template and its source.

    Examples:
        emdash rules show spec      # Show spec.md template
        emdash rules show tasks     # Show tasks.md template
        emdash rules show project   # Show project.md template
        emdash rules show focus     # Show focus.md template (team focus)
        emdash rules show pr-review # Show pr-review.md template
    """
    from emdash.templates.loader import get_template_path, load_template

    try:
        path, tier = get_template_path(template)

        tier_colors = {
            "project": "green",
            "user": "cyan",
            "default": "yellow",
        }
        tier_labels = {
            "project": "project-local (.emdash-rules/)",
            "user": "user-wide (~/.emdash-rules/)",
            "default": "built-in default",
        }

        color = tier_colors.get(tier, "white")
        label = tier_labels.get(tier, tier)

        console.print(f"[bold]Template:[/bold] {template}")
        console.print(f"[bold]Source:[/bold] [{color}]{label}[/{color}]")
        console.print(f"[bold]Path:[/bold] {path}")
        console.print()
        console.print("[bold]Content:[/bold]")
        console.print("─" * 60)

        content = load_template(template)
        # Show first 50 lines
        lines = content.split("\n")[:50]
        for line in lines:
            console.print(f"[dim]{line}[/dim]")
        if len(content.split("\n")) > 50:
            console.print(f"[dim]... ({len(content.split(chr(10))) - 50} more lines)[/dim]")

        console.print("─" * 60)

    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        raise click.Abort()


@rules.command("list")
def rules_list():
    """List all templates and their active sources.

    Example:
        emdash rules list
    """
    from emdash.templates.loader import list_templates

    templates = list_templates()

    tier_colors = {
        "project": "green",
        "user": "cyan",
        "default": "yellow",
        "error": "red",
    }

    console.print("[bold]Active Templates:[/bold]")
    console.print()

    for t in templates:
        color = tier_colors.get(t["tier"], "white")
        status = "✓" if t["exists"] else "✗"
        console.print(f"  [{color}]{status}[/{color}] [bold]{t['name']}[/bold]")
        console.print(f"      Source: [{color}]{t['tier']}[/{color}]")
        console.print(f"      Path: [dim]{t['path']}[/dim]")
        console.print()


# =============================================================================
# Team Commands - Team Activity Analysis
# =============================================================================

@cli.group()
def team():
    """Team activity and collaboration analysis commands."""
    _configure_db_for_cwd()


@team.command("focus")
@click.option("--days", default=14, help="Time window for recency (default: 14 days)")
@click.option("--model", default=DEFAULT_MODEL, help="LLM model (claude-* or gpt-*)")
@click.option("--json", "output_json", is_flag=True, help="Output raw data as JSON")
@click.option("--save", is_flag=True, help="Save markdown report to team/ directory")
@click.option("--no-graph", is_flag=True, help="Skip graph traversal for faster results")
@click.option("--quiet", is_flag=True, help="Hide progress output")
def team_focus(days: int, model: str, output_json: bool, save: bool, no_graph: bool, quiet: bool):
    """Get team's recent focus and work-in-progress.

    Analyzes recent activity to show:
    - Hot areas (directories with recent commits)
    - Hot files (individual files with recent activity)
    - Open PRs (current work-in-progress)
    - Recently merged PRs (completed work)
    - Active contributors

    Uses an LLM to synthesize the data into actionable insights.

    Examples:
        emdash team focus                      # Default 14-day analysis
        emdash team focus --save               # Save markdown to team/ directory
        emdash team focus --days 7             # Last 7 days
        emdash team focus --json               # Raw data as JSON
        emdash team focus --no-graph           # Skip graph expansion (faster)
    """
    import json as json_lib
    import os
    from datetime import datetime
    from rich.markdown import Markdown

    try:
        from emdash.planning.team_focus import TeamFocusAnalyzer

        if not quiet:
            console.print()
            console.print("[bold cyan]Team Focus Analysis[/bold cyan]")
            console.print(f"[dim]Time window: {days} days | Model: {model}[/dim]")
            console.print()

        connection = get_connection()
        connection.connect()

        analyzer = TeamFocusAnalyzer(connection=connection, model=model)

        if output_json:
            # Output raw data as JSON
            if not quiet:
                console.print("[dim]Gathering data...[/dim]")

            data = analyzer.get_raw_data(days=days)
            console.print(json_lib.dumps(data, indent=2, default=str))

        else:
            # Generate LLM summary
            if not quiet:
                with console.status("[bold cyan]Gathering data and generating summary...[/bold cyan]"):
                    summary = analyzer.analyze(
                        days=days,
                        include_graph_context=not no_graph,
                    )
            else:
                summary = analyzer.analyze(
                    days=days,
                    include_graph_context=not no_graph,
                )

            # Output to console
            if quiet:
                # Raw markdown output for programmatic use
                print(summary)
            else:
                console.print()
                console.print(Markdown(summary))
                console.print()

            # Save to file if requested
            if save:
                team_dir = "team"
                os.makedirs(team_dir, exist_ok=True)
                
                timestamp = datetime.now().strftime("%Y-%m-%d")
                md_path = os.path.join(team_dir, f"focus-{timestamp}.md")
                
                with open(md_path, 'w') as f:
                    f.write(f"# Team Focus Analysis\n")
                    f.write(f"*Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')} | Last {days} days*\n\n")
                    f.write(summary)
                
                console.print(f"[bold green]✓[/bold green] Saved to: [cyan]{md_path}[/cyan]")
                console.print()

    except ValueError as e:
        console.print(f"[bold red]✗[/bold red] Configuration error: {e}")
        console.print("[dim]Make sure ANTHROPIC_API_KEY or OPENAI_API_KEY is set.[/dim]")
        raise click.Abort()
    except Exception as e:
        console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("Team focus error:")
        raise click.Abort()


@cli.command("research")
@click.argument("goal")
@click.option("--max-iterations", default=5, help="Maximum research-critique iterations")
@click.option("--budget", default=100, help="Maximum tool calls budget")
@click.option("--output", "-o", default=None, help="Output file path for report")
@click.option("--model", default=DEFAULT_MODEL, help="LLM model for planning/critic/synthesis")
@click.option("--researcher-model", default=DEFAULT_MODEL, help="LLM model for researcher (fast)")
@click.option("--quiet", is_flag=True, help="Hide progress output")
@click.option("--output-format", type=click.Choice(["rich", "json"]), default="rich", help="Output format (rich for terminal, json for programmatic use)")
def research(goal: str, max_iterations: int, budget: int, output: str, model: str, researcher_model: str, quiet: bool, output_format: str):
    """Deep research with multi-LLM loops and critic evaluation.

    Performs fact-grounded, team-values-driven research on the codebase.
    Uses a multi-agent architecture with:
    - Planner: Creates research questions
    - Researcher: Executes tool macros, collects evidence
    - Critic: Evaluates and gates progress
    - Synthesizer: Generates final report

    GOAL is what you want to research about the codebase.

    Examples:
        emdash research "How does the agent toolkit work?"
        emdash research "What are the risks of modifying session management?" --max-iterations 5
        emdash research "Who owns the analytics module?" --output report.md
        emdash research "What is X?" --output-format json  # For VS Code extension
    """
    from rich.markdown import Markdown
    from rich.panel import Panel
    import json

    try:
        from emdash.agent.research import DeepResearchAgent
        from emdash.agent.events import AgentEventEmitter
        from emdash.agent.handlers import RichConsoleHandler, JSONStreamHandler

        # Create emitter and add appropriate handler based on output format
        emitter = AgentEventEmitter(agent_name="DeepResearchAgent")
        
        if output_format == "json":
            emitter.add_handler(JSONStreamHandler())
        elif not quiet:
            emitter.add_handler(RichConsoleHandler(
                console=console,
                verbose=True,
            ))
            console.print()
            console.print(Panel(
                f"[bold cyan]Deep Research Agent[/bold cyan]\n\n"
                f"Goal: {goal}\n"
                f"Max iterations: {max_iterations}\n"
                f"Budget: {budget} tool calls",
                border_style="cyan",
            ))
            console.print()

        agent = DeepResearchAgent(
            planner_model=model,
            researcher_model=researcher_model,
            critic_model=model,
            synthesizer_model=model,
            max_iterations=max_iterations,
            verbose=not quiet and output_format != "json",
            emitter=emitter,
        )

        report = agent.research(
            goal=goal,
            budgets={"tool_calls": budget, "tokens": 100000, "time_s": 300},
        )

        # Save to file if output specified
        if output:
            with open(output, "w") as f:
                f.write(report)
            if output_format == "json":
                print(json.dumps({"type": "saved", "data": {"path": output}}))
            else:
                console.print(f"[bold green]✓[/bold green] Report saved to {output}")
                console.print()

        # Final output
        if output_format == "json":
            print(json.dumps({"type": "complete", "data": {"content": report}}))
        elif not quiet:
            console.print("[bold]Research Report:[/bold]")
            console.print("─" * 60)
            try:
                console.print(Markdown(report))
            except Exception:
                console.print(report)
            console.print("─" * 60)
        else:
            # Quiet mode: print raw report for programmatic use
            print(report)

    except ValueError as e:
        if output_format == "json":
            print(json.dumps({"type": "error", "data": {"message": str(e)}}))
        else:
            console.print(f"[bold red]✗[/bold red] Configuration error: {e}")
            console.print("[dim]Make sure ANTHROPIC_API_KEY or OPENAI_API_KEY is set.[/dim]")
        raise click.Abort()
    except Exception as e:
        if output_format == "json":
            print(json.dumps({"type": "error", "data": {"message": str(e)}}))
        else:
            console.print(f"[bold red]✗[/bold red] Error: {e}")
        log.exception("Research error:")
        raise click.Abort()


@rules.command("setup")
@click.option("--model", default=DEFAULT_MODEL, help="LLM model to use")
@click.option("--global", "global_", is_flag=True, help="Save to ~/.emdash-rules instead of ./.emdash-rules")
def rules_setup(model: str, global_: bool):
    """Interactive setup to customize your rules based on your preferences.

    This command asks you questions about your project and preferences,
    then generates customized rules for spec.md, tasks.md, and PROJECT.md.

    Examples:
        emdash rules setup
        emdash rules setup --global
        emdash rules setup --model gpt-4o
    """
    from pathlib import Path
    from rich.prompt import Prompt, Confirm
    from rich.panel import Panel
    from rich.markdown import Markdown

    from emdash.templates.loader import EMDASH_RULES_DIR, load_template, TEMPLATE_NAMES
    from emdash.agent.providers import get_provider

    console.print(Panel(
        "[cyan]Welcome to EmDash Rules Setup![/cyan]\n\n"
        "I'll ask you a few questions about your project and preferences,\n"
        "then generate customized rules for your specs and implementation plans.",
        title="[bold]Rules Setup[/bold]",
        border_style="cyan",
    ))
    console.print()

    # Gather preferences through questions
    questions = {}

    # Question 1: Project type
    console.print("[bold]1. What type of project is this?[/bold]")
    console.print("   [dim]Examples: web app, API service, CLI tool, library, mobile app, or describe your own[/dim]")
    questions["project_type"] = Prompt.ask("Project type", default="web application")
    console.print()

    # Question 2: Team size
    console.print("[bold]2. What's your team size?[/bold]")
    console.print("   [dim]Examples: solo, small (2-5), medium (6-15), large (15+), or describe[/dim]")
    questions["team_size"] = Prompt.ask("Team size", default="small team")
    console.print()

    # Question 3: Spec detail level
    console.print("[bold]3. How detailed should specs be?[/bold]")
    console.print("   [dim]Examples: minimal, standard, detailed, or describe your preference[/dim]")
    questions["spec_detail"] = Prompt.ask("Detail level", default="standard")
    console.print()

    # Question 4: What matters most in specs
    console.print("[bold]4. What matters most in your specs?[/bold]")
    console.print("   [dim]Examples: user flows, edge cases, API contracts, performance, security[/dim]")
    questions["spec_priorities"] = Prompt.ask("Priorities", default="user flows, edge cases")
    console.print()

    # Question 5: Task granularity
    console.print("[bold]5. How granular should implementation tasks be?[/bold]")
    console.print("   [dim]Examples: coarse (few large tasks), medium, fine (many small tasks)[/dim]")
    questions["task_granularity"] = Prompt.ask("Task granularity", default="medium")
    console.print()

    # Question 6: Code review process
    console.print("[bold]6. Do you use code reviews?[/bold]")
    questions["has_code_review"] = Prompt.ask("Code reviews", default="yes")
    console.print()

    # Question 7: Testing requirements
    console.print("[bold]7. What's your testing approach?[/bold]")
    console.print("   [dim]Examples: minimal, standard, comprehensive, TDD, or describe[/dim]")
    questions["testing_approach"] = Prompt.ask("Testing approach", default="standard")
    console.print()

    # Question 8: Any specific things to include/exclude
    console.print("[bold]8. Anything specific to add or avoid in your docs?[/bold]")
    console.print("   [dim]Examples: 'always mention performance', 'no mermaid diagrams', 'include accessibility'[/dim]")
    custom_rules = Prompt.ask("Custom rules (or Enter to skip)", default="")
    questions["custom_rules"] = custom_rules if custom_rules else "none"
    console.print()

    # Now use LLM to generate customized templates
    console.print("[bold blue]Generating customized rules based on your preferences...[/bold blue]")
    console.print()

    try:
        provider = get_provider(model)
    except ValueError as e:
        console.print(f"[bold red]✗[/bold red] {e}")
        raise click.Abort()

    # Load default templates as base
    default_spec = load_template("spec")
    default_tasks = load_template("tasks")
    default_project = load_template("project")

    # Build the prompt for the LLM
    preferences_text = "\n".join([f"- {k}: {v}" for k, v in questions.items()])

    system_prompt = """You are helping customize documentation rules for a software project.
You will receive the user's preferences and the default templates.
Your job is to modify each template to better fit their needs.

Rules for modifications:
- Keep the same markdown structure and sections
- Adjust the content, examples, and emphasis based on preferences
- Keep it readable and practical
- Don't add unnecessary complexity
- Output valid markdown

You must output exactly 3 templates, clearly separated by these markers:
--- SPEC TEMPLATE ---
(spec template content)
--- TASKS TEMPLATE ---
(tasks template content)
--- PROJECT TEMPLATE ---
(project template content)
--- END ---"""

    user_prompt = f"""Here are the user's preferences:
{preferences_text}

Here are the default templates to customize:

=== DEFAULT SPEC TEMPLATE ===
{default_spec}

=== DEFAULT TASKS TEMPLATE ===
{default_tasks}

=== DEFAULT PROJECT TEMPLATE ===
{default_project}

Please customize these templates based on the user's preferences. Keep the same overall structure but adjust:
- Level of detail based on their preference
- Emphasis on what matters to them
- Team size considerations (e.g., reviewer section for solo vs team)
- Testing requirements
- Any custom rules they mentioned

Output the three customized templates using the markers specified."""

    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": user_prompt},
    ]

    with console.status("[bold cyan]Thinking..."):
        response = provider.chat(messages)

    content = response.content or ""

    # Parse the response to extract templates
    templates = {}
    if "--- SPEC TEMPLATE ---" in content:
        try:
            spec_start = content.index("--- SPEC TEMPLATE ---") + len("--- SPEC TEMPLATE ---")
            spec_end = content.index("--- TASKS TEMPLATE ---")
            templates["spec"] = content[spec_start:spec_end].strip()

            tasks_start = content.index("--- TASKS TEMPLATE ---") + len("--- TASKS TEMPLATE ---")
            tasks_end = content.index("--- PROJECT TEMPLATE ---")
            templates["tasks"] = content[tasks_start:tasks_end].strip()

            project_start = content.index("--- PROJECT TEMPLATE ---") + len("--- PROJECT TEMPLATE ---")
            project_end = content.index("--- END ---") if "--- END ---" in content else len(content)
            templates["project"] = content[project_start:project_end].strip()
        except ValueError:
            console.print("[yellow]Warning: Could not parse all templates. Using defaults for missing ones.[/yellow]")

    if not templates:
        console.print("[bold red]✗[/bold red] Failed to generate templates. Please try again.")
        raise click.Abort()

    # Show preview
    console.print()
    console.print("[bold green]✓[/bold green] Templates generated!")
    console.print()

    for name in ["spec", "tasks", "project"]:
        if name in templates:
            console.print(f"[bold]Preview: {name}.md.template[/bold]")
            console.print("─" * 40)
            preview = templates[name][:500] + "..." if len(templates[name]) > 500 else templates[name]
            console.print(f"[dim]{preview}[/dim]")
            console.print()

    # Confirm save
    if not Confirm.ask("Save these customized rules?", default=True):
        console.print("[yellow]Cancelled. No changes made.[/yellow]")
        return

    # Save templates
    if global_:
        target_dir = Path.home() / EMDASH_RULES_DIR
    else:
        target_dir = Path.cwd() / EMDASH_RULES_DIR

    target_dir.mkdir(parents=True, exist_ok=True)

    for name, content in templates.items():
        filename = TEMPLATE_NAMES[name]
        filepath = target_dir / filename
        with open(filepath, "w") as f:
            f.write(content)
        console.print(f"[green]✓[/green] Saved {filepath}")

    console.print()
    console.print("[bold green]Done![/bold green] Your customized rules are ready.")
    console.print(f"[dim]Edit them anytime at: {target_dir}/[/dim]")


# ============================================================================
# Search Command (for VS Code extension)
# ============================================================================


def _get_code_snippet(file_path: str, name: str, entity_type: str, max_lines: int = 10) -> str:
    """Extract code snippet for a function or class from a file."""
    import os
    import re
    
    if not file_path or not os.path.exists(file_path):
        return ""
    
    try:
        with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
            content = f.read()
            lines = content.split('\n')
        
        is_python = file_path.endswith('.py')
        is_ts_js = file_path.endswith(('.ts', '.tsx', '.js', '.jsx'))
        escaped_name = re.escape(name)
        
        start_line = None
        
        # Find the definition line based on language
        for i, line in enumerate(lines):
            stripped = line.strip()
            
            if entity_type == "Class":
                # Python: class Name(...)  or class Name:
                # TS/JS: class Name { or class Name extends ...
                if is_python and re.match(rf'^class\s+{escaped_name}\s*[\(:]', stripped):
                    start_line = i
                    break
                elif is_ts_js and re.match(rf'^(export\s+)?(abstract\s+)?class\s+{escaped_name}[\s\{{<]', stripped):
                    start_line = i
                    break
            else:
                # Python: def name( or async def name(
                # TS/JS: function name(, const name =, async function name(, name(, export function name(
                if is_python:
                    if re.match(rf'^(async\s+)?def\s+{escaped_name}\s*\(', stripped):
                        start_line = i
                        break
                elif is_ts_js:
                    # Match various function patterns
                    patterns = [
                        rf'^(export\s+)?(async\s+)?function\s+{escaped_name}\s*[\(<]',  # function name( or function name<
                        rf'^(export\s+)?(const|let|var)\s+{escaped_name}\s*[=:]',  # const name = 
                        rf'^(export\s+)?(async\s+)?{escaped_name}\s*[\(<]',  # name( as method
                        rf'^\s*{escaped_name}\s*[\(=:]',  # name( or name: or name =
                    ]
                    for pat in patterns:
                        if re.match(pat, stripped):
                            start_line = i
                            break
                    if start_line is not None:
                        break
        
        # Fallback: search for name anywhere
        if start_line is None:
            for i, line in enumerate(lines):
                if name in line and ('function' in line or 'def ' in line or 'class ' in line or f'{name}(' in line or f'{name} =' in line):
                    start_line = i
                    break
        
        if start_line is None:
            return ""
        
        # Extract snippet (up to max_lines)
        end_line = min(start_line + max_lines, len(lines))
        snippet_lines = lines[start_line:end_line]
        
        # If we cut off mid-function, add ellipsis
        if end_line < len(lines) and len(snippet_lines) == max_lines:
            snippet_lines.append("    ...")
        
        return '\n'.join(snippet_lines)
        
    except Exception:
        return ""


def _make_relative_path(file_path: str, workspace: str) -> str:
    """Convert absolute path to relative path, showing repo-relative paths for .emdash repos."""
    import os
    import re
    if not file_path:
        return ""
    try:
        # Check if this is a .emdash/repos path
        # Pattern: ~/.emdash/repos/{repo_hash}/{path}
        match = re.search(r'\.emdash/repos/[^/]+/(.+)$', file_path)
        if match:
            return match.group(1)
        
        # Standard relative path from workspace
        if file_path.startswith(workspace):
            rel = os.path.relpath(file_path, workspace)
            return rel
        
        # Just return filename if nothing else works
        return os.path.basename(file_path)
    except Exception:
        return file_path


@cli.command("search")
@click.argument("query")
@click.option("--semantic/--text", "use_semantic", default=True, help="Use semantic (embedding) or text search")
@click.option("--limit", default=20, help="Maximum number of results")
@click.option("--types", default="Function,Class", help="Entity types to search (comma-separated)")
@click.option("--min-score", default=0.3, help="Minimum similarity score (0-1, semantic only)")
@click.option("--importance/--no-importance", default=True, help="Use importance-weighted ranking (semantic only)")
@click.option("--output-format", type=click.Choice(["json", "table"]), default="json", help="Output format")
@click.option("--snippets/--no-snippets", default=True, help="Include code snippets in results")
def search(query: str, use_semantic: bool, limit: int, types: str, min_score: float, importance: bool, output_format: str, snippets: bool):
    """Search codebase by semantic similarity or text.

    QUERY is the search term or natural language description.

    Examples:
        emdash search "authentication handler" --semantic
        emdash search "AuthHandler" --text
        emdash search "how does login work" --limit 10
        emdash search "database connection" --output-format table
    """
    import json
    import os

    try:
        from emdash.planning.similarity import SimilaritySearch

        connection = get_connection()
        similarity = SimilaritySearch(connection)
        
        # Get workspace root (current directory)
        workspace = os.getcwd()

        entity_types = [t.strip() for t in types.split(",")]

        if use_semantic:
            if importance:
                results = similarity.importance_weighted_search(
                    query=query,
                    entity_types=entity_types,
                    limit=limit,
                    min_score=min_score,
                )
            else:
                results = similarity.find_similar_code(
                    query=query,
                    entity_types=entity_types,
                    limit=limit,
                    min_score=min_score,
                )
        else:
            # Text search
            results = similarity._fallback_code_search(
                query=query,
                entity_types=entity_types,
                limit=limit,
            )

        # Enhance results with code snippets and relative paths
        for r in results:
            file_path = r.get("file_path", "")
            
            # Convert to relative path
            r["relative_path"] = _make_relative_path(file_path, workspace)
            
            # Add code snippet
            if snippets and file_path:
                r["code_snippet"] = _get_code_snippet(
                    file_path, 
                    r.get("name", ""), 
                    r.get("type", "Function"),
                    max_lines=8
                )

        # Format output
        if output_format == "json":
            output = {
                "query": query,
                "mode": "semantic" if use_semantic else "text",
                "count": len(results),
                "results": results,
            }
            print(json.dumps(output, indent=2, default=str))
        else:
            # Table format
            if not results:
                console.print("[yellow]No results found.[/yellow]")
                if use_semantic:
                    console.print("[dim]Try --text for exact matching or check if embeddings are indexed.[/dim]")
                return

            console.print(f"[bold]Found {len(results)} results for:[/bold] {query}\n")

            for r in results:
                entity_type = r.get("type", "Unknown")
                name = r.get("name", "unknown")
                rel_path = r.get("relative_path", r.get("file_path", ""))
                score = r.get("score", 0)
                combined_score = r.get("combined_score")
                code_snippet = r.get("code_snippet", "")

                # Icon based on type
                icon = "📦" if entity_type == "Class" else "ƒ"

                console.print(f"[bold]{icon} {name}[/bold] ({entity_type})")
                if rel_path:
                    console.print(f"  [dim]{rel_path}[/dim]")
                if combined_score is not None:
                    console.print(f"  [cyan]Score: {combined_score:.2f}[/cyan]")
                elif score:
                    console.print(f"  [cyan]Score: {score:.2f}[/cyan]")
                if code_snippet:
                    console.print()
                    for line in code_snippet.split('\n')[:6]:
                        console.print(f"    [dim]{line}[/dim]")
                console.print()

    except Exception as e:
        if output_format == "json":
            import json
            print(json.dumps({"error": str(e)}, indent=2))
        else:
            console.print(f"[bold red]✗[/bold red] Error: {e}")
            log.exception("Search error:")
        raise click.Abort()


# =============================================================================
# Swarm Commands - Multi-Agent Parallel Execution
# =============================================================================

@cli.group()
def swarm():
    """Multi-agent parallel execution with git worktrees."""
    _configure_db_for_cwd()


@swarm.command("run")
@click.argument("tasks", nargs=-1, required=True)
@click.option("--model", "-m", default=DEFAULT_MODEL, help="LLM model for agents")
@click.option("--workers", "-w", default=3, help="Max parallel workers")
@click.option("--timeout", "-t", default=600, help="Timeout per task (seconds)")
@click.option("--base", "-b", default="main", help="Base branch for worktrees")
@click.option("--no-merge", is_flag=True, help="Skip merge phase after completion")
@click.option("--llm-merge", is_flag=True, help="Use LLM for merge conflict assistance")
@click.option("--keep-worktrees", is_flag=True, help="Keep worktrees after merge (default: auto-cleanup)")
def swarm_run(
    tasks: tuple[str],
    model: str,
    workers: int,
    timeout: int,
    base: str,
    no_merge: bool,
    llm_merge: bool,
    keep_worktrees: bool,
):
    """Run multiple agents in parallel on separate tasks.

    Each task gets its own git worktree and branch. Agents work independently,
    then results are merged back to the base branch.

    TASKS are task descriptions, one per agent.

    Examples:
        emdash swarm run "Add user authentication" "Fix login bug"
        emdash swarm run "Refactor database layer" --workers 1 --timeout 1200
        emdash swarm run "Add tests" "Update docs" --no-merge
    """
    from emdash.swarm import SwarmRunner, TaskStatus

    repo_root = Path(".")

    runner = SwarmRunner(
        repo_root=repo_root,
        model=model,
        max_workers=workers,
        timeout_per_task=timeout,
        base_branch=base,
    )

    task_list = list(tasks)

    console.print()
    console.print(f"[bold cyan]Starting Swarm[/bold cyan]")
    console.print(f"[dim]Tasks: {len(task_list)} | Workers: {workers} | Model: {model} | Base: {base}[/dim]")
    console.print()

    # Progress callback
    def on_progress(task, result):
        status_color = "green" if result.success else "red"
        status_text = "COMPLETED" if result.success else "FAILED"
        console.print(f"[{status_color}]{status_text}[/{status_color}] {task.title[:60]}")

    # Run the swarm
    state = runner.run(task_list, progress_callback=on_progress)

    console.print()
    console.print("[bold]Summary:[/bold]")
    for task in state.tasks:
        status_color = {
            TaskStatus.COMPLETED: "green",
            TaskStatus.FAILED: "red",
            TaskStatus.MERGED: "cyan",
        }.get(task.status, "yellow")
        console.print(f"  [{status_color}]{task.status.value:12}[/{status_color}] {task.title[:50]}")
        if task.completion_summary:
            summary = task.completion_summary[:80].replace("\n", " ")
            console.print(f"    [dim]{summary}...[/dim]")

    # Merge phase
    if not no_merge:
        completed = sum(1 for t in state.tasks if t.status == TaskStatus.COMPLETED)
        if completed > 0:
            console.print()
            console.print("[bold cyan]Merge Phase[/bold cyan]")
            if not keep_worktrees:
                console.print("[dim]Worktrees will be cleaned up after successful merge[/dim]")

            merge_results = runner.merge_completed(
                use_llm=llm_merge,
                cleanup_worktrees=not keep_worktrees,
            )

            for result in merge_results:
                task = next(t for t in state.tasks if t.id == result.task_id)
                if result.success:
                    console.print(f"[green]Merged ({result.merge_type})[/green] {task.title[:50]}")
                else:
                    console.print(f"[red]Conflicts[/red] {task.title[:50]}")
                    if result.conflicts:
                        console.print(f"  [dim]Files: {', '.join(result.conflicts[:3])}[/dim]")

    # Show worktree info only if they weren't cleaned up
    if no_merge or keep_worktrees:
        console.print()
        console.print(f"[dim]Worktrees at: .emdash-worktrees/[/dim]")
        console.print(f"[dim]Run 'emdash swarm cleanup' to remove worktrees[/dim]")


@swarm.command("status")
def swarm_status():
    """Show status of current swarm execution."""
    from emdash.swarm import SwarmRunner

    runner = SwarmRunner.load(Path("."))
    if not runner:
        console.print("[yellow]No active swarm found[/yellow]")
        return

    status = runner.get_status()

    console.print()
    console.print(f"[bold cyan]Swarm {status['swarm_id']}[/bold cyan]")
    console.print(f"[dim]Created: {status['created_at']} | Base: {status['base_branch']}[/dim]")
    console.print()

    # Summary counts
    console.print(f"[bold]Tasks:[/bold] {status['total_tasks']}")
    console.print(f"  Pending:   {status['pending']}")
    console.print(f"  Running:   {status['running']}")
    console.print(f"  Completed: [green]{status['completed']}[/green]")
    console.print(f"  Failed:    [red]{status['failed']}[/red]")
    console.print(f"  Merged:    [cyan]{status['merged']}[/cyan]")
    console.print()

    # Individual tasks
    console.print("[bold]Task Details:[/bold]")
    for task in status["tasks"]:
        status_color = {
            "completed": "green",
            "failed": "red",
            "merged": "cyan",
            "running": "yellow",
        }.get(task["status"], "dim")
        console.print(f"  [{status_color}]{task['status']:12}[/{status_color}] {task['title'][:50]}")
        console.print(f"    [dim]Branch: {task['branch']} | Files: {task['files_modified']}[/dim]")


@swarm.command("sessions")
def swarm_sessions():
    """Show active emdash agent sessions."""
    from emdash.swarm.session_manager import SessionManager

    manager = SessionManager(Path("."))
    sessions = manager.get_active_sessions()

    if not sessions:
        console.print("[dim]No active sessions[/dim]")
        return

    console.print()
    console.print(f"[bold cyan]Active Sessions ({len(sessions)})[/bold cyan]")
    console.print()

    for session in sessions:
        location = session.worktree_path or "[main repo]"
        console.print(f"  [bold]{session.id}[/bold] (pid {session.pid})")
        console.print(f"    Location: [cyan]{location}[/cyan]")
        if session.branch:
            console.print(f"    Branch:   [cyan]{session.branch}[/cyan]")
        if session.task_hint:
            console.print(f"    Task:     [dim]{session.task_hint[:60]}[/dim]")
        console.print(f"    Started:  [dim]{session.started_at}[/dim]")
        console.print()


@swarm.command("cleanup")
@click.option("--force", "-f", is_flag=True, help="Force cleanup without confirmation")
def swarm_cleanup(force: bool):
    """Clean up all swarm worktrees and branches."""
    from emdash.swarm import WorktreeManager

    manager = WorktreeManager(Path("."))

    worktrees = manager.list_worktrees()
    if not worktrees:
        console.print("[dim]No worktrees to clean up[/dim]")
        return

    console.print(f"[yellow]Found {len(worktrees)} worktrees:[/yellow]")
    for wt in worktrees:
        console.print(f"  - {wt.task_slug} ({wt.branch})")

    if not force:
        if not click.confirm("\nRemove all worktrees and branches?"):
            console.print("[dim]Cancelled[/dim]")
            return

    count = manager.cleanup_all()
    console.print(f"[green]Removed {count} worktrees[/green]")


@swarm.command("merge")
@click.option("--llm", is_flag=True, help="Use LLM for merge conflict assistance")
@click.option("--target", "-t", default=None, help="Target branch (default: base branch)")
@click.option("--keep-worktrees", is_flag=True, help="Keep worktrees after merge (default: auto-cleanup)")
def swarm_merge(llm: bool, target: str, keep_worktrees: bool):
    """Merge all completed task branches.

    After successful merge, worktrees are automatically cleaned up.
    Use --keep-worktrees to preserve them for inspection.
    """
    from emdash.swarm import SwarmRunner, TaskStatus

    runner = SwarmRunner.load(Path("."))
    if not runner:
        console.print("[red]No swarm state found[/red]")
        return

    if not runner.state:
        console.print("[red]No tasks to merge[/red]")
        return

    completed = [t for t in runner.state.tasks if t.status == TaskStatus.COMPLETED]
    if not completed:
        console.print("[yellow]No completed tasks to merge[/yellow]")
        return

    console.print()
    console.print(f"[bold cyan]Merging {len(completed)} completed tasks[/bold cyan]")
    if llm:
        console.print("[dim]Using LLM assistance for conflicts[/dim]")
    if not keep_worktrees:
        console.print("[dim]Worktrees will be cleaned up after successful merge[/dim]")
    console.print()

    results = runner.merge_completed(
        use_llm=llm,
        target_branch=target,
        cleanup_worktrees=not keep_worktrees,
    )

    for result in results:
        task = next(t for t in runner.state.tasks if t.id == result.task_id)
        if result.success:
            console.print(f"[green]Merged ({result.merge_type})[/green] {task.title[:50]}")
        else:
            console.print(f"[red]Failed[/red] {task.title[:50]}")
            if result.conflicts:
                console.print(f"  [dim]Conflicts: {', '.join(result.conflicts)}[/dim]")
            if result.error_message:
                console.print(f"  [dim]{result.error_message[:100]}[/dim]")


# =============================================================================
# Auth Commands - GitHub Authentication
# =============================================================================

@cli.group()
def auth():
    """Manage GitHub authentication for Rove."""
    pass


@auth.command("login")
@click.option("--scopes", "-s", multiple=True, help="Additional OAuth scopes to request")
@click.option("--no-browser", is_flag=True, help="Don't open browser automatically")
def auth_login(scopes: tuple, no_browser: bool):
    """Authenticate with GitHub using device flow.

    This uses the same authentication flow as GitHub CLI (gh).
    You'll be given a code to enter at github.com/login/device.

    Examples:
        emdash auth login
        emdash auth login --scopes read:org
        emdash auth login --no-browser
    """
    from emdash.auth import GitHubAuth, get_auth_status

    # Check if already authenticated
    status = get_auth_status()
    if status["authenticated"] and status["source"] == "emdash auth":
        console.print(f"[yellow]Already logged in as @{status['username']}[/yellow]")
        console.print("[dim]Run 'emdash auth logout' first to re-authenticate[/dim]")
        return

    # Build scope list
    from emdash.auth.github import DEFAULT_SCOPES
    scope_list = list(DEFAULT_SCOPES)
    for s in scopes:
        if s not in scope_list:
            scope_list.append(s)

    # Perform login
    auth = GitHubAuth()
    config = auth.login(scopes=scope_list, open_browser=not no_browser)

    if not config:
        console.print("[red]Authentication failed[/red]")
        raise click.Abort()


@auth.command("logout")
def auth_logout():
    """Remove stored GitHub authentication.

    This removes the token stored by 'emdash auth login'.
    Environment variable tokens (GITHUB_TOKEN) are not affected.

    Example:
        emdash auth logout
    """
    from emdash.auth import GitHubAuth

    auth = GitHubAuth()
    if auth.logout():
        console.print("[green]✓[/green] Logged out successfully")
    else:
        console.print("[yellow]No stored authentication found[/yellow]")


@auth.command("status")
def auth_status():
    """Show current GitHub authentication status.

    Example:
        emdash auth status
    """
    from emdash.auth import get_auth_status

    status = get_auth_status()

    console.print()
    if status["authenticated"]:
        console.print("[green]✓[/green] Authenticated")
        console.print(f"  Source: {status['source']}")
        if status["username"]:
            console.print(f"  User: @{status['username']}")
        if status["scopes"]:
            console.print(f"  Scopes: {', '.join(status['scopes'])}")
    else:
        console.print("[yellow]✗[/yellow] Not authenticated")
        console.print()
        console.print("[dim]To authenticate, run:[/dim]")
        console.print("  emdash auth login")
        console.print()
        console.print("[dim]Or set environment variable:[/dim]")
        console.print("  export GITHUB_TOKEN=ghp_...")
    console.print()


# =============================================================================
# CONFIG COMMANDS
# =============================================================================


@cli.group()
def config():
    """Manage EmDash configuration.

    EmDash looks for configuration in two places:
      1. ~/.config/emdash/config (user-level defaults)
      2. .env in the current directory (project-level overrides)

    Use 'emdash config show' to see current settings.
    Use 'emdash config init' to create a configuration file.
    """
    pass


@config.command("show")
def config_show():
    """Show current configuration status.

    Displays which providers are configured and ready to use.

    Example:
        emdash config show
    """
    from emdash.core.config import get_config, get_user_config_path, get_local_env_path
    from emdash.auth import get_auth_status

    cfg = get_config()
    user_config = get_user_config_path()
    local_env = get_local_env_path()

    console.print()
    console.print("[bold]EmDash Configuration[/bold]")
    console.print()

    # Config file locations
    console.print("[cyan]Config Files:[/cyan]")
    if user_config.exists():
        console.print(f"  [green]✓[/green] User config: {user_config}")
    else:
        console.print(f"  [dim]✗[/dim] User config: {user_config} [dim](not found)[/dim]")

    if local_env.exists():
        console.print(f"  [green]✓[/green] Project config: {local_env}")
    else:
        console.print(f"  [dim]✗[/dim] Project config: {local_env} [dim](not found)[/dim]")
    console.print()

    # LLM Providers
    console.print("[cyan]LLM Providers:[/cyan]")
    console.print("  [dim]For agent chat, spec refinement, and PROJECT.md generation[/dim]")

    if cfg.anthropic.is_available:
        console.print("  [green]✓[/green] Anthropic (Claude)")
    else:
        console.print("  [dim]✗[/dim] Anthropic [dim]- set ANTHROPIC_API_KEY[/dim]")

    if cfg.openai.is_available:
        console.print("  [green]✓[/green] OpenAI (GPT)")
    else:
        console.print("  [dim]✗[/dim] OpenAI [dim]- set OPENAI_API_KEY[/dim]")

    if cfg.fireworks.is_available:
        console.print("  [green]✓[/green] Fireworks AI (default)")
    else:
        console.print("  [dim]✗[/dim] Fireworks [dim]- set FIREWORKS_API_KEY[/dim]")

    # Check if any LLM is available
    any_llm = cfg.anthropic.is_available or cfg.openai.is_available or cfg.fireworks.is_available
    if not any_llm:
        console.print()
        console.print("  [yellow]Warning:[/yellow] No LLM provider configured.")
        console.print("  [dim]Agent commands require at least one API key.[/dim]")
    console.print()

    # Embedding Providers
    console.print("[cyan]Embedding Providers:[/cyan]")
    console.print("  [dim]For semantic search and similarity features[/dim]")

    if cfg.fireworks.is_available:
        console.print("  [green]✓[/green] Fireworks AI (default)")
    elif cfg.openai.is_available:
        console.print("  [green]✓[/green] OpenAI")
    else:
        console.print("  [dim]✗[/dim] No embedding provider")
        console.print("  [dim]Set FIREWORKS_API_KEY or OPENAI_API_KEY for embeddings[/dim]")
    console.print()

    # GitHub Integration
    console.print("[cyan]GitHub Integration:[/cyan]")
    console.print("  [dim]For PR analysis, issues, and team insights[/dim]")

    auth_status = get_auth_status()
    if auth_status["authenticated"]:
        source = auth_status["source"]
        username = auth_status.get("username", "")
        if username:
            console.print(f"  [green]✓[/green] Authenticated as @{username} [dim]({source})[/dim]")
        else:
            console.print(f"  [green]✓[/green] Authenticated [dim]({source})[/dim]")
    else:
        console.print("  [dim]✗[/dim] Not authenticated")
        console.print("  [dim]Run 'emdash auth login' or set GITHUB_TOKEN[/dim]")
    console.print()

    # Database
    console.print("[cyan]Database:[/cyan]")
    db_path = Path(cfg.kuzu.database_path)
    if db_path.is_absolute():
        display_path = db_path
    else:
        display_path = Path.cwd() / db_path
    console.print(f"  Path: {display_path}")
    if display_path.exists():
        console.print("  [green]✓[/green] Database exists")
    else:
        console.print("  [dim]✗[/dim] Not indexed yet [dim](run 'emdash index')[/dim]")
    console.print()


@config.command("init")
@click.option("--global", "is_global", is_flag=True, help="Create config in ~/.config/emdash/")
@click.option("--force", is_flag=True, help="Overwrite existing config file")
def config_init(is_global: bool, force: bool):
    """Create a configuration file with documentation.

    Creates a config file with all available settings documented.
    By default, creates .env in the current directory.

    Examples:
        emdash config init              # Create .env in current directory
        emdash config init --global     # Create ~/.config/emdash/config
        emdash config init --force      # Overwrite existing file
    """
    from emdash.core.config import get_config_dir, get_user_config_path

    if is_global:
        config_dir = get_config_dir()
        config_path = get_user_config_path()
    else:
        config_dir = Path.cwd()
        config_path = config_dir / ".env"

    # Check if file exists
    if config_path.exists() and not force:
        console.print(f"[yellow]Config file already exists:[/yellow] {config_path}")
        console.print("[dim]Use --force to overwrite[/dim]")
        return

    # Create directory if needed
    config_dir.mkdir(parents=True, exist_ok=True)

    # Write config template
    config_content = """# EmDash Configuration
# ====================
# This file is loaded automatically by EmDash.
# Environment variables can override these settings.

# LLM API Keys (at least one required for agent features)
# --------------------------------------------------------
# Get your API keys from:
#   - Anthropic: https://console.anthropic.com/
#   - OpenAI: https://platform.openai.com/api-keys
#   - Fireworks: https://fireworks.ai/api-keys

# ANTHROPIC_API_KEY=sk-ant-...
# OPENAI_API_KEY=sk-...
# FIREWORKS_API_KEY=fw_...

# GitHub Integration (optional)
# -----------------------------
# For PR analysis and team insights.
# Alternatively, run 'emdash auth login' for browser-based auth.

# GITHUB_TOKEN=ghp_...

# Agent Configuration
# -------------------
# EMDASH_MAX_CONTEXT_MESSAGES=25  # Messages to send to LLM (5-100)
# EMDASH_MAX_ITERATIONS=50        # Max tool calls per session (10-200)
# EMDASH_LLM_REASONING=false      # Enable extended reasoning

# Database Configuration
# ----------------------
# KUZU_DATABASE_PATH=.emdash/index/kuzu_db  # Path to knowledge graph

# Ingestion Configuration
# -----------------------
# MAX_WORKERS=4       # Parallel workers (1-16)
# BATCH_SIZE=1000     # Batch size for processing (100-10000)
# GIT_DEPTH=          # Git history depth (empty = full history)
# AST_ONLY=false      # Skip git history layer

# Logging
# -------
# LOG_LEVEL=WARNING  # DEBUG, INFO, WARNING, ERROR
"""

    with open(config_path, "w") as f:
        f.write(config_content)

    console.print(f"[green]✓[/green] Created config file: {config_path}")
    console.print()
    console.print("[dim]Edit the file to add your API keys, then run:[/dim]")
    console.print("  emdash config show  [dim]# Verify configuration[/dim]")
    console.print("  emdash onboard      [dim]# Set up a repository[/dim]")


@config.command("path")
@click.option("--global", "is_global", is_flag=True, help="Show global config path")
def config_path(is_global: bool):
    """Show config file path.

    Example:
        emdash config path           # Show local .env path
        emdash config path --global  # Show ~/.config/emdash/config path
    """
    from emdash.core.config import get_user_config_path

    if is_global:
        path = get_user_config_path()
    else:
        path = Path.cwd() / ".env"

    console.print(str(path))


if __name__ == "__main__":
    cli()
