"""ChatScreen - the main chat screen with split-pane support.

This is the main chat interface that:
1. Composes child widgets (sidebar + pane(s))
2. Handles Messages from children
3. Dispatches commands for async operations
4. Coordinates with AppStore for API access
5. Supports split-pane view for comparing models

Now implemented as a proper Textual Screen for proper keybinding isolation.
"""

from typing import TYPE_CHECKING
from uuid import UUID

from textual import on, work
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal
from textual.message import Message

from alloy_runtime_sdk.api_client.client import ApiClient
from alloy_runtime_types.dtos.sessions import UpdateSessionRequest

from cli.infrastructure.injection.resolver import ResolvedMessage
from cli.infrastructure.local_storage import get_storage
from cli.infrastructure.tui.clipboard import copy_to_clipboard
from cli.tui.chat.commands.base import CommandResult
from cli.tui.chat.commands.create_session import CreateSessionCommand
from cli.tui.chat.commands.load_session import LoadSessionCommand
from cli.tui.chat.commands.regenerate import RegenerateCommand
from cli.tui.chat.commands.reload_session import (
    ReloadSessionCommand,
    ReloadSessionResult,
)
from cli.tui.chat.commands.send_message import SendMessageCommand
from cli.tui.chat.commands.undo import UndoCommand
from cli.tui.chat.messages import (
    AgentQuickSelected,
    CopyToClipboardRequested,
    FocusInputRequested,
    FocusSessionSearchRequested,
    NewSessionRequested,
    OpenEditorRequested,
    RecentSessionSelected,
    RegenerateRequested,
    SendMessageRequested,
    SessionSelected,
    SessionsLoadError,
    SlashCommandExecuted,
    UndoRequested,
    UnknownSlashCommand,
)
from cli.tui.chat.editor import EditorError, open_in_editor
from cli.tui.chat.pane import ChatPane
from cli.tui.chat.services.injection import InjectionService
from cli.tui.chat.services.name_generator import SessionNameGenerator
from cli.tui.chat.store import ChatStore
from cli.tui.chat.types import ChatPhase, extract_message_text
from cli.tui.chat.widgets.chat_input import ChatInput
from cli.tui.chat.widgets.message_display import MessageDisplay
from cli.tui.chat.widgets.session_sidebar import SessionSidebar
from cli.tui.screens.nav_screen import NavScreen
from cli.tui.widgets.new_session_modal import NewSessionConfig, NewSessionModal
from alloy_runtime_sdk.logging.config import get_logger

logger = get_logger(__name__)

# Storage keys for preferences
PREF_AUTO_NAME_SESSIONS = "auto_name_sessions"

if TYPE_CHECKING:
    from cli.tui.app import AlloyRuntimeApp


class PaneFocusChanged(Message):
    """Message posted when the active pane changes in split view."""

    def __init__(self, pane_id: str) -> None:
        self.pane_id = pane_id
        super().__init__()


class ChatScreen(NavScreen):
    """Main chat screen - orchestrates child widgets and handles commands.

    Supports both single-pane and split-pane modes for comparing models.

    Architecture:
        - ChatStore: Centralized reactive state (one per pane)
        - SessionSidebar: Session list with search (shared, controls active pane)
        - ChatPane: Individual chat area (header, messages, input)

    Split View:
        - Toggle with Ctrl+Shift+S to enable/disable split view
        - Switch between panes with Ctrl+Tab
        - Sync mode sends messages to both panes simultaneously

    Responsibilities:
        - Compose child widgets with shared/separate stores
        - Handle Messages from children
        - Execute commands for async API operations
        - Coordinate state transitions
        - Manage split-pane layout and focus
    """

    app: "AlloyRuntimeApp"

    SCREEN_ID = "chat"

    DEFAULT_CSS = """
    ChatScreen #screen-root {
        height: 1fr;
    }
    
    ChatScreen #chat-root {
        height: 100%;
        width: 100%;
    }
    
    ChatScreen #split-container {
        width: 1fr;
        height: 100%;
    }
    
    ChatScreen #panes-container {
        width: 1fr;
        height: 100%;
    }
    
    ChatScreen .sync-indicator {
        dock: bottom;
        height: 1;
        background: $warning;
        color: $text;
        text-align: center;
        display: none;
    }
    
    ChatScreen .sync-indicator.visible {
        display: block;
    }
    """

    # Screen-specific bindings (Ctrl+ prefixed to not conflict with typing)
    # NavScreen.BINDINGS provides global navigation (c, a, t, s, m, d, ?)
    BINDINGS = NavScreen.BINDINGS + [
        # Session management
        Binding("ctrl+n", "new_session", "New Chat", show=True),
        Binding("ctrl+l", "clear_display", "Clear", show=False),
        # Chat operations
        Binding("ctrl+r", "regenerate", "Regenerate", show=True),
        Binding("ctrl+u", "undo", "Undo", show=True),
        Binding("ctrl+y", "copy_last", "Copy", show=True),
        # Navigation - use escape to focus input
        Binding("escape", "cancel_or_focus", "Focus Input", show=False),
        # UI toggles
        Binding("ctrl+b", "toggle_sidebar", "Sidebar", show=True),
        # Split view (Ctrl+Shift to avoid conflicts)
        Binding("ctrl+shift+s", "toggle_split", "Split View", show=False),
        Binding("ctrl+shift+tab", "switch_pane", "Switch Pane", show=False),
        Binding("ctrl+shift+y", "toggle_sync", "Sync Input", show=False),
    ]

    def __init__(self) -> None:
        """Initialize the chat screen with stores for both panes."""
        super().__init__()
        # Primary (left) pane store - always exists
        self._left_store = ChatStore()
        # Secondary (right) pane store - only used in split mode
        self._right_store = ChatStore()

        # Split view state
        self._split_mode = False
        self._active_pane: str = "left"  # "left" or "right"
        self._sync_mode = False  # Send to both panes when True

        # Injection service (initialized on first use)
        self._injection_service: InjectionService | None = None

    @property
    def _store(self) -> ChatStore:
        """Get the active pane's store."""
        return self._left_store if self._active_pane == "left" else self._right_store

    @property
    def _active_pane_widget(self) -> ChatPane | None:
        """Get the currently active ChatPane widget."""
        pane_id = "left-pane" if self._active_pane == "left" else "right-pane"
        try:
            return self.query_one(f"#{pane_id}", ChatPane)
        except Exception:
            return None

    def compose_content(self) -> ComposeResult:
        """Compose the chat layout with child widgets."""
        with Horizontal(id="chat-root"):
            yield SessionSidebar(self._left_store)
            with Horizontal(id="panes-container"):
                # Left pane (always visible)
                yield ChatPane(
                    self._left_store,
                    pane_id="left",
                    show_border=False,
                    id="left-pane",
                )
                # Right pane (hidden by default, shown in split mode)
                right_pane = ChatPane(
                    self._right_store,
                    pane_id="right",
                    show_border=True,
                    id="right-pane",
                )
                right_pane.display = False
                yield right_pane

    def on_mount(self) -> None:
        """Initialize pane focus state and injection service."""
        self._update_pane_focus_indicators()
        # Initialize injection service in background
        self._initialize_injection_service()

    def on_screen_resume(self) -> None:
        """Called when screen becomes active - focus the chat input."""
        self.call_after_refresh(self.focus_input)

    # =========================================================================
    # Split View Management
    # =========================================================================

    def action_toggle_split(self) -> None:
        """Toggle split-pane view."""
        self._split_mode = not self._split_mode

        # Show/hide right pane
        try:
            right_pane = self.query_one("#right-pane", ChatPane)
            left_pane = self.query_one("#left-pane", ChatPane)

            right_pane.display = self._split_mode

            # Update border visibility
            left_pane.set_show_border(self._split_mode)
            if self._split_mode:
                left_pane.add_class(
                    "focused-pane" if self._active_pane == "left" else "unfocused-pane"
                )
            else:
                left_pane.remove_class("focused-pane")
                left_pane.remove_class("unfocused-pane")
                # Reset to left pane when disabling split
                self._active_pane = "left"
                self._sync_mode = False

            self._update_pane_focus_indicators()

            status = "enabled" if self._split_mode else "disabled"
            self.app.notify(f"Split view {status}", severity="information")

        except Exception as e:
            logger.debug(f"Failed to toggle split view: {e}")

    def action_switch_pane(self) -> None:
        """Switch focus between left and right panes."""
        if not self._split_mode:
            self.app.notify(
                "Split view not enabled. Press Ctrl+Shift+S to enable.",
                severity="warning",
            )
            return

        # Toggle active pane
        self._active_pane = "right" if self._active_pane == "left" else "left"
        self._update_pane_focus_indicators()

        # Update sidebar to use new active pane's store
        self._update_sidebar_store()

        # Focus the input in the new active pane
        pane = self._active_pane_widget
        if pane:
            pane.focus_input()

        self.app.notify(f"Switched to {self._active_pane} pane", severity="information")

    def action_toggle_sync(self) -> None:
        """Toggle synchronized input mode (send to both panes)."""
        if not self._split_mode:
            self.app.notify(
                "Split view not enabled. Press Ctrl+Shift+S to enable.",
                severity="warning",
            )
            return

        self._sync_mode = not self._sync_mode
        status = "enabled" if self._sync_mode else "disabled"
        self.app.notify(
            f"Sync mode {status} - messages sent to {'both panes' if self._sync_mode else 'active pane only'}",
            severity="information",
        )

    def _update_pane_focus_indicators(self) -> None:
        """Update visual focus indicators on panes."""
        if not self._split_mode:
            return

        try:
            left_pane = self.query_one("#left-pane", ChatPane)
            right_pane = self.query_one("#right-pane", ChatPane)

            left_pane.set_focused(self._active_pane == "left")
            right_pane.set_focused(self._active_pane == "right")
        except Exception:
            pass

    def _update_sidebar_store(self) -> None:
        """Update sidebar to reflect the active pane's store.

        Note: This is a limitation - the sidebar is created with left_store.
        For full implementation, we'd need to make the sidebar reactive to store changes.
        For now, session selection affects the active pane.
        """
        # The sidebar always shows sessions, but selection loads into active pane
        pass

    # =========================================================================
    # Message Handlers (from child widgets)
    # =========================================================================

    @on(SessionSelected)
    def handle_session_selected(self, message: SessionSelected) -> None:
        """Load messages when a session is selected."""
        logger.info("session_selected", session_id=str(message.session_id))
        self._execute_load_session(message.session_id)

    @on(AgentQuickSelected)
    def handle_agent_quick_selected(self, message: AgentQuickSelected) -> None:
        """Create a new session with the selected agent immediately."""
        logger.info(
            "agent_quick_selected",
            agent_id=str(message.agent_id),
            agent_name=message.agent_name,
        )
        self._execute_quick_agent_session(message.agent_id, message.agent_name)

    @on(RecentSessionSelected)
    def handle_recent_session_selected(self, message: RecentSessionSelected) -> None:
        """Load a recent session from the welcome screen."""
        logger.info("recent_session_selected", session_id=str(message.session_id))
        self._execute_load_session(message.session_id)

    @on(SendMessageRequested)
    def handle_send_message(self, message: SendMessageRequested) -> None:
        """Send a message and wait for the final response."""
        if message.content:
            logger.info(
                "send_message_requested",
                content_length=len(message.content),
                sync_mode=self._sync_mode,
                split_mode=self._split_mode,
            )
            if self._sync_mode and self._split_mode:
                # Send to both panes
                self._execute_send_message_sync(message.content)
            else:
                # Send to active pane only
                self._execute_send_message(message.content)

    @on(RegenerateRequested)
    def handle_regenerate(self, message: RegenerateRequested) -> None:
        """Regenerate the last response."""
        self.action_regenerate()

    @on(UndoRequested)
    def handle_undo(self, message: UndoRequested) -> None:
        """Undo the last conversation turn."""
        self.action_undo()

    @on(CopyToClipboardRequested)
    def handle_copy(self, message: CopyToClipboardRequested) -> None:
        """Copy content to clipboard.

        If the label is '_copy_last_request_', this is a request to fetch
        and copy the last assistant message from the server.
        """
        if message.label == "_copy_last_request_":
            # This is a copy_last request from ChatTextArea
            self.action_copy_last()
        else:
            # Regular copy request with content
            copy_to_clipboard(self.app, message.content, message.label)

    @on(FocusInputRequested)
    def handle_focus_input(self, message: FocusInputRequested) -> None:
        """Focus the chat input."""
        self.action_focus_input()

    @on(FocusSessionSearchRequested)
    def handle_focus_search(self, message: FocusSessionSearchRequested) -> None:
        """Focus the session search."""
        self.action_focus_session_search()

    @on(NewSessionRequested)
    def handle_new_session(self, message: NewSessionRequested) -> None:
        """Open new session modal."""
        self.action_new_session()

    @on(SessionsLoadError)
    def handle_sessions_error(self, message: SessionsLoadError) -> None:
        """Handle session loading errors."""
        self.app.notify(f"Failed to load sessions: {message.error}", severity="error")

    @on(SlashCommandExecuted)
    def handle_slash_command(self, message: SlashCommandExecuted) -> None:
        """Handle slash commands that require screen-level actions."""
        logger.debug(
            "slash_command_executed", command=message.command_name, args=message.args
        )
        command_name = message.command_name

        # Map command names to actions
        if command_name in ("new", "n"):
            self.action_new_session()
        elif command_name in ("sessions", "s", "list"):
            self.action_focus_session_search()
        elif command_name in ("undo", "u", "z"):
            self.action_undo()
        elif command_name in ("regenerate", "r", "regen"):
            self.action_regenerate()
        elif command_name in ("split",):
            self.action_toggle_split()
        elif command_name in ("sync",):
            self.action_toggle_sync()
        elif command_name in ("switch", "sw"):
            self.action_switch_pane()
        # Note: clear, help, copy are handled directly in ChatInput

    @on(UnknownSlashCommand)
    def handle_unknown_command(self, message: UnknownSlashCommand) -> None:
        """Handle unknown slash commands."""
        self.app.notify(
            f"Unknown command: {message.command_text}. Type /help for available commands.",
            severity="warning",
        )

    @on(OpenEditorRequested)
    def handle_open_editor(self, message: OpenEditorRequested) -> None:
        """Open external editor for message composition."""
        self._open_external_editor(message.current_text)

    # =========================================================================
    # Actions (triggered by bindings)
    # =========================================================================

    def action_new_session(self) -> None:
        """Create a new chat session via modal."""
        self._show_new_session_modal()

    def action_regenerate(self) -> None:
        """Regenerate the last AI response."""
        state = self._store.state
        session = state.session

        if not session:
            self.app.notify("No active session", severity="warning")
            return

        if state.phase != ChatPhase.IDLE:
            self.app.notify("Please wait for the current operation", severity="warning")
            return

        if not session.is_agent_mode and not session.is_model_mode:
            self.app.notify(
                "No agent or model configured for this session", severity="warning"
            )
            return

        self._execute_regenerate()

    def action_undo(self) -> None:
        """Undo the last conversation turn."""
        state = self._store.state

        if not state.session:
            self.app.notify("No active session", severity="warning")
            return

        if state.phase != ChatPhase.IDLE:
            self.app.notify("Please wait for the current operation", severity="warning")
            return

        messages = state.messages
        if len(messages) < 2:
            self.app.notify("Nothing to undo", severity="warning")
            return

        # Check last two messages form a user->assistant turn
        last = messages[-1]
        second_last = messages[-2]
        if not (second_last.role == "user" and last.role == "assistant"):
            self.app.notify("No complete turn to undo", severity="warning")
            return

        self._execute_undo()

    def action_copy_last(self) -> None:
        """Copy the last assistant response to clipboard (fetches from server)."""
        state = self._store.state

        if not state.session:
            self.app.notify("No active session", severity="warning")
            return

        # Fetch from server to get post-processed content
        self._execute_copy_last()

    def action_clear_display(self) -> None:
        """Clear the current session and show welcome."""
        self._store.clear_session()
        pane = self._active_pane_widget
        if pane:
            message_display = pane.query_one(MessageDisplay)
            message_display.show_welcome()

    def action_focus_session_search(self) -> None:
        """Focus the session search input."""
        # Make sure sidebar is visible first
        if not self._left_store.state.sidebar_visible:
            self._left_store.set_sidebar_visible(True)
        sidebar = self.query_one(SessionSidebar)
        sidebar.action_focus_search()

    def action_toggle_sidebar(self) -> None:
        """Toggle the session sidebar visibility."""
        self._left_store.toggle_sidebar()
        visible = self._left_store.state.sidebar_visible
        self.app.notify(
            f"Sidebar {'shown' if visible else 'hidden'}",
            severity="information",
        )

    def action_focus_input(self) -> None:
        """Focus the chat input in the active pane."""
        self.focus_input()

    def action_cancel_or_focus(self) -> None:
        """Focus the chat input.

        The TUI chat flow is non-streaming, so Escape no longer attempts to abort
        an in-flight generation.
        """
        self.focus_input()

    def focus_input(self) -> None:
        """Focus the chat input in the active pane.

        Public method for external callers (e.g., when switching to chat tab).
        """
        pane = self._active_pane_widget
        if pane:
            pane.focus_input()

    def _open_external_editor(self, current_text: str) -> None:
        """Open text in external editor and update input with result.

        Uses Textual's suspend() to temporarily yield control to the editor.
        """
        from cli.tui.chat.widgets.chat_input import ChatTextArea

        try:
            with self.app.suspend():
                edited_text = open_in_editor(current_text)

            if edited_text is not None:
                # Update the chat input with edited content in active pane
                pane = self._active_pane_widget
                if pane:
                    chat_input = pane.query_one(ChatInput)
                    text_area = chat_input.query_one("#chat-input", ChatTextArea)
                    text_area.text = edited_text
                    # Move cursor to end
                    text_area.move_cursor(text_area.document.end)
                    chat_input.focus_input()
                    self.app.notify("Editor content loaded", severity="information")
        except EditorError as e:
            self.app.notify(str(e), severity="error")

    # =========================================================================
    # Command Execution (async operations)
    # =========================================================================

    async def _get_client(self) -> ApiClient:
        """Get the shared server client."""
        return await self.app.store.get_client()

    async def _get_injection_service(self) -> InjectionService:
        """Get or create the injection service."""
        if self._injection_service is None:
            client = await self._get_client()
            self._injection_service = InjectionService(client)
        return self._injection_service

    @work(exclusive=False, group="injection_init")
    async def _initialize_injection_service(self) -> None:
        """Initialize the injection service and refresh its cache.

        Called on mount to pre-populate autocomplete data.
        """
        try:
            service = await self._get_injection_service()
            await service.refresh_cache()

            # Pass service to chat input widgets in both panes
            self._update_chat_inputs_with_injection_service(service)

            logger.debug(
                "injection_service_initialized",
                extra={
                    "fragments": len(service.fragments),
                    "content": len(service.content_parts),
                    "schemas": len(service.schemas),
                },
            )
        except Exception as e:
            logger.debug(
                "injection_service_init_failed",
                extra={"error": str(e)},
            )

    def _update_chat_inputs_with_injection_service(
        self, service: InjectionService
    ) -> None:
        """Update ChatInput widgets with the injection service."""
        try:
            # Update left pane
            left_pane = self.query_one("#left-pane", ChatPane)
            left_input = left_pane.query_one(ChatInput)
            left_input.set_injection_service(service)

            # Update right pane (if exists)
            try:
                right_pane = self.query_one("#right-pane", ChatPane)
                right_input = right_pane.query_one(ChatInput)
                right_input.set_injection_service(service)
            except Exception:
                pass  # Right pane may not exist yet
        except Exception as e:
            logger.debug(f"Failed to update chat inputs with injection service: {e}")

    @work(exclusive=True)
    async def _show_new_session_modal(self) -> None:
        """Show the new session modal."""
        client = await self._get_client()
        modal = NewSessionModal(client=client)
        self.app.push_screen(modal, self._on_new_session_created)

    def _on_new_session_created(self, config: NewSessionConfig | None) -> None:
        """Handle result from new session modal."""
        if config is None:
            return  # Cancelled

        # Validate: must have either agent or model selected
        if not config.is_agent_mode and not config.is_model_mode:
            self.app.notify(
                "Please select an agent or model to start a chat session",
                severity="warning",
            )
            return

        self._execute_create_session(config)

    @work(exclusive=True)
    async def _execute_create_session(self, config: NewSessionConfig) -> None:
        """Execute the create session command."""
        client = await self._get_client()

        command = CreateSessionCommand(
            client=client,
            store=self._store,
            name=config.name,
            # Agent mode
            agent_id=config.agent_id,
            agent_name=config.agent_name,
            # Model mode
            provider_key=config.provider_key,
            provider_model_name=config.provider_model_name,
            system_instruction=config.system_instruction,
        )

        result = await command.execute()

        if result.success:
            # Focus input in active pane and refresh sidebar
            pane = self._active_pane_widget
            if pane:
                pane.focus_input()

            sidebar = self.query_one(SessionSidebar)
            sidebar.refresh_sessions()

            mode = "agent" if config.is_agent_mode else "model"
            self.app.notify(f"Session created ({mode} mode)!", severity="information")
        else:
            self.app.notify(
                f"Failed to create session: {result.error}", severity="error"
            )

    @work(exclusive=True)
    async def _execute_quick_agent_session(
        self, agent_id: UUID, agent_name: str
    ) -> None:
        """Create a new session with the selected agent (no modal, immediate creation).

        This is triggered from the welcome screen quick start list.
        """
        client = await self._get_client()

        command = CreateSessionCommand(
            client=client,
            store=self._store,
            name=None,  # Will be auto-named after first message
            agent_id=agent_id,
            agent_name=agent_name,
            # Model mode params not used
            provider_key=None,
            provider_model_name=None,
            system_instruction=None,
        )

        result = await command.execute()

        if result.success:
            # Focus input in active pane and refresh sidebar
            pane = self._active_pane_widget
            if pane:
                pane.focus_input()

            sidebar = self.query_one(SessionSidebar)
            sidebar.refresh_sessions()

            self.app.notify(f"Chat started with {agent_name}", severity="information")
        else:
            self.app.notify(f"Failed to start chat: {result.error}", severity="error")

    @work(exclusive=True)
    async def _execute_load_session(self, session_id: UUID) -> None:
        """Execute the load session command."""
        client = await self._get_client()

        command = LoadSessionCommand(
            client=client,
            store=self._store,
            session_id=session_id,
        )

        result = await command.execute()

        if result.success:
            # Focus input in active pane
            pane = self._active_pane_widget
            if pane:
                pane.focus_input()
        else:
            self.app.notify(f"Failed to load session: {result.error}", severity="error")

    @work(exclusive=True)
    async def _execute_send_message(self, user_message: str) -> None:
        """Execute the send message command for the active pane."""
        state = self._store.state
        session = state.session

        if not session:
            self.app.notify("No active session", severity="warning")
            return

        # Check that we have either agent or model configured
        if not session.is_agent_mode and not session.is_model_mode:
            self.app.notify(
                "No agent or model configured for this session", severity="warning"
            )
            return

        client = await self._get_client()
        injection_service = await self._get_injection_service()

        def on_injection_resolved(resolved: ResolvedMessage) -> None:
            """Callback when injections are resolved."""
            injected_items: list[str] = []
            if resolved.fragments_used:
                injected_items.append(f"{len(resolved.fragments_used)} fragment(s)")
            if resolved.content_used:
                injected_items.append(f"{len(resolved.content_used)} content item(s)")
            if resolved.schemas_used:
                injected_items.append(f"{len(resolved.schemas_used)} schema(s)")
            if injected_items:
                self.app.notify(
                    f"Injected: {', '.join(injected_items)}", severity="information"
                )

        command = SendMessageCommand(
            client=client,
            store=self._store,
            session_id=session.session_id,
            user_message=user_message,
            # Agent mode
            agent_id=session.agent_id,
            # Model mode
            provider_key=session.provider_key,
            provider_model_name=session.model_name,
            system_instruction=session.system_instruction,
            # Injection support
            injection_service=injection_service,
            on_injection_resolved=on_injection_resolved,
        )

        result = await command.execute()

        if result.success:
            reload_result = await self._reload_current_session()
            self._store.finish_generation()
            self._store.clear_pending_user_message()

            if reload_result.success:
                # Trigger auto-naming for sessions without a name (non-blocking)
                self._maybe_generate_session_name(user_message)
            else:
                self.app.notify(
                    f"Message sent, but refresh failed: {reload_result.error}",
                    severity="error",
                )
        else:
            self._store.clear_pending_user_message()
            self.app.notify(f"Error: {result.error}", severity="error")

    @work(exclusive=False, group="sync_send")
    async def _execute_send_message_sync(self, user_message: str) -> None:
        """Execute the send message command for both panes simultaneously."""
        client = await self._get_client()

        # Send to left pane
        left_state = self._left_store.state
        if left_state.session and (
            left_state.session.is_agent_mode or left_state.session.is_model_mode
        ):
            self._send_to_pane(client, self._left_store, user_message)

        # Send to right pane
        right_state = self._right_store.state
        if right_state.session and (
            right_state.session.is_agent_mode or right_state.session.is_model_mode
        ):
            self._send_to_pane(client, self._right_store, user_message)

    @work(exclusive=False, group="pane_send")
    async def _send_to_pane(
        self, client: ApiClient, store: ChatStore, user_message: str
    ) -> None:
        """Send a message to a specific pane's session."""
        state = store.state
        session = state.session

        if not session:
            return

        command = SendMessageCommand(
            client=client,
            store=store,
            session_id=session.session_id,
            user_message=user_message,
            agent_id=session.agent_id,
            provider_key=session.provider_key,
            provider_model_name=session.model_name,
            system_instruction=session.system_instruction,
        )

        result = await command.execute()

        if result.success:
            reload_cmd = ReloadSessionCommand(
                client=client,
                store=store,
                session_id=session.session_id,
            )
            reload_result = await reload_cmd.execute()
            store.finish_generation()
            store.clear_pending_user_message()

            if not reload_result.success:
                self.app.notify(
                    f"Sync refresh failed: {reload_result.error}",
                    severity="error",
                )
        else:
            store.clear_pending_user_message()
            self.app.notify(f"Sync send failed: {result.error}", severity="error")

    @work(exclusive=False, group="name_generation")
    async def _maybe_generate_session_name(self, first_message: str) -> None:
        """Generate a session name if this is the first message and auto-naming is enabled.

        This runs in a separate worker group so it doesn't block the main chat flow.

        Args:
            first_message: The user's first message in the session.
        """
        logger.info(
            "session_auto_naming_worker_started",
            first_message_preview=first_message,
        )

        # Re-read state fresh (worker runs async, state may have changed)
        state = self._store.state
        session = state.session

        if not session:
            logger.info("session_auto_naming_skipped_no_session")
            return

        logger.info(
            "session_auto_naming_session_found",
            session_id=str(session.session_id),
            session_name=session.session_name,
            provider_key=session.provider_key,
            model_name=session.model_name,
            agent_id=str(session.agent_id) if session.agent_id else None,
        )

        # Skip if session already has a name
        if session.session_name:
            logger.info(
                "session_auto_naming_skipped_has_name",
                session_name=session.session_name,
            )
            return

        # Check if this is the first exchange (should have exactly 1 user and 1 assistant message)
        # Count by role to handle cases where there might be system messages or other roles
        user_messages = sum(1 for m in state.messages if m.role == "user")
        assistant_messages = sum(1 for m in state.messages if m.role == "assistant")
        logger.info(
            "session_auto_naming_message_count",
            total_messages=len(state.messages),
            user_messages=user_messages,
            assistant_messages=assistant_messages,
            session_id=str(session.session_id),
        )
        if user_messages != 1 or assistant_messages != 1:
            logger.info(
                "session_auto_naming_skipped_not_first_exchange",
                user_messages=user_messages,
                assistant_messages=assistant_messages,
            )
            return

        # Check if auto-naming is enabled (default: True)
        storage = get_storage()
        auto_naming_enabled = storage.get("preferences", PREF_AUTO_NAME_SESSIONS, True)
        logger.info(
            "session_auto_naming_preference_check",
            auto_naming_enabled=auto_naming_enabled,
        )
        if not auto_naming_enabled:
            logger.info("session_auto_naming_skipped_disabled_in_preferences")
            return

        logger.info(
            "session_auto_naming_calling_generator",
            session_id=str(session.session_id),
            first_message_length=len(first_message),
        )

        try:
            client = await self._get_client()
            generator = SessionNameGenerator()
            generated_name = await generator.generate_name(
                client,
                first_message,
                session_provider_key=session.provider_key,
                session_model_name=session.model_name,
            )

            logger.info(
                "session_auto_naming_generator_returned",
                generated_name=generated_name,
                session_id=str(session.session_id),
            )

            if generated_name:
                # Update session on server
                logger.info(
                    "session_auto_naming_updating_server",
                    session_id=str(session.session_id),
                    generated_name=generated_name,
                )
                await client.update_session(
                    session.session_id,
                    UpdateSessionRequest(name=generated_name),
                )

                # Re-read current state to get latest message count
                current_state = self._store.state

                # Update local store with new name
                self._store.set_session(
                    session_id=session.session_id,
                    session_name=generated_name,
                    agent_id=session.agent_id,
                    agent_name=session.agent_name,
                    message_count=len(current_state.messages),
                    provider_key=session.provider_key,
                    model_name=session.model_name,
                    system_instruction=session.system_instruction,
                )

                # Refresh sidebar to show new name
                sidebar = self.query_one(SessionSidebar)
                sidebar.refresh_sessions()

                # Notify user that session was named
                self.app.notify(
                    f"Session named: {generated_name}", severity="information"
                )

                logger.info(
                    "session_auto_naming_completed",
                    session_id=str(session.session_id),
                    generated_name=generated_name,
                )
            else:
                logger.warning(
                    "session_auto_naming_no_name_generated",
                    session_id=str(session.session_id),
                )

        except Exception as e:
            # Log error but don't show to user - auto-naming is a nice-to-have feature
            logger.warning(
                "session_auto_naming_failed",
                session_id=str(session.session_id),
                error=str(e),
                error_type=type(e).__name__,
            )

    @work(exclusive=True)
    async def _execute_regenerate(self) -> None:
        """Execute the regenerate command."""
        state = self._store.state
        session = state.session

        if not session:
            return

        # Check that we have either agent or model configured
        if not session.is_agent_mode and not session.is_model_mode:
            return

        client = await self._get_client()

        command = RegenerateCommand(
            client=client,
            store=self._store,
            session_id=session.session_id,
            # Agent mode
            agent_id=session.agent_id,
            # Model mode
            provider_key=session.provider_key,
            provider_model_name=session.model_name,
            system_instruction=session.system_instruction,
        )

        result = await command.execute()

        if result.success:
            reload_result = await self._reload_current_session()
            self._store.finish_generation()

            if reload_result.success:
                self.app.notify("Response regenerated", severity="information")
            else:
                self.app.notify(
                    f"Regenerate completed, but refresh failed: {reload_result.error}",
                    severity="error",
                )
        else:
            self.app.notify(f"Regenerate failed: {result.error}", severity="error")

    @work(exclusive=True)
    async def _execute_undo(self) -> None:
        """Execute the undo command."""
        state = self._store.state
        messages = state.messages

        if len(messages) < 2:
            return

        client = await self._get_client()

        command = UndoCommand(
            client=client,
            user_message=messages[-2],
            assistant_message=messages[-1],
        )

        result = await command.execute()

        if result.success and result.data:
            await self._reload_current_session()
            self.app.notify(
                f"Undone: {result.data.user_preview}", severity="information"
            )
        else:
            self.app.notify(f"Undo failed: {result.error}", severity="error")

    async def _reload_current_session(self) -> CommandResult[ReloadSessionResult]:
        """Reload the current session messages."""
        state = self._store.state
        if not state.session:
            return CommandResult[ReloadSessionResult](
                success=False,
                error="No active session to reload",
            )

        client = await self._get_client()

        command = ReloadSessionCommand(
            client=client,
            store=self._store,
            session_id=state.session.session_id,
        )
        return await command.execute()

    @work(exclusive=True)
    async def _execute_copy_last(self) -> None:
        """Fetch the last assistant message from server and copy to clipboard.

        Fetches from server to ensure we get post-processed content
        (e.g., after output schema regex processing).
        """
        state = self._store.state
        if not state.session:
            return

        client = await self._get_client()

        try:
            # Fetch last assistant message from server with role filter
            response = await client.list_session_messages(
                session_id=state.session.session_id,
                role="assistant",
                limit=1,
                order="desc",
            )

            if not response.messages:
                self.app.notify("No assistant message to copy", severity="warning")
                return

            # Extract text from content parts (excludes thinking content)
            message = response.messages[0]
            text = extract_message_text(message)

            if not text:
                self.app.notify("Assistant message is empty", severity="warning")
                return

            # Copy to clipboard
            copy_to_clipboard(self.app, text, "Last response")

        except Exception as e:
            self.app.notify(f"Failed to copy: {e}", severity="error")
