"""Interactive prompt with slash command autocomplete and spec approval menu."""

import os
import re
from typing import Callable, Optional, Union, List, Tuple
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from threading import Thread
import queue

from prompt_toolkit import PromptSession
from prompt_toolkit.application import Application
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.completion import Completer, Completion, ThreadedCompleter
from prompt_toolkit.formatted_text import HTML, FormattedText
from prompt_toolkit.filters import has_completions
from prompt_toolkit.history import InMemoryHistory
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.layout import Layout, HSplit, VSplit, Window, FormattedTextControl
from prompt_toolkit.layout.dimension import Dimension
from prompt_toolkit.styles import Style
from prompt_toolkit.widgets import Box, Frame, Label, RadioList, TextArea

from rich.console import Console
from rich.panel import Panel
from rich.text import Text

from emdash.agent.providers.base import ImageContent
from emdash.utils.image import (
    is_clipboard_image_available,
    read_clipboard_image,
    ClipboardImageError,
    get_image_info,
)
from emdash.graph.connection import get_connection
from emdash.utils.logger import log


# =============================================================================
# File Search Utility (Task 5)
# =============================================================================

@dataclass
class FileSearchResult:
    """Represents a file search result."""
    path: str  # Full path
    name: str  # Just the filename
    extension: str  # File extension (e.g., '.py')
    display_name: str  # Name for display (e.g., 'main.py')
    
    @classmethod
    def from_path(cls, full_path: str) -> "FileSearchResult":
        """Create from a full file path."""
        path = Path(full_path)
        return cls(
            path=full_path,
            name=path.name,
            extension=path.suffix.lower(),
            display_name=path.name,
        )


def search_files_in_graph(query: str, limit: int = 20) -> List[FileSearchResult]:
    """Search for files in the graph database.
    
    Args:
        query: Partial filename to search for (e.g., "main", "test")
        limit: Maximum number of results to return
    
    Returns:
        List of FileSearchResult objects matching the query
    """
    try:
        connection = get_connection()
        conn = connection.connect()
        
        # Search by filename pattern - use LIKE for partial matching
        # Note: Kuzu uses $ for parameters
        search_pattern = f"%{query}%"
        
        query_sql = """
            MATCH (f:File)
            WHERE f.name LIKE $pattern OR f.path LIKE $pattern
            RETURN f.path, f.name, f.extension
            ORDER BY f.commit_count DESC, f.commit_importance DESC
            LIMIT $limit
        """
        
        result = conn.execute(query_sql, {
            "pattern": search_pattern,
            "limit": limit,
        })
        
        results = []
        columns = result.get_column_names()
        while result.has_next():
            values = result.get_next()
            row = dict(zip(columns, values))
            results.append(FileSearchResult(
                path=row.get("f.path", ""),
                name=row.get("f.name", ""),
                extension=row.get("f.extension", ""),
                display_name=row.get("f.name", ""),
            ))
        
        return results
        
    except Exception as e:
        log.debug(f"Graph file search failed: {e}")
        return []


def search_files_in_filesystem(query: str, root: Path = None, limit: int = 20) -> List[FileSearchResult]:
    """Fallback search for files in the filesystem.
    
    Args:
        query: Partial filename to search for
        root: Root directory to search from (defaults to current working directory)
        limit: Maximum number of results
    
    Returns:
        List of FileSearchResult objects
    """
    if root is None:
        root = Path.cwd()
    
    results = []
    query_lower = query.lower()
    
    # Common code file extensions to include
    code_extensions = {'.py', '.ts', '.tsx', '.js', '.jsx', '.go', '.rs', '.java', 
                       '.c', '.cpp', '.h', '.hpp', '.rb', '.swift', '.kt', '.scala',
                       '.md', '.json', '.yaml', '.yml', '.toml', '.ini', '.cfg',
                       '.html', '.css', '.scss', '.vue', '.svelte'}
    
    try:
        # Walk directory and find matching files
        for root_dir, dirs, files in os.walk(root):
            # Skip hidden and common ignore directories
            dirs[:] = [d for d in dirs if not d.startswith('.') and d not in 
                      ('__pycache__', 'node_modules', 'target', 'build', '.venv', 'venv')]
            
            for filename in files:
                if query_lower in filename.lower():
                    full_path = os.path.join(root_dir, filename)
                    ext = Path(filename).suffix.lower()
                    
                    # Include all files or filter to common code files
                    results.append(FileSearchResult(
                        path=full_path,
                        name=filename,
                        extension=ext,
                        display_name=filename,
                    ))
                    
                    if len(results) >= limit:
                        return results
                        
    except Exception as e:
        log.debug(f"Filesystem search failed: {e}")
    
    return results


def search_files(query: str, limit: int = 20) -> List[FileSearchResult]:
    """Search for files, trying graph first, then filesystem fallback.
    
    Args:
        query: Partial filename to search for
        limit: Maximum number of results to return
    
    Returns:
        List of FileSearchResult objects
    """
    if not query or len(query) < 1:
        return []
    
    # Try graph first
    results = search_files_in_graph(query, limit)
    if results:
        return results
    
    # Fallback to filesystem
    return search_files_in_filesystem(query, limit=limit)


# =============================================================================
# Slash Commands (Existing)
# =============================================================================

# Slash commands with descriptions
SLASH_COMMANDS = {
    "/plan": "Switch to plan mode (explore codebase, create specs)",
    "/tasks": "Switch to tasks mode (generate task lists)",
    "/code": "Switch to code mode (execute file changes)",
    "/image": "Paste image from clipboard",
    "/mode": "Show current mode",
    "/spec": "Show current specification",
    "/reset": "Reset session state",
    "/save": "Save current spec to disk",
    "/mcp": "Manage MCP servers (list, add, remove, describe)",
    "/agents": "Switch to custom agent or list available agents",
    "/create-agent": "Create a new custom agent interactively",
    "/add-rule": "Add a rule based on conversation context",
    "/kanban": "Open Kanban board for task management",
    "/logout": "Log out from GitHub",
    "/kill": "Kill other emdash processes holding database lock",
    "/help": "Show available commands",
}


class SlashCommandCompleter(Completer):
    """Completer for slash commands."""

    def __init__(self, commands: dict[str, str] | None = None):
        self.commands = commands or SLASH_COMMANDS

    def get_completions(self, document, complete_event):
        text = document.text_before_cursor

        # Only complete if we're at the start or after whitespace, and text starts with /
        if not text.startswith("/"):
            return

        # Get the word being typed
        word = text

        # Find matching commands
        for cmd, description in self.commands.items():
            if cmd.startswith(word):
                # Calculate how much we've already typed
                yield Completion(
                    cmd,
                    start_position=-len(word),
                    display=HTML(f"<command>{cmd}</command>"),
                    display_meta=description,
                )


# =============================================================================
# File Mention Completer (Task 1)
# =============================================================================

# Style for @ mention completions
FILE_COMPLETION_STYLE = Style.from_dict({
    "completion-menu.completion.file": "bg:#1a1a2e #00ccff",
    "completion-menu.completion.file.current": "bg:#0088aa #ffffff bold",
    "completion-menu.meta.completion.file": "bg:#1a1a2e #ffcc00",
    "completion-menu.meta.completion.file.current": "bg:#aa8800 #ffffff",
})


class FileMentionCompleter(Completer):
    """Completer for @ file mentions.
    
    Shows file autocomplete from the project. When a file is selected:
    - User sees: @filename
    - LLM receives: the full relative path
    
    Example:
        User types: "Fix bug in @main"
        Dropdown shows: main.py, main.ts, ...
        After selection: "Fix bug in @main.py" (display) → full path sent to LLM
    """

    def __init__(
        self,
        search_func: callable = None,
        max_results: int = 15,
        show_paths: bool = True,
    ):
        """Initialize the file mention completer.
        
        Args:
            search_func: Function that takes (query, limit) and returns FileSearchResult[]
                         Defaults to using search_files()
            max_results: Maximum number of completions to show
            show_paths: Whether to show relative path as metadata
        """
        self.search_func = search_func or search_files
        self.max_results = max_results
        self.show_paths = show_paths

    def get_completions(self, document, complete_event):
        """Get file completions for @ mentions.
        
        Detects if cursor is after @ symbol and yields matching files.
        """
        text = document.text_before_cursor
        
        # Find if we're in an @ mention pattern
        # Pattern: @filename_prefix where cursor is at the end of prefix
        at_match = self._find_at_mention_position(text)
        if at_match is None:
            return
        
        prefix = at_match["prefix"]
        
        # Skip if prefix is empty (just typed @)
        if not prefix:
            return
        
        # Search for matching files
        try:
            results = self.search_func(prefix, limit=self.max_results)
        except Exception as e:
            log.debug(f"File search failed: {e}")
            return
        
        for result in results:
            # Display: just the filename
            display = result.display_name
            
            # Metadata: show path (relative to cwd or shortened path)
            if self.show_paths:
                # Try to make path relative to current working directory
                try:
                    cwd = Path.cwd()
                    if result.path.startswith(str(cwd)):
                        relative_path = Path(result.path).relative_to(cwd)
                        meta = f"  ({relative_path})"
                    else:
                        meta = f"  ({result.path})"
                except ValueError:
                    meta = f"  ({result.path})"
            else:
                meta = ""
            
            # Calculate start position to replace the @ prefix (including @)
            start_pos = at_match["start"]
            
            yield Completion(
                result.display_name,  # Text inserted (just filename)
                start_position=-(len(prefix) + 1),  # Include @ in replacement
                display=display,
                display_meta=meta,
            )

    def _find_at_mention_position(self, text: str) -> dict | None:
        """Find if cursor is in an @ mention pattern.
        
        Returns dict with:
            - start: position of @ in text
            - prefix: text after @ up to cursor
            Or None if not in @ mention.
        """
        cursor = len(text)
        
        # Check if we're in the middle of an @ mention
        # Pattern: ...@filename_prefix...
        
        # Find all @ positions
        at_positions = [i for i, c in enumerate(text) if c == '@']
        
        for at_pos in at_positions:
            # Get the word starting from this @
            after_at = text[at_pos + 1:cursor]
            
            # Check if this looks like a file mention (alphanumeric, dots, dashes, underscores)
            # Allow: letters, numbers, dots, dashes, underscores, slash, hyphens
            if after_at and re.match(r'^[\w.\-/]+$', after_at):
                return {
                    "start": at_pos,
                    "prefix": after_at,
                }
        
        return None


# =============================================================================
# File Mention Lexer (Input Highlighting)
# =============================================================================

class FileMentionLexer(Lexer):
    """Lexer that highlights @filename mentions in the input.

    Applies background highlighting to @mentions so they stand out visually.
    """

    def lex_document(self, document):
        """Return a function that returns styled text for a given line."""
        def get_line(lineno):
            line = document.lines[lineno]

            # Find all @mentions and apply styling
            result = []
            pos = 0

            # Pattern: @ followed by filename chars (alphanumeric, dots, dashes, underscores, slashes)
            for match in re.finditer(r'@[\w.\-/]+', line):
                # Add unstyled text before the match
                if match.start() > pos:
                    result.append(('', line[pos:match.start()]))

                # Add styled @mention with background color
                result.append(('class:mention', match.group()))
                pos = match.end()

            # Add remaining unstyled text
            if pos < len(line):
                result.append(('', line[pos:]))

            return result

        return get_line


# =============================================================================
# Combined Multi-Completer (Task 2)
# =============================================================================

class CombinedMentionCompleter(Completer):
    """Combined completer for slash commands and @ file mentions.
    
    Detects which type of completion to show based on cursor position:
    - Starts with / → Slash commands
    - Contains @ before cursor → File mentions
    """

    def __init__(
        self,
        slash_commands: dict[str, str] = None,
        file_completer: FileMentionCompleter = None,
    ):
        """Initialize combined completer.
        
        Args:
            slash_commands: Dict of slash commands → descriptions
            file_completer: FileMentionCompleter instance (creates default if None)
        """
        self.slash_commands = slash_commands or SLASH_COMMANDS
        self.file_completer = file_completer or FileMentionCompleter()
        self._slash_completer = SlashCommandCompleter(self.slash_commands)

    def get_completions(self, document, complete_event):
        """Get completions based on context."""
        text = document.text_before_cursor
        
        # Check if cursor is at start of line (or after whitespace) and starts with /
        stripped = text.lstrip()
        if stripped.startswith("/") and (len(text) == len(stripped) or 
                                           text[len(stripped)-1] in " \t"):
            # Use slash command completer
            yield from self._slash_completer.get_completions(document, complete_event)
            return
        
        # Check if cursor is after @
        at_pos = self._find_at_position(text)
        if at_pos is not None:
            # Use file mention completer
            yield from self.file_completer.get_completions(document, complete_event)
            return

    def _find_at_position(self, text: str) -> int | None:
        """Find the position of @ before cursor if we're in a mention.
        
        Returns the index of @ if cursor is in an @ mention pattern.
        """
        cursor = len(text)
        
        # Find @ characters
        for i, c in enumerate(text):
            if c == '@':
                # Check if text after @ looks like a file mention
                after_at = text[i+1:cursor]
                if after_at and re.match(r'^[\w.\-/]+$', after_at):
                    return i
        
        return None


# =============================================================================
# Input Processing for @ Mentions (Task 3)
# =============================================================================

@dataclass
class ProcessedInput:
    """Result of processing user input with @ mentions."""
    raw_text: str  # Original user input (what user typed)
    processed_text: str  # Text with @ mentions expanded to full paths for LLM
    mentions: list[dict]  # Metadata about each mention
    
    def has_mentions(self) -> bool:
        """Check if input contains any @ mentions."""
        return len(self.mentions) > 0
    
    def get_file_paths(self) -> list[str]:
        """Get list of all file paths used in mentions."""
        return [m["full_path"] for m in self.mentions]


def detect_at_mentions(text: str) -> list[dict]:
    """Detect @ mentions in text and return their positions and targets.
    
    Returns list of dicts with:
        - start: character position of @
        - end: character position after the filename
        - display_name: what user sees (filename only)
        - full_path: actual path sent to LLM
    """
    mentions = []
    cursor = 0
    
    while cursor < len(text):
        # Find next @
        at_pos = text.find('@', cursor)
        if at_pos == -1:
            break
        
        # Extract filename after @
        end = at_pos + 1
        while end < len(text) and (text[end].isalnum() or 
                                    text[end] in '._-/\\'):
            end += 1
        
        display_name = text[at_pos + 1:end]
        
        if display_name:  # Valid mention
            mentions.append({
                "start": at_pos,
                "end": end,
                "display_name": display_name,
                "full_path": display_name,  # Will be resolved later
            })
        
        cursor = end
    
    return mentions


def resolve_mentions_to_paths(
    text: str, 
    mentions: list[dict],
    search_func: callable = None
) -> str:
    """Resolve @ mentions in text to full paths.
    
    Args:
        text: Original text with @ display names
        mentions: List of mention metadata from detect_at_mentions()
        search_func: Function to search for files (defaults to search_files)
    
    Returns:
        Text with display names replaced by full paths
    """
    if search_func is None:
        search_func = search_files
    
    result = text
    
    # Process mentions in reverse order to preserve positions
    for mention in reversed(mentions):
        display_name = mention["display_name"]
        
        # Search for the file
        results = search_func(display_name, limit=1)
        
        if results:
            # Found a match - use full path
            full_path = results[0].path
            
            # Calculate relative path from current working directory
            try:
                cwd = Path.cwd()
                if full_path.startswith(str(cwd)):
                    relative = Path(full_path).relative_to(cwd)
                    replacement = str(relative)
                else:
                    replacement = full_path
            except ValueError:
                replacement = full_path
            
            # Replace @display_name with @replacement in text
            # Need to replace from @ to end of display name
            pattern = f"@{re.escape(display_name)}"
            result = re.sub(pattern, f"@{replacement}", result, count=1)
            
            mention["full_path"] = full_path
            mention["resolved_path"] = replacement
        else:
            # No match found - keep display name but mark as unresolved
            mention["full_path"] = display_name
            mention["resolved_path"] = display_name
    
    return result


def process_input_with_mentions(
    text: str,
    search_func: callable = None
) -> ProcessedInput:
    """Process user input, resolving @ mentions to full paths.
    
    Args:
        text: The user's input text
        search_func: Optional custom file search function
    
    Returns:
        ProcessedInput with raw text, processed text for LLM, and mention metadata
    """
    # Detect all @ mentions
    mentions = detect_at_mentions(text)
    
    # Resolve mentions to actual paths
    processed_text = resolve_mentions_to_paths(text, mentions, search_func)
    
    return ProcessedInput(
        raw_text=text,
        processed_text=processed_text,
        mentions=mentions,
    )


# =============================================================================
# Styles (Updated)
# =============================================================================

# Style for the prompt (updated with @ mention styles)
PROMPT_STYLE = Style.from_dict({
    # Prompt colors
    "prompt.mode.plan": "#ffcc00 bold",      # Yellow for plan
    "prompt.mode.tasks": "#cc66ff bold",     # Magenta for tasks
    "prompt.mode.code": "#00cc66 bold",      # Green for code

    # Input styling
    "prompt.prefix": "#888888",              # > symbol
    "prompt.hint": "#666666",                # ↵ send hint
    "prompt.tokens": "#888888",              # Token counter
    "prompt.tokens.input": "#00cc66",        # Input tokens (green)
    "prompt.tokens.output": "#cc66ff",       # Output tokens (purple)
    "prompt.separator": "#333333",           # Line separator

    # Completion menu
    "completion-menu": "bg:#1a1a2e #ffffff",
    "completion-menu.completion": "bg:#1a1a2e #ffffff",
    "completion-menu.completion.current": "bg:#4a4a6e #ffffff bold",
    "completion-menu.meta.completion": "bg:#1a1a2e #888888",
    "completion-menu.meta.completion.current": "bg:#4a4a6e #aaaaaa",

    # Scrollbar
    "scrollbar.background": "bg:#1a1a2e",
    "scrollbar.button": "bg:#4a4a6e",

    # Command highlighting
    "command": "#00ccff bold",
    
    # File mention styling (@ mentions)
    "mention.at": "#00ccff bold",           # @ symbol
    "mention.file": "#ffcc00",              # File name in completion
    "mention.file.current": "#ffffff bold", # Selected file
    "mention.path": "#888888",              # Path metadata
})


# Style for the prompt
PROMPT_STYLE = Style.from_dict({
    # Prompt colors
    "prompt.mode.plan": "#ffcc00 bold",      # Yellow for plan
    "prompt.mode.tasks": "#cc66ff bold",     # Magenta for tasks
    "prompt.mode.code": "#00cc66 bold",      # Green for code

    # Input styling
    "prompt.prefix": "#888888",              # > symbol
    "prompt.hint": "#666666",                # ↵ send hint
    "prompt.tokens": "#888888",              # Token counter
    "prompt.tokens.input": "#00cc66",        # Input tokens (green)
    "prompt.tokens.output": "#cc66ff",       # Output tokens (purple)
    "prompt.separator": "#333333",           # Line separator

    # Completion menu
    "completion-menu": "bg:#1a1a2e #ffffff",
    "completion-menu.completion": "bg:#1a1a2e #ffffff",
    "completion-menu.completion.current": "bg:#4a4a6e #ffffff bold",
    "completion-menu.meta.completion": "bg:#1a1a2e #888888",
    "completion-menu.meta.completion.current": "bg:#4a4a6e #aaaaaa",

    # Scrollbar
    "scrollbar.background": "bg:#1a1a2e",
    "scrollbar.button": "bg:#4a4a6e",

    # Command highlighting
    "command": "#00ccff bold",

    # @ mention highlighting in input (background color)
    "mention": "bg:#264f78 #ffffff",  # Blue background, white text
})


@dataclass
class PromptResult:
    """Result from an agent prompt session."""
    text: str
    images: list[ImageContent] = field(default_factory=list)


class AgentPrompt:
    """Interactive prompt for the coding agent with autocomplete and token counter."""

    def __init__(
        self,
        get_mode: Callable[[], str],
        commands: dict[str, str] | None = None,
        enable_image_paste: bool = True,
    ):
        """Initialize the prompt.

        Args:
            get_mode: Callable that returns current mode name
            commands: Optional custom commands dict
            enable_image_paste: Whether to enable image paste functionality
        """
        self.get_mode = get_mode
        self.commands = commands or SLASH_COMMANDS
        self.history = InMemoryHistory()
        self.enable_image_paste = enable_image_paste
        self.console = Console()

        # Token tracking
        self._input_tokens = 0
        self._output_tokens = 0

        # Pending images from clipboard paste
        self._pending_images: list[ImageContent] = []

        # Create combined completer (slash commands + @ file mentions)
        self.combined_completer = CombinedMentionCompleter(
            slash_commands=self.commands,
            file_completer=FileMentionCompleter(),
        )
        
        # Use threaded completer for async file searching
        self.completer = ThreadedCompleter(self.combined_completer)

        # Create key bindings
        self.bindings = self._create_bindings()

        # Create session with lexer for @ mention highlighting
        self.session: PromptSession = PromptSession(
            completer=self.completer,
            lexer=FileMentionLexer(),  # Highlight @mentions with background
            style=PROMPT_STYLE,
            history=self.history,
            key_bindings=self.bindings,
            complete_while_typing=True,
            complete_in_thread=True,
            rprompt=self._get_right_prompt,  # Right side prompt
        )

    def update_tokens(self, input_tokens: int, output_tokens: int):
        """Update token counters.

        Args:
            input_tokens: Number of input tokens used
            output_tokens: Number of output tokens generated
        """
        self._input_tokens = input_tokens
        self._output_tokens = output_tokens

    def add_tokens(self, input_tokens: int, output_tokens: int):
        """Add to token counters.

        Args:
            input_tokens: Input tokens to add
            output_tokens: Output tokens to add
        """
        self._input_tokens += input_tokens
        self._output_tokens += output_tokens

    def _get_right_prompt(self) -> list:
        """Get right-side prompt with send hint."""
        return [
            ("class:prompt.hint", "↵ send"),
        ]

    def _format_tokens(self, count: int) -> str:
        """Format token count with K suffix for thousands."""
        if count >= 1000:
            return f"{count / 1000:.1f}K"
        return str(count)

    def _create_bindings(self) -> KeyBindings:
        """Create custom key bindings."""
        bindings = KeyBindings()

        @bindings.add("c-c")
        def _(event):
            """Handle Ctrl+C."""
            event.app.exit(exception=KeyboardInterrupt())

        @bindings.add("c-d")
        def _(event):
            """Handle Ctrl+D (exit)."""
            event.app.exit(result="exit")

        @bindings.add("enter", filter=has_completions)
        def accept_completion(event):
            """Accept the current completion when Enter is pressed with menu open."""
            b = event.current_buffer
            if b.complete_state and b.complete_state.current_completion:
                b.apply_completion(b.complete_state.current_completion)
                b.complete_state = None
            else:
                # No completion selected, submit normally
                b.validate_and_handle()

        # Ctrl+V for paste (when supported by terminal)
        if self.enable_image_paste:
            @bindings.add("c-v")
            def paste_image(event):
                """Handle Ctrl+V for image paste."""
                try:
                    image_data = read_clipboard_image()
                    if image_data:
                        info = get_image_info(image_data)
                        self._pending_images.append(ImageContent(
                            image_data=image_data,
                            format="png",
                        ))
                        # Show feedback
                        self.console.print(
                            f"[green]✓[/green] [image_{len(self._pending_images)}] attached"
                        )
                    else:
                        self.console.print(
                            "[yellow]No image in clipboard[/yellow]"
                        )
                except ClipboardImageError as e:
                    self.console.print(
                        f"[red]Failed to read clipboard:[/red] {e}"
                    )

        return bindings

    def _get_prompt_text(self) -> list:
        """Get formatted prompt text - simple > prefix."""
        # Simple prompt prefix
        prompt_parts = [
            ("class:prompt.prefix", "> "),
        ]

        # Add image indicator if images are pending
        if self._pending_images:
            image_count = len(self._pending_images)
            prompt_parts.append(("class:prompt.tokens.input", f"📎{image_count} "))

        return prompt_parts

    def _show_token_bar(self):
        """Show token counter bar above input."""
        if self._input_tokens > 0 or self._output_tokens > 0:
            input_str = self._format_tokens(self._input_tokens)
            output_str = self._format_tokens(self._output_tokens)
            total = self._input_tokens + self._output_tokens
            total_str = self._format_tokens(total)

            # Get terminal width
            import shutil
            term_width = shutil.get_terminal_size().columns

            # Create token info string
            token_info = f"tokens: ↑{input_str} ↓{output_str} (Σ{total_str})"

            # Right-align and print with separator
            padding = term_width - len(token_info) - 1
            self.console.print(f"{'─' * padding}[dim]{token_info}[/dim]", highlight=False)

    def prompt(self, process_mentions: bool = True) -> PromptResult:
        """Show prompt and get user input.

        Args:
            process_mentions: If True, process @ mentions in input (resolve to paths)
                             If False, return raw input as-is

        Returns:
            PromptResult with text and any attached images

        Raises:
            KeyboardInterrupt: If user presses Ctrl+C
            EOFError: If user presses Ctrl+D
        """
        try:
            # Show token bar above input
            self._show_token_bar()

            result = self.session.prompt(
                self._get_prompt_text(),
            )

            # Clear pending images after getting input
            images = self._pending_images.copy()
            self._pending_images = []

            # Process @ mentions if requested
            if process_mentions and '@' in result:
                processed = process_input_with_mentions(result)
                result = processed.processed_text

            return PromptResult(
                text=result,
                images=images
            )
        except KeyboardInterrupt:
            raise
        except EOFError:
            return PromptResult(text="exit", images=[])

    def check_clipboard_for_image(self) -> Optional[ImageContent]:
        """Check clipboard and return image if available.

        Returns:
            ImageContent if clipboard has image, None otherwise
        """
        try:
            if is_clipboard_image_available():
                image_data = read_clipboard_image()
                if image_data:
                    return ImageContent(image_data=image_data, format="png")
        except ClipboardImageError:
            pass
        return None

    def read_clipboard_image(self) -> Optional[bytes]:
        """Read image from clipboard (raw bytes).

        Returns:
            Raw image bytes or None
        """
        try:
            if is_clipboard_image_available():
                return read_clipboard_image()
        except ClipboardImageError:
            pass
        return None

    def clear_images(self) -> None:
        """Clear pending images."""
        self._pending_images = []

    def get_pending_images(self) -> list[ImageContent]:
        """Get pending images and clear the list."""
        images = self._pending_images.copy()
        self._pending_images = []
        return images

    def prompt_with_metadata(self, text: str = None) -> tuple[ProcessedInput, PromptResult]:
        """Get input and processed metadata about @ mentions.
        
        Args:
            text: Optional pre-filled text for the prompt
            
        Returns:
            Tuple of (ProcessedInput, PromptResult)
            - ProcessedInput: Contains raw text, processed text for LLM, and mention metadata
            - PromptResult: Contains text (same as processed_text) and images
        """
        try:
            # Show token bar above input
            self._show_token_bar()

            user_text = text if text is not None else self.session.prompt(
                self._get_prompt_text(),
            )

            # Clear pending images
            images = self._pending_images.copy()
            self._pending_images = []

            # Process @ mentions
            processed = process_input_with_mentions(user_text)

            result = PromptResult(
                text=processed.processed_text,
                images=images
            )

            return processed, result

        except KeyboardInterrupt:
            raise
        except EOFError:
            return ProcessedInput(raw_text="exit", processed_text="exit", mentions=[]), PromptResult(text="exit", images=[])

    def add_command(self, name: str, description: str):
        """Add a custom command.

        Args:
            name: Command name (should start with /)
            description: Command description
        """
        self.commands[name] = description
        # Recreate completer with updated commands
        self.completer = SlashCommandCompleter(self.commands)
        self.session.completer = self.completer


def create_agent_prompt(
    get_mode: Callable[[], str],
    extra_commands: dict[str, str] | None = None,
    enable_image_paste: bool = True,
) -> AgentPrompt:
    """Create an agent prompt with autocomplete.

    Args:
        get_mode: Callable returning current mode name
        extra_commands: Additional commands to add
        enable_image_paste: Whether to enable image paste functionality

    Returns:
        Configured AgentPrompt instance
    """
    commands = SLASH_COMMANDS.copy()
    if extra_commands:
        commands.update(extra_commands)

    return AgentPrompt(get_mode=get_mode, commands=commands, enable_image_paste=enable_image_paste)


# =============================================================================
# Spec Approval Menu
# =============================================================================

class SpecApprovalChoice(Enum):
    """Choices for spec approval."""
    TASKS = "tasks"
    CODE = "code"
    REFINE = "refine"
    ABORT = "abort"


@dataclass
class SpecApprovalResult:
    """Result from spec approval menu."""
    choice: SpecApprovalChoice
    feedback: str = ""  # Only used when choice is REFINE


# Style for the approval menu
APPROVAL_STYLE = Style.from_dict({
    "dialog": "bg:#1a1a2e",
    "dialog.body": "bg:#1a1a2e #ffffff",
    "dialog frame.label": "#00ccff bold",

    # Radio list
    "radio-list": "bg:#1a1a2e",
    "radio": "#00cc66",
    "radio-selected": "bg:#4a4a6e #ffffff bold",

    # Labels
    "label": "#ffffff",
    "label.title": "#00ccff bold",
    "label.description": "#888888",

    # Selected item
    "selected": "bg:#4a4a6e #ffffff bold",
    "pointer": "#00ccff bold",

    # Keys
    "key": "#ffcc00 bold",
})


class SpecApprovalMenu:
    """Interactive menu for spec approval with tasks/code/refine/abort options."""

    MENU_OPTIONS = [
        (SpecApprovalChoice.TASKS, "tasks", "Generate implementation task breakdown"),
        (SpecApprovalChoice.CODE, "code", "Start coding directly"),
        (SpecApprovalChoice.REFINE, "refine", "Provide feedback to improve the spec"),
        (SpecApprovalChoice.ABORT, "abort", "Cancel and discard the spec"),
    ]

    def __init__(self):
        self.console = Console()

    def show(self) -> SpecApprovalResult:
        """Show the approval menu and get user selection.

        Returns:
            SpecApprovalResult with the user's choice
        """
        while True:
            choice = self._show_menu()

            # If refine was selected, get feedback
            if choice == SpecApprovalChoice.REFINE:
                feedback = self._get_feedback()
                # Empty feedback (ESC pressed) returns to menu
                if feedback is None:
                    continue
                return SpecApprovalResult(choice=choice, feedback=feedback)

            return SpecApprovalResult(choice=choice)

    def _show_menu(self) -> SpecApprovalChoice:
        """Display the menu using rich (preserves scrolling) and prompt for choice."""
        # Print menu with rich (allows scrolling up to see the spec)
        self.console.print()
        self.console.print("[bold cyan]What would you like to do next?[/bold cyan]")
        self.console.print()
        self.console.print("  [yellow][1][/yellow] [bold]tasks[/bold]      Generate implementation task breakdown")
        self.console.print("  [yellow][2][/yellow] [bold]code[/bold]       Start coding directly")
        self.console.print("  [yellow][3][/yellow] [bold]refine[/bold]     Provide feedback to improve the spec")
        self.console.print("  [yellow][4][/yellow] [bold]abort[/bold]      Cancel and discard the spec")
        self.console.print()
        self.console.print("[dim]Enter 1-4, or type: tasks, code, refine, abort[/dim]")
        self.console.print()

        # Simple prompt - allows normal terminal scrolling
        session = PromptSession()
        try:
            response = session.prompt(
                [("class:prompt.mode.plan", "choice"), ("", " > ")],
                style=PROMPT_STYLE,
            ).strip().lower()

            # Map response to choice
            choice_map = {
                "1": SpecApprovalChoice.TASKS,
                "tasks": SpecApprovalChoice.TASKS,
                "t": SpecApprovalChoice.TASKS,
                "2": SpecApprovalChoice.CODE,
                "code": SpecApprovalChoice.CODE,
                "c": SpecApprovalChoice.CODE,
                "3": SpecApprovalChoice.REFINE,
                "refine": SpecApprovalChoice.REFINE,
                "r": SpecApprovalChoice.REFINE,
                "4": SpecApprovalChoice.ABORT,
                "abort": SpecApprovalChoice.ABORT,
                "a": SpecApprovalChoice.ABORT,
                "q": SpecApprovalChoice.ABORT,
                "": SpecApprovalChoice.ABORT,
            }

            return choice_map.get(response, SpecApprovalChoice.ABORT)

        except (KeyboardInterrupt, EOFError):
            return SpecApprovalChoice.ABORT

    def _get_feedback(self) -> Optional[str]:
        """Get refinement feedback from user.

        Returns:
            Feedback string, or None if ESC was pressed (go back to menu)
        """
        self.console.print()
        self.console.print("[bold cyan]Refinement Feedback[/bold cyan]")
        self.console.print("[dim]What changes would you like? (Esc to go back, Enter to submit)[/dim]")
        self.console.print()

        # Create key bindings with ESC support
        bindings = KeyBindings()

        @bindings.add("escape")
        def go_back(event):
            """ESC returns to menu."""
            event.app.exit(result=None)

        @bindings.add("c-c")
        def cancel(event):
            """Ctrl+C returns to menu."""
            event.app.exit(result=None)

        session = PromptSession(key_bindings=bindings)
        try:
            feedback = session.prompt(
                [("class:prompt.mode.plan", "feedback"), ("", " > ")],
                style=PROMPT_STYLE,
            )
            return feedback.strip() if feedback else None
        except (KeyboardInterrupt, EOFError):
            return None


def show_spec_approval_menu() -> SpecApprovalResult:
    """Show the spec approval menu.

    Returns:
        SpecApprovalResult with user's choice and optional feedback
    """
    menu = SpecApprovalMenu()
    return menu.show()


# =============================================================================
# Tasks Approval Menu (after task generation)
# =============================================================================

class TasksApprovalChoice(Enum):
    """Choices for tasks approval."""
    IMPLEMENT = "implement"
    REFINE = "refine"
    PLAN = "plan"
    ABORT = "abort"


@dataclass
class TasksApprovalResult:
    """Result from tasks approval menu."""
    choice: TasksApprovalChoice
    feedback: str = ""


class TasksApprovalMenu:
    """Interactive menu shown after task generation."""

    def __init__(self):
        self.console = Console()

    def show(self) -> TasksApprovalResult:
        """Show the tasks approval menu."""
        while True:
            choice = self._show_menu()

            if choice == TasksApprovalChoice.REFINE:
                feedback = self._get_feedback()
                if feedback is None:
                    continue
                return TasksApprovalResult(choice=choice, feedback=feedback)

            return TasksApprovalResult(choice=choice)

    def _show_menu(self) -> TasksApprovalChoice:
        """Display the menu and prompt for choice."""
        self.console.print()
        self.console.print("[bold cyan]What would you like to do next?[/bold cyan]")
        self.console.print()
        self.console.print("  [yellow][1][/yellow] [bold]implement[/bold]  Start coding (switch to code mode)")
        self.console.print("  [yellow][2][/yellow] [bold]refine[/bold]     Modify the task breakdown")
        self.console.print("  [yellow][3][/yellow] [bold]plan[/bold]       Go back to planning")
        self.console.print("  [yellow][4][/yellow] [bold]abort[/bold]      Cancel")
        self.console.print()
        self.console.print("[dim]Enter 1-4, or type: implement, refine, plan, abort[/dim]")
        self.console.print()

        session = PromptSession()
        try:
            response = session.prompt(
                [("class:prompt.mode.tasks", "choice"), ("", " > ")],
                style=PROMPT_STYLE,
            ).strip().lower()

            choice_map = {
                "1": TasksApprovalChoice.IMPLEMENT,
                "implement": TasksApprovalChoice.IMPLEMENT,
                "i": TasksApprovalChoice.IMPLEMENT,
                "code": TasksApprovalChoice.IMPLEMENT,
                "c": TasksApprovalChoice.IMPLEMENT,
                "2": TasksApprovalChoice.REFINE,
                "refine": TasksApprovalChoice.REFINE,
                "r": TasksApprovalChoice.REFINE,
                "3": TasksApprovalChoice.PLAN,
                "plan": TasksApprovalChoice.PLAN,
                "p": TasksApprovalChoice.PLAN,
                "4": TasksApprovalChoice.ABORT,
                "abort": TasksApprovalChoice.ABORT,
                "a": TasksApprovalChoice.ABORT,
                "q": TasksApprovalChoice.ABORT,
                "": TasksApprovalChoice.IMPLEMENT,  # Default to implement
            }

            return choice_map.get(response, TasksApprovalChoice.ABORT)

        except (KeyboardInterrupt, EOFError):
            return TasksApprovalChoice.ABORT

    def _get_feedback(self) -> Optional[str]:
        """Get refinement feedback."""
        self.console.print()
        self.console.print("[bold cyan]Task Refinement[/bold cyan]")
        self.console.print("[dim]What changes would you like? (Esc to go back)[/dim]")
        self.console.print()

        bindings = KeyBindings()

        @bindings.add("escape")
        def go_back(event):
            event.app.exit(result=None)

        @bindings.add("c-c")
        def cancel(event):
            event.app.exit(result=None)

        session = PromptSession(key_bindings=bindings)
        try:
            feedback = session.prompt(
                [("class:prompt.mode.tasks", "feedback"), ("", " > ")],
                style=PROMPT_STYLE,
            )
            return feedback.strip() if feedback else None
        except (KeyboardInterrupt, EOFError):
            return None


def show_tasks_approval_menu() -> TasksApprovalResult:
    """Show the tasks approval menu.

    Returns:
        TasksApprovalResult with user's choice and optional feedback
    """
    menu = TasksApprovalMenu()
    return menu.show()
