"""Promethian CLI - Interactive AI agent interface."""

from __future__ import annotations

import asyncio
import json
import os
import subprocess
import sys
import tty
import termios
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Callable, Any

from rich.console import Console, Group
from rich.panel import Panel
from rich.markdown import Markdown
from rich.table import Table
from rich.prompt import Prompt, Confirm
from rich.live import Live
from rich.spinner import Spinner
from rich.text import Text
from rich.tree import Tree
from rich.rule import Rule

from promethian.config import PromethianConfig, set_config
from promethian.core.progress import ProgressEvent, ProgressStage
from promethian.core.conversation_manager import ConversationManager


def check_for_upgrade() -> tuple[str | None, str | None]:
    """
    Check PyPI for a newer version of promethian.

    Returns:
        Tuple of (latest_version, current_version) if upgrade available, (None, None) otherwise.
    """
    from promethian import __version__
    import urllib.request
    import urllib.error

    try:
        # Quick timeout to avoid blocking CLI startup
        with urllib.request.urlopen(
            "https://pypi.org/pypi/promethian/json",
            timeout=2
        ) as response:
            data = json.loads(response.read().decode())
            latest_version = data["info"]["version"]

            # Compare versions (simple string comparison works for semver)
            if latest_version != __version__:
                # Parse version numbers for proper comparison
                def parse_version(v: str) -> tuple:
                    return tuple(int(x) for x in v.split("."))

                try:
                    if parse_version(latest_version) > parse_version(__version__):
                        return latest_version, __version__
                except (ValueError, TypeError):
                    # Fallback to string comparison
                    if latest_version > __version__:
                        return latest_version, __version__

    except (urllib.error.URLError, json.JSONDecodeError, KeyError, TimeoutError):
        # Network error or PyPI unavailable - silently continue
        pass

    return None, None


def prompt_and_upgrade(latest_version: str, current_version: str) -> bool:
    """
    Prompt user to upgrade and perform the upgrade if confirmed.

    Returns:
        True if upgrade was performed, False otherwise.
    """
    console.print()
    console.print(Panel(
        f"[yellow]A new version of Promethian is available![/yellow]\n\n"
        f"Current: [dim]{current_version}[/dim]\n"
        f"Latest:  [green]{latest_version}[/green]\n\n"
        f"[dim]Run 'pip install --upgrade promethian' to update[/dim]",
        title="Upgrade Available",
        border_style="yellow"
    ))

    if Confirm.ask("[cyan]Upgrade now?[/cyan]", default=True):
        console.print()
        console.print("[dim]Upgrading...[/dim]")

        try:
            result = subprocess.run(
                [sys.executable, "-m", "pip", "install", "--upgrade", "promethian"],
                capture_output=True,
                text=True,
                timeout=60,
            )

            if result.returncode == 0:
                console.print("[green]Upgrade successful![/green]")
                console.print("[yellow]Please restart promethian to use the new version.[/yellow]")
                return True
            else:
                console.print(f"[red]Upgrade failed:[/red] {result.stderr}")

        except subprocess.TimeoutExpired:
            console.print("[red]Upgrade timed out. Please run manually:[/red]")
            console.print("[dim]pip install --upgrade promethian[/dim]")
        except Exception as e:
            console.print(f"[red]Upgrade error:[/red] {e}")

    console.print()
    return False


def getch() -> str:
    """Get a single character from stdin without waiting for enter."""
    # Check if stdin is a TTY
    if not sys.stdin.isatty():
        return '\n'  # Return newline to skip interactive input

    fd = sys.stdin.fileno()
    try:
        old_settings = termios.tcgetattr(fd)
    except termios.error:
        return '\n'  # Not a TTY - skip

    try:
        tty.setraw(sys.stdin.fileno())
        ch = sys.stdin.read(1)
        # Handle escape sequences (arrow keys)
        if ch == '\x1b':
            ch2 = sys.stdin.read(1)
            ch3 = sys.stdin.read(1)
            if ch2 == '[':
                if ch3 == 'A':
                    return 'UP'
                elif ch3 == 'B':
                    return 'DOWN'
                elif ch3 == 'C':
                    return 'RIGHT'
                elif ch3 == 'D':
                    return 'LEFT'
            return 'ESC'
        return ch
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)


class FeedbackCollector:
    """
    Collects user feedback asynchronously during task execution.

    Users can type feedback at any time. After the current step completes,
    the agent checks for pending feedback and incorporates it into replanning.
    """

    def __init__(self):
        self._feedback_queue: list[str] = []
        self._current_input: str = ""
        self._running = False
        self._thread: threading.Thread | None = None
        self._lock = threading.Lock()

    def start(self) -> None:
        """Start collecting feedback in background thread."""
        import threading
        import select

        self._running = True
        self._feedback_queue = []
        self._current_input = ""

        def collect_input():
            fd = sys.stdin.fileno()

            # Check if stdin is a TTY - if not, skip input collection
            if not sys.stdin.isatty():
                return

            try:
                old_settings = termios.tcgetattr(fd)
            except termios.error:
                # Not a TTY or terminal not available - skip input collection
                return

            try:
                # Set terminal to non-canonical mode (character by character)
                tty.setcbreak(fd)

                while self._running:
                    # Check if input is available (non-blocking)
                    if select.select([sys.stdin], [], [], 0.1)[0]:
                        ch = sys.stdin.read(1)

                        if ch == '\n' or ch == '\r':
                            # Enter pressed - submit feedback
                            with self._lock:
                                if self._current_input.strip():
                                    self._feedback_queue.append(self._current_input.strip())
                                    # Print confirmation
                                    sys.stdout.write(f"\r\033[K[dim]Feedback queued: {self._current_input[:50]}{'...' if len(self._current_input) > 50 else ''}[/dim]\n")
                                    sys.stdout.flush()
                                self._current_input = ""
                        elif ch == '\x7f' or ch == '\x08':
                            # Backspace
                            with self._lock:
                                if self._current_input:
                                    self._current_input = self._current_input[:-1]
                                    sys.stdout.write('\b \b')
                                    sys.stdout.flush()
                        elif ch == '\x03':
                            # Ctrl+C - stop
                            self._running = False
                            break
                        elif ch.isprintable():
                            # Regular character
                            with self._lock:
                                self._current_input += ch
                                sys.stdout.write(ch)
                                sys.stdout.flush()

            except Exception:
                pass  # Silently handle any terminal errors
            finally:
                try:
                    termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
                except Exception:
                    pass

        self._thread = threading.Thread(target=collect_input, daemon=True)
        self._thread.start()

    def stop(self) -> None:
        """Stop collecting feedback."""
        self._running = False
        if self._thread:
            self._thread.join(timeout=0.5)
            self._thread = None

    def get_pending_feedback(self) -> list[str]:
        """Get and clear any pending feedback."""
        with self._lock:
            feedback = self._feedback_queue.copy()
            self._feedback_queue.clear()
            return feedback

    def has_feedback(self) -> bool:
        """Check if there's pending feedback."""
        with self._lock:
            return len(self._feedback_queue) > 0


# Import threading for FeedbackCollector
import threading


@dataclass
class SessionStats:
    """Track cumulative session statistics."""
    total_tokens: int = 0
    total_cost: float = 0.0
    tasks_completed: int = 0

    def update(self, usage: dict | None) -> None:
        """Update stats from task usage."""
        if usage:
            self.total_tokens += usage.get("total_input_tokens", 0) + usage.get("total_output_tokens", 0)
            self.total_cost += usage.get("estimated_cost_usd", 0.0)
        self.tasks_completed += 1

    def format_compact(self) -> str:
        """Format stats for status line."""
        if self.total_tokens == 0:
            return ""
        return f"[dim]{self.total_tokens:,} tokens · ${self.total_cost:.4f}[/dim]"


@dataclass
class SettingOption:
    """A setting option for the interactive editor."""
    key: str
    label: str
    current_value: Any
    options: list[tuple[Any, str]] | None = None  # (value, display) pairs for select
    value_type: str = "select"  # "select", "number", "boolean"
    min_value: float | None = None
    max_value: float | None = None
    description: str = ""


console = Console()


class InteractiveSettings:
    """Interactive settings editor with arrow key navigation."""

    def __init__(self, config: PromethianConfig):
        self.config = config
        self.settings = self._build_settings()
        self.selected_idx = 0
        self.editing = False
        self.edit_value = ""
        self.edit_option_idx = 0

    def _build_settings(self) -> list[SettingOption]:
        """Build the list of editable settings."""
        model_options = [
            ("claude-opus-4-5-20251101", "Opus 4.5 (most capable)"),
            ("claude-sonnet-4-5-20250514", "Sonnet 4.5 (balanced)"),
            ("claude-haiku-4-5-20251001", "Haiku 4.5 (fastest)"),
        ]

        sub_agent_options = [
            (None, "Same as main model"),
            ("claude-opus-4-5-20251101", "Opus 4.5 (most capable)"),
            ("claude-sonnet-4-5-20250514", "Sonnet 4.5 (balanced)"),
            ("claude-haiku-4-5-20251001", "Haiku 4.5 (fastest)"),
        ]

        return [
            SettingOption(
                key="model",
                label="Model",
                current_value=self.config.model,
                options=model_options,
                value_type="select",
                description="Main model for planning and orchestration",
            ),
            SettingOption(
                key="sub_agent_model",
                label="Sub-Agent Model",
                current_value=self.config.sub_agent_model,
                options=sub_agent_options,
                value_type="select",
                description="Model for step execution (can be faster/cheaper)",
            ),
            SettingOption(
                key="max_tool_iterations",
                label="Max Tool Iterations",
                current_value=self.config.max_tool_iterations,
                value_type="number",
                min_value=1,
                max_value=50,
                description="Max tool calls per step (controls API costs)",
            ),
            SettingOption(
                key="max_self_heal_attempts",
                label="Max Self-Heal Attempts",
                current_value=self.config.max_self_heal_attempts,
                value_type="number",
                min_value=1,
                max_value=10,
                description="Max retries on failure before giving up",
            ),
            SettingOption(
                key="confidence_threshold",
                label="Confidence Threshold",
                current_value=self.config.confidence_threshold,
                value_type="number",
                min_value=0.0,
                max_value=1.0,
                description="Minimum confidence for direct execution",
            ),
            SettingOption(
                key="track_usage",
                label="Track Usage",
                current_value=self.config.track_usage,
                options=[(True, "Yes"), (False, "No")],
                value_type="boolean",
                description="Show token/cost statistics after tasks",
            ),
            SettingOption(
                key="share_logs",
                label="Share Session Logs",
                current_value=self.config.share_logs,
                options=[
                    ("none", "None"),
                    ("failures", "Failures Only"),
                    ("all", "All Sessions"),
                ],
                value_type="select",
                description="Help improve Promethian by sharing session data",
            ),
        ]

    def _get_current_option_idx(self, setting: SettingOption) -> int:
        """Get the index of the current value in options."""
        if setting.options:
            for i, (val, _) in enumerate(setting.options):
                if val == setting.current_value:
                    return i
        return 0

    def _render(self, changes: dict) -> str:
        """Render the settings display."""
        lines = []
        lines.append("[bold]Settings[/bold]")
        lines.append("[dim]↑↓ navigate  ←→ change value  S save & exit  Q quit without saving[/dim]")
        if changes:
            lines.append(f"[yellow]({len(changes)} unsaved change{'s' if len(changes) > 1 else ''})[/yellow]")
        lines.append("")

        for i, setting in enumerate(self.settings):
            is_selected = i == self.selected_idx

            # Build the value display
            if setting.value_type == "select" or setting.value_type == "boolean":
                if self.editing and is_selected:
                    # Show all options with current selection highlighted
                    option_parts = []
                    for j, (val, display) in enumerate(setting.options or []):
                        if j == self.edit_option_idx:
                            option_parts.append(f"[reverse]{display}[/reverse]")
                        else:
                            option_parts.append(f"[dim]{display}[/dim]")
                    value_display = " | ".join(option_parts)
                else:
                    # Show current value
                    current_display = setting.current_value
                    if setting.options:
                        for val, display in setting.options:
                            if val == setting.current_value:
                                current_display = display
                                break
                    value_display = str(current_display)
            else:  # number
                if self.editing and is_selected:
                    value_display = f"[reverse]{self.edit_value}_[/reverse]"
                else:
                    value_display = str(setting.current_value)

            # Build the line
            if is_selected:
                prefix = "[cyan]>[/cyan]"
                label_style = "[cyan]"
            else:
                prefix = " "
                label_style = "[dim]"

            line = f"{prefix} {label_style}{setting.label}:[/{label_style.strip('[]')}] {value_display}"
            lines.append(line)

            # Show description for selected item
            if is_selected and setting.description:
                lines.append(f"   [dim italic]{setting.description}[/dim italic]")

        return "\n".join(lines)

    def _validate_number(self, value_str: str, setting: SettingOption) -> tuple[bool, Any]:
        """Validate a number input."""
        try:
            if "." in value_str:
                value = float(value_str)
            else:
                value = int(value_str)

            if setting.min_value is not None and value < setting.min_value:
                return False, f"Value must be >= {setting.min_value}"
            if setting.max_value is not None and value > setting.max_value:
                return False, f"Value must be <= {setting.max_value}"

            return True, value
        except ValueError:
            return False, "Invalid number"

    def run(self) -> dict[str, Any] | None:
        """Run the interactive settings editor. Returns changed settings or None if cancelled."""
        changes = {}

        while True:
            # Clear and render
            console.clear()
            console.print(self._render(changes))

            # Get input
            key = getch()

            if key == 'ESC' or key == 'q' or key == 'Q':
                if changes:
                    console.print("\n[yellow]Discarding unsaved changes...[/yellow]")
                    import time
                    time.sleep(0.5)
                return None

            elif key == '\r' or key == '\n':  # Enter
                if self.editing:
                    # Confirm edit
                    setting = self.settings[self.selected_idx]
                    if setting.value_type in ("select", "boolean"):
                        new_value = setting.options[self.edit_option_idx][0]
                        if new_value != setting.current_value:
                            setting.current_value = new_value
                            changes[setting.key] = new_value
                    else:  # number
                        valid, result = self._validate_number(self.edit_value, setting)
                        if valid:
                            if result != setting.current_value:
                                setting.current_value = result
                                changes[setting.key] = result
                        else:
                            console.print(f"[red]Invalid: {result}[/red]")
                            getch()  # Wait for any key
                            continue
                    self.editing = False
                else:
                    # Start editing or save and exit
                    setting = self.settings[self.selected_idx]
                    if setting.value_type in ("select", "boolean"):
                        self.editing = True
                        self.edit_option_idx = self._get_current_option_idx(setting)
                    else:
                        self.editing = True
                        self.edit_value = str(setting.current_value)

            elif key == 'UP':
                if not self.editing:
                    self.selected_idx = (self.selected_idx - 1) % len(self.settings)

            elif key == 'DOWN':
                if not self.editing:
                    self.selected_idx = (self.selected_idx + 1) % len(self.settings)

            elif key == 'LEFT':
                setting = self.settings[self.selected_idx]
                if setting.value_type in ("select", "boolean"):
                    if not self.editing:
                        self.editing = True
                        self.edit_option_idx = self._get_current_option_idx(setting)
                    self.edit_option_idx = (self.edit_option_idx - 1) % len(setting.options)
                    # Auto-confirm for select/boolean
                    new_value = setting.options[self.edit_option_idx][0]
                    if new_value != setting.current_value:
                        setting.current_value = new_value
                        changes[setting.key] = new_value
                    self.editing = False

            elif key == 'RIGHT':
                setting = self.settings[self.selected_idx]
                if setting.value_type in ("select", "boolean"):
                    if not self.editing:
                        self.editing = True
                        self.edit_option_idx = self._get_current_option_idx(setting)
                    self.edit_option_idx = (self.edit_option_idx + 1) % len(setting.options)
                    # Auto-confirm for select/boolean
                    new_value = setting.options[self.edit_option_idx][0]
                    if new_value != setting.current_value:
                        setting.current_value = new_value
                        changes[setting.key] = new_value
                    self.editing = False

            elif key == '\x7f' or key == '\x08':  # Backspace
                if self.editing and self.settings[self.selected_idx].value_type == "number":
                    self.edit_value = self.edit_value[:-1]

            elif key == 's' or key == 'S':  # Save and exit
                return changes if changes else None

            elif key.isprintable() and self.editing:
                setting = self.settings[self.selected_idx]
                if setting.value_type == "number":
                    # Only allow digits and decimal point
                    if key.isdigit() or (key == '.' and '.' not in self.edit_value):
                        self.edit_value += key

        return changes


def run_interactive_settings(config: PromethianConfig) -> tuple[PromethianConfig, bool]:
    """Run the interactive settings editor and return updated config."""
    editor = InteractiveSettings(config)
    changes = editor.run()

    if not changes:
        console.clear()
        console.print("[dim]Settings unchanged[/dim]")
        return config, False

    # Apply changes to config
    config_data = load_config() or {}

    new_config_kwargs = {
        "anthropic_api_key": config.anthropic_api_key,
        "promethian_api_key": config.promethian_api_key,
        "promethian_api_url": config.promethian_api_url,
        "model": config.model,
        "sub_agent_model": config.sub_agent_model,
        "max_tool_iterations": config.max_tool_iterations,
        "track_usage": config.track_usage,
        "confidence_threshold": config.confidence_threshold,
        "max_self_heal_attempts": config.max_self_heal_attempts,
        "share_logs": config.share_logs,
    }

    for key, value in changes.items():
        config_data[key] = value
        new_config_kwargs[key] = value

    save_config(config_data)
    new_config = PromethianConfig(**new_config_kwargs)
    set_config(new_config)

    console.clear()
    console.print("[green]Settings saved![/green]")
    console.print()

    # Show what changed
    for key, value in changes.items():
        if key in ("model", "sub_agent_model"):
            display_value = format_model_name(value) if value else "[dim]same as model[/dim]"
        else:
            display_value = str(value)
        console.print(f"  [cyan]{key}[/cyan]: {display_value}")

    return new_config, True

# Config file location
CONFIG_DIR = Path.home() / ".promethian"
CONFIG_FILE = CONFIG_DIR / "config.json"
CONVERSATION_FILE = CONFIG_DIR / "conversation_history.json"


def load_config() -> dict | None:
    """Load config from file."""
    if CONFIG_FILE.exists():
        try:
            return json.loads(CONFIG_FILE.read_text())
        except Exception:
            return None
    return None


def save_config(config_data: dict) -> None:
    """Save config to file."""
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    CONFIG_FILE.write_text(json.dumps(config_data, indent=2))
    # Set restrictive permissions (owner only)
    CONFIG_FILE.chmod(0o600)


def get_existing_token() -> str | None:
    """Get existing token from config if present."""
    config_data = load_config() or {}
    return config_data.get("promethian_auth_token")


def run_setup() -> dict:
    """Run first-time setup wizard."""
    import secrets

    console.print()
    console.print(Panel(
        "[bold cyan]Welcome to Promethian![/bold cyan]\n\n"
        "You'll need an Anthropic API key - [link]https://console.anthropic.com[/link]\n\n"
        "[dim]Your config will be stored in ~/.promethian/config.json[/dim]",
        title="First-Time Setup",
        border_style="cyan"
    ))
    console.print()

    api_url = "https://promethian-api.fly.dev"

    # Anthropic API key (required)
    anthropic_key = None
    while not anthropic_key:
        anthropic_key = Prompt.ask(
            "[bold]Anthropic API key[/bold]",
            password=True,
        )
        if not anthropic_key:
            console.print("[red]Anthropic API key is required[/red]")
        elif not anthropic_key.startswith("sk-ant-"):
            if not Confirm.ask("[yellow]Key doesn't look like an Anthropic key. Use anyway?[/yellow]"):
                anthropic_key = None

    # Generate device token (no email required)
    promethian_token = f"pmt_{secrets.token_urlsafe(32)}"

    config_data = {
        "anthropic_api_key": anthropic_key,
        "promethian_auth_token": promethian_token,
        "promethian_api_url": api_url,
        "model": "claude-opus-4-5-20251101",
    }

    save_config(config_data)

    console.print()
    console.print("[green]Setup complete![/green]")
    console.print("[dim]Rate limit: 50 queries/hour (free tier)[/dim]")
    console.print()

    return config_data


def get_config_or_setup() -> PromethianConfig:
    """Get config from file or run setup if needed."""
    import secrets

    config_data = load_config() or {}

    # Check env vars first, then config file
    anthropic_key = os.environ.get("ANTHROPIC_API_KEY") or config_data.get("anthropic_api_key")
    auth_token = os.environ.get("PROMETHIAN_AUTH_TOKEN") or config_data.get("promethian_auth_token")

    # Auto-generate token if API key is set but token isn't
    if anthropic_key and not auth_token:
        auth_token = f"pmt_{secrets.token_urlsafe(32)}"
        # Save to config for future use
        config_data["promethian_auth_token"] = auth_token
        if "anthropic_api_key" not in config_data:
            config_data["anthropic_api_key"] = anthropic_key
        save_config(config_data)

    # Run setup if missing API key (interactive prompt needed)
    if not anthropic_key:
        config_data = run_setup()
        anthropic_key = config_data.get("anthropic_api_key")
        auth_token = config_data.get("promethian_auth_token")

    return PromethianConfig(
        anthropic_api_key=anthropic_key,
        promethian_api_key=auth_token,
        promethian_api_url=os.environ.get("PROMETHIAN_API_URL") or config_data.get("promethian_api_url", "https://promethian-api.fly.dev"),
        model=config_data.get("model", "claude-opus-4-5-20251101"),
        sub_agent_model=config_data.get("sub_agent_model"),
        max_tool_iterations=config_data.get("max_tool_iterations", 10),
        max_self_heal_attempts=config_data.get("max_self_heal_attempts", 3),
        confidence_threshold=config_data.get("confidence_threshold", 0.7),
        track_usage=config_data.get("track_usage", True),
        verbose_logging=config_data.get("verbose_logging", False),
        share_logs=config_data.get("share_logs", "failures"),
    )


def print_welcome():
    """Print welcome message."""
    from promethian import __version__
    console.print()
    console.print(Panel(
        f"[bold]Promethian[/bold] [dim]v{__version__}[/dim] - Self-improving AI Agent\n\n"
        "[dim]Type a task to run it, or use these commands:[/dim]\n\n"
        "  [cyan]/help[/cyan]     Show all commands\n"
        "  [cyan]/search[/cyan]   Search knowledge base\n"
        "  [cyan]/config[/cyan]   Show configuration\n"
        "  [cyan]/quit[/cyan]     Exit Promethian",
        border_style="blue"
    ))
    console.print()


def print_input_prompt(stats: SessionStats | None = None) -> str:
    """Print styled input prompt with horizontal rules and stats. Returns user input."""
    # Stats line (right-aligned)
    stats_text = stats.format_compact() if stats else ""

    # Print separator with stats
    console.print()
    if stats_text:
        # Print rule with stats on the right
        console.print(Rule(style="dim"))
        console.print(stats_text, justify="right")
    else:
        console.print(Rule(style="dim"))

    # Get input with styled prompt
    user_input = console.input("[bold cyan]>[/bold cyan] ").strip()

    # Print bottom rule
    console.print(Rule(style="dim"))

    return user_input


def print_help():
    """Print help message."""
    console.print(Panel(
        "[bold]Commands[/bold]\n\n"
        "  [cyan]/help[/cyan]              Show this help\n"
        "  [cyan]/search <query>[/cyan]    Search skills and tools\n"
        "  [cyan]/config[/cyan]            Show current configuration\n"
        "  [cyan]/settings[/cyan]          Interactive settings editor\n"
        "  [cyan]/setup[/cyan]             Re-run API key setup\n"
        "  [cyan]/history[/cyan]           Show conversation history\n"
        "  [cyan]/cache[/cyan]             Show tool cache stats\n"
        "  [cyan]/clear[/cyan]             Clear history, cache, and screen\n"
        "  [cyan]/feedback <text>[/cyan]   Send feedback to improve Promethian\n"
        "  [cyan]/quit[/cyan]              Exit Promethian\n\n"
        "[bold]Shortcuts[/bold]\n\n"
        "  [cyan]Ctrl+C[/cyan]             Cancel current task\n"
        "  [cyan]Ctrl+D[/cyan]             Exit Promethian\n\n"
        "[dim]Any other input is treated as a task for the agent to run.[/dim]",
        title="Help",
        border_style="cyan"
    ))


# Valid model options
VALID_MODELS = {
    "claude-opus-4-5-20251101": "Opus 4.5 (most capable)",
    "claude-sonnet-4-5-20250514": "Sonnet 4.5 (balanced)",
    "claude-haiku-4-5-20251001": "Haiku 4.5 (fastest)",
}


def format_model_name(model: str) -> str:
    """Format model name for display."""
    if model == "claude-opus-4-5-20251101":
        return "[bold magenta]Opus 4.5[/bold magenta]"
    elif model == "claude-sonnet-4-5-20250514":
        return "[bold blue]Sonnet 4.5[/bold blue]"
    elif model == "claude-haiku-4-5-20251001":
        return "[bold green]Haiku 4.5[/bold green]"
    else:
        return model


def print_config(config: PromethianConfig):
    """Print current configuration."""
    table = Table(title="Configuration", border_style="dim")
    table.add_column("Setting", style="cyan")
    table.add_column("Value")

    table.add_row("Model", format_model_name(config.model))
    table.add_row("Sub-Agent Model", format_model_name(config.sub_agent_model) if config.sub_agent_model else "[dim]same as model[/dim]")
    table.add_row("Confidence Threshold", str(config.confidence_threshold))
    table.add_row("Max Self-Heal", str(config.max_self_heal_attempts))
    table.add_row("Max Tool Iterations", f"{config.max_tool_iterations} [dim](per step)[/dim]")
    table.add_row("Track Usage", "[green]Yes[/green]" if config.track_usage else "[dim]No[/dim]")

    # Format share_logs display
    share_logs_display = {
        "none": "[dim]None[/dim]",
        "failures": "[yellow]Failures Only[/yellow]",
        "all": "[green]All Sessions[/green]",
    }.get(config.share_logs, config.share_logs)
    table.add_row("Share Session Logs", share_logs_display)

    console.print(table)
    console.print()
    console.print("[dim]Use /settings to change configuration[/dim]")


def print_usage(usage: dict | None):
    """Print usage statistics."""
    if not usage:
        return

    console.print()
    table = Table(title="Usage Statistics", border_style="dim", show_header=True)
    table.add_column("Model", style="cyan")
    table.add_column("Input", justify="right")
    table.add_column("Output", justify="right")
    table.add_column("Calls", justify="right")
    table.add_column("Cost", justify="right", style="yellow")

    for model, stats in usage.get("by_model", {}).items():
        # Shorten model name for display
        short_model = model.replace("claude-", "").replace("-20250514", "").replace("-20241022", "").replace("-20251101", "")

        # Show cache info if available
        cache_read = stats.get("cache_read_tokens", 0)
        cache_write = stats.get("cache_write_tokens", 0)
        input_display = f"{stats['input_tokens']:,}"
        if cache_read > 0:
            input_display += f" [dim]({cache_read:,} cached)[/dim]"

        table.add_row(
            short_model,
            input_display,
            f"{stats['output_tokens']:,}",
            str(stats["api_calls"]),
            f"${stats['estimated_cost_usd']:.4f}",
        )

    # Add totals row
    table.add_row(
        "[bold]Total[/bold]",
        f"[bold]{usage['total_input_tokens']:,}[/bold]",
        f"[bold]{usage['total_output_tokens']:,}[/bold]",
        f"[bold]{usage['total_api_calls']}[/bold]",
        f"[bold yellow]${usage['estimated_cost_usd']:.4f}[/bold yellow]",
    )

    console.print(table)

    # Show cache savings if any
    if usage.get("cache_savings_usd", 0) > 0:
        console.print(f"[dim green]Cache savings: ${usage['cache_savings_usd']:.4f}[/dim green]")


class VerboseProgressDisplay:
    """Displays verbose progress during task execution with nested tree structure."""

    # Tree drawing characters
    TREE_BRANCH = "├─"
    TREE_LAST = "⎿ "
    TREE_PIPE = "│ "
    TREE_SPACE = "  "

    def __init__(self):
        self.current_stage: ProgressStage | None = None
        self.plan_steps: list[str] = []
        self.step_statuses: dict[int, str] = {}  # idx -> "pending" | "in_progress" | "completed" | "failed"
        self.current_step_idx: int = 0
        self.in_step: bool = False
        self.skills_count: int = 0
        self.tools_count: int = 0

    def _render_plan_progress(self) -> None:
        """Render the plan with current progress status."""
        if not self.plan_steps:
            return

        console.print()
        for i, step_desc in enumerate(self.plan_steps):
            status = self.step_statuses.get(i, "pending")
            desc = step_desc

            if status == "completed":
                icon = "[green]✓[/green]"
                style = "dim"
            elif status == "failed":
                icon = "[red]✗[/red]"
                style = "red"
            elif status == "in_progress":
                icon = "[yellow]⧖[/yellow]"
                style = "white"
            else:  # pending
                icon = "[dim]○[/dim]"
                style = "dim"

            console.print(f"  {icon} [cyan]{i + 1}.[/cyan] [{style}]{desc}[/{style}]")
        console.print()

    def _print_main(self, text: str, style: str = "") -> None:
        """Print a main-level item."""
        if style:
            console.print(f"[{style}]{text}[/{style}]")
        else:
            console.print(text)

    def _print_nested(self, text: str, last: bool = False, depth: int = 1) -> None:
        """Print a nested item with tree structure."""
        prefix = self.TREE_LAST if last else self.TREE_BRANCH
        indent = self.TREE_SPACE * (depth - 1)
        console.print(f"[dim]{indent}{prefix}[/dim] {text}")

    def _print_detail(self, text: str, depth: int = 2) -> None:
        """Print a detail line under a nested item."""
        indent = self.TREE_SPACE * depth
        console.print(f"[dim]{indent}{text}[/dim]")

    def handle_progress(self, event: ProgressEvent) -> None:
        """Handle a progress event and display it."""
        stage = event.stage
        details = event.details

        if stage == ProgressStage.SESSION_START:
            self._print_main("Starting session...", "cyan")

        elif stage == ProgressStage.RETRIEVING_SKILLS:
            self._print_nested("Retrieving skills from knowledge base")

        elif stage == ProgressStage.SKILLS_RETRIEVED:
            count = details.get("count", 0)
            self.skills_count = count
            skills = details.get("skills", [])
            if count > 0:
                self._print_detail(f"[green]✓[/green] Found {count} relevant skill(s)")
                for skill in skills[:3]:  # Show first 3
                    self._print_detail(f"  • {skill}", depth=2)
                if len(skills) > 3:
                    self._print_detail(f"  [dim]... and {len(skills) - 3} more[/dim]", depth=2)
            else:
                self._print_detail("[dim]No skills found[/dim]")

        elif stage == ProgressStage.RETRIEVING_TOOLS:
            self._print_nested("Retrieving tools from knowledge base")

        elif stage == ProgressStage.TOOLS_RETRIEVED:
            count = details.get("count", 0)
            self.tools_count = count
            tools = details.get("tools", [])
            if count > 0:
                self._print_detail(f"[green]✓[/green] Found {count} relevant tool(s)")
                for tool in tools[:3]:  # Show first 3
                    self._print_detail(f"  • {tool}", depth=2)
                if len(tools) > 3:
                    self._print_detail(f"  [dim]... and {len(tools) - 3} more[/dim]", depth=2)
            else:
                self._print_detail("[dim]No tools found[/dim]")

        elif stage == ProgressStage.GATHERING_CONTEXT:
            self._print_nested("Gathering context")

        elif stage == ProgressStage.CONTEXT_GATHERED:
            has_context = details.get("has_context", False)
            if has_context:
                self._print_detail("[green]✓[/green] Context gathered")
            else:
                self._print_detail("[dim]No additional context needed[/dim]")

        elif stage == ProgressStage.PLANNING:
            # Show actual message if provided, otherwise default to "Planning..."
            if event.message and "directly" in event.message.lower():
                self._print_main("Answering directly...", "cyan")
            else:
                self._print_main("Planning...", "yellow")

        elif stage == ProgressStage.PLAN_CREATED:
            steps = details.get("steps", [])
            self.plan_steps = steps
            self.step_statuses = {i: "pending" for i in range(len(steps))}
            confidence = details.get("confidence", 0)
            strategy = details.get("strategy", "unknown")

            self._print_main(f"[green]Plan created[/green] [dim](confidence: {confidence:.0%}, strategy: {strategy})[/dim]")
            self._render_plan_progress()

        elif stage == ProgressStage.BUILDING_TOOLS:
            count = details.get("count", 0)
            self._print_main(f"Building {count} custom tool(s)...", "yellow")

        elif stage == ProgressStage.TOOLS_BUILT:
            tools = details.get("tools", [])
            for tool in tools:
                self._print_nested(f"[green]✓[/green] Built tool: [cyan]{tool}[/cyan]", last=True)

        elif stage == ProgressStage.TOOL_BUILD_FAILED:
            tool_desc = details.get("tool_description", "unknown")
            error = details.get("error", "Unknown error")
            self._print_nested(f"[red]✗ Tool build failed:[/red] {tool_desc}")
            self._print_detail(f"[red]Error:[/red] {error}")

        elif stage == ProgressStage.EXECUTING_PLAN:
            total = details.get("total_steps", len(self.plan_steps))
            self._print_main(f"Executing plan ({total} step{'s' if total != 1 else ''})...", "blue")

        elif stage == ProgressStage.EXECUTING_STEP:
            self.in_step = True
            self.current_step_idx = event.current or 0
            total = event.total or len(self.plan_steps)
            step_desc = details.get("step_description", "")

            # Mark step as in progress and render updated plan
            if self.current_step_idx > 0:
                self.step_statuses[self.current_step_idx - 1] = "in_progress"
            self._render_plan_progress()

            # Also show current step detail
            self._print_main(f"[blue]► Executing:[/blue] {step_desc}")

        elif stage == ProgressStage.STEP_COMPLETED:
            attempts = details.get("attempts", 1)

            # Mark step as completed
            if self.current_step_idx > 0:
                self.step_statuses[self.current_step_idx - 1] = "completed"

            if attempts > 1:
                self._print_nested(f"[green]✓ Completed[/green] [dim](after {attempts} attempts)[/dim]", last=True)
            else:
                self._print_nested("[green]✓ Completed[/green]", last=True)
            self.in_step = False

        elif stage == ProgressStage.STEP_FAILED:
            error = details.get("error", "Unknown error")

            # Mark step as failed
            if self.current_step_idx > 0:
                self.step_statuses[self.current_step_idx - 1] = "failed"

            self._print_nested(f"[red]✗ Failed:[/red] {error}", last=True)
            self.in_step = False

        elif stage == ProgressStage.SELF_HEALING:
            attempt = details.get("attempt", 1)
            max_attempts = details.get("max_attempts", 3)
            tool_name = details.get("tool_name", "")
            error = details.get("error", "")

            self._print_nested(f"[yellow]↻ Self-healing[/yellow] [dim](attempt {attempt}/{max_attempts})[/dim]")
            if tool_name:
                self._print_detail(f"Tool failed: [cyan]{tool_name}[/cyan]")
            if error:
                self._print_detail(f"Error: {error}")

        elif stage == ProgressStage.HEAL_ATTEMPT:
            attempt = details.get("attempt", 2)
            max_attempts = details.get("max_attempts", 3)
            suggestion = details.get("suggestion", "")
            self._print_nested(f"[yellow]→ Retrying[/yellow] [dim](attempt {attempt}/{max_attempts})[/dim]")
            if suggestion:
                self._print_detail(f"Fix: {suggestion}")

        elif stage == ProgressStage.USER_FEEDBACK:
            feedback = details.get("feedback", "")
            self._print_main(f"[cyan]📝 User feedback received[/cyan]")
            if feedback:
                # Show truncated feedback
                preview = feedback[:80] + "..." if len(feedback) > 80 else feedback
                self._print_nested(f"[dim]{preview}[/dim]")

        elif stage == ProgressStage.REPLANNING:
            after_step = details.get("after_step", "")
            has_feedback = details.get("has_feedback", False)
            reason = "based on feedback" if has_feedback else f"after {after_step}"
            self._print_main(f"[yellow]Replanning...[/yellow] [dim]({reason})[/dim]")

        elif stage == ProgressStage.PLAN_UPDATED:
            added = details.get("added_steps", [])
            removed = details.get("removed_steps", [])
            new_count = details.get("new_step_count", 0)

            self._print_nested(f"[cyan]Plan updated[/cyan] [dim]({new_count} steps remaining)[/dim]")
            if added:
                self._print_detail(f"[green]+[/green] Added {len(added)} step(s)")
            if removed:
                self._print_detail(f"[red]-[/red] Removed {len(removed)} step(s)")

        elif stage == ProgressStage.TOOL_STARTING:
            tool_name = details.get("tool_name", "unknown")
            self._print_nested(f"Running tool: [cyan]{tool_name}[/cyan]")

        elif stage == ProgressStage.TOOL_COMPLETED:
            tool_name = details.get("tool_name", "unknown")
            self._print_detail(f"[green]✓[/green] {tool_name} completed")

        elif stage == ProgressStage.TOOL_FAILED:
            tool_name = details.get("tool_name", "unknown")
            error = details.get("error", "")
            self._print_detail(f"[red]✗[/red] {tool_name} failed: {error}")

        elif stage == ProgressStage.LOOP_ITERATION:
            iteration = details.get("iteration", 1)
            max_iter = details.get("max_iterations", 10)
            self._print_nested(f"[yellow]↻ Agent loop[/yellow] [dim](iteration {iteration}/{max_iter})[/dim]")

        elif stage == ProgressStage.LOOP_TOOL_CALL:
            tool_name = details.get("tool_name", "unknown")
            tool_idx = details.get("tool_index", 1)
            total = details.get("total_tools", 1)
            if total > 1:
                self._print_detail(f"[cyan]→[/cyan] {tool_name} [dim]({tool_idx}/{total})[/dim]")
            else:
                self._print_detail(f"[cyan]→[/cyan] {tool_name}")

        elif stage == ProgressStage.LEARNING:
            self._print_main("Learning from result...", "magenta")

        elif stage == ProgressStage.SKILL_LEARNED:
            skill_name = details.get("skill_name", "")
            if skill_name:
                self._print_nested(f"[green]✓[/green] Learned skill: [cyan]{skill_name}[/cyan]", last=True)

        elif stage == ProgressStage.SYNTHESIZING:
            self._print_main("Synthesizing final output...", "blue")

        elif stage == ProgressStage.TASK_COMPLETED:
            skills_learned = details.get("skills_learned", 0)
            tools_built = details.get("tools_built", 0)
            console.print()
            self._print_main("[green]✓ Task completed[/green]")
            if skills_learned > 0 or tools_built > 0:
                parts = []
                if skills_learned > 0:
                    parts.append(f"{skills_learned} skill(s) learned")
                if tools_built > 0:
                    parts.append(f"{tools_built} tool(s) built")
                self._print_nested(f"[dim]{', '.join(parts)}[/dim]", last=True)

        elif stage == ProgressStage.TASK_FAILED:
            error = details.get("error", "Unknown error")
            console.print()
            self._print_main(f"[red]✗ Task failed:[/red] {error}")


async def run_task(
    agent,
    task: str,
    show_usage: bool = False,
    conversation_manager: ConversationManager | None = None,
    api_client=None,
    config: PromethianConfig | None = None,
) -> tuple[str | None, ConversationManager, dict | None]:
    """Run a task with the agent. Returns (result, updated_conversation_manager, usage)."""
    console.print()

    # Initialize conversation manager if not provided
    if conversation_manager is None:
        conversation_manager = ConversationManager()

    # Add user message
    conversation_manager.add_user_message(task)

    # Get context from conversation history
    context = conversation_manager.get_context_for_prompt() if conversation_manager.has_history else None

    # Create verbose progress display
    progress_display = VerboseProgressDisplay()

    def progress_callback(event: ProgressEvent) -> None:
        progress_display.handle_progress(event)
        # Show hint about feedback on first step
        if event.stage.value == "executing_step" and event.details.get("current") == 1:
            console.print("[dim]Tip: Type feedback anytime - it will be applied after the current step[/dim]")

    # Create feedback collector for real-time user input
    feedback_collector = FeedbackCollector()

    # Re-initialize agent with progress callback and feedback getter
    from promethian.core.agent import PromethianAgent
    agent_with_progress = PromethianAgent(
        config=agent.config,
        enable_context_gathering=agent.enable_context_gathering,
        progress_callback=progress_callback,
        feedback_getter=feedback_collector.get_pending_feedback,
    )

    # Start collecting feedback
    feedback_collector.start()

    try:
        result = await agent_with_progress.run(task, context=context)
    except Exception as e:
        error_msg = str(e)
        console.print(f"[red]Error:[/red] {error_msg}")

        # Provide additional context for common errors
        if "Usage limit" in error_msg:
            console.print("[dim]Tip: Your session limit has been reached. Wait for reset or upgrade your plan.[/dim]")
        elif "Active session" in error_msg:
            console.print("[dim]Tip: Try running the task again. If this persists, a session may be stuck.[/dim]")
        elif "401" in error_msg or "Unauthorized" in error_msg.lower():
            console.print("[dim]Tip: Your API key may be invalid. Run /setup to reconfigure.[/dim]")
        elif "connection" in error_msg.lower() or "network" in error_msg.lower():
            console.print("[dim]Tip: Check your network connection and try again.[/dim]")
        elif "timeout" in error_msg.lower():
            console.print("[dim]Tip: The request timed out. Try a simpler task or try again later.[/dim]")

        return None, conversation_manager, None
    finally:
        # Stop feedback collection
        feedback_collector.stop()
        await agent_with_progress.close()

    console.print()

    if result.status.value == "success":
        # Don't print "Task completed" again - it's already shown by progress events
        if result.final_output:
            console.print()
            # Try to render as markdown, fall back to plain text
            try:
                console.print(Markdown(str(result.final_output)))
            except Exception:
                console.print(str(result.final_output))

        # Show learning summary if anything was learned
        if result.skills_learned or result.tools_built:
            console.print()
            console.print("[dim]Learned:[/dim]", end=" ")
            parts = []
            if result.skills_learned:
                parts.append(f"{len(result.skills_learned)} skill(s)")
            if result.tools_built:
                parts.append(f"{len(result.tools_built)} tool(s)")
            console.print("[dim]" + ", ".join(parts) + "[/dim]")
    else:
        console.print(f"[red]Task failed:[/red] {result.error}")

    # Show execution time and usage
    console.print(f"[dim]({result.execution_time:.1f}s)[/dim]")

    # Display usage statistics if available
    if show_usage and result.usage:
        print_usage(result.usage)

    console.print()

    # Update conversation manager with assistant response
    files_modified = []
    tools_used = []
    for step_result in result.step_results:
        tools_used.extend(step_result.tools_used or [])

    conversation_manager.add_assistant_message(
        content=str(result.final_output) if result.final_output else "Task completed",
        files_modified=files_modified,
        tools_used=tools_used,
        outcome=result.status.value,
    )

    # Auto-summarize if needed (run in background, don't block)
    try:
        await conversation_manager.maybe_summarize()
    except Exception:
        pass  # Don't fail task on summarization error

    # Send session log if opt-in sharing is enabled
    if api_client and config and config.share_logs != "none":
        try:
            success = result.status.value == "success"
            # Count self-heal attempts from step results
            self_heal_attempts = sum(
                (sr.attempts or 1) - 1 for sr in result.step_results
            ) if result.step_results else 0
            # Task "struggled" if it had self-heal attempts or failed
            struggled = self_heal_attempts > 0 or not success

            # Only send if: share_logs == "all" OR (share_logs == "failures" AND struggled)
            should_send = config.share_logs == "all" or (config.share_logs == "failures" and struggled)

            if should_send:
                await api_client.send_session_log(
                    task=task,
                    success=success,
                    execution_time=result.execution_time,
                    steps_executed=len(result.step_results) if result.step_results else 0,
                    self_heal_attempts=self_heal_attempts,
                    error_message=result.error if not success else None,
                    tools_used=tools_used,
                    log_type="failure" if struggled else "all",
                )
        except Exception:
            pass  # Don't fail task on telemetry error

    return str(result.final_output) if result.final_output else None, conversation_manager, result.usage


async def run_search(api_client, query: str) -> None:
    """Search the knowledge base."""
    try:
        # Start session
        await api_client.start_session(f"CLI search: {query}")

        # Search skills
        skills, _ = await api_client.search_skills(query, max_results=5)
        if skills:
            table = Table(title="Skills", border_style="dim")
            table.add_column("Name", style="cyan")
            table.add_column("Description")
            table.add_column("Score", justify="right")

            for skill in skills:
                desc = skill.description[:50] + "..." if len(skill.description) > 50 else skill.description
                table.add_row(skill.name, desc, f"{skill.similarity_score:.2f}")

            console.print(table)
            console.print()

        # Search tools
        tools, _ = await api_client.search_tools(query, max_results=5)
        if tools:
            table = Table(title="Tools", border_style="dim")
            table.add_column("Name", style="green")
            table.add_column("Description")
            table.add_column("Score", justify="right")

            for tool in tools:
                desc = tool.description[:50] + "..." if len(tool.description) > 50 else tool.description
                table.add_row(tool.name, desc, f"{tool.similarity_score:.2f}")

            console.print(table)

        if not skills and not tools:
            console.print("[dim]No results found[/dim]")

        # Complete session
        await api_client.complete_session(overall_success=True, notes="CLI search")

    except Exception as e:
        console.print(f"[red]Search error:[/red] {e}")


def validate_model(model: str) -> str:
    """Validate and normalize model name."""
    # Allow short names
    model_aliases = {
        "opus": "claude-opus-4-5-20251101",
        "opus-4.5": "claude-opus-4-5-20251101",
        "opus4.5": "claude-opus-4-5-20251101",
        "sonnet": "claude-sonnet-4-5-20250514",
        "sonnet-4.5": "claude-sonnet-4-5-20250514",
        "sonnet4.5": "claude-sonnet-4-5-20250514",
        "haiku": "claude-haiku-4-5-20251001",
        "haiku-4.5": "claude-haiku-4-5-20251001",
        "haiku4.5": "claude-haiku-4-5-20251001",
    }

    normalized = model_aliases.get(model.lower(), model)

    if normalized not in VALID_MODELS:
        raise ValueError(
            f"Invalid model. Valid options: {', '.join(VALID_MODELS.keys())} "
            f"or shortcuts: opus, sonnet, haiku"
        )

    return normalized


async def handle_set_command(key: str, value: str, config: PromethianConfig, agent):
    """Handle /set command to update configuration."""
    from promethian.core.agent import PromethianAgent

    # Map of valid settings
    valid_settings = {
        "max_tool_iterations": int,
        "track_usage": lambda v: v.lower() in ("true", "1", "yes"),
        "model": validate_model,
        "sub_agent_model": lambda v: validate_model(v) if v.lower() != "none" else None,
        "confidence_threshold": float,
        "max_self_heal_attempts": int,
        "share_logs": lambda v: v.lower() if v.lower() in ("none", "failures", "all") else (_ for _ in ()).throw(ValueError("Must be: none, failures, or all")),
    }

    if key not in valid_settings:
        console.print(f"[red]Unknown setting:[/red] {key}")
        console.print(f"[dim]Valid settings: {', '.join(valid_settings.keys())}[/dim]")
        return config, agent

    try:
        # Parse value
        parsed_value = valid_settings[key](value)

        # Update config file
        config_data = load_config() or {}
        config_data[key] = parsed_value
        save_config(config_data)

        # Create new config with updated value
        new_config_kwargs = {
            "anthropic_api_key": config.anthropic_api_key,
            "promethian_api_key": config.promethian_api_key,
            "promethian_api_url": config.promethian_api_url,
            "model": config.model,
            "sub_agent_model": config.sub_agent_model,
            "max_tool_iterations": config.max_tool_iterations,
            "track_usage": config.track_usage,
            "confidence_threshold": config.confidence_threshold,
            "max_self_heal_attempts": config.max_self_heal_attempts,
            "share_logs": config.share_logs,
        }
        new_config_kwargs[key] = parsed_value
        new_config = PromethianConfig(**new_config_kwargs)
        set_config(new_config)

        # Reinitialize agent with new config
        if agent:
            await agent.close()
        agent = PromethianAgent(new_config)

        # Format display value for models
        display_value = parsed_value
        if key in ("model", "sub_agent_model") and parsed_value:
            display_value = format_model_name(parsed_value)
        elif key == "sub_agent_model" and parsed_value is None:
            display_value = "[dim]same as model[/dim]"

        console.print(f"[green]Set {key} = {display_value}[/green]")
        return new_config, agent

    except ValueError as e:
        console.print(f"[red]Invalid value for {key}:[/red] {e}")
        return config, agent


async def interactive_loop(config: PromethianConfig) -> None:
    """Main interactive REPL loop."""
    from promethian.core.agent import PromethianAgent
    from promethian.api.client import PromethianAPIClient

    agent = None
    api_client = None
    # Track conversation with auto-summarization for unlimited context
    conversation_manager = ConversationManager()
    # Track session stats for status line
    session_stats = SessionStats()

    try:
        agent = PromethianAgent(config)

        if config.promethian_api_key:
            api_client = PromethianAPIClient(
                api_url=config.promethian_api_url,
                api_key=config.promethian_api_key,
            )
    except Exception as e:
        console.print(f"[yellow]Warning:[/yellow] {e}")
        console.print("[dim]Some features may be unavailable.[/dim]")
        console.print()

    print_welcome()

    while True:
        try:
            # Get user input with styled prompt
            user_input = print_input_prompt(session_stats)

            if not user_input:
                continue

            # Handle commands
            if user_input.startswith("/"):
                parts = user_input[1:].split(maxsplit=1)
                cmd = parts[0].lower()
                args = parts[1] if len(parts) > 1 else ""

                if cmd in ("quit", "exit", "q"):
                    break

                elif cmd == "help":
                    print_help()

                elif cmd == "config":
                    print_config(config)

                elif cmd == "setup":
                    config_data = run_setup()
                    config = PromethianConfig(**{
                        k: v for k, v in config_data.items() if v is not None
                    })
                    set_config(config)
                    # Reinitialize agent
                    if agent:
                        await agent.close()
                    agent = PromethianAgent(config)

                elif cmd == "clear":
                    # Clear screen, conversation history, and tool cache
                    from promethian.core.tool_cache import get_tool_cache

                    conversation_manager.clear()
                    if CONVERSATION_FILE.exists():
                        CONVERSATION_FILE.unlink()

                    cache = get_tool_cache()
                    tools_cleared = cache.clear()

                    console.clear()
                    print_welcome()
                    console.print(f"[dim]Cleared conversation history and {tools_cleared} cached tool(s)[/dim]")

                elif cmd == "history":
                    # Show conversation history
                    console.print("[bold]Conversation History[/bold]")
                    console.print(f"  Messages: {conversation_manager.message_count}")
                    console.print(f"  Has summary: {'Yes' if conversation_manager._summary else 'No'}")
                    if conversation_manager._summary:
                        console.print(f"  Summarized messages: {conversation_manager._summary.messages_summarized}")
                    console.print(f"  Recent messages: {len(conversation_manager._recent_messages)}")
                    if CONVERSATION_FILE.exists():
                        size_kb = CONVERSATION_FILE.stat().st_size / 1024
                        console.print(f"  File size: {size_kb:.1f} KB")

                elif cmd == "cache":
                    # Show tool cache stats
                    from promethian.core.tool_cache import get_tool_cache
                    cache = get_tool_cache()
                    stats = cache.stats()
                    console.print("[bold]Tool Cache[/bold]")
                    console.print(f"  Cached tools: {stats['count']}")
                    console.print(f"  Size: {stats['total_size_kb']} KB")
                    if stats['oldest']:
                        console.print(f"  Oldest: {stats['oldest']}")
                    if stats['newest']:
                        console.print(f"  Newest: {stats['newest']}")

                elif cmd == "search":
                    if not args:
                        console.print("[yellow]Usage:[/yellow] /search <query>")
                    elif not api_client:
                        console.print("[yellow]Could not connect to Promethian API.[/yellow]")
                    else:
                        await run_search(api_client, args)

                elif cmd == "feedback":
                    if not args:
                        console.print("[yellow]Usage:[/yellow] /feedback <your feedback text>")
                        console.print("[dim]Example: /feedback The web search results could be more relevant[/dim]")
                    elif not api_client:
                        console.print("[yellow]Could not connect to Promethian API.[/yellow]")
                    else:
                        try:
                            result = await api_client.send_feedback(args)
                            console.print("[green]Thank you![/green] Your feedback has been submitted.")
                        except Exception as e:
                            console.print(f"[red]Failed to send feedback:[/red] {e}")

                elif cmd == "settings":
                    # Run interactive settings editor
                    new_config, changed = run_interactive_settings(config)
                    if changed:
                        config = new_config
                        # Reinitialize agent with new config
                        if agent:
                            await agent.close()
                        agent = PromethianAgent(config)

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

            else:
                # Run as task
                if not agent:
                    console.print("[red]Agent not initialized.[/red]")
                    console.print("[dim]Run /setup to configure.[/dim]")
                else:
                    _, conversation_manager, usage = await run_task(
                        agent, user_input,
                        conversation_manager=conversation_manager,
                        api_client=api_client,
                        config=config,
                    )
                    session_stats.update(usage)

        except KeyboardInterrupt:
            console.print("\n[dim]Cancelled. Type /quit to exit.[/dim]")

        except EOFError:
            break

        except Exception as e:
            console.print(f"[red]Error:[/red] {e}")

    # Cleanup
    console.print()
    console.print("[dim]Goodbye![/dim]")

    if agent:
        await agent.close()
    if api_client:
        await api_client.close()


def main():
    """Main entry point."""
    import argparse

    parser = argparse.ArgumentParser(
        description="Promethian - Self-improving AI Agent",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument(
        "task",
        nargs="*",
        help="Task to run (optional - enters interactive mode if not provided)"
    )
    parser.add_argument(
        "--setup",
        action="store_true",
        help="Run setup wizard"
    )
    parser.add_argument(
        "--version",
        action="store_true",
        help="Show version"
    )
    parser.add_argument(
        "--no-upgrade-check",
        action="store_true",
        help="Skip upgrade check"
    )
    parser.add_argument(
        "-p", "--print",
        action="store_true",
        dest="print_mode",
        help="Print mode: run task and print output to stdout (no interactive UI)"
    )
    parser.add_argument(
        "--output-format",
        choices=["text", "json"],
        default="text",
        help="Output format for print mode (default: text)"
    )
    parser.add_argument(
        "--max-turns",
        type=int,
        default=None,
        help="Maximum number of agentic turns (limits execution)"
    )

    args = parser.parse_args()

    if args.version:
        from promethian import __version__
        console.print(f"Promethian v{__version__}")
        return

    if args.setup:
        run_setup()
        return

    # Check for upgrades (unless disabled)
    if not args.no_upgrade_check:
        latest, current = check_for_upgrade()
        if latest:
            if prompt_and_upgrade(latest, current):
                # Upgrade completed, exit so user restarts with new version
                return

    # Get config (runs setup if needed)
    config = get_config_or_setup()
    set_config(config)

    if args.task:
        # Run single task and exit
        task = " ".join(args.task)
        if args.print_mode:
            # Print mode - minimal output, suitable for scripting
            asyncio.run(run_print_mode(
                config, task,
                output_format=args.output_format,
                max_turns=args.max_turns,
            ))
        else:
            asyncio.run(run_single_task(config, task, max_turns=args.max_turns))
    else:
        # Enter interactive mode
        asyncio.run(interactive_loop(config))


async def run_single_task(
    config: PromethianConfig,
    task: str,
    max_turns: int | None = None,
) -> None:
    """Run a single task and exit."""
    from promethian.core.agent import PromethianAgent

    # Override max_tool_iterations if max_turns specified
    if max_turns is not None:
        config.max_tool_iterations = max_turns

    console.print(Panel(f"[bold]Task:[/bold] {task}", title="Promethian", border_style="blue"))

    # Load conversation history for multi-turn context
    conversation_manager = ConversationManager.load_from_file(str(CONVERSATION_FILE))

    async with PromethianAgent(config) as agent:
        _, conversation_manager, _ = await run_task(
            agent, task, show_usage=True, conversation_manager=conversation_manager
        )

    # Save conversation history for future runs
    try:
        conversation_manager.save_to_file(str(CONVERSATION_FILE))
    except Exception:
        pass  # Don't fail on save errors


async def run_print_mode(
    config: PromethianConfig,
    task: str,
    output_format: str = "text",
    max_turns: int | None = None,
) -> None:
    """
    Run a task in print mode - minimal output suitable for scripting.

    This mode:
    - Suppresses interactive UI elements
    - Prints only the final result to stdout
    - Supports JSON output format for parsing
    """
    from promethian.core.agent import PromethianAgent

    # Override max_tool_iterations if max_turns specified
    if max_turns is not None:
        config.max_tool_iterations = max_turns

    try:
        async with PromethianAgent(config) as agent:
            result = await agent.run(task)

            if output_format == "json":
                # JSON output for scripting
                output = {
                    "success": result.status.value == "success",
                    "task": task,
                    "result": result.final_output,
                    "steps_executed": len(result.step_results) if result.step_results else 0,
                }
                print(json.dumps(output, indent=2))
            else:
                # Plain text output
                if result.final_output:
                    print(result.final_output)
                elif result.error:
                    print(f"Error: {result.error}", file=sys.stderr)

    except Exception as e:
        if output_format == "json":
            print(json.dumps({"success": False, "error": str(e)}))
        else:
            print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()
