"""Main application orchestrator for Kollabor CLI."""

import asyncio
import logging
import os
import re
import sys
from pathlib import Path
from typing import Any, Dict, Optional

from kollabor_config import ConfigService
from kollabor_events import EventBus
from kollabor_events.models import EventType, CommandResult
from kollabor_events.ready_message import ReadyMessageCollector
from kollabor_tui.input_handler import InputHandler
from kollabor_tui.terminal_renderer import TerminalRenderer
from kollabor_tui.status import StatusNavigationManager
from kollabor_tui import EventDrivenRenderLoop
from kollabor_tui.visual_effects import VisualEffects
from .llm import LLMService
from kollabor_plugins import KollaborPluginSDK
from kollabor_ai import KollaborConversationLogger, ProfileManager
from kollabor_agent import AgentManager
from kollabor_agent.mcp_integration import MCPIntegration
from .logging import setup_from_config
from kollabor_plugins import PluginRegistry
from .updates import VersionCheckService
from kollabor_events.dict_utils import deep_merge
from .version import __version__

logger = logging.getLogger(__name__)



class TerminalLLMChat:
    """Main Kollabor CLI application.

    Orchestrates all components including rendering, input handling,
    event processing, and plugin management.
    """

    def __init__(
        self,
        args=None,
        system_prompt_file: str | None = None,
        agent_name: str | None = None,
        profile_name: str | None = None,
        save_profile: bool = False,
        save_local: bool = False,
        skill_names: list[str] | None = None,
        plugin_registry=None,
    ) -> None:
        """Initialize the chat application.

        Args:
            args: Parsed CLI arguments namespace (includes plugin args).
            system_prompt_file: Optional path to a custom system prompt file
                               (overrides all other system prompt sources)
            agent_name: Optional agent name to use (e.g., "lint-editor")
            profile_name: Optional LLM profile name to use (e.g., "claude")
            save_profile: If True, save auto-created profile to config
            save_local: If True with save_profile, save to local project config
            skill_names: Optional list of skill names to load for the agent
            plugin_registry: Pre-initialized plugin registry (for startup optimization)
        """
        # Store CLI args for plugins to access
        self.args = args

        # Get configuration directory using standard resolution
        from kollabor_config.config_utils import (
            ensure_config_directory,
            get_conversations_dir,
            get_project_data_dir,
            initialize_config,
            initialize_system_prompt,
            initialize_user_directories,
            set_cli_system_prompt_file,
        )

        # Set CLI system prompt override if provided
        if system_prompt_file:
            set_cli_system_prompt_file(system_prompt_file)

        # Check if this is first install BEFORE creating directories
        global_config_path = Path.home() / ".kollabor-cli" / "config.json"
        self._is_first_install = not global_config_path.exists()

        self.config_dir = ensure_config_directory()
        logger.info(f"Using config directory: {self.config_dir}")

        # Set ROOT socket name for nested agents to inherit
        # All agents (user and spawned) register on this socket for visibility in /t view
        os.environ["KOLLABOR_ROOT_SOCKET"] = Path.cwd().name
        logger.debug(f"Set KOLLABOR_ROOT_SOCKET={os.environ['KOLLABOR_ROOT_SOCKET']}")

        # Initialize config.json (creates global with profiles, copies to local)
        initialize_config()

        # Initialize system prompt (copies default.md to config directories)
        initialize_system_prompt()

        # Initialize user-customizable directories (themes, status-widgets, mcp, plugins)
        initialize_user_directories()

        # Flag to indicate if we're in pipe mode (for plugins to check)
        self.pipe_mode = False

        # Flag to indicate if we're in simple mode (no fancy UI)
        self.simple_mode = getattr(args, "simple", False) if args else False

        # Initialize plugin registry (use pre-initialized if provided for startup optimization)
        if plugin_registry is not None:
            self.plugin_registry = plugin_registry
            logger.debug("Using pre-initialized plugin registry from CLI")
        else:
            # Try package installation directory first (for pip install), then cwd (for development)
            package_dir = Path(
                __file__
            ).parent.parent  # Go up from core/ to package root
            plugins_dir = package_dir / "plugins"
            if not plugins_dir.exists():
                plugins_dir = Path.cwd() / "plugins"  # Fallback for development mode
                logger.info(f"Using development plugins directory: {plugins_dir}")
            else:
                logger.info(f"Using installed package plugins directory: {plugins_dir}")

            self.plugin_registry = PluginRegistry(plugins_dir)
            self.plugin_registry.load_all_plugins()

        # Initialize configuration service with plugin registry
        # Always use fast_mode to skip expensive system prompt rendering in __init__
        # (LLM service builds its own system prompt independently in _build_system_prompt)
        self.config = ConfigService(
            self.config_dir / "config.json", self.plugin_registry, fast_mode=True
        )

        # Note: plugin configs are already merged in _initialize_config() via
        # load_complete_config(). No need to call update_from_plugins() again -
        # it would re-read all configs, re-discover plugins, and write to disk
        # which triggers the file watcher causing redundant reloads.

        # Initialize profile manager (for LLM endpoint profiles)
        self.profile_manager = ProfileManager(self.config)
        if profile_name:
            # CLI --profile is a one-time override, don't persist active selection
            if not self.profile_manager.set_active_profile(profile_name, persist=False):
                logger.warning(f"Profile '{profile_name}' not found, using default")
            elif save_profile:
                # Save profile to config if --save was used
                profile = self.profile_manager.get_profile(profile_name)
                if profile:
                    self.profile_manager.save_profile_values_to_config(
                        profile, local=save_local
                    )
                    location = "local project" if save_local else "global"
                    logger.info(f"Saved profile '{profile_name}' to {location} config")

        # Initialize agent manager (for agent/skill system)
        self.agent_manager = AgentManager(self.config)
        # Load default agent using priority system (CLI > project > global > fallback)
        if not self.agent_manager.load_default_agent(agent_name):
            logger.warning(
                "Failed to load any agent, system may not function correctly"
            )

        # If agent has a preferred profile, use it (unless profile was explicitly set)
        # Don't persist agent's profile - it's automatic based on agent selection
        if not profile_name:
            agent_profile = self.agent_manager.get_preferred_profile()
            if agent_profile:
                if self.profile_manager.set_active_profile(
                    agent_profile, persist=False
                ):
                    logger.info(f"Using agent's preferred profile: {agent_profile}")

        # Load skills if specified (requires an active agent)
        # Store for later injection into conversation after llm_service is initialized
        self._pending_skill_names = skill_names or []
        if skill_names:
            if self.agent_manager.get_active_agent():
                for skill_name in skill_names:
                    if self.agent_manager.load_skill(skill_name):
                        logger.info(f"Loaded skill: {skill_name}")
                    else:
                        logger.warning(f"Skill '{skill_name}' not found")
            else:
                logger.warning("Cannot load skills without an active agent")

        # Reconfigure logging now that config system is available
        # Skip for help mode to avoid creating log files
        if not getattr(self.args, "_help_pending", False):
            setup_from_config(self.config.config_manager.config)

        # Initialize version check service
        self.version_check_service = VersionCheckService(
            config=self.config, current_version=__version__
        )

        # Initialize core components
        self.event_bus = EventBus(config=self.config.config_manager.config)

        # Register services created before event_bus for lookup via service registry
        self.event_bus.register_service("profile_manager", self.profile_manager)
        self.event_bus.register_service("agent_manager", self.agent_manager)

        # Initialize widget-based status system
        from kollabor_tui.status import (
            StatusWidgetRegistry,
            StatusLayoutManager,
            StatusLayoutRenderer,
            register_core_widgets,
            register_script_widgets,
            ScriptWidgetManager,
        )

        self.widget_registry = StatusWidgetRegistry()
        register_core_widgets(self.widget_registry)

        # Register script-based widgets from ~/.kollabor-cli/status-widgets/
        self.script_widget_manager = ScriptWidgetManager()
        script_count = register_script_widgets(
            self.widget_registry, script_manager=self.script_widget_manager
        )
        if script_count > 0:
            logger.info(f"Registered {script_count} script widget(s)")

        self.layout_manager = StatusLayoutManager(self.config)
        self.layout_renderer = StatusLayoutRenderer(
            widget_registry=self.widget_registry,
            layout_manager=self.layout_manager,
        )
        # Propagate simple_mode to layout_renderer if enabled
        if self.simple_mode:
            self.layout_renderer.simple_mode = True
        logger.info("Widget-based status system initialized")

        # Initialize renderer with config
        self.renderer = TerminalRenderer(self.event_bus, self.config)

        # Propagate simple_mode to renderer and message_renderer if enabled
        if self.simple_mode:
            self.renderer.simple_mode = True
            if hasattr(self.renderer, "message_renderer"):
                self.renderer.message_renderer.simple_mode = True

        # Connect widget renderer to terminal renderer
        # Wire Application's layout_renderer (StatusLayoutRenderer) into TerminalRenderer
        # so the terminal can render status widgets. This is NOT self-referencing -
        # self.layout_renderer (Application) -> self.renderer.layout_renderer (TerminalRenderer)
        self.renderer.layout_renderer = self.layout_renderer

        # Initialize shell command service (for ! prefix commands)
        from kollabor_agent.shell_command_service import ShellCommandService

        self.shell_command_service = ShellCommandService(
            event_bus=self.event_bus, config=self.config, renderer=self.renderer
        )
        logger.info("Shell command service initialized")

        self.input_handler = InputHandler(
            self.event_bus,
            self.renderer,
            self.config,
            shell_command_service=self.shell_command_service,
        )

        # Give terminal renderer access to input handler for modal state checking
        self.renderer.input_handler = self.input_handler

        # Initialize status navigation manager for Tab navigation in status area
        self.navigation_manager = StatusNavigationManager(
            renderer=self.renderer,
            coordinator=self.renderer.message_coordinator,
            event_bus=self.event_bus,
            config=self.config,
        )
        self.navigation_manager.set_layout(self.layout_manager.get_layout())
        self.navigation_manager.set_widget_registry(self.widget_registry)
        self.navigation_manager.set_layout_manager(
            self.layout_manager
        )  # For layout editing
        self.navigation_manager.app = self  # Wire app for command execution
        self.layout_renderer.set_navigation_manager(self.navigation_manager)
        self.input_handler.navigation_manager = self.navigation_manager
        logger.info("Status navigation manager initialized")

        # Initialize visual effects system
        self.visual_effects = VisualEffects()

        # Initialize slash command system
        logger.info("About to initialize slash command system")
        self._initialize_slash_commands()

        # Initialize fullscreen plugin commands
        self._initialize_fullscreen_commands()
        logger.info("Slash command system initialization completed")

        # Initialize LLM core service components
        self.project_data_dir = get_project_data_dir()
        conversations_dir = get_conversations_dir()
        conversations_dir.mkdir(parents=True, exist_ok=True)
        self.conversation_logger = KollaborConversationLogger(conversations_dir)
        self.mcp_integration = MCPIntegration(event_bus=self.event_bus)
        self.plugin_sdk = KollaborPluginSDK()

        # Initialize permission system
        from kollabor_agent.permissions import (
            PermissionManager,
            RiskAssessor,
            PERMISSION_CONFIG_DEFAULTS,
            RiskAssessmentRules,
        )
        from .llm.permissions import PermissionHook

        # Merge permission config defaults
        self.config.config_manager.config = deep_merge(
            PERMISSION_CONFIG_DEFAULTS, self.config.config_manager.config
        )

        # Log permission config for debugging
        perm_config = self.config.config_manager.config.get("kollabor", {}).get(
            "permissions", {}
        )
        enabled = perm_config.get("enabled")
        mode = perm_config.get("approval_mode")
        logger.info(f"Permission config loaded: enabled={enabled}, mode={mode}")

        # Initialize risk assessor
        risk_rules = RiskAssessmentRules()
        self.risk_assessor = RiskAssessor(risk_rules, self.config.config_manager.config)

        # Initialize permission manager
        self.permission_manager = PermissionManager(
            config=self.config.config_manager.config,
            risk_assessor=self.risk_assessor,
            event_bus=self.event_bus,
            config_service=self.config,  # Pass ConfigService for persistence
        )

        logger.info(
            f"Permission manager initialized with mode: "
            f"{self.permission_manager.approval_mode}"
        )

        # Register permission_manager for service lookup
        self.event_bus.register_service("permission_manager", self.permission_manager)

        # Wire permission manager to layout manager for UI
        # Use self.renderer.layout_manager (LayoutManager from core/io/layout.py)
        # which has the show_permission_prompt method
        if hasattr(self.renderer, "layout_manager") and hasattr(
            self.renderer.layout_manager, "show_permission_prompt"
        ):
            # Import response handler
            from kollabor_agent.permissions import handle_confirmation_response

            # Create wrapper that converts ConfirmationResponse to PermissionDecision
            async def confirmation_callback_wrapper(details):
                # Show prompt and get user response
                response = await self.renderer.layout_manager.show_permission_prompt(
                    details
                )
                # Convert to PermissionDecision using response handler
                return await handle_confirmation_response(
                    self.permission_manager, details, response
                )

            self.permission_manager.set_confirmation_callback(
                confirmation_callback_wrapper
            )
            logger.info("Permission confirmation callback wired to layout manager")
        else:
            logger.warning(
                "Layout manager not available - permission prompts will be denied"
            )

        # Create permission hook (will be registered during async initialization)
        self.permission_hook = PermissionHook(self.permission_manager)

        logger.info(
            "Permission system initialized (hook will be registered during startup)"
        )

        # Initialize LLM service
        self.llm_service = LLMService(
            config=self.config,
            event_bus=self.event_bus,
            renderer=self.renderer,
            profile_manager=self.profile_manager,
            agent_manager=self.agent_manager,
        )

        # Register llm_service for service lookup
        self.event_bus.register_service("llm_service", self.llm_service)

        # Check for MCP configuration and provide user feedback
        mcp_config = Path.home() / ".kollabor-cli" / "mcp" / "mcp_settings.json"
        if not mcp_config.exists():
            logger.info("No MCP configuration found")
            # Display one-time setup message (not added to history to avoid clutter)
            try:
                setup_message = (
                    "[MCP Setup]\n"
                    "No MCP servers configured. External tools disabled.\n"
                    "Run '/mcp setup' to configure Model Context Protocol servers."
                )
                # Use display_thinking to show in thinking area without adding to history
                if hasattr(self.renderer, "display_thinking"):
                    self.renderer.display_thinking(setup_message, add_to_history=False)
                else:
                    logger.info(setup_message)
            except Exception as e:
                logger.warning(f"Failed to display MCP setup message: {e}")
        else:
            logger.info(f"MCP configuration found at {mcp_config}")

        # Set up widget context now that all services are available
        from kollabor_tui.status import WidgetContext

        widget_context = WidgetContext(
            llm_service=self.llm_service,
            profile_manager=self.profile_manager,
            agent_manager=self.agent_manager,
            config=self.config,
            layout_manager=self.layout_manager,
        )
        self.layout_renderer.set_context(widget_context)
        # Store reference for plugin widget access
        self._widget_context = widget_context

        # Create widget API for plugins
        from kollabor_tui.status import StatusWidgetAPI

        self._widget_api = StatusWidgetAPI(self.widget_registry)

        # Register hot reload callbacks for config changes
        self.config.register_reload_callback(self._on_config_reload)

        # Inject active skills as user messages (after LLM service is ready)
        active_agent = self.agent_manager.get_active_agent()
        if active_agent and active_agent.active_skills:
            for skill_name in active_agent.active_skills:
                skill = active_agent.get_skill(skill_name)
                if skill:
                    skill_message = f"## Skill: {skill_name}\n\n{skill.content}"
                    self.llm_service._add_conversation_message("user", skill_message)
                    logger.debug(f"Injected skill as user message: {skill_name}")

        # Configure renderer and UI settings (shared with hot reload)
        self._apply_renderer_config()
        self._apply_ui_theme()

        # Dynamically instantiate all discovered plugins
        self.plugin_instances = self.plugin_registry.instantiate_plugins(
            self.event_bus, self.renderer, self.config
        )

        # Task tracking for race condition prevention
        self.running = False
        self._startup_complete = False
        self._startup_ready = asyncio.Event()
        self._background_tasks = []
        self._task_lock = asyncio.Lock()

        logger.info("Kollabor CLI initialized")

    async def start(self, initial_message: str | None = None) -> None:
        """Start the chat application with guaranteed cleanup.

        Args:
            initial_message: Optional message to send after startup completes.
                           If provided, sends this message as the first user input
                           but stays in interactive mode for continued conversation.
        """
        logger.info("Application starting")

        render_task = None
        input_task = None
        self._needs_terminal_cleanup = (
            False  # Track if we need terminal restore on exit
        )

        # Handle early exits before try/finally (no cleanup needed)
        # Fast path for -h/--help: show help without banner
        if getattr(self.args, "_help_pending", False):
            logger.info("Help mode - skipping banner and LLM initialization")
            await self._initialize_plugins_for_commands()
            from .cli import print_full_help

            start_time = getattr(self.args, "_start_time", None)
            print_full_help(
                self.args._parser, self.input_handler.command_registry, start_time
            )
            logging.shutdown()  # Close log file handlers
            return

        # Fast path for unknown args: validate before banner/LLM initialization
        unknown_args = getattr(self.args, "_unknown_args", [])
        if unknown_args:
            logger.info("Validating unknown args before banner")
            await self._initialize_plugins_for_commands()
            from .cli import process_cli_command

            try:
                cli_command = process_cli_command(
                    self.args, self.input_handler.command_registry
                )
                # Valid command - store and continue to full initialization
                self.args.cli_command = cli_command
                self.args._unknown_args = []  # Clear to prevent re-processing
            except ValueError as e:
                # Invalid command - print error and return (caller handles exit code)
                self._print_error(str(e))
                logger.error(f"Invalid CLI command: {e}")
                return

        try:
            # Display banner for interactive/command mode
            await self._display_banner()
            self._needs_terminal_cleanup = True  # Now we need cleanup on exit

            # Register startup_ready event for service lookup (gates user input)
            self.event_bus.register_service("startup_ready", self._startup_ready)

            # Create event-driven render loop EARLY (before heavy init)
            self.render_loop = EventDrivenRenderLoop(
                render_callback=self._render_callback,
                input_callback=self._input_callback,
                target_fps=20.0,  # Periodic renders for animations
                input_poll_rate=100.0,  # 100Hz input polling
                name="MainRenderLoop",
            )
            logger.info("Event-driven render loop created")

            # Wire render loop to renderer for script widget refresh triggers
            self.renderer.render_loop = self.render_loop

            # Wire render loop to message coordinator for immediate input box render
            if hasattr(self.renderer, "message_coordinator"):
                self.renderer.message_coordinator.set_render_loop(self.render_loop)
                logger.info("Render loop wired to message coordinator")

            # Start main loops (input bar appears NOW - fast startup)
            self.running = True

            # CRITICAL: Set input_handler.running=True BEFORE starting render loop
            # to prevent race condition where render loop exits immediately
            self.input_handler.running = True

            render_task = self.create_background_task(
                self.render_loop.run(), "event_driven_render_loop"
            )
            input_task = self.create_background_task(
                self.input_handler.start(), "input_handler"
            )

            # Register LLM hooks early so USER_INPUT events are captured
            # (the handler gates on _startup_ready before doing real work)
            await self.llm_service.register_hooks()

            # Launch deferred startup as background task
            deferred_task = self.create_background_task(
                self._deferred_startup(initial_message=initial_message),
                "deferred_startup",
            )

            # Wait for completion
            await asyncio.gather(render_task, input_task)

        except KeyboardInterrupt:
            print("\r\n")
            # print("\r\nInterrupted by user")
            logger.info("Application interrupted by user")
        except Exception as e:
            logger.error(f"Application error during startup: {e}")
            raise
        finally:
            # Guaranteed cleanup - always runs regardless of how we exit
            logger.info("Executing guaranteed cleanup")
            await self.cleanup()

    async def _deferred_startup(self, initial_message: str | None = None) -> None:
        """Run heavy initialization in background after render loop starts.

        This allows the input bar to appear immediately while LLM, plugins,
        and version check initialize in the background. User input is gated
        by _startup_ready event so messages typed during init are held, not lost.
        """
        try:
            # Fire-and-forget: version check (network, 5s timeout)
            self.create_background_task(
                self._check_for_updates(), "version_check"
            )

            # Load config-based hooks EARLY (before LLM/MCP init so connect events fire)
            await self._load_config_hooks()

            # Initialize LLM core service (system prompt runs in thread)
            await self._initialize_llm_core()
            await asyncio.sleep(0)  # Yield to let render loop process frames

            # Check if provider has configuration error and display warning
            await self._display_provider_warning()

            # Initialize all plugins dynamically
            await self._initialize_plugins()
            await asyncio.sleep(0)  # Yield after plugin init

            # Process CLI command if provided (after plugins so all commands are registered)
            cli_command = await self._process_cli_command()
            if cli_command:
                exit_code = await self._execute_cli_command(cli_command)

                # Wait for any modal triggered by the CLI command to finish
                # (modal commands like /resume search emit MODAL_TRIGGER and return
                # immediately - we must wait for user interaction to complete)
                if hasattr(self, "input_handler") and hasattr(self.input_handler, "_modal_controller"):
                    mc = self.input_handler._modal_controller
                    from kollabor_events.models import CommandMode
                    while mc.command_mode != CommandMode.NORMAL:
                        await asyncio.sleep(0.1)

                # Determine if we should stay in interactive mode
                should_stay = getattr(self.args, "stay", False)

                # Some commands default to --stay when not in pipe mode
                if not should_stay and not self.pipe_mode:
                    cmd_name = cli_command.get("name", "")
                    cmd_def = cli_command.get("command_def")
                    stay_commands = {"permissions", "resume", "branch", "fork"}
                    if cmd_name in stay_commands:
                        should_stay = True
                        logger.info(f"{cmd_name} command defaults to interactive mode")
                    elif cmd_def and getattr(cmd_def, "plugin_name", "") == "fullscreen_integrator":
                        should_stay = True
                        logger.info("Fullscreen plugin command defaults to interactive mode")

                if not should_stay:
                    logger.info(f"CLI command complete, exiting with code {exit_code}")
                    await self.cleanup()
                    sys.exit(exit_code)

                logger.info("CLI command complete, continuing to interactive mode")

            # Display any CLI-loaded skills in the UI
            if hasattr(self, "_pending_skill_display") and self._pending_skill_display:
                if hasattr(self.renderer, "message_coordinator"):
                    self.renderer.message_coordinator.display_message_sequence(
                        self._pending_skill_display
                    )
                    logger.info(
                        f"Displayed {len(self._pending_skill_display)} skill messages"
                    )
                self._pending_skill_display = []

            # Display ready message with all stats collected
            await self._display_ready_message()

            # Wire script widget refresh scheduler
            await self.script_widget_manager.initialize_refresh_scheduler(
                self.event_bus
            )
            if hasattr(self.script_widget_manager, "_scheduler"):
                self.script_widget_manager._scheduler.set_render_loop(self.render_loop)
                logger.info("Render loop wired to refresh scheduler")

            # Wire display controller render loop
            if hasattr(self.input_handler, "display_controller"):
                self.input_handler.display_controller.set_render_loop(self.render_loop)
                logger.info("Render loop wired to display controller")

            # Emit SYSTEM_STARTUP event (hooks + plugins can observe)
            await self.event_bus.emit_with_hooks(
                EventType.SYSTEM_STARTUP,
                {"source": "application", "version": __version__},
                "application",
            )

            # Mark startup as complete
            self._startup_complete = True
            self._startup_ready.set()
            logger.info("Application startup complete (deferred)")

            # Check for first-run wizard after startup is ready
            await self._check_first_run_wizard()

            # Send initial message if provided
            if initial_message:
                logger.info(f"Sending initial message: {initial_message[:50]}...")
                await asyncio.sleep(0.1)
                await self.llm_service.process_user_input(initial_message)

        except asyncio.CancelledError:
            logger.info("Deferred startup cancelled")
            raise
        except Exception as e:
            logger.error(f"Error during deferred startup: {e}")
            import traceback
            traceback.print_exc()
        finally:
            # Always set startup_ready to prevent deadlocks on waiting input
            if not self._startup_ready.is_set():
                self._startup_ready.set()
                logger.warning("Startup ready set in finally (startup may be incomplete)")

    async def start_pipe_mode(self, piped_input: str, timeout: int = 120) -> None:
        """Start in pipe mode: process input and exit after response.

        Args:
            piped_input: Input text from stdin/pipe
            timeout: Maximum time to wait for processing in seconds (default: 120)
        """
        # Set a flag to indicate we're in pipe mode (plugins can check this)
        self.pipe_mode = True
        self.renderer.pipe_mode = True  # Also set on renderer for llm_service access
        # Propagate pipe_mode to message renderer
        if hasattr(self.renderer, "message_renderer"):
            self.renderer.message_renderer.pipe_mode = True

        try:
            # Initialize LLM core service
            await self._initialize_llm_core()

            # Register LLM hooks (in interactive mode this is done early in start())
            await self.llm_service.register_hooks()

            # Check if provider has configuration error - exit early in pipe mode
            api_service = self.llm_service.api_service
            if not api_service.is_provider_available():
                error_msg = api_service.get_provider_error()
                if error_msg:
                    print(
                        "Error: LLM provider not available due to configuration error:"
                    )
                    print(f"{error_msg}")
                    print()
                    print("Use /profile to fix the configuration.")
                    # Clean up before exiting
                    await self.cleanup()
                    return

            # Initialize plugins (they should check self.pipe_mode if needed)
            await self._initialize_plugins()

            # Mark startup as complete
            self._startup_complete = True
            self.running = True
            logger.info("Pipe mode initialized with plugins")

            # Send input to LLM and wait for response
            # The LLM service will handle the response display
            await self.llm_service.process_user_input(piped_input)

            # Wait for processing to start (max 10 seconds)
            start_timeout = 10
            start_wait = 0
            while not self.llm_service.is_processing and start_wait < start_timeout:
                await asyncio.sleep(0.1)
                start_wait += 0.1

            # Wait for processing to complete (including all tool calls and continuations)
            max_wait = timeout
            wait_time = 0
            while (
                self.llm_service.is_processing
                and not self.llm_service.cancel_processing
                and wait_time < max_wait
            ):
                await asyncio.sleep(0.1)
                wait_time += 0.1

            # Check if processing is still active after timeout
            timed_out = (
                self.llm_service.is_processing
                and not self.llm_service.cancel_processing
            )

            # Give a tiny bit of extra time for final display rendering
            await asyncio.sleep(0.2)

            if timed_out:
                # Timeout expired before processing completed
                import sys

                logger.warning(
                    f"Pipe mode timeout reached after {timeout}s - processing incomplete"
                )
                print(
                    f"\nWarning: Timeout reached after {timeout}s - response may be incomplete",
                    file=sys.stderr,
                )
                # Use exit code 124 (GNU timeout convention)
                sys.exit(124)
            else:
                logger.info("Pipe mode processing complete")

        except KeyboardInterrupt:
            logger.info("Pipe mode interrupted by user")
        except Exception as e:
            logger.error(f"Pipe mode error: {e}")
            import traceback

            traceback.print_exc()
            raise
        finally:
            # Cleanup
            self.running = False
            # Keep pipe_mode=True during cleanup so cancellation messages can be suppressed
            await self.cleanup()
            # DON'T reset pipe_mode here - let main.py's finally block check it to avoid double cleanup

    async def _display_banner(self) -> None:
        """Display Kollabor banner (version check deferred to background)."""
        # In simple mode, display minimal banner
        if self.simple_mode:
            print(f"Kollabor CLI v{__version__}")
            print()
        else:
            # Display Kollabor banner with version from package metadata
            kollabor_banner = self.renderer.create_kollabor_banner(f"v{__version__}")
            print(kollabor_banner)
            print()

    async def _display_ready_message(self) -> None:
        """Display ready message with stats from core and plugins."""
        # In simple mode, display minimal ready message
        if self.simple_mode:
            self.renderer.message_coordinator.display_message_sequence(
                [("system", "Ready. Type your message and press Enter.", {})]
            )
            return

        # Collect ready message stats from core and plugins
        ready_collector = ReadyMessageCollector()

        await self._add_core_ready_stats(ready_collector)

        # Emit SYSTEM_READY event for plugins to contribute
        await self.event_bus.emit_with_hooks(
            event_type=EventType.SYSTEM_READY,
            data={"collector": ready_collector},
            source="application",
        )

        # Format ready stats
        stats_message = ready_collector.format_for_display(max_items=6)
        logger.debug(f"Ready stats formatted message: '{stats_message}'")
        logger.debug(f"Ready stats item count: {ready_collector.get_count()}")

        # Ready message with gradient and bold Enter
        ready_msg = "Ready! "
        if stats_message:
            ready_msg += f"({stats_message}) "

        # Get global width for proper text wrapping
        from kollabor_tui.terminal_state import get_global_terminal_state

        ts = get_global_terminal_state()
        width = ts.get_global_width() if ts else 80

        # Wrap the stats part to fit within width, then add the instruction
        import textwrap

        wrapped_lines = textwrap.wrap(ready_msg.strip(), width=width)
        instruction = "Type your message and press Enter."

        # Build pre-formatted ready text
        output_parts = []
        for line in wrapped_lines:
            output_parts.append(
                self.visual_effects.apply_message_gradient(line, "dim_white")
            )
        output_parts.append("")
        gradient_instruction = self.visual_effects.apply_message_gradient(
            instruction[:-6], "dim_white"  # "Type your message and press "
        )
        bold_enter = f"\033[1m{'Enter'}\033[0m"
        gradient_end = self.visual_effects.apply_message_gradient(".", "dim_white")
        output_parts.append(gradient_instruction + bold_enter + gradient_end)
        output_parts.append("")

        ready_text = "\n".join(output_parts)
        self.renderer.message_coordinator.display_raw_text(ready_text)

    async def _check_for_updates(self) -> None:
        """Check for updates and display notification if newer version available."""
        try:
            # Initialize version check service
            await self.version_check_service.initialize()

            # Check for updates (uses cache if valid)
            release_info = await self.version_check_service.check_for_updates()

            # Display notification if newer version available
            if release_info:
                update_msg = (
                    f"\033[1;33mUpdate available:\033[0m "
                    f"v{release_info.version} is now available "
                    f"(current: v{__version__})\n"
                    f"\033[2;36mDownload:\033[0m {release_info.url}"
                )
                self.renderer.message_coordinator.display_raw_text(update_msg)
                logger.info(f"Update available: {release_info.version}")

        except Exception as e:
            # Graceful degradation - log but don't crash startup
            logger.warning(f"Failed to check for updates: {e}")

    async def _add_core_ready_stats(self, collector: ReadyMessageCollector) -> None:
        """Add core component statistics to ready message collector.

        Args:
            collector: ReadyMessageCollector to add stats to
        """
        logger.debug("Collecting core ready stats...")
        try:
            # System prompt modules count
            from kollabor_ai import PromptRenderer
            from kollabor_config.config_utils import (
                get_system_prompt_content,
                get_system_prompt_path,
            )

            raw_prompt = get_system_prompt_content()
            prompt_path = get_system_prompt_path()
            renderer = PromptRenderer(base_path=prompt_path.parent)
            includes = renderer.get_all_includes(raw_prompt)
            count = len(includes) if includes else 0
            logger.debug(f"Ready stats - System prompt modules count: {count}")
            if includes:
                collector.add(
                    category="system prompt",
                    count=len(includes),
                    label="modules",
                    priority=1000,
                    source="core",
                )

            # Hook count (all registered hooks across all event types)
            # Use hook_registry.hooks dict directly or get stats
            hook_stats = self.event_bus.hook_registry.get_registry_stats()
            hook_count = hook_stats.get("total_hooks", 0)
            logger.debug(f"Ready stats - Hook count: {hook_count}")
            if hook_count > 0:
                collector.add(
                    category="hooks",
                    count=hook_count,
                    label="active",
                    priority=900,
                    source="core",
                )

            # Plugin count (use instantiated plugins, not just discovered classes)
            plugin_count = (
                len(self.plugin_instances) if hasattr(self, "plugin_instances") else 0
            )
            logger.debug(f"Ready stats - Plugin count: {plugin_count}")
            if plugin_count > 0:
                collector.add(
                    category="plugins",
                    count=plugin_count,
                    label="active",
                    priority=800,
                    source="core",
                )

            # Status views count (old API - may not exist)
            status_view_count = 0
            if (
                hasattr(self.renderer, "status_renderer")
                and self.renderer.status_renderer
            ):
                if hasattr(self.renderer.status_renderer, "view_registry"):
                    status_view_count = len(
                        getattr(
                            self.renderer.status_renderer.view_registry, "views", []
                        )
                    )
            logger.debug(f"Ready stats - Status view count: {status_view_count}")
            if status_view_count > 0:
                collector.add(
                    category="status views",
                    count=status_view_count,
                    label="available",
                    priority=700,
                    source="core",
                )

        except Exception as e:
            logger.warning(f"Failed to collect core ready stats: {e}")

    async def _initialize_llm_core(self) -> None:
        """Initialize LLM core service components."""
        # Initialize LLM service
        await self.llm_service.initialize()
        logger.info("LLM core service initialized")

        # Inject CLI-loaded skills into conversation now that llm_service is ready
        # Also store for UI display after renderer is ready
        self._pending_skill_display = []
        if hasattr(self, "_pending_skill_names") and self._pending_skill_names:
            agent = self.agent_manager.get_active_agent()
            if agent:
                for skill_name in self._pending_skill_names:
                    skill = agent.get_skill(skill_name)
                    if skill and skill.content:
                        skill_message = f"## Skill: {skill_name}\n\n{skill.content}"
                        self.llm_service._add_conversation_message(
                            "user", skill_message
                        )
                        # Store for UI display
                        self._pending_skill_display.append(("user", skill_message, {}))
                        self._pending_skill_display.append(
                            ("system", f"[ok] Loaded skill: {skill_name}", {})
                        )
                        logger.info(
                            f"Injected skill content into conversation: {skill_name}"
                        )

        # Note: system_commands.llm_service uses dynamic lookup via event_bus.get_service()
        # so no manual wiring is needed here

        # Initialize conversation logger
        await self.conversation_logger.initialize()
        logger.info("Conversation logger initialized")

        # Note: MCP server discovery is handled in background by llm_service.initialize()
        # to avoid blocking startup (see llm_service._background_mcp_discovery)

        # Note: LLM service hooks are registered early in start() for fast input bar,
        # so we skip register_hooks() here. The handler gates on _startup_ready.

        # Register permission hook if available
        if hasattr(self, "permission_hook"):
            await self.permission_hook.register(self.event_bus)
            logger.info("Permission hook registered")

    async def _display_provider_warning(self) -> None:
        """Display warning if LLM provider has configuration error."""
        api_service = self.llm_service.api_service
        if not api_service.is_provider_available():
            error_msg = api_service.get_provider_error()
            if error_msg:
                # Truncate long error messages
                if len(error_msg) > 200:
                    error_msg = error_msg[:200] + "..."
                warning_text = (
                    f"\033[33m{'=' * 60}\033[0m\n"
                    f"\033[1;33m[!] LLM Provider Configuration Error\033[0m\n\n"
                    f"\033[33m{error_msg}\033[0m\n\n"
                    f"\033[1;37mUse /profile to fix the configuration.\033[0m\n"
                    f"\033[33m{'=' * 60}\033[0m"
                )
                self.renderer.message_coordinator.display_raw_text(warning_text)

    async def _initialize_plugins_for_commands(self) -> None:
        """Initialize plugins minimally for command registration only.

        Used by -h/--help to get plugin commands without starting
        background tasks or full LLM initialization.
        """
        initialized_instances = set()

        for plugin_name, plugin_instance in self.plugin_instances.items():
            instance_id = id(plugin_instance)
            if instance_id in initialized_instances:
                continue
            initialized_instances.add(instance_id)

            if hasattr(plugin_instance, "initialize"):
                # Minimal kwargs - no llm_service since it's not initialized
                init_kwargs = {
                    "args": self.args,
                    "event_bus": self.event_bus,
                    "config": self.config,
                    "command_registry": getattr(
                        self.input_handler, "command_registry", None
                    ),
                    "input_handler": self.input_handler,
                    "renderer": self.renderer,
                    "llm_service": None,  # Not available in list-commands mode
                    "conversation_logger": None,
                    "conversation_manager": None,
                }

                try:
                    import inspect

                    sig = inspect.signature(plugin_instance.initialize)
                    if len(sig.parameters) > 0:
                        await plugin_instance.initialize(**init_kwargs)
                    else:
                        await plugin_instance.initialize()
                    logger.debug(f"Initialized plugin for commands: {plugin_name}")
                except Exception as e:
                    # Log but don't fail - plugin might need llm_service
                    logger.debug(f"Plugin {plugin_name} init skipped: {e}")

        logger.info("Plugins initialized for command listing")

    async def _initialize_plugins(self) -> None:
        """Initialize all discovered plugins."""
        # Deduplicate plugin instances by ID (same instance may be stored under multiple keys)
        initialized_instances = set()

        for plugin_name, plugin_instance in self.plugin_instances.items():
            instance_id = id(plugin_instance)

            # Skip if we've already initialized this instance
            if instance_id in initialized_instances:
                continue

            initialized_instances.add(instance_id)

            if hasattr(plugin_instance, "initialize"):
                # Pass command registry, input handler, llm_service, renderer, and args to plugins
                init_kwargs = {
                    "args": self.args,  # Parsed CLI arguments including plugin args
                    "event_bus": self.event_bus,
                    "config": self.config,
                    "command_registry": getattr(
                        self.input_handler, "command_registry", None
                    ),
                    "input_handler": self.input_handler,
                    "renderer": self.renderer,
                    "llm_service": self.llm_service,
                    # Use llm_service's conversation_logger (the one actively logging)
                    "conversation_logger": getattr(
                        self.llm_service,
                        "conversation_logger",
                        self.conversation_logger,
                    ),
                    "conversation_manager": getattr(
                        self.llm_service, "conversation_manager", None
                    ),
                }

                # Check if initialize method accepts keyword arguments
                import inspect

                sig = inspect.signature(plugin_instance.initialize)
                if len(sig.parameters) > 0:
                    await plugin_instance.initialize(**init_kwargs)
                else:
                    await plugin_instance.initialize()
                logger.debug(f"Initialized plugin: {plugin_name}")

            if hasattr(plugin_instance, "register_hooks"):
                await plugin_instance.register_hooks()
                logger.debug(f"Registered hooks for plugin: {plugin_name}")

            # Yield between plugin inits so render loop stays responsive
            await asyncio.sleep(0)

        # Register system commands hooks (for modal command handling)
        if hasattr(self, "system_commands") and self.system_commands:
            await self.system_commands.register_hooks()
            logger.debug("Registered hooks for system commands")

        # Register application-level hooks (help overlay, etc.)
        await self._register_application_hooks()
        logger.debug("Registered application-level hooks")

        # Set plugin instances on LLM service for system prompt additions
        if hasattr(self, "llm_service") and self.llm_service:
            self.llm_service.set_plugin_instances(self.plugin_instances)

            # Inject TmuxPlugin into ToolExecutor for unified terminal execution
            tmux_plugin = None
            for plugin_name, plugin_instance in self.plugin_instances.items():
                if plugin_instance.__class__.__name__ == "TmuxPlugin":
                    tmux_plugin = plugin_instance
                    logger.info(
                        "Found TmuxPlugin instance for ToolExecutor integration"
                    )
                    break

            if tmux_plugin and hasattr(self.llm_service, "tool_executor"):
                self.llm_service.tool_executor.tmux_plugin = tmux_plugin
                logger.info(
                    "Injected TmuxPlugin into ToolExecutor for unified terminal execution"
                )
            elif not tmux_plugin:
                logger.debug(
                    "TmuxPlugin not available - ToolExecutor will use fallback ShellExecutor"
                )

            # Check if any plugin wants to add to system prompt and rebuild if needed
            additions = self.llm_service._prompt_builder._get_plugin_system_prompt_additions()
            if additions:
                self.llm_service.rebuild_system_prompt()
                logger.info(
                    f"System prompt rebuilt with {len(additions)} plugin additions"
                )

    async def _load_config_hooks(self) -> None:
        """Load JSON-based hooks from hooks.json (Claude Code compatible)."""
        try:
            from kollabor.config_hooks import ConfigHookLoader

            loader = ConfigHookLoader(self.event_bus, self.config)
            count = await loader.load_and_register()
            self._config_hook_loader = loader
            if count > 0:
                logger.info(f"Loaded {count} config hooks")
        except Exception as e:
            logger.warning(f"Config hooks load failed: {e}")

    def _initialize_slash_commands(self) -> None:
        """Initialize the slash command system with core commands."""
        logger.info("Starting slash command system initialization...")
        try:
            from kollabor.commands.system_commands import SystemCommandsPlugin

            logger.info("SystemCommandsPlugin imported successfully")

            # Create and register system commands
            # Note: llm_service is passed if available, but may be None at this point
            self.system_commands = SystemCommandsPlugin(
                command_registry=self.input_handler.command_registry,
                event_bus=self.event_bus,
                config_manager=self.config,
                llm_service=getattr(self, "llm_service", None),
                profile_manager=getattr(self, "profile_manager", None),
                agent_manager=getattr(self, "agent_manager", None),
            )
            logger.info("SystemCommandsPlugin instance created")

            # Register all system commands
            # Note: system_commands accesses app services via event_bus.get_service()
            self.system_commands.register_commands()
            logger.info("System commands registration completed")

            # Register MCP commands if MCP integration is available
            if (
                hasattr(self, "llm_service")
                and self.llm_service
                and hasattr(self.llm_service, "mcp_integration")
            ):
                from kollabor.commands.mcp_command import register_mcp_commands

                try:
                    self.mcp_commands = register_mcp_commands(
                        command_registry=self.input_handler.command_registry,
                        mcp_integration=self.llm_service.mcp_integration,
                        renderer=self.renderer,
                        app=self,
                    )
                    logger.info("MCP commands registered successfully")
                except Exception as e:
                    logger.warning(f"Failed to register MCP commands: {e}")

            # /status-setup is a fullscreen plugin in plugins/fullscreen/status_setup_plugin.py

            stats = self.input_handler.command_registry.get_registry_stats()
            logger.info("Slash command system initialized with system commands")
            logger.info(f"[INFO] {stats['total_commands']} commands registered")

        except Exception as e:
            logger.error(f"Failed to initialize slash command system: {e}")
            import traceback

            logger.error(f"[INFO] Traceback: {traceback.format_exc()}")

    async def _check_first_run_wizard(self) -> None:
        """Check if this is first run and launch setup wizard if needed."""
        try:
            # Only show wizard on first install (when global config didn't exist before)
            if not self._is_first_install:
                logger.info("Not a first install, skipping wizard")
                return

            # Double-check the config flag in case wizard was already run
            setup_completed = self.config.get("application.setup_completed", False)
            if setup_completed:
                logger.info("Setup already completed, skipping wizard")
                return

            # Check if we have the fullscreen integrator
            if (
                not hasattr(self, "fullscreen_integrator")
                or not self.fullscreen_integrator
            ):
                logger.warning("Fullscreen integrator not available, skipping wizard")
                return

            # Check if setup plugin is registered
            if "setup" not in self.fullscreen_integrator.registered_plugins:
                logger.info("Setup wizard plugin not found, skipping")
                return

            logger.info("First run detected - launching setup wizard")

            # Get the setup plugin instance and pass managers
            plugin_class = self.fullscreen_integrator.registered_plugins["setup"]
            plugin_instance = plugin_class()
            plugin_instance.set_managers(self.config, self.profile_manager)

            # Ensure fullscreen manager is initialized (it's lazily created in command handlers)
            if not self.fullscreen_integrator._fullscreen_manager:
                from kollabor_tui.fullscreen import FullScreenManager

                self.fullscreen_integrator._fullscreen_manager = FullScreenManager(
                    self.fullscreen_integrator.event_bus,
                    self.fullscreen_integrator.terminal_renderer,
                )

            # Register and launch
            self.fullscreen_integrator._fullscreen_manager.register_plugin(
                plugin_instance
            )
            await self.fullscreen_integrator._fullscreen_manager.launch_plugin("setup")

            # Check wizard completion status and mark setup as completed
            if plugin_instance.completed:
                logger.info("Setup wizard completed successfully")
            elif plugin_instance.skipped:
                logger.info("Setup wizard skipped by user")
            else:
                logger.info("Setup wizard exited")

            # Mark setup as completed to avoid showing wizard on next startup
            self.config.set("application.setup_completed", True)

        except Exception as e:
            logger.error(f"Error launching setup wizard: {e}")
            import traceback

            logger.error(f"Setup wizard traceback: {traceback.format_exc()}")
            # Don't fail startup if wizard fails
            # Mark as completed so we don't retry
            self.config.set("application.setup_completed", True)

    async def _show_first_run_help_modal(self) -> None:
        """Show first-run help modal for interactive status widgets."""
        try:
            from kollabor_tui.status.help_system import show_first_run_help
            from kollabor_events.models import UIConfig

            logger.info("Checking if first-run help modal should be shown...")

            # Create a simple callback that shows modal via modal_controller
            async def show_modal_callback(ui_config: UIConfig) -> None:
                logger.info("Showing first-run help modal...")
                if hasattr(self.input_handler, "_modal_controller"):
                    await self.input_handler._modal_controller._enter_modal_mode(
                        ui_config
                    )
                    logger.info("First-run help modal displayed successfully")
                else:
                    logger.warning(
                        "modal_controller not available, skipping first-run help modal"
                    )

            # Show first-run help (checks internal config flag)
            result = await show_first_run_help(
                show_modal_callback=show_modal_callback,
                config=self.config,
            )

            logger.info(f"First-run help result: {result}")

        except Exception as e:
            logger.error(f"Error showing first-run help modal: {e}")
            import traceback

            logger.error(f"First-run help traceback: {traceback.format_exc()}")

    def _initialize_fullscreen_commands(self) -> None:
        """Initialize dynamic fullscreen plugin commands."""
        try:
            from kollabor.fullscreen.command_integration import FullScreenCommandIntegrator

            # Create the integrator with managers for plugins that need them
            self.fullscreen_integrator = FullScreenCommandIntegrator(
                command_registry=self.input_handler.command_registry,
                event_bus=self.event_bus,
                config=self.config,
                profile_manager=self.profile_manager,
                terminal_renderer=self.renderer,
                app=self,  # Pass app reference for plugins that need full access
            )

            # Discover and register all fullscreen plugins
            # Use same plugin directory resolution as main plugin registry
            package_dir = Path(__file__).parent.parent
            plugins_dir = package_dir / "plugins"
            if not plugins_dir.exists():
                plugins_dir = Path.cwd() / "plugins"
            registered_count = self.fullscreen_integrator.discover_and_register_plugins(
                plugins_dir
            )

            logger.info(
                f"Fullscreen plugin commands initialized: "
                f"{registered_count} plugins registered"
            )

        except Exception as e:
            logger.error(f"Failed to initialize fullscreen commands: {e}")
            import traceback

            logger.error(f"Fullscreen commands traceback: {traceback.format_exc()}")

    async def _render_callback(self, delta_time: float, trigger) -> bool:
        """Render callback for event-driven loop.

        Args:
            delta_time: Time since last frame
            trigger: RenderTrigger (INPUT, TIMER, FORCED, INITIAL)

        Returns:
            True to continue, False to exit loop
        """
        # Check if we need to stop
        if not self.running:
            return False

        # Render active area
        try:
            await self.renderer.render_active_area()
        except Exception as e:
            logger.error(f"Render callback error: {e}", exc_info=True)
            # Continue loop despite render error

        # Continue loop
        return True

    async def _input_callback(self):
        """Input polling callback for event-driven loop.

        Returns:
            (input_processed, should_exit)
            - (True, False): Input processed, don't exit
            - (True, True): Input processed and should exit
            - (False, False): No input
        """
        # Check if input handler has processed input since last poll
        if hasattr(self.input_handler, "_input_loop_manager"):
            processed = self.input_handler._input_loop_manager.has_pending_input()

            # Check if we should exit
            should_exit = not self.running or not self.input_handler.running

            return (processed, should_exit)

        return (False, not self.running)

    def create_background_task(self, coro, name: str = "unnamed"):
        """Create and track a background task with automatic cleanup.

        Args:
            coro: Coroutine to run as background task
            name: Human-readable name for the task

        Returns:
            The created asyncio.Task
        """
        task = asyncio.create_task(coro)
        task.set_name(name)
        self._background_tasks.append(task)
        logger.debug(f"Created background task: {name}")

        # Add callback to remove task from tracking when done
        def remove_task(t):
            try:
                self._background_tasks.remove(t)
                logger.debug(f"Background task completed: {name}")
            except ValueError:
                pass  # Task already removed

        task.add_done_callback(remove_task)
        return task

    # =========================================================================
    # CLI Slash Command Support
    # =========================================================================

    async def _process_cli_command(self) -> Optional[dict]:
        """Process CLI command from args once command registry is available.

        Returns:
            CLI command dict if valid command found, None otherwise.
        """
        from .cli import process_cli_command

        try:
            cli_command = process_cli_command(
                self.args, self.input_handler.command_registry
            )
            return cli_command
        except ValueError as e:
            self._print_error(str(e))
            logger.error(f"CLI command parse error (post-init): {e}")
            # Return None instead of sys.exit() - terminal is in raw mode,
            # sys.exit here would leave it corrupted. Caller falls through
            # to interactive mode which is safer than a broken terminal.
            return None

    async def _execute_cli_command(self, cli_command: dict) -> int:
        """Execute a CLI slash command with security and permissions.

        Args:
            cli_command: Dict with 'name', 'args', 'raw', 'requires_interactive', 'command_def'

        Returns:
            Exit code: 0 = success, 1 = failure, 2 = error

        State Management:
            When using --stay flag, the application enters interactive mode after
            the CLI command completes. Any state changes made by the command
            (permissions, config, etc.) persist into the interactive session.

            Example: `kollab --permissions trust --stay` leaves trust mode active.

            Commands that should not persist state should set `cli_hidden=True`
            and document interactive-only usage.
        """
        try:
            # Check if command requires interactive mode
            if cli_command.get("requires_interactive") and not getattr(
                self.args, "stay", False
            ):
                self._print_error(
                    f"Command /{cli_command['name']} requires interactive mode.\n"
                    f"Start kollabor: kollab\n"
                    f"Then run: /{cli_command['name']} {' '.join(cli_command['args'])}"
                )
                return 2

            # Parse the command through slash parser
            command = self.input_handler.slash_parser.parse_command(cli_command["raw"])
            if not command:
                self._print_error(f"Failed to parse command: {cli_command['raw']}")
                return 2

            # Execute through command executor (includes permission checks!)
            result = await self.input_handler.command_executor.execute_command(
                command, self.event_bus
            )

            # Display result with sanitized output (skip when modal is the display)
            if result and result.message and not (result.ui_config and result.ui_config.type == "modal"):
                self._print_command_result(result)

            # Determine exit code
            if not result:
                return 1
            if not result.success:
                return 1
            return 0

        except KeyboardInterrupt:
            logger.info("CLI command interrupted by user")
            return 130  # Standard SIGINT exit code
        except Exception as e:
            logger.error(f"CLI command execution failed: {e}", exc_info=True)
            self._print_error(f"Error: {e}")
            return 1

    def _print_command_result(self, result: "CommandResult") -> None:
        """Print command result with sanitized output.

        Strips ANSI escape sequences if not a TTY.
        """
        message = result.message

        # Strip ANSI codes if output is redirected
        if not sys.stdout.isatty():
            # Remove all ANSI escape sequences
            ansi_escape = re.compile(r"\x1b\[[0-9;]*[mKH]|\x1b\][^\x07]*\x07")
            message = ansi_escape.sub("", message)

        # Print with appropriate color if TTY
        if sys.stdout.isatty():
            if result.display_type == "error":
                print(f"\033[31m{message}\033[0m\n")
            elif result.display_type == "warning":
                print(f"\033[38;5;208m{message}\033[0m\n")
            elif result.display_type == "success":
                print(f"\033[32m{message}\033[0m\n")
            else:
                print(message)
        else:
            print(message)

    def _print_error(self, message: str) -> None:
        """Print error message to stderr."""
        if sys.stderr.isatty():
            print(f"\033[31mError:\033[0m {message}", file=sys.stderr)
        else:
            print(f"Error: {message}", file=sys.stderr)

    async def cleanup(self) -> None:
        """Clean up all resources and cancel background tasks.

        This method is guaranteed to run on all exit paths via finally block.
        Ensures no orphaned tasks or resources remain.
        """
        logger.info("Starting application cleanup...")

        # Cancel all tracked background tasks
        if self._background_tasks:
            logger.info(f"Cancelling {len(self._background_tasks)} background tasks")
            for task in self._background_tasks[
                :
            ]:  # Copy list to avoid modification during iteration
                if not task.done():
                    task.cancel()

            # Wait for cancelled tasks individually (asyncio.gather wrapping
            # cancelled tasks creates nested future chains that cause recursive
            # cancel() calls → RecursionError on deep task trees)
            pending = [t for t in self._background_tasks if not t.done()]
            if pending:
                try:
                    done, still_pending = await asyncio.wait(
                        pending, timeout=5.0
                    )
                    if still_pending:
                        logger.warning(f"{len(still_pending)} tasks did not complete within timeout")
                except Exception as e:
                    logger.error(f"Error during task cleanup: {e}")

        # Clear task list
        self._background_tasks.clear()

        # Mark startup as incomplete
        self._startup_complete = False
        self.running = False

        # Call full shutdown to cleanup other resources
        await self.shutdown()

        logger.info("Application cleanup complete")

    def _apply_renderer_config(self) -> None:
        """Apply thinking effect and shimmer config to renderer.

        Called from __init__ and _on_config_reload to avoid duplication.
        """
        if not hasattr(self, "renderer"):
            return

        thinking_effect = self.config.get("terminal.thinking_effect", "shimmer")
        shimmer_speed = self.config.get("terminal.shimmer_speed", 3)
        shimmer_wave_width = self.config.get("terminal.shimmer_wave_width", 4)
        thinking_limit = self.config.get("terminal.thinking_message_limit", 2)

        self.renderer.set_thinking_effect(thinking_effect)
        self.renderer.configure_shimmer(shimmer_speed, shimmer_wave_width)
        self.renderer.configure_thinking_limit(thinking_limit)

    def _apply_ui_theme(self) -> None:
        """Apply UI theme and border style from config.

        Called from __init__ and _on_config_reload to avoid duplication.
        """
        from kollabor_tui.design_system import set_theme, THEMES, set_border_style, BORDER_STYLES

        ui_theme = self.config.get("kollabor.ui.theme", "lime")
        try:
            set_theme(ui_theme)
            logger.debug(f"UI theme set to: {ui_theme}")
        except ValueError:
            available = list(THEMES.keys())
            logger.warning(
                f"Unknown theme '{ui_theme}', using 'lime'. Available: {available}"
            )

        border_style = self.config.get("kollabor.ui.border_style", "half_blocks")
        if border_style in BORDER_STYLES:
            set_border_style(border_style)
            logger.debug(f"Border style set to: {border_style}")
        else:
            available = list(BORDER_STYLES.keys())
            logger.warning(
                f"Unknown border style '{border_style}', using 'half_blocks'. "
                f"Available: {available}"
            )

    def _on_config_reload(self) -> None:
        """Handle configuration reload (hot reload support).

        Called when configuration changes via /config modal or file watcher.
        Updates all services with new configuration values.
        """
        logger.info("Hot reloading application configuration...")

        # Reload LLM service settings
        if hasattr(self, "llm_service"):
            self.llm_service.reload_config()

        # Reload logging level
        from .logging import set_level

        new_level = self.config.get("logging.level", "INFO")
        set_level(new_level)

        # Reload renderer and UI settings (shared with init)
        self._apply_renderer_config()
        self._apply_ui_theme()

        # Invalidate render cache so new theme colors take effect
        if hasattr(self, "renderer") and self.renderer:
            self.renderer.invalidate_render_cache()

        # Reload terminal width configuration (invalidate cache)
        from kollabor_tui.terminal_state import reload_width_config

        reload_width_config()
        logger.debug("Terminal width configuration reloaded")

        logger.info("Hot reload complete")

    def get_system_status(self):
        """Get current system status for monitoring and debugging.

        Returns:
            Dictionary containing system status information
        """
        return {
            "running": self.running,
            "startup_complete": self._startup_complete,
            "background_tasks": len(self._background_tasks),
            "plugins_loaded": len(self.plugin_instances),
            "task_names": [task.get_name() for task in self._background_tasks],
        }

    def get_widget_api(self):
        """Get the status widget API for plugin widget registration.

        Plugins can use this API to register their own status widgets
        that users can add to the status area.

        Returns:
            StatusWidgetAPI instance or None if not initialized

        Example usage in a plugin:
            widget_api = self.app.get_widget_api()
            if widget_api:
                widget_api.register_widget(
                    id="my-widget",
                    name="My Widget",
                    description="Shows custom info",
                    render_fn=self._render_widget,
                )
        """
        return getattr(self, "_widget_api", None)

    async def _register_application_hooks(self) -> None:
        """Register application-level event hooks.

        Registers hooks for:
        - SHOW_HELP_OVERLAY: F1 or ? key to show keyboard shortcuts
        - SHOW_FIRST_RUN_HELP: First Tab into navigation mode shows welcome help
        """
        from kollabor_events.models import Hook, HookPriority, EventType

        # Register help overlay handler
        help_overlay_hook = Hook(
            plugin_name="application",
            name="show_help_overlay",
            event_type=EventType.SHOW_HELP_OVERLAY,
            callback=self._handle_show_help_overlay,
            priority=HookPriority.DISPLAY,
        )
        await self.event_bus.register_hook(help_overlay_hook)
        logger.debug("Registered SHOW_HELP_OVERLAY hook")

        # Register first-run help handler
        first_run_help_hook = Hook(
            plugin_name="application",
            name="show_first_run_help",
            event_type=EventType.SHOW_FIRST_RUN_HELP,
            callback=self._handle_first_run_help_event,
            priority=HookPriority.DISPLAY,
        )
        await self.event_bus.register_hook(first_run_help_hook)
        logger.debug("Registered SHOW_FIRST_RUN_HELP hook")

    async def _handle_show_help_overlay(
        self, event_data: Dict[str, Any], context: str = None
    ) -> Dict[str, Any]:
        """Handle SHOW_HELP_OVERLAY event (triggered by F1 or ? key).

        Shows a modal with keyboard shortcuts for navigation and interaction.
        Detects current mode (INPUT, NAVIGATION, INTERACTION) and displays it.

        Args:
            event_data: Event data (contains 'source' key).
            context: Hook execution context.

        Returns:
            Result dict with success status.
        """
        try:
            from kollabor_tui.status import show_help_overlay
            from kollabor_events.models import UIConfig

            # Determine current mode
            mode = "INPUT"  # Default mode

            if hasattr(self, "navigation_manager") and self.navigation_manager:
                state = self.navigation_manager.state
                if state.interaction_active:
                    mode = "INTERACTION"
                elif await state.is_active():
                    mode = "NAVIGATION"

            logger.info(f"Showing help overlay (current mode: {mode})")

            # Create modal callback for showing the help overlay
            async def show_modal_callback(ui_config: UIConfig) -> Dict[str, Any]:
                """Callback to show modal via ModalController."""
                if hasattr(self.input_handler, "_modal_controller"):
                    modal_controller = self.input_handler._modal_controller
                    # Use the modal controller's trigger handler
                    return await modal_controller._handle_modal_trigger(
                        {"ui_config": ui_config}, "help_overlay"
                    )
                return {"success": False, "error": "Modal controller not available"}

            # Show help overlay via the modal system
            result = await show_help_overlay(
                show_modal_callback=show_modal_callback,
                current_mode=mode,
            )

            logger.info(f"Help overlay result: {result}")
            return result

        except Exception as e:
            logger.error(f"Error handling SHOW_HELP_OVERLAY event: {e}", exc_info=True)
            return {"success": False, "error": str(e)}

    async def _handle_first_run_help_event(
        self, event_data: Dict[str, Any], context: str = None
    ) -> Dict[str, Any]:
        """Handle SHOW_FIRST_RUN_HELP event (triggered on first Tab into navigation).

        Shows a welcome modal explaining the new interactive status widgets feature.

        Args:
            event_data: Event data (contains 'ui_config' key with modal configuration).
            context: Hook execution context.

        Returns:
            Result dict with success status.
        """
        try:
            ui_config = event_data.get("ui_config")
            if not ui_config:
                return {"success": False, "error": "No ui_config provided"}

            logger.info("Showing first-run help modal via event handler")

            # Show modal via modal controller
            if hasattr(self.input_handler, "_modal_controller"):
                await self.input_handler._modal_controller._enter_modal_mode(ui_config)
                logger.info("First-run help modal displayed successfully")
                return {"success": True}
            else:
                logger.warning("modal_controller not available for first-run help")
                return {"success": False, "error": "Modal controller not available"}

        except Exception as e:
            logger.error(
                f"Error handling SHOW_FIRST_RUN_HELP event: {e}", exc_info=True
            )
            return {"success": False, "error": str(e)}

    async def shutdown(self) -> None:
        """Shutdown the application gracefully."""
        logger.info("Application shutting down")
        self.running = False

        # Emit SYSTEM_SHUTDOWN event for config hooks
        try:
            await self.event_bus.emit_with_hooks(
                EventType.SYSTEM_SHUTDOWN,
                {"source": "application", "reason": "user_exit"},
                "application",
            )
        except Exception as e:
            logger.debug(f"SYSTEM_SHUTDOWN emit failed: {e}")

        # Drain pending async config hook tasks
        if hasattr(self, "_config_hook_loader"):
            try:
                await self._config_hook_loader.shutdown()
            except Exception:
                pass

        # Stop script widget refresh scheduler
        await self.script_widget_manager.shutdown_refresh_scheduler()

        # Stop input handler
        await self.input_handler.stop()

        # Shutdown LLM core service
        await self.llm_service.shutdown()
        await self.conversation_logger.shutdown()
        await self.mcp_integration.shutdown()
        logger.info("LLM core service shutdown complete")

        # Shutdown version check service
        if hasattr(self, "version_check_service"):
            await self.version_check_service.shutdown()
            logger.debug("Version check service shutdown complete")

        # Shutdown all plugins dynamically
        for plugin_name, plugin_instance in self.plugin_instances.items():
            if hasattr(plugin_instance, "shutdown"):
                try:
                    await plugin_instance.shutdown()
                    logger.debug(f"Shutdown plugin: {plugin_name}")
                except Exception as e:
                    logger.warning(f"Error shutting down plugin {plugin_name}: {e}")

        # Only do terminal cleanup if we entered interactive mode
        if getattr(self, "_needs_terminal_cleanup", False):
            # Clear active area (input box) before restoring terminal
            if not self.pipe_mode:
                self.renderer.clear_active_area(force=True)

            # Restore terminal
            self.renderer.exit_raw_mode()
            # Only show cursor if not in pipe mode
            if not self.pipe_mode:
                print("\033[?25h")  # Show cursor

        logger.info("Application shutdown complete")
