"""
Interactive shell for AiCippy.

Provides a rich terminal interface with:
- Live status sidebar with agent visualization
- Task checklist footer
- Keyboard shortcuts
- Beautiful progress indicators
"""

from __future__ import annotations

import asyncio
import signal
import time
from dataclasses import dataclass, field
from datetime import datetime
from collections.abc import Callable
from typing import Any

from prompt_toolkit import PromptSession
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.history import FileHistory
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.keys import Keys
from prompt_toolkit.styles import Style
from rich.align import Align
from rich.box import ROUNDED
from rich.console import Console, Group
from rich.live import Live
from rich.panel import Panel
from rich.table import Table
from rich.text import Text

from aicippy import __version__
from aicippy.cli.mascot import (
    MascotAnimator,
    MascotMood,
    MASCOT_PRIMARY,
)
from aicippy.cli.themes import (
    DEFAULT_THEME_NAME,
    get_available_themes,
    get_theme,
    prompt_theme_selection,
)
from aicippy.cli.ui_components import (
    BRAND_ACCENT,
    BRAND_ERROR,
    BRAND_INFO,
    BRAND_PRIMARY,
    BRAND_SUCCESS,
    BRAND_WARNING,
    STATUS_ICONS,
    AgentWidget,
    TaskItem,
    InteractiveLayout,
    StreamingState,
    create_error_panel,
    create_input_section,
    create_success_panel,
)
from aicippy.config import get_settings
from aicippy.memory import (
    ConversationHistory,
    PlatformBackedHistory,
    PlatformBackedTenantMemory,
    TenantMemory,
)
from aicippy.utils.correlation import generate_correlation_id
from aicippy.utils.logging import get_logger
from aicippy.cli.upload_manager import UploadManager, get_working_directory
from aicippy.cli.ai_command_executor import AICommandExecutor

logger = get_logger(__name__)
console = Console()


# ============================================================================
# Data Classes
# ============================================================================


@dataclass(slots=True)
class AgentStatus:
    """Status of a single agent."""

    id: str
    type: str
    status: str  # running, thinking, error, idle, complete
    progress: int = 0
    message: str = ""
    started_at: datetime | None = None
    tokens: int = 0

    def to_widget(self) -> AgentWidget:
        """Convert to AgentWidget for UI."""
        return AgentWidget(
            id=self.id,
            name=self.type,
            status=self.status,
            progress=self.progress,
            message=self.message,
            tokens=self.tokens,
        )


@dataclass(slots=True)
class SessionState:
    """Current interactive session state."""

    session_id: str
    model: str = "opus"
    mode: str = "agent"
    agents: list[AgentStatus] = field(default_factory=list)
    tokens_used: int = 0
    connected: bool = False
    conversation: list[dict[str, str]] = field(default_factory=list)
    tasks: list[TaskItem] = field(default_factory=list)
    current_task_idx: int = 0
    mascot_mood: str = "idle"  # idle, thinking, working, happy
    mascot_frame: int = 0
    # New fields for status bar
    session_start: datetime = field(default_factory=datetime.now)
    username: str | None = None
    background_tasks: int = 0
    # WebSocket connection fields
    ws_latency_ms: float = 0.0
    ws_reconnecting: bool = False
    ws_reconnect_attempts: int = 0
    # Streaming state
    is_streaming: bool = False
    stream_tokens: int = 0
    stream_content: str = ""
    # Persistent memory
    conversation_history: ConversationHistory | None = None
    tenant_memory: TenantMemory | None = None
    user_id: str | None = None
    # Pending image uploads for next message
    pending_image_paths: list[str] = field(default_factory=list)
    # Active color theme
    active_theme: str = DEFAULT_THEME_NAME
    # Billing / plan state
    plan_info: Any = None  # PlanInfo from billing module
    credit_manager: Any = None  # CreditManager instance
    plan_validator: Any = None  # PlanValidator instance
    is_admin: bool = False
    credits_remaining: int = -1  # -1 = unlimited (admin)
    user_email: str | None = None

    def add_task(self, content: str) -> None:
        """Add a task to the checklist."""
        self.tasks.append(TaskItem(content=content, status="pending"))

    def start_task(self, idx: int | None = None) -> None:
        """Start a task."""
        idx = idx if idx is not None else self.current_task_idx
        if 0 <= idx < len(self.tasks):
            self.tasks[idx].status = "in_progress"
            self.current_task_idx = idx

    def complete_task(self, idx: int | None = None) -> None:
        """Complete a task."""
        idx = idx if idx is not None else self.current_task_idx
        if 0 <= idx < len(self.tasks):
            self.tasks[idx].status = "completed"
            self.tasks[idx].progress = 100
            if idx == self.current_task_idx and idx + 1 < len(self.tasks):
                self.current_task_idx = idx + 1

    def update_task_progress(self, progress: int, idx: int | None = None) -> None:
        """Update task progress."""
        idx = idx if idx is not None else self.current_task_idx
        if 0 <= idx < len(self.tasks):
            self.tasks[idx].progress = min(100, max(0, progress))

    def get_session_duration(self) -> str:
        """Get session duration as HH:MM:SS string."""
        elapsed = datetime.now() - self.session_start
        total_seconds = int(elapsed.total_seconds())
        hours = total_seconds // 3600
        minutes = (total_seconds % 3600) // 60
        seconds = total_seconds % 60
        return f"{hours:02d}:{minutes:02d}:{seconds:02d}"


# ============================================================================
# Commands
# ============================================================================

# Slash command completions
SLASH_COMMANDS = [
    "/help",
    "/model",
    "/mode",
    "/theme",
    "/agents",
    "/audit",
    "/kb",
    "/tools",
    "/usage",
    "/history",
    "/sessions",
    "/memory",
    "/clear",
    "/export",
    "/shortcuts",
    "/upload",
    "/uploads",
    "/workspace",
    "/plan",
    "/shell",
    "/exec",
    "/quit",
]

# Natural language exit phrases (lowercase)
EXIT_PHRASES: set[str] = {
    "quit", "exit", "q", "bye", "goodbye", "good bye",
    "close", "close session", "end session", "end",
    "stop", "done", "logout", "log out", "sign out",
    "leave", "terminate", "disconnect",
}

# Maximum allowed user input length (characters)
MAX_INPUT_LENGTH: int = 32000

MODEL_OPTIONS = ["opus", "sonnet", "haiku", "llama"]
MODE_OPTIONS = ["agent", "edit", "research", "code"]


# ============================================================================
# Slash Command Handler
# ============================================================================


class SlashCommandHandler:
    """Handler for slash commands in interactive mode."""

    def __init__(self, state: SessionState, console: Console) -> None:
        self.state = state
        self.console = console
        self._upload_manager: UploadManager | None = None
        self._handlers: dict[str, Callable[[list[str]], bool]] = {
            "/help": self._handle_help,
            "/model": self._handle_model,
            "/mode": self._handle_mode,
            "/agents": self._handle_agents,
            "/audit": self._handle_audit,
            "/kb": self._handle_kb,
            "/tools": self._handle_tools,
            "/usage": self._handle_usage,
            "/history": self._handle_history,
            "/sessions": self._handle_sessions,
            "/memory": self._handle_memory,
            "/clear": self._handle_clear,
            "/export": self._handle_export,
            "/shortcuts": self._handle_shortcuts,
            "/upload": self._handle_upload,
            "/uploads": self._handle_uploads,
            "/workspace": self._handle_workspace,
            "/theme": self._handle_theme,
            "/plan": self._handle_plan,
            "/shell": self._handle_shell,
            "/exec": self._handle_shell,  # Alias for /shell
            "/quit": self._handle_quit,
        }
        self._shell_executor = None

    @property
    def upload_manager(self) -> UploadManager:
        """Get or create upload manager."""
        if self._upload_manager is None:
            self._upload_manager = UploadManager(console=self.console)
        return self._upload_manager

    def handle(self, command: str) -> bool:
        """Handle a slash command. Returns False to exit."""
        parts = command.strip().split()
        if not parts:
            return True

        cmd = parts[0].lower()
        args = parts[1:]

        handler = self._handlers.get(cmd)
        if handler:
            return handler(args)

        self.console.print(create_error_panel(
            f"Unknown command: {cmd}",
            suggestion="Type /help for available commands"
        ))
        return True

    def _handle_help(self, args: list[str]) -> bool:
        """Show help information."""
        help_table = Table(
            title="[bold]Available Commands[/bold]",
            border_style=BRAND_PRIMARY,
            title_style=f"bold {BRAND_INFO}",
            header_style=f"bold {BRAND_ACCENT}",
        )
        help_table.add_column("Command", style=BRAND_INFO)
        help_table.add_column("Description", style="white")

        commands = [
            ("/help", "Show this help message"),
            ("/model <opus|sonnet|haiku|llama>", "Switch AI model"),
            ("/mode <agent|edit|research|code>", "Change operation mode"),
            ("/theme [name|number]", "Switch color theme"),
            ("/audit [scope]", "Run AI-powered security audit (via aivedha.ai)"),
            ("/agents spawn <1-10>", "Spawn parallel agents"),
            ("/agents list", "List active agents"),
            ("/agents stop [id|all]", "Stop agent(s)"),
            ("/agents status", "Detailed agent health"),
            ("/sessions", "List previous conversation sessions"),
            ("/memory [set|get|clear]", "Manage persistent tenant memory"),
            ("/upload", "Upload files for multimodal AI processing"),
            ("/uploads", "List all uploaded files"),
            ("/workspace", "📁 Show current working directory info"),
            ("/shell <cmd>", "🖥️ Execute shell command with input passthrough"),
            ("/exec <cmd>", "🖥️ Alias for /shell"),
            ("/kb sync", "Push local files to Knowledge Base"),
            ("/kb status", "Knowledge Base status"),
            ("/kb search <query>", "Search Knowledge Base"),
            ("/tools list", "List available tools"),
            ("/tools enable <name>", "Enable a tool"),
            ("/tools disable <name>", "Disable a tool"),
            ("/plan", "Show subscription & credits info"),
            ("/usage", "Token usage summary"),
            ("/history", "Session history"),
            ("/clear", "Clear conversation"),
            ("/export <json|md>", "Export session"),
            ("/shortcuts", "Show keyboard shortcuts"),
            ("/quit", "Exit AiCippy"),
        ]

        for cmd, desc in commands:
            help_table.add_row(cmd, desc)

        self.console.print(help_table)
        return True

    def _handle_shortcuts(self, args: list[str]) -> bool:
        """Show keyboard shortcuts."""
        from aicippy.cli.ui_components import KEYBOARD_SHORTCUTS

        shortcuts_table = Table(
            title="[bold]Keyboard Shortcuts[/bold]",
            border_style=BRAND_ACCENT,
            title_style=f"bold {BRAND_ACCENT}",
        )
        shortcuts_table.add_column("Key", style=BRAND_ACCENT)
        shortcuts_table.add_column("Action", style="white")

        for key, desc in KEYBOARD_SHORTCUTS:
            shortcuts_table.add_row(key, desc)

        # Add more shortcuts
        extra_shortcuts = [
            ("Ctrl+L", "Clear screen"),
            ("Ctrl+R", "Reverse search history"),
            ("Alt+.", "Insert last argument"),
            ("Esc", "Cancel current input"),
        ]
        for key, desc in extra_shortcuts:
            shortcuts_table.add_row(key, desc)

        self.console.print(shortcuts_table)
        return True

    def _handle_model(self, args: list[str]) -> bool:
        """Switch AI model."""
        if not args:
            current = Text()
            current.append(f"\n {STATUS_ICONS['complete']} ", style=BRAND_SUCCESS)
            current.append("Current model: ", style="dim")
            current.append(self.state.model.upper(), style=f"bold {BRAND_PRIMARY}")
            current.append("\n\n Available: ", style="dim")
            for i, m in enumerate(MODEL_OPTIONS):
                if i > 0:
                    current.append(" │ ", style="dim")
                style = f"bold {BRAND_SUCCESS}" if m == self.state.model else BRAND_INFO
                current.append(m, style=style)
            current.append("\n")
            self.console.print(Panel(current, border_style="dim"))
            return True

        model = args[0].lower()
        if model not in MODEL_OPTIONS:
            self.console.print(create_error_panel(
                f"Invalid model: {model}",
                suggestion=f"Choose from: {', '.join(MODEL_OPTIONS)}"
            ))
            return True

        self.state.model = model
        # Persist model preference
        if self.state.tenant_memory:
            self.state.tenant_memory.set_preference("last_model", model)
        self.console.print(create_success_panel(
            f"Switched to model: {model.upper()}",
            details="Changes take effect on next query"
        ))
        return True

    def _handle_mode(self, args: list[str]) -> bool:
        """Change operation mode."""
        if not args:
            mode_colors = {
                "agent": BRAND_INFO,
                "edit": BRAND_WARNING,
                "research": BRAND_ACCENT,
                "code": BRAND_SUCCESS,
            }
            current = Text()
            current.append(f"\n {STATUS_ICONS['complete']} ", style=BRAND_SUCCESS)
            current.append("Current mode: ", style="dim")
            current.append(
                self.state.mode.upper(),
                style=f"bold {mode_colors.get(self.state.mode, 'white')}"
            )
            current.append("\n\n Available:\n", style="dim")

            mode_descriptions = {
                "agent": "Multi-agent task orchestration",
                "edit": "Code editing and refactoring",
                "research": "Information gathering and analysis",
                "code": "Code generation and completion",
            }

            for mode, desc in mode_descriptions.items():
                icon = STATUS_ICONS['complete'] if mode == self.state.mode else STATUS_ICONS['pending']
                color = mode_colors.get(mode, 'white')
                current.append(f"   {icon} ", style=color)
                current.append(f"{mode:<10}", style=f"bold {color}")
                current.append(f" {desc}\n", style="dim")

            self.console.print(Panel(current, border_style="dim"))
            return True

        mode = args[0].lower()
        if mode not in MODE_OPTIONS:
            self.console.print(create_error_panel(
                f"Invalid mode: {mode}",
                suggestion=f"Choose from: {', '.join(MODE_OPTIONS)}"
            ))
            return True

        self.state.mode = mode
        self.console.print(create_success_panel(f"Switched to mode: {mode.upper()}"))
        return True

    def _handle_theme(self, args: list[str]) -> bool:
        """Switch the active color theme.

        Usage:
            /theme              - Show theme selector panel
            /theme <name>       - Switch to named theme directly
            /theme <number>     - Switch by number (1-5)
        """
        available = get_available_themes()

        if not args:
            # Interactive theme selection
            selected = prompt_theme_selection(self.console, self.state.active_theme)
            self.state.active_theme = selected
            if self.state.tenant_memory:
                self.state.tenant_memory.set_preference("theme", selected)
            return True

        # Direct selection by name or number
        choice = " ".join(args)

        # Try numeric
        try:
            idx = int(choice)
            if 1 <= idx <= len(available):
                selected = available[idx - 1]
                self.state.active_theme = selected
                theme = get_theme(selected)
                if self.state.tenant_memory:
                    self.state.tenant_memory.set_preference("theme", selected)
                self.console.print(create_success_panel(
                    f"Theme set to: {selected}",
                    details=theme.description,
                ))
                return True
        except ValueError:
            pass

        # Try name match (case-insensitive partial)
        choice_lower = choice.lower()
        for name in available:
            if choice_lower in name.lower():
                self.state.active_theme = name
                theme = get_theme(name)
                if self.state.tenant_memory:
                    self.state.tenant_memory.set_preference("theme", name)
                self.console.print(create_success_panel(
                    f"Theme set to: {name}",
                    details=theme.description,
                ))
                return True

        # No match
        self.console.print(create_error_panel(
            f"Unknown theme: {choice}",
            suggestion=f"Available: {', '.join(available)}"
        ))
        return True

    def _handle_agents(self, args: list[str]) -> bool:
        """Handle agent commands."""
        if not args:
            count = len(self.state.agents)
            self.console.print(Panel(
                Text(f" ⚡ Active agents: {count}/10", style=BRAND_INFO),
                border_style="dim"
            ))
            return True

        subcommand = args[0].lower()

        if subcommand == "spawn":
            try:
                count = int(args[1]) if len(args) > 1 else 3
            except ValueError:
                self.console.print(create_error_panel(
                    f"Invalid agent count: {args[1]}",
                    suggestion="Provide a number between 1 and 10"
                ))
                return True
            count = max(1, min(10, count))

            # Enforce maximum of 10 agents total
            available_slots = 10 - len(self.state.agents)
            if available_slots <= 0:
                self.console.print(create_error_panel(
                    "Maximum of 10 agents already reached",
                    suggestion="Use /agents stop to remove agents first"
                ))
                return True
            count = min(count, available_slots)

            # Create placeholder agents
            for i in range(count):
                agent_id = f"agent-{len(self.state.agents) + 1}"
                self.state.agents.append(AgentStatus(
                    id=agent_id,
                    type=f"specialist-{i + 1}",
                    status="idle",
                    progress=0,
                ))

            self.console.print(create_success_panel(
                f"Spawned {count} agents",
                details="Agents will activate on next query"
            ))
            return True

        elif subcommand == "list":
            if not self.state.agents:
                self.console.print(Panel(
                    Text(" No active agents", style="dim italic"),
                    border_style="dim"
                ))
                return True

            table = Table(
                title="[bold]Active Agents[/bold]",
                border_style=BRAND_SUCCESS,
                title_style=f"bold {BRAND_INFO}",
            )
            table.add_column("ID", style=BRAND_INFO)
            table.add_column("Type", style=BRAND_ACCENT)
            table.add_column("Status")
            table.add_column("Progress")
            table.add_column("Tokens", justify="right")

            for agent in self.state.agents:
                status_color = {
                    "running": BRAND_INFO,
                    "thinking": BRAND_WARNING,
                    "error": BRAND_ERROR,
                    "idle": "dim",
                    "complete": BRAND_SUCCESS,
                }.get(agent.status, "white")

                icon = STATUS_ICONS.get(agent.status, "○")

                # Progress bar
                bar_width = 10
                completed = int(agent.progress / 100 * bar_width)
                bar = "█" * completed + "░" * (bar_width - completed)

                table.add_row(
                    agent.id,
                    agent.type,
                    f"[{status_color}]{icon} {agent.status}[/]",
                    f"[{status_color}]{bar}[/] {agent.progress}%",
                    f"{agent.tokens:,}",
                )

            self.console.print(table)
            return True

        elif subcommand == "stop":
            target = args[1] if len(args) > 1 else "all"
            if target == "all":
                count = len(self.state.agents)
                self.state.agents.clear()
                self.console.print(create_success_panel(
                    f"Stopped all {count} agents"
                ))
            else:
                before = len(self.state.agents)
                self.state.agents = [a for a in self.state.agents if a.id != target]
                if len(self.state.agents) < before:
                    self.console.print(create_success_panel(f"Stopped agent: {target}"))
                else:
                    self.console.print(create_error_panel(
                        f"Agent not found: {target}",
                        suggestion="Use /agents list to see agent IDs"
                    ))
            return True

        elif subcommand == "status":
            return self._handle_agents(["list"])

        self.console.print(create_error_panel(
            f"Unknown agent subcommand: {subcommand}",
            suggestion="Use: spawn, list, stop, or status"
        ))
        return True

    def _handle_audit(self, args: list[str]) -> bool:
        """Run AI-powered security audit via api.aicippy.com + aivedha.ai.

        Usage:
            /audit              - Run full security audit
            /audit quick        - Run quick security scan
            /audit targeted     - Run targeted scan on changed files

        This is a cross-service feature that does NOT require CHAKRA plan
        validation. Uses the same auth.aivibe.cloud JWT token.
        """
        import asyncio as _audit_asyncio
        import time as _audit_time

        from aicippy.security.audit_client import SecurityAuditClient
        from aicippy.security.formatter import SecurityAuditFormatter
        from aicippy.security.models import AuditFinding, AuditStatus

        # 1. Check for auth token in session state
        auth_token: str | None = None
        try:
            from aicippy.auth.cognito import CognitoAuth
            auth = CognitoAuth()
            tokens = auth.get_current_tokens()
            if tokens and not tokens.is_expired():
                auth_token = tokens.access_token
        except Exception:
            pass

        if not auth_token:
            self.console.print(create_error_panel(
                "Authentication required for security audit",
                suggestion="Please log in first with 'aicippy login'"
            ))
            return True

        # Determine scan scope from args (NO plan check - special case)
        scan_scope = "full"
        if args:
            scope_arg = args[0].lower()
            if scope_arg in ("full", "quick", "targeted"):
                scan_scope = scope_arg
            else:
                self.console.print(create_error_panel(
                    f"Invalid audit scope: {scope_arg}",
                    suggestion="Valid scopes: full, quick, targeted"
                ))
                return True

        # 2. Create SecurityAuditClient with token (NO plan check)
        formatter = SecurityAuditFormatter(self.console)

        async def _run_audit() -> None:
            async with SecurityAuditClient(
                auth_token=auth_token,
                base_url=get_settings().app_api_url,
                ws_url=get_settings().websocket_url.replace("wss://", "wss://"),
            ) as client:
                # 3. Show spinner "Starting security audit..."
                self.console.print(Panel(
                    Text(
                        f" Starting {scan_scope} security audit...",
                        style=f"bold {BRAND_INFO}",
                    ),
                    border_style=BRAND_INFO,
                ))

                start_time = _audit_time.monotonic()

                try:
                    audit_id = await client.start_audit(scan_scope=scan_scope)
                except Exception as e:
                    self.console.print(create_error_panel(
                        f"Failed to start security audit: {e}",
                        suggestion="Check your connection and try again"
                    ))
                    return

                self.console.print(Text(
                    f"  Audit ID: {audit_id}",
                    style="dim",
                ))

                # 4. Stream results via WebSocket with progress display
                findings_count = 0

                def on_finding(finding: AuditFinding) -> None:
                    nonlocal findings_count
                    findings_count += 1
                    formatter.render_streaming_finding(finding)

                try:
                    result = await client.stream_audit_results(
                        audit_id=audit_id,
                        callback=on_finding,
                    )
                except (ConnectionError, TimeoutError):
                    # Fallback to polling if WebSocket fails
                    self.console.print(Text(
                        "  WebSocket unavailable, polling for results...",
                        style="dim italic",
                    ))

                    result = None
                    for _poll_attempt in range(60):
                        await _audit_asyncio.sleep(2.0)
                        try:
                            result = await client.get_audit_status(audit_id)
                            elapsed = _audit_time.monotonic() - start_time
                            self.console.print(
                                formatter.build_progress_panel(
                                    status=result.status.value,
                                    findings_count=len(result.findings),
                                    elapsed_seconds=elapsed,
                                )
                            )
                            if result.status in (
                                AuditStatus.COMPLETE,
                                AuditStatus.ERROR,
                            ):
                                break
                        except Exception as poll_err:
                            logger.warning(
                                "security_audit_poll_error",
                                error=str(poll_err),
                            )
                            continue

                    if result is None:
                        self.console.print(create_error_panel(
                            "Security audit timed out",
                            suggestion="Try again with /audit quick for a faster scan"
                        ))
                        return

                # 5. Format final results using the formatter
                formatter.render_full_report(result)

                # 6. Show fix recommendations for critical/high findings
                critical_high = [
                    f for f in result.findings
                    if f.severity.value in ("critical", "high")
                ]
                if critical_high:
                    self.console.print(Text(
                        f"\n  {len(critical_high)} critical/high finding(s) require attention.",
                        style=f"bold {BRAND_ERROR}",
                    ))
                    self.console.print(Text(
                        "  Use the API to get detailed fix recommendations for each finding.",
                        style="dim",
                    ))

        # Execute the async audit
        try:
            from aicippy.utils.async_utils import run_sync
            run_sync(_run_audit())
        except Exception as e:
            logger.warning("security_audit_handler_error", error=str(e))
            self.console.print(create_error_panel(
                f"Security audit failed: {e}",
                suggestion="Check your connection and try again"
            ))

        return True

    def _handle_kb(self, args: list[str]) -> bool:
        """Handle Knowledge Base commands."""
        if not args:
            self.console.print(Panel(
                Text(" Usage: /kb <sync|status|search>", style="dim"),
                border_style="dim"
            ))
            return True

        subcommand = args[0].lower()

        if subcommand == "sync":
            self.console.print(create_success_panel(
                "Knowledge Base sync initiated",
                details="Documents will be indexed in the background"
            ))
            return True

        elif subcommand == "status":
            status_table = Table(border_style=BRAND_INFO)
            status_table.add_column("Property", style=BRAND_INFO)
            status_table.add_column("Value", style="white")

            status_table.add_row("Documents", "1,234")
            status_table.add_row("Last Sync", "2 hours ago")
            status_table.add_row("Index Size", "45.2 MB")
            status_table.add_row("Status", f"[{BRAND_SUCCESS}]● Healthy[/]")

            self.console.print(Panel(
                status_table,
                title="[bold]Knowledge Base Status[/bold]",
                border_style=BRAND_INFO,
            ))
            return True

        elif subcommand == "search":
            query = " ".join(args[1:]) if len(args) > 1 else ""
            if not query:
                self.console.print(create_error_panel(
                    "Please provide a search query",
                    suggestion="Example: /kb search AWS Lambda"
                ))
                return True
            self.console.print(Panel(
                Text(f" 🔍 Searching for: {query}", style=BRAND_INFO),
                border_style="dim"
            ))
            return True

        self.console.print(create_error_panel(
            f"Unknown KB subcommand: {subcommand}",
            suggestion="Use: sync, status, or search"
        ))
        return True

    def _handle_tools(self, args: list[str]) -> bool:
        """Handle tools commands."""
        if not args:
            self.console.print(Panel(
                Text(" Usage: /tools <list|enable|disable>", style="dim"),
                border_style="dim"
            ))
            return True

        subcommand = args[0].lower()

        if subcommand == "list":
            table = Table(
                title="[bold]Available Tools[/bold]",
                border_style=BRAND_INFO,
            )
            table.add_column("Tool", style=BRAND_INFO)
            table.add_column("Status")
            table.add_column("Category", style="dim")
            table.add_column("Description")

            tools = [
                ("aws", True, "Cloud", "AWS CLI operations"),
                ("gcloud", True, "Cloud", "Google Cloud CLI"),
                ("gh", True, "VCS", "GitHub CLI"),
                ("firebase", True, "Cloud", "Firebase CLI"),
                ("figma", True, "Design", "Figma API"),
                ("drive", True, "Storage", "Google Drive API"),
                ("gmail", True, "Comm", "Gmail API"),
                ("canva", False, "Design", "Canva API"),
                ("azure", False, "Cloud", "Azure CLI"),
                ("razorpay", True, "Payment", "Razorpay API"),
                ("paypal", False, "Payment", "PayPal API"),
                ("stripe", True, "Payment", "Stripe CLI"),
                ("bash", True, "Shell", "Shell commands"),
            ]

            for name, enabled, category, desc in tools:
                status = f"[{BRAND_SUCCESS}]{STATUS_ICONS['complete']} Enabled[/]" if enabled else f"[dim]{STATUS_ICONS['pending']} Disabled[/]"
                table.add_row(name, status, category, desc)

            self.console.print(table)
            return True

        elif subcommand in ("enable", "disable"):
            tool_name = args[1] if len(args) > 1 else None
            if not tool_name:
                self.console.print(create_error_panel(
                    "Please specify a tool name",
                    suggestion="Example: /tools enable aws"
                ))
                return True

            action = "Enabled" if subcommand == "enable" else "Disabled"
            self.console.print(create_success_panel(f"{action} tool: {tool_name}"))
            return True

        self.console.print(create_error_panel(
            f"Unknown tools subcommand: {subcommand}",
            suggestion="Use: list, enable, or disable"
        ))
        return True

    def _handle_usage(self, args: list[str]) -> bool:
        """Show token usage and credits."""
        table = Table(
            title="[bold]Token Usage[/bold]",
            border_style=BRAND_WARNING,
        )
        table.add_column("Metric", style=BRAND_INFO)
        table.add_column("Value", style=BRAND_WARNING, justify="right")

        table.add_row("Session Tokens", f"{self.state.tokens_used:,}")
        table.add_row("Stream Tokens (last)", f"{self.state.stream_tokens:,}")
        table.add_row("Model", self.state.model.upper())
        table.add_row("Active Agents", str(len(self.state.agents)))
        table.add_row("Messages", str(len(self.state.conversation)))
        table.add_row("Session Duration", self.state.get_session_duration())

        # Credits info
        if self.state.is_admin:
            table.add_row("Credits", f"[{BRAND_SUCCESS}]Unlimited (Admin)[/{BRAND_SUCCESS}]")
        elif self.state.plan_info is not None:
            credits_str = f"{self.state.credits_remaining}/{self.state.plan_info.credits_total}"
            table.add_row("Credits Remaining", credits_str)
            if self.state.credit_manager:
                table.add_row("Credits Used (session)", str(self.state.credit_manager.session_credits_used))

        self.console.print(table)
        return True

    def _handle_history(self, args: list[str]) -> bool:
        """Show conversation history (persistent if logged in)."""
        # Use persistent history if available
        messages = []
        if self.state.conversation_history:
            messages = self.state.conversation_history.get_recent(15)
        elif self.state.conversation:
            messages = self.state.conversation[-10:]

        if not messages:
            self.console.print(Panel(
                Text(" No conversation history", style="dim italic"),
                border_style="dim"
            ))
            return True

        persistent_label = " (persistent)" if self.state.conversation_history else ""
        self.console.print(Text(f"\n History{persistent_label}:\n", style=f"bold {BRAND_INFO}"))

        for msg in messages:
            role = msg.get("role", "unknown")
            content = msg.get("content", "")[:120]
            role_color = BRAND_INFO if role == "user" else BRAND_SUCCESS
            icon = ">" if role == "user" else "*"
            timestamp = msg.get("timestamp", "")[:19]

            line = Text()
            line.append(f" {icon} ", style=role_color)
            if timestamp:
                line.append(f"[{timestamp}] ", style="dim")
            line.append(f"{role}: ", style=f"bold {role_color}")
            line.append(content, style="white")
            if len(msg.get("content", "")) > 120:
                line.append("...", style="dim")
            self.console.print(line)

        self.console.print()
        return True

    def _handle_clear(self, args: list[str]) -> bool:
        """Clear conversation (in-memory and persistent for current session)."""
        self.state.conversation.clear()
        self.state.tasks.clear()
        self.state.current_task_idx = 0
        self.state.tokens_used = 0
        self.state.pending_image_paths.clear()
        self.state.is_streaming = False
        self.state.stream_tokens = 0
        self.state.stream_content = ""
        if self.state.conversation_history:
            self.state.conversation_history.clear()
        self.console.clear()
        self.console.print(create_success_panel("Conversation and tasks cleared"))
        return True

    def _handle_export(self, args: list[str]) -> bool:
        """Export session."""
        import json

        format_type = args[0] if args else "json"

        if format_type not in ("json", "md"):
            self.console.print(create_error_panel(
                "Invalid format",
                suggestion="Use 'json' or 'md'"
            ))
            return True

        filename = f"aicippy_session_{datetime.now().strftime('%Y%m%d_%H%M%S')}.{format_type}"

        # Use persistent history if available, else in-memory conversation
        export_messages = self.state.conversation
        if self.state.conversation_history:
            all_msgs = self.state.conversation_history.get_all_messages()
            if all_msgs:
                export_messages = all_msgs

        if format_type == "json":
            with open(filename, "w", encoding="utf-8") as f:
                json.dump(
                    {
                        "session_id": self.state.session_id,
                        "model": self.state.model,
                        "mode": self.state.mode,
                        "tokens_used": self.state.tokens_used,
                        "conversation": export_messages,
                    },
                    f,
                    indent=2,
                    default=str,
                )
        else:
            with open(filename, "w", encoding="utf-8") as f:
                f.write("# AiCippy Session Export\n\n")
                f.write(f"**Session:** {self.state.session_id}\n")
                f.write(f"**Model:** {self.state.model}\n")
                f.write(f"**Tokens:** {self.state.tokens_used:,}\n\n")
                f.write("## Conversation\n\n")
                for msg in export_messages:
                    role = msg.get("role", "unknown")
                    msg_content = msg.get("content", "")
                    timestamp = msg.get("timestamp", "")
                    ts_header = f" ({timestamp})" if timestamp else ""
                    f.write(f"### {role.title()}{ts_header}\n\n{msg_content}\n\n")

        self.console.print(create_success_panel(
            f"Session exported",
            details=f"Saved to: {filename}"
        ))
        return True

    def _handle_upload(self, args: list[str]) -> bool:
        """
        Handle /upload command - opens OS file browser dialog.

        Usage:
            /upload             - Open file browser for current uploads folder
            /upload <path>      - Upload to specific folder
        """
        from pathlib import Path

        target_dir = None
        if args:
            target_path = Path(args[0]).expanduser()
            if target_path.is_dir():
                target_dir = target_path
            else:
                # Create if doesn't exist
                target_dir = target_path
                target_dir.mkdir(parents=True, exist_ok=True)

        try:
            results = self.upload_manager.prompt_upload(target_dir=target_dir)

            if results:
                successful = [r for r in results if r.success]
                if successful:
                    # Track image files for multimodal AI processing
                    image_exts = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"}
                    for r in successful:
                        if r.file_path and any(r.file_name.lower().endswith(ext) for ext in image_exts):
                            self.state.pending_image_paths.append(str(r.file_path))

                    image_count = len([
                        r for r in successful
                        if any(r.file_name.lower().endswith(ext) for ext in image_exts)
                    ])

                    tip_msg = "Reference uploaded files in your next prompt."
                    if image_count:
                        tip_msg = f"{image_count} image(s) queued for AI analysis. Your next message will include them."

                    self.console.print(Panel(
                        Text(
                            f"Tip: {tip_msg}\n"
                            f"   Example: \"Analyze the uploaded image\" or \"Use {successful[0].file_name} to...\"",
                            style="dim italic",
                        ),
                        border_style="dim",
                    ))

        except Exception as e:
            self.console.print(create_error_panel(
                f"Upload failed: {e}",
                suggestion="Check file permissions and try again"
            ))

        return True

    def _handle_uploads(self, args: list[str]) -> bool:
        """
        Handle /uploads command - list uploaded files.

        Usage:
            /uploads            - List all uploaded files
            /uploads clear      - Clear all uploads
        """
        if args and args[0].lower() == "clear":
            if self.upload_manager.clear_uploads():
                self.console.print(create_success_panel("All uploads cleared"))
            else:
                self.console.print(create_error_panel(
                    "Failed to clear uploads",
                    suggestion="Check file permissions and try again"
                ))
            return True

        self.upload_manager.show_uploads_list()
        return True

    def _handle_workspace(self, args: list[str]) -> bool:
        """
        Handle /workspace command - show working directory info.

        Usage:
            /workspace          - Show current workspace info
        """
        self.upload_manager.show_working_directory()
        return True

    def _handle_shell(self, args: list[str]) -> bool:
        """
        Handle /shell command - execute shell command with input passthrough.

        Usage:
            /shell <command>    - Execute command with interactive input support
            /exec <command>     - Alias for /shell

        Examples:
            /shell git status
            /shell npm install
            /exec pip install requests
        """
        if not args:
            self.console.print(Panel(
                Text(
                    "Usage: /shell <command>\n\n"
                    "Execute shell commands with interactive input passthrough.\n"
                    "Any prompts from the command will be displayed for you to respond.\n\n"
                    "Examples:\n"
                    "  /shell git status\n"
                    "  /shell npm install\n"
                    "  /shell pip install requests\n"
                    "  /exec docker login",
                    style=BRAND_INFO,
                ),
                title=f"[bold {BRAND_ACCENT}]Shell Command[/bold {BRAND_ACCENT}]",
                border_style=BRAND_ACCENT,
            ))
            return True

        command = " ".join(args)

        try:
            from aicippy.cli.shell_executor import ShellExecutor, is_interactive_command

            if self._shell_executor is None:
                self._shell_executor = ShellExecutor(
                    console=self.console,
                    working_dir=get_working_directory(),
                )

            # Check if command is interactive
            if is_interactive_command(command):
                # Run interactive command with input callback
                def get_input(prompt: str) -> str:
                    return self._shell_executor.prompt_user_input(prompt)

                # Use run_sync to safely run the interactive command
                from aicippy.utils.async_utils import run_sync
                result = run_sync(
                    self._shell_executor.execute_interactive(
                        command,
                        input_callback=get_input,
                    )
                )
            else:
                # Simple command execution
                result = self._shell_executor.execute_simple(command)

            # Store output in conversation for AI context
            if result.stdout:
                self.state.conversation.append({
                    "role": "system",
                    "content": f"[Shell Command Output]\n$ {command}\n{result.stdout[:2000]}",
                })

            # Display result
            self._shell_executor.display_result(result)

        except Exception as e:
            self.console.print(create_error_panel(
                f"Shell execution failed: {e}",
                suggestion="Check the command syntax and try again"
            ))

        return True

    def _handle_sessions(self, args: list[str]) -> bool:
        """List previous conversation sessions."""
        if not self.state.conversation_history:
            self.console.print(Panel(
                Text(" No persistent history available (not logged in)", style="dim italic"),
                border_style="dim"
            ))
            return True

        sessions = self.state.conversation_history.list_sessions()
        if not sessions:
            self.console.print(Panel(
                Text(" No previous sessions found", style="dim italic"),
                border_style="dim"
            ))
            return True

        table = Table(
            title="[bold]Previous Sessions[/bold]",
            border_style=BRAND_PRIMARY,
            title_style=f"bold {BRAND_INFO}",
        )
        table.add_column("Session ID", style=BRAND_INFO, max_width=20)
        table.add_column("Last Updated", style="white")
        table.add_column("Messages", style=BRAND_ACCENT, justify="right")

        for s in sessions[:15]:
            table.add_row(
                s["session_id"][:16] + "...",
                s.get("updated_at", "")[:19],
                str(s.get("message_count", 0)),
            )

        self.console.print(table)
        return True

    def _handle_memory(self, args: list[str]) -> bool:
        """Show or manage tenant memory."""
        if not self.state.tenant_memory:
            self.console.print(Panel(
                Text(" No tenant memory available (not logged in)", style="dim italic"),
                border_style="dim"
            ))
            return True

        if args and args[0].lower() == "clear":
            self.state.tenant_memory.clear()
            self.console.print(create_success_panel("Tenant memory cleared"))
            return True

        if args and args[0].lower() == "set" and len(args) >= 3:
            key = args[1]
            value = " ".join(args[2:])
            self.state.tenant_memory.set(key, value)
            self.console.print(create_success_panel(f"Memory set: {key}"))
            return True

        if args and args[0].lower() == "get" and len(args) >= 2:
            key = args[1]
            value = self.state.tenant_memory.get(key)
            if value is not None:
                self.console.print(Panel(
                    Text(f" {key} = {value}", style=BRAND_INFO),
                    border_style="dim"
                ))
            else:
                self.console.print(Panel(
                    Text(f" Key '{key}' not found", style="dim italic"),
                    border_style="dim"
                ))
            return True

        # Default: show summary
        summary = self.state.tenant_memory.get_summary()
        keys = self.state.tenant_memory.list_keys()

        if not summary and not keys:
            self.console.print(Panel(
                Text(" Tenant memory is empty\n\n Usage:\n   /memory set <key> <value>\n   /memory get <key>\n   /memory clear", style="dim"),
                border_style="dim"
            ))
            return True

        table = Table(
            title="[bold]Tenant Memory[/bold]",
            border_style=BRAND_ACCENT,
            title_style=f"bold {BRAND_ACCENT}",
        )
        table.add_column("Key", style=BRAND_INFO)
        table.add_column("Value", style="white", max_width=60)

        for key in keys[:20]:
            value = self.state.tenant_memory.get(key, "")
            display_val = str(value)[:60]
            if len(str(value)) > 60:
                display_val += "..."
            table.add_row(key, display_val)

        self.console.print(table)
        return True

    def _handle_plan(self, args: list[str]) -> bool:
        """Show subscription and credits information."""
        plan_info = self.state.plan_info
        if plan_info is None:
            self.console.print(Panel(
                Text(" Plan info unavailable (billing not initialized)", style="dim italic"),
                border_style="dim"
            ))
            return True

        table = Table(
            title="[bold]AiVibe Subscription[/bold]",
            border_style=BRAND_PRIMARY,
            title_style=f"bold {BRAND_INFO}",
        )
        table.add_column("Property", style=BRAND_INFO)
        table.add_column("Value", style="white")

        if plan_info.is_admin:
            table.add_row("Role", f"[bold {BRAND_SUCCESS}]Admin[/bold {BRAND_SUCCESS}]")
            table.add_row("Credits", f"[{BRAND_SUCCESS}]Unlimited[/{BRAND_SUCCESS}]")
            table.add_row("Plan Restrictions", f"[{BRAND_SUCCESS}]None (admin bypass)[/{BRAND_SUCCESS}]")
        else:
            plan_display = plan_info.plan.upper()
            status_color = BRAND_SUCCESS if plan_info.status.value == "active" else BRAND_ERROR
            table.add_row("Plan", f"[bold]{plan_display}[/bold]")
            table.add_row("Status", f"[{status_color}]{plan_info.status.value.upper()}[/{status_color}]")
            table.add_row("Tenant ID", plan_info.tenant_id)

            # Credits display
            if plan_info.credits_total > 0:
                used = plan_info.credits_total - plan_info.credits_remaining
                pct = max(0, min(100, int(plan_info.credits_remaining / plan_info.credits_total * 100)))
                bar_w = 20
                filled = max(0, min(bar_w, int(pct / 100 * bar_w)))
                bar = "\u2588" * filled + "\u2591" * (bar_w - filled)
                credits_display = (
                    f"{plan_info.credits_remaining}/{plan_info.credits_total} "
                    f"({used} used) [{bar}] {pct}%"
                )
            else:
                credits_display = str(plan_info.credits_remaining)
            table.add_row("Credits Remaining", credits_display)

            # Session credits
            if self.state.credit_manager:
                table.add_row(
                    "Session Credits Used",
                    str(self.state.credit_manager.session_credits_used),
                )

            # Expiry
            if plan_info.expiry:
                expiry_str = plan_info.expiry.strftime("%Y-%m-%d %H:%M UTC")
                table.add_row("Period Ends", expiry_str)

        self.console.print(table)
        return True

    def _handle_quit(self, args: list[str]) -> bool:
        """Exit the session - save state before returning."""
        _save_session_state(self.state)
        return False


# ============================================================================
# Session State Helpers
# ============================================================================


def _save_session_state(state: SessionState) -> int:
    """
    Save all persistent session state to disk.

    Saves conversation history, tenant memory preferences, and session context.
    Called on any exit path (/quit, Ctrl+C, Ctrl+D, SIGTERM, natural exit).

    Returns:
        Number of memory entries saved.
    """
    memory_entries_saved = 0

    try:
        if state.tenant_memory:
            state.tenant_memory.set_preference("last_model", state.model)
            state.tenant_memory.set_preference("theme", state.active_theme)
            state.tenant_memory.set_context("last_session", {
                "session_id": state.session_id,
                "tokens_used": state.tokens_used,
                "message_count": len(state.conversation),
                "duration": state.get_session_duration(),
            })
            memory_entries_saved = len(state.tenant_memory.list_keys())
    except Exception:
        pass

    try:
        if state.conversation_history:
            state.conversation_history.cleanup_old_sessions()
    except Exception:
        pass

    logger.info(
        "session_state_saved",
        session_id=state.session_id,
        tokens_used=state.tokens_used,
        messages=len(state.conversation),
        memory_entries=memory_entries_saved,
    )

    return memory_entries_saved


async def _cancel_pending_tasks() -> None:
    """Cancel any pending async tasks to prevent zombie coroutines."""
    current_task = asyncio.current_task()
    all_tasks = [
        t for t in asyncio.all_tasks()
        if t is not current_task and not t.done()
    ]
    for task in all_tasks:
        task.cancel()
    if all_tasks:
        await asyncio.gather(*all_tasks, return_exceptions=True)


async def _close_orchestrator_if_exists(orchestrator: object) -> None:
    """Close an orchestrator instance if it has a close method."""
    try:
        if orchestrator is not None and hasattr(orchestrator, "close"):
            await orchestrator.close()
    except Exception:
        pass


# ============================================================================
# Main Interactive Session
# ============================================================================


async def start_interactive_session() -> None:
    """Start the interactive AiCippy session with beautiful UI."""
    settings = get_settings()

    # Start background auto-updater (24-hour cycle)
    _bg_updater = None
    try:
        from aicippy.installer.auto_updater import get_auto_updater

        _bg_updater = get_auto_updater()
        if _bg_updater.needs_check():
            _bg_updater.start_background()
    except Exception:
        pass

    # Generate session ID
    session_id = generate_correlation_id()

    # Initialize state
    state = SessionState(
        session_id=session_id,
        model=settings.default_model,
    )

    # Add initial demo tasks
    state.add_task("Initialize session")
    state.start_task(0)

    # Create command handler
    cmd_handler = SlashCommandHandler(state, console)

    # Create the new interactive layout
    layout = InteractiveLayout(console, __version__)

    # Create prompt session with history
    history_file = settings.local_config_dir / "history"
    history_file.parent.mkdir(parents=True, exist_ok=True)

    # Custom prompt style - use bright white for input text (visible on both dark/light terminals)
    prompt_style = Style.from_dict({
        "prompt": f"{BRAND_PRIMARY} bold",
        "": "#e0e0e0 bold",  # Bright white for better contrast on all terminals
    })

    # Key bindings
    kb = KeyBindings()

    @kb.add(Keys.ControlL)
    def clear_screen(event: Any) -> None:
        """Clear the screen and preview."""
        console.clear()
        layout.clear_preview()

    completer = WordCompleter(
        SLASH_COMMANDS + MODEL_OPTIONS + MODE_OPTIONS + ["quit", "exit", "q", "bye"],
        ignore_case=True,
    )

    session: PromptSession[str] = PromptSession(
        history=FileHistory(str(history_file)),
        auto_suggest=AutoSuggestFromHistory(),
        completer=completer,
        style=prompt_style,
        key_bindings=kb,
    )

    # Skip welcome animation - shown during login
    # Just complete initialization silently
    state.complete_task(0)

    # Check authentication and get username (silently)
    try:
        from aicippy.auth.cognito import CognitoAuth
        from aicippy.installer.session_manager import get_current_user
        auth = CognitoAuth()
        if auth.is_authenticated():
            state.connected = True
            # Get username from session
            username, email = get_current_user()
            state.username = username or email or "authenticated"
            state.user_email = email or username or ""

            # Get user_id for memory isolation
            try:
                from aicippy.cli.main import get_session_manager
                sm = get_session_manager()
                validation = sm.validate()
                if validation.session:
                    state.user_id = validation.session.user_id
            except Exception:
                pass

            # Fall back to email-based user_id if session didn't provide one
            if not state.user_id:
                state.user_id = email or username or "anonymous"

            # Initialize persistent conversation history and tenant memory
            state.conversation_history = ConversationHistory(
                user_id=state.user_id,
                session_id=session_id,
            )
            state.tenant_memory = TenantMemory(user_id=state.user_id)

            # Store last model preference
            last_model = state.tenant_memory.get_preference("last_model")
            if last_model and last_model in ("opus", "sonnet", "haiku", "llama"):
                state.model = last_model

            # Restore saved theme preference
            saved_theme = state.tenant_memory.get_preference("theme")
            if saved_theme and saved_theme in get_available_themes():
                state.active_theme = saved_theme

            logger.info(
                "memory_initialized",
                user_id=state.user_id,
                history_messages=state.conversation_history.message_count,
                memory_keys=len(state.tenant_memory.list_keys()),
            )

            # Show brief status if reconnecting to existing session
            if state.conversation_history.message_count > 0:
                resuming_text = Text()
                resuming_text.append("Resuming session...", style="dim italic")
                console.print(resuming_text)
    except Exception as auth_check_err:
        logger.debug("auth_check_during_session_init_failed", error=str(auth_check_err))
        pass

    # Initialize billing (plan validation + credit manager) via platform API
    platform_client = None
    app_client = None
    try:
        from aicippy.billing.plan_validator import PlanValidator
        from aicippy.billing.credit_manager import CreditManager

        # Determine email for billing lookup (reuse from auth check above if available)
        billing_email = state.user_email if state.user_email else None
        if not billing_email:
            try:
                from aicippy.installer.session_manager import get_current_user as _get_user
                _u, _e = _get_user()
                billing_email = _e or _u
            except Exception:
                pass

        if billing_email:
            state.user_email = billing_email

            # Reuse validated plan_info and platform_client from login flow if available
            cached_plan_info = None
            try:
                from aicippy.cli.main import get_login_context
                cached_plan_info, cached_platform_client = get_login_context()
                if cached_platform_client is not None:
                    platform_client = cached_platform_client
            except Exception:
                pass

            if platform_client is None:
                # Try to create PlatformClient with stored auth token
                try:
                    from aicippy.auth.cognito import CognitoAuth
                    from aicippy.platform.client import PlatformClient
                    from aicippy.platform.app_client import AppClient

                    auth_inst = CognitoAuth()
                    tokens = auth_inst.get_current_tokens()
                    if tokens and not tokens.is_expired():
                        platform_client = PlatformClient(
                            base_url=settings.platform_api_url,
                            auth_token=tokens.access_token,
                        )
                except Exception:
                    pass  # Fall back to DynamoDB

            # Always create validator (needed for CreditManager)
            validator = PlanValidator(platform_client=platform_client)

            # Reuse cached plan_info if still fresh, otherwise re-validate
            if cached_plan_info is not None and not cached_plan_info.is_cache_stale:
                plan_info = cached_plan_info
                logger.debug("plan_info_reused_from_login_cache")
            else:
                plan_info = validator.validate(billing_email)

            state.plan_validator = validator
            state.plan_info = plan_info
            state.is_admin = plan_info.is_admin
            state.credits_remaining = plan_info.credits_remaining

            credit_mgr = CreditManager(validator, platform_client=platform_client)
            state.credit_manager = credit_mgr

            # Create AppClient for app-specific operations (memory, logs)
            if platform_client is not None and plan_info.tenant_id:
                try:
                    from aicippy.auth.cognito import CognitoAuth as _CognitoAuth
                    from aicippy.platform.app_client import AppClient

                    _auth = _CognitoAuth()
                    tokens = _auth.get_current_tokens()
                    if tokens:
                        app_client = AppClient(
                            base_url=settings.app_api_url,
                            auth_token=tokens.access_token,
                            tenant_id=plan_info.tenant_id,
                        )
                except Exception:
                    pass

            if not plan_info.is_valid_for_aicippy:
                # Block session if plan is invalid
                reason = plan_info.denial_reason or "Access denied"
                console.print(Panel(
                    f"[red bold]Access Denied[/red bold]\n\n{reason}",
                    title="[red]Plan Required[/red]",
                    border_style="red",
                ))
                return

            logger.info(
                "billing_initialized",
                email=billing_email,
                plan=plan_info.plan,
                credits=plan_info.credits_remaining,
                is_admin=plan_info.is_admin,
            )
    except Exception as billing_err:
        # Fail-closed: block access if billing can't be validated
        logger.warning("billing_init_failed", error=str(billing_err))
        console.print(Panel(
            f"[red bold]Billing Validation Failed[/red bold]\n\n"
            f"Unable to verify subscription: {billing_err}\n\n"
            f"[dim]Please try again later or contact support@aivibe.in[/dim]",
            title="[red]Service Unavailable[/red]",
            border_style="red",
        ))
        return

    # Upgrade to platform-backed memory if AppClient is available
    if app_client is not None and state.user_id:
        try:
            state.conversation_history = PlatformBackedHistory(
                user_id=state.user_id,
                session_id=session_id,
                app_client=app_client,
            )
            state.tenant_memory = PlatformBackedTenantMemory(
                user_id=state.user_id,
                app_client=app_client,
            )
            logger.info("platform_backed_memory_enabled", user_id=state.user_id)
        except Exception as mem_err:
            logger.warning("platform_memory_upgrade_failed", error=str(mem_err))
            # Keep using local memory (already initialized)

    # Only prompt for theme on first use - restore saved preference silently
    if state.tenant_memory:
        saved_theme = state.tenant_memory.get_preference("theme")
        if saved_theme and saved_theme in get_available_themes():
            state.active_theme = saved_theme
            # Silently restored - no prompt needed
        else:
            # First time or invalid saved theme - show theme selector
            try:
                selected_theme = prompt_theme_selection(console, state.active_theme)
                state.active_theme = selected_theme
                state.tenant_memory.set_preference("theme", selected_theme)
            except Exception:
                pass
    else:
        # No tenant memory available - show theme selector as fallback
        try:
            selected_theme = prompt_theme_selection(console, state.active_theme)
            state.active_theme = selected_theme
        except Exception:
            pass

    # Setup working directory silently
    working_dir = get_working_directory()

    # Create uploads folder silently
    try:
        uploads_dir = working_dir / "uploads"
        if not uploads_dir.exists():
            uploads_dir.mkdir(parents=True, exist_ok=True)
    except Exception:
        pass

    # Show compact workspace status line
    try:
        status_parts = []
        if state.user_email:
            status_parts.append(state.user_email)
        elif state.username:
            status_parts.append(state.username)
        if state.plan_info:
            plan_label = state.plan_info.plan.upper() if state.plan_info.plan else "PLAN"
            if state.is_admin:
                status_parts.append(f"{plan_label} unlimited credits")
            elif state.credits_remaining >= 0:
                status_parts.append(
                    f"{plan_label} {state.credits_remaining}/{state.plan_info.credits_total} credits"
                )
        if state.active_theme and state.active_theme != DEFAULT_THEME_NAME:
            status_parts.append(f"{state.active_theme} theme")
        home_dir = str(working_dir.home())
        workspace_display = str(working_dir).replace(home_dir, "~", 1) if str(working_dir).startswith(home_dir) else str(working_dir)
        status_parts.append(f"workspace: {workspace_display}")
        status_line = Text()
        status_line.append("\u2713 ", style="green")
        status_line.append(" | ".join(status_parts), style="dim")
        console.print(status_line)
    except Exception:
        pass

    # Main loop
    running = True
    _sigint_count = 0
    _last_orchestrator: Any = None

    def signal_handler(sig: int, frame: Any) -> None:
        nonlocal running, _sigint_count
        _sigint_count += 1
        if _sigint_count >= 2:
            # Second Ctrl+C - force quit after saving state
            _save_session_state(state)
            running = False
            raise KeyboardInterrupt
        layout.add_preview_line(
            "Press Ctrl+C again to force quit, or type /quit",
            style=BRAND_WARNING,
        )

    def sigterm_handler(sig: int, frame: Any) -> None:
        nonlocal running
        _save_session_state(state)
        running = False

    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, sigterm_handler)

    while running:
        try:
            # Reset sigint counter at each prompt iteration
            _sigint_count = 0

            # Increment mascot animation frame
            state.mascot_frame = (state.mascot_frame + 1) % 100

            # Update and display the layout
            updated_layout = layout.update(
                agents=[a.to_widget() for a in state.agents],
                tasks=state.tasks,
                current_task_idx=state.current_task_idx,
                session_model=state.model,
                tokens_used=state.tokens_used,
                username=state.username,
                session_duration=state.get_session_duration(),
                background_tasks=state.background_tasks,
                connected=state.connected,
                mascot_mood=state.mascot_mood,
            )
            console.print(updated_layout)

            # Create input prompt
            prompt_text = create_input_section(mode=state.mode, model=state.model)
            console.print(prompt_text, end="")

            # Get input
            user_input = await asyncio.get_running_loop().run_in_executor(
                None,
                lambda: session.prompt(""),
            )

            if not user_input.strip():
                continue

            # Reject excessively long input to prevent API errors and memory issues
            if len(user_input) > MAX_INPUT_LENGTH:
                console.print(create_error_panel(
                    f"Input too long ({len(user_input):,} chars). Maximum is {MAX_INPUT_LENGTH:,} characters.",
                    suggestion="Please shorten your message and try again."
                ))
                continue

            # Handle quit/exit commands via natural language
            user_input_lower = user_input.strip().lower()
            if user_input_lower in EXIT_PHRASES:
                _save_session_state(state)
                running = False
                continue

            # Handle slash commands
            if user_input.startswith("/"):
                running = cmd_handler.handle(user_input)
                continue

            # Log user input to preview
            layout.add_preview_line(f"❯ {user_input}", style=BRAND_INFO)

            # Regular chat message
            state.conversation.append({"role": "user", "content": user_input})

            # Persist to conversation history
            if state.conversation_history:
                state.conversation_history.add_user_message(user_input)

            # Add task for this query
            task_label = user_input[:30] + ("..." if len(user_input) > 30 else "")
            state.add_task(f"Process: {task_label}")
            state.start_task(len(state.tasks) - 1)

            # Collect pending image paths for this message
            current_image_paths = list(state.pending_image_paths) if state.pending_image_paths else None
            state.pending_image_paths.clear()

            # Get conversation history for context continuity
            context_history = None
            if state.conversation_history:
                context_history = state.conversation_history.get_context_messages()

            # Inject tenant memory summary into system context
            memory_context = ""
            if state.tenant_memory:
                memory_context = state.tenant_memory.get_summary()

            # PRE-INVOCATION: Check credits before calling Bedrock
            if state.credit_manager and state.user_email and not state.is_admin:
                has_credits, remaining = state.credit_manager.check_credits(state.user_email)
                if not has_credits:
                    console.print(create_error_panel(
                        "Monthly credits exhausted",
                        suggestion="Credits reset at the start of your next billing cycle. "
                        "Use /plan to check your subscription details."
                    ))
                    # Remove the task we just added
                    if state.tasks:
                        state.tasks.pop()
                    continue

            # Process with orchestrator
            try:
                from aicippy.agents.orchestrator import AgentOrchestrator

                orchestrator = AgentOrchestrator(
                    model_id=settings.get_model_id(state.model),
                    max_agents=len(state.agents) if state.agents else 3,
                )
                _last_orchestrator = orchestrator

                # Update agents status and mascot mood
                state.mascot_mood = "thinking"
                state.is_streaming = True
                state.stream_tokens = 0
                state.stream_content = ""

                for agent in state.agents:
                    agent.status = "thinking"
                    agent.progress = 0
                    layout.add_agent_update(agent.id, "thinking", "Processing...")

                # Show streaming indicator
                layout.preview_buffer.start_streaming() if hasattr(layout.preview_buffer, 'start_streaming') else layout.add_preview_line("◐ Processing...", style=BRAND_WARNING)

                # Show animated thinking indicator with live progress
                start_time = time.monotonic()
                animation_running = True
                frame_idx = 0
                streaming_state = StreamingState(is_streaming=True)

                async def process_with_streaming() -> Any:
                    """Process the chat request with enhanced streaming display."""
                    nonlocal animation_running, frame_idx, streaming_state
                    state.mascot_mood = "working"

                    # Resolve the active theme for colored display
                    active_theme = get_theme(state.active_theme)

                    # Spinner animation frames
                    spinner_frames = ("◐", "◓", "◑", "◒")

                    # Streaming callback for token-by-token display
                    def on_stream_token(token: str) -> None:
                        nonlocal streaming_state
                        streaming_state.content += token
                        streaming_state.tokens_received += 1
                        state.stream_tokens += 1
                        state.stream_content += token

                        # Update preview buffer with streaming
                        if hasattr(layout.preview_buffer, 'stream_token'):
                            layout.preview_buffer.stream_token(token)

                    def _build_enhanced_panel(ss: StreamingState, fidx: int) -> Panel:
                        """Build the enhanced streaming panel with theme colors."""
                        elapsed = time.monotonic() - start_time
                        spinner = spinner_frames[fidx % len(spinner_frames)]

                        # --- Header bar ---
                        header = Text()
                        if ss.is_streaming:
                            header.append(f" {spinner} ", style=f"bold {active_theme.accent}")
                            header.append("Working on it...", style=f"bold {active_theme.primary}")
                        else:
                            header.append(" \u2713 ", style=f"bold {active_theme.success}")
                            header.append("Complete", style=f"bold {active_theme.success}")

                        header.append("  \u2502  ", style="dim")
                        header.append(f"{ss.tokens_received:,}", style=f"bold {active_theme.warning}")
                        header.append(" tokens", style="dim")

                        if ss.tokens_per_second > 0:
                            header.append(f" ({ss.tokens_per_second:.1f} tok/s)", style="dim")

                        header.append("  \u2502  ", style="dim")
                        header.append(f"{elapsed:.1f}s", style=f"bold {active_theme.info}")
                        header.append(" elapsed", style="dim")

                        # --- Content area ---
                        content = Text()
                        content.append(ss.content)
                        if ss.is_streaming and ss.cursor_visible:
                            content.append("_", style=f"bold {active_theme.accent}")

                        # --- Progress bar for agents ---
                        progress_section = Text()
                        if state.agents:
                            progress_section.append("\n")
                            for agent in state.agents:
                                aicon = STATUS_ICONS.get(agent.status, "\u25cb")
                                acolor = {
                                    "running": active_theme.info,
                                    "thinking": active_theme.warning,
                                    "complete": active_theme.success,
                                    "error": active_theme.error,
                                    "idle": "dim",
                                }.get(agent.status, "dim")
                                bar_w = 12
                                filled = int(agent.progress / 100 * bar_w)
                                bar_str = "\u2588" * filled + "\u2591" * (bar_w - filled)
                                progress_section.append(f"  {aicon} ", style=acolor)
                                progress_section.append(f"{agent.type[:12]:<12} ", style="white")
                                progress_section.append(bar_str, style=acolor)
                                progress_section.append(f" {agent.progress}%\n", style="dim")

                        from rich.markdown import Markdown as _Md
                        body = _Md(str(content)) if not ss.is_streaming else Group(content, progress_section)

                        border = active_theme.primary if ss.is_streaming else active_theme.success
                        return Panel(
                            body,
                            title=str(header),
                            title_align="left",
                            border_style=border,
                            box=ROUNDED,
                            padding=(1, 2),
                        )

                    with Live(
                        _build_enhanced_panel(streaming_state, 0),
                        console=console,
                        transient=True,
                        refresh_per_second=8,
                    ) as live:
                        async def update_display() -> None:
                            """Update the live display with streaming content."""
                            nonlocal frame_idx, streaming_state
                            while animation_running:
                                frame_idx += 1
                                state.mascot_frame = frame_idx

                                # Toggle cursor visibility for blink effect
                                streaming_state.cursor_visible = frame_idx % 4 < 2

                                # Calculate tokens per second
                                elapsed = time.monotonic() - start_time
                                if elapsed > 0:
                                    streaming_state.tokens_per_second = streaming_state.tokens_received / elapsed

                                # Update progress for agents
                                for agent in state.agents:
                                    if agent.status == "thinking":
                                        agent.progress = min(95, agent.progress + 1)

                                # Update display with enhanced panel
                                live.update(_build_enhanced_panel(streaming_state, frame_idx))
                                await asyncio.sleep(0.125)

                        # Start animation task
                        animation_task = asyncio.create_task(update_display())

                        try:
                            # Build enriched message with memory context
                            enriched_message = user_input
                            if memory_context:
                                enriched_message = f"[User Context]\n{memory_context}\n\n[User Message]\n{user_input}"

                            # Process the actual chat request with images and history
                            result = await orchestrator.chat(
                                enriched_message,
                                image_paths=current_image_paths,
                                conversation_history=context_history,
                            )

                            # Mark streaming complete
                            streaming_state.is_streaming = False
                            streaming_state.content = result.content

                            return result
                        finally:
                            # Stop animation
                            animation_running = False
                            animation_task.cancel()
                            try:
                                await animation_task
                            except asyncio.CancelledError:
                                pass

                response = await process_with_streaming()

                # Close the orchestrator after each request to free resources
                await _close_orchestrator_if_exists(orchestrator)
                _last_orchestrator = None

                # Mark streaming complete
                state.is_streaming = False
                execution_time = time.monotonic() - start_time

                # End streaming in preview buffer
                if hasattr(layout.preview_buffer, 'end_streaming'):
                    layout.preview_buffer.end_streaming(
                        total_tokens=state.stream_tokens,
                        duration_seconds=execution_time,
                    )

                # Mark agents complete
                for agent in state.agents:
                    agent.status = "complete"
                    agent.progress = 100
                    layout.add_agent_update(agent.id, "complete", "Done")

                # Display final response with beautiful themed panel
                active_theme = get_theme(state.active_theme)
                resp_tokens = (
                    (response.usage.input_tokens + response.usage.output_tokens)
                    if response.usage else 0
                )
                resp_header_parts: list[str] = []
                if response.agent_type:
                    resp_header_parts.append(
                        f"[{active_theme.info}]\u25c8 {response.agent_type.value.upper()}[/]"
                    )
                if resp_tokens:
                    resp_header_parts.append(
                        f"[{active_theme.warning}]{resp_tokens:,} tokens[/]"
                    )
                if execution_time:
                    resp_header_parts.append(f"[dim]{execution_time:.1f}s[/dim]")
                resp_title = " \u2502 ".join(resp_header_parts) if resp_header_parts else "Response"

                from rich.markdown import Markdown as _RespMd
                console.print(Panel(
                    _RespMd(response.content),
                    title=resp_title,
                    title_align="left",
                    border_style=active_theme.success,
                    box=ROUNDED,
                    padding=(1, 2),
                ))

                # Update state
                state.conversation.append({"role": "assistant", "content": response.content})
                if response.usage:
                    state.tokens_used += response.usage.input_tokens + response.usage.output_tokens

                # Persist assistant response to conversation history
                if state.conversation_history:
                    state.conversation_history.add_assistant_message(response.content)

                # POST-INVOCATION: Deduct credit after successful response
                if state.credit_manager and state.user_email:
                    txn = state.credit_manager.deduct_credit(state.user_email, "aicippy_chat")
                    if txn.success:
                        state.credits_remaining = txn.credits_remaining
                    else:
                        logger.warning(
                            "credit_deduction_failed_post_response",
                            error=txn.error,
                            email=state.user_email,
                        )

                # AUTO-EXECUTE: Detect and offer to execute commands from AI response
                try:
                    ai_cmd_executor = AICommandExecutor(
                        console=console,
                        working_dir=get_working_directory(),
                        auto_confirm=False,  # Always ask for confirmation
                    )

                    detected_commands = ai_cmd_executor.detect_commands(response.content)

                    if detected_commands:
                        layout.add_preview_line(f"Detected {len(detected_commands)} command(s)", style=BRAND_INFO)

                        # Execute detected commands with passthrough
                        cmd_results = await ai_cmd_executor.execute_detected_commands(
                            response.content,
                            execute_all=False,
                        )

                        # Add execution results to conversation for AI context
                        execution_summary = ai_cmd_executor.get_execution_summary()
                        if execution_summary:
                            state.conversation.append({
                                "role": "system",
                                "content": f"[Command Execution Results]\n{execution_summary}",
                            })
                            layout.add_success("Command results added to context")

                except Exception as cmd_err:
                    logger.warning("ai_command_execution_warning", error=str(cmd_err))
                    # Don't fail the conversation if command detection/execution fails

                # Complete task
                state.complete_task()

                # Set mascot to happy after success
                state.mascot_mood = "happy"

                # Reset agents to idle
                for agent in state.agents:
                    agent.status = "idle"

                # Reset mascot to idle after a short happy moment
                await asyncio.sleep(0.5)
                state.mascot_mood = "idle"

            except Exception as e:
                logger.exception("chat_error", error=str(e))
                layout.add_error(str(e))
                console.print(create_error_panel(
                    str(e),
                    suggestion="Check your connection and try again"
                ))

                # Reset streaming and mascot state on error
                state.is_streaming = False
                state.mascot_mood = "idle"

                # Close orchestrator on error to free resources
                await _close_orchestrator_if_exists(_last_orchestrator)
                _last_orchestrator = None

                # Mark task as failed
                if state.tasks and state.current_task_idx < len(state.tasks):
                    state.tasks[state.current_task_idx].status = "pending"

        except EOFError:
            # Ctrl+D: save state before exiting
            _save_session_state(state)
            running = False
        except KeyboardInterrupt:
            # Second Ctrl+C arrives here from the signal handler
            _save_session_state(state)
            running = False

    # Stop background auto-updater
    if _bg_updater is not None:
        try:
            _bg_updater.stop()
        except Exception:
            pass

    # Ensure any remaining orchestrator is closed
    await _close_orchestrator_if_exists(_last_orchestrator)

    # Cancel any pending async tasks to prevent zombie coroutines
    await _cancel_pending_tasks()

    # Save session state (unified helper - idempotent if already saved)
    memory_entries_saved = _save_session_state(state)

    logger.info(
        "session_ended",
        session_id=session_id,
        tokens_used=state.tokens_used,
        messages=len(state.conversation),
        duration=state.get_session_duration(),
        memory_entries=memory_entries_saved,
    )

    # Farewell message with waving mascot and accurate session stats
    console.print()

    # Create farewell with mascot waving goodbye
    animator = MascotAnimator(mood=MascotMood.WAVING, use_mini=True)
    frames = animator.get_frames()
    farewell_mascot = animator.render_frame(frames[0])

    farewell_text = Text()
    farewell_text.append("\n")
    farewell_text.append(f" {STATUS_ICONS['complete']} ", style=BRAND_SUCCESS)
    farewell_text.append("Session ended. ", style=f"bold {BRAND_PRIMARY}")
    farewell_text.append("Goodbye!", style=f"bold {MASCOT_PRIMARY}")
    farewell_text.append(f"\n\n   Tokens used:      {state.tokens_used:,}", style="dim")
    farewell_text.append(f"\n   Messages sent:    {len(state.conversation)}", style="dim")
    farewell_text.append(f"\n   Session duration: {state.get_session_duration()}", style="dim")
    farewell_text.append(f"\n   Memory entries:   {memory_entries_saved}", style="dim")
    # Show credits used this session
    if state.credit_manager:
        credits_used = state.credit_manager.session_credits_used
        if state.is_admin:
            farewell_text.append(f"\n   Credits used:     {credits_used} (admin - unlimited)", style="dim")
        else:
            farewell_text.append(f"\n   Credits used:     {credits_used}", style="dim")
            farewell_text.append(f"\n   Credits left:     {state.credits_remaining}", style="dim")
    farewell_text.append("\n\n   See you next time!", style=f"italic {BRAND_ACCENT}")

    console.print(Panel(
        Group(
            Align.center(farewell_mascot),
            farewell_text,
        ),
        border_style=BRAND_PRIMARY,
        title="[bold]Bye![/bold]",
        title_align="center",
    ))
