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

import asyncio
import logging
import re
import sys
from pathlib import Path
from importlib.metadata import version as get_version, PackageNotFoundError

from .config import ConfigService

def _get_version_from_pyproject() -> str:
    """Read version from pyproject.toml for development mode."""
    try:
        pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
        if pyproject_path.exists():
            content = pyproject_path.read_text()
            for line in content.splitlines():
                if line.startswith("version ="):
                    # Extract version from: version = "0.4.10"
                    return line.split("=")[1].strip().strip('"').strip("'")
    except Exception:
        pass
    return None  # Return None if not found

def _is_running_from_source() -> bool:
    """Check if we're running from source (development mode) vs installed package."""
    try:
        # If pyproject.toml exists in parent directory, we're running from source
        pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
        return pyproject_path.exists()
    except Exception:
        return False

# Get version: prefer pyproject.toml when running from source, otherwise use installed version
if _is_running_from_source():
    # Development mode: use pyproject.toml
    __version__ = _get_version_from_pyproject() or "0.0.0"
else:
    # Production mode: use installed package version
    try:
        __version__ = get_version("kollabor")
    except PackageNotFoundError:
        __version__ = "0.0.0"
from .events import EventBus
from .events.models import EventType, Event
from .events.ready_message import ReadyMessageCollector
from .io import InputHandler, TerminalRenderer
from .io.visual_effects import VisualEffects
from .llm import LLMService, KollaborConversationLogger, MCPIntegration, KollaborPluginSDK
from .llm.profile_manager import ProfileManager
from .llm.agent_manager import AgentManager
from .logging import setup_from_config
from .plugins import PluginRegistry
from .updates import VersionCheckService

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,
    ) -> 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
        """
        # Store CLI args for plugins to access
        self.args = args

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

        # 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}")

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

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

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

        # Initialize plugin registry and discover plugins
        # 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
        self.config = ConfigService(self.config_dir / "config.json", self.plugin_registry)

        # Update config file with plugin configurations
        self.config.update_from_plugins()

        # 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)
        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
        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)

        # Initialize status view registry for flexible status display (legacy)
        from .io.status_renderer import StatusViewRegistry
        from .io.config_status_view import ConfigStatusView
        self.status_registry = StatusViewRegistry(self.event_bus)

        # Add config status view to registry
        config_status_view = ConfigStatusView(self.config, self.event_bus)
        config_view_config = config_status_view.get_status_view_config()
        self.status_registry.register_status_view("core", config_view_config)

        # Initialize new widget-based status system
        from .io.status import (
            StatusWidgetRegistry,
            StatusLayoutManager,
            StatusLayoutRenderer,
            register_core_widgets,
        )
        self.widget_registry = StatusWidgetRegistry()
        register_core_widgets(self.widget_registry)
        self.layout_manager = StatusLayoutManager(self.config)
        self.layout_renderer = StatusLayoutRenderer(
            widget_registry=self.widget_registry,
            layout_manager=self.layout_manager,
        )
        logger.info("Widget-based status system initialized")

        # Initialize renderer with status registry and config
        self.renderer = TerminalRenderer(self.event_bus, self.config)
        if hasattr(self.renderer, 'status_renderer'):
            self.renderer.status_renderer.status_registry = self.status_registry

        # Connect new widget renderer to terminal renderer
        self.renderer.layout_renderer = self.layout_renderer

        # Initialize shell command service (for ! prefix commands)
        from .llm.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 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()
        self.plugin_sdk = KollaborPluginSDK()
        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,
        )

        # Set up widget context now that all services are available
        from .io.status import WidgetContext
        widget_context = WidgetContext(
            llm_service=self.llm_service,
            profile_manager=self.profile_manager,
            agent_manager=self.agent_manager,
            config=self.config,
        )
        self.layout_renderer.set_context(widget_context)
        # Store reference for plugin widget access
        self._widget_context = widget_context

        # Create widget API for plugins
        from .io.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 with thinking effect and shimmer parameters
        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)

        # Configure UI theme
        from .ui.design_system import set_theme, THEMES
        ui_theme = self.config.get("core.ui.theme", "lime")
        if ui_theme in THEMES:
            set_theme(ui_theme)
            logger.debug(f"UI theme set to: {ui_theme}")
        else:
            logger.warning(f"Unknown theme '{ui_theme}', using 'lime'. Available: {list(THEMES.keys())}")
        
        # 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._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.
        """
        # Display banner first (without ready message)
        await self._display_banner()

        logger.info("Application starting")

        render_task = None
        input_task = None

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

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

            # Initialize all plugins dynamically
            await self._initialize_plugins()

            # Register default core status views
            await self._register_core_status_views()

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

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

            # Start main loops with task tracking (needed for raw mode and input routing)
            self.running = True
            render_task = self.create_background_task(
                self._render_loop(), "render_loop"
            )
            input_task = self.create_background_task(
                self.input_handler.start(), "input_handler"
            )

            # Wait a moment for input handler to initialize (enter raw mode, register hooks)
            await asyncio.sleep(0.1)

            # Check for first-run and launch setup wizard if needed
            await self._check_first_run_wizard()

            # Send initial message if provided
            if initial_message:
                logger.info(f"Sending initial message: {initial_message[:50]}...")
                # Wait a bit longer to ensure input handler is fully ready
                await asyncio.sleep(0.2)
                # Process initial message through LLM service
                await self.llm_service.process_user_input(initial_message)

            # 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 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 and conversation renderer
        if hasattr(self.renderer, 'message_renderer'):
            self.renderer.message_renderer.pipe_mode = True
            if hasattr(self.renderer.message_renderer, 'conversation_renderer'):
                self.renderer.message_renderer.conversation_renderer.pipe_mode = True

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

            # 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(f"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

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

            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 and check for updates."""
        # Display Kollabor banner with version from package metadata
        kollabor_banner = self.renderer.create_kollabor_banner(f"v{__version__}")
        print(kollabor_banner)

        # Check for updates
        await self._check_for_updates()
        print()

    async def _display_ready_message(self) -> None:
        """Display ready message with stats from core and plugins."""
        # 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}) "
        ready_msg += "Type your message and press "
        enter_text = "Enter"
        end_text = "."

        # Apply white to dim white gradient to the message
        gradient_msg = self.visual_effects.apply_message_gradient(ready_msg, "dim_white")
        bold_enter = f"\033[1m{enter_text}\033[0m"  # Bold Enter
        gradient_end = self.visual_effects.apply_message_gradient(end_text, "dim_white")

        print(gradient_msg + bold_enter + gradient_end)
        print()
        print()
        print()
        print()

    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__})"
                )
                download_msg = f"\033[2;36mDownload:\033[0m {release_info.url}"

                print(update_msg)
                print(download_msg)
                print()  # Spacing before plugin list

                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 .utils.prompt_renderer import PromptRenderer
            from .utils.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)
            logger.debug(f"Ready stats - System prompt modules count: {len(includes) if includes else 0}")
            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)
            hook_count = sum(
                len(hooks) for hooks in self.event_bus.hook_registry.get_all_hooks().values()
            )
            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
            plugin_count = len(self.plugin_registry.list_plugins())
            logger.debug(f"Ready stats - Plugin count: {plugin_count}")
            if plugin_count > 0:
                collector.add(
                    category="plugins",
                    count=plugin_count,
                    label="loaded",
                    priority=800,
                    source="core"
                )

            # Status views count
            status_view_count = len(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")

        # Update system_commands with llm_service reference (it was None during init)
        if hasattr(self, 'system_commands') and self.system_commands:
            self.system_commands.llm_service = self.llm_service
            logger.debug("Updated system_commands with llm_service reference")

        # 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)

        # Register LLM service hooks for user input processing
        await self.llm_service.register_hooks()

    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:
                # Display warning in yellow/orange color
                print("\033[33m" + "=" * 60 + "\033[0m")
                print("\033[1;33m[!] LLM Provider Configuration Error\033[0m")
                print()
                # Truncate long error messages
                if len(error_msg) > 200:
                    error_msg = error_msg[:200] + "..."
                print(f"\033[33m{error_msg}\033[0m")
                print()
                print("\033[1;37mUse /profile to fix the configuration.\033[0m")
                print("\033[33m" + "=" * 60 + "\033[0m")
                print()

    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}")

        # 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")

        # 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._get_plugin_system_prompt_additions()
            if additions:
                self.llm_service.rebuild_system_prompt()
                logger.info(f"System prompt rebuilt with {len(additions)} plugin additions")

    def _initialize_slash_commands(self) -> None:
        """Initialize the slash command system with core commands."""
        logger.info("Starting slash command system initialization...")
        try:
            from core.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
            self.system_commands.register_commands()
            logger.info("System commands registration completed")

            # /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 core.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)

    def _initialize_fullscreen_commands(self) -> None:
        """Initialize dynamic fullscreen plugin commands."""
        try:
            from core.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: {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_loop(self) -> None:
        """Main rendering loop for status updates."""
        logger.info("Render loop starting...")
        while self.running:
            try:
                # Render active area (status views use content providers)
                await self.renderer.render_active_area()

                # Use configured FPS for render timing
                render_fps = self.config.get("terminal.render_fps", 20)
                await asyncio.sleep(1.0 / render_fps)

            except Exception as e:
                logger.error(f"Render loop error: {e}")
                error_delay = self.config.get("terminal.render_error_delay", 0.1)
                await asyncio.sleep(error_delay)

    async def _register_core_status_views(self) -> None:
        """Register default core status views."""
        try:
            from .io.core_status_views import CoreStatusViews
            core_views = CoreStatusViews(
                llm_service=self.llm_service,
                config=self.config,
                profile_manager=getattr(self, 'profile_manager', None),
                agent_manager=getattr(self, 'agent_manager', None),
            )

            # Wire up plugin references for task info display
            # Plugin names are clean_name: TmuxPlugin -> 'tmux'
            if hasattr(self, 'plugin_instances'):
                core_views.tmux_plugin = self.plugin_instances.get('tmux')

            core_views.register_all_views_v3(self.status_registry)
        except Exception as e:
            logger.error(f"Failed to register core status views: {e}")

    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

    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 all tasks to complete with timeout
            if self._background_tasks:
                try:
                    await asyncio.wait_for(
                        asyncio.gather(*self._background_tasks, return_exceptions=True),
                        timeout=5.0
                    )
                except asyncio.TimeoutError:
                    logger.warning("Some 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 _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 terminal renderer settings
        if hasattr(self, 'renderer'):
            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)

        # Reload UI theme
        from .ui.design_system import set_theme, THEMES
        ui_theme = self.config.get("core.ui.theme", "lime")
        if ui_theme in THEMES:
            set_theme(ui_theme)
            logger.debug(f"UI theme reloaded: {ui_theme}")

        # Reload terminal width configuration (invalidate cache)
        from .io.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 shutdown(self) -> None:
        """Shutdown the application gracefully."""
        logger.info("Application shutting down")
        self.running = False
        
        # 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}")
        
        # 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")