"""Chat input widget for composing and sending messages."""

from typing import TYPE_CHECKING, Callable

from textual import on
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import Vertical
from textual.events import Key
from textual.widget import Widget
from textual.widgets import TextArea

from cli.infrastructure.injection.parser import detect_partial_injection, has_injections
from cli.infrastructure.tui.selectable import CopyOnSelectTextArea
from cli.tui.chat.messages import (
    CopyToClipboardRequested,
    OpenEditorRequested,
    SendMessageRequested,
    SlashCommandExecuted,
    UnknownSlashCommand,
)
from cli.tui.chat.services.injection import InjectionService
from cli.tui.chat.slash_commands import SlashContext, get_default_registry
from cli.tui.chat.store import ChatStore
from cli.tui.chat.types import ChatState
from cli.tui.chat.widgets.injection_popup import InjectionSuggestionPopup
from alloy_runtime_sdk.logging.config import get_logger

logger = get_logger(__name__)

if TYPE_CHECKING:
    from cli.tui.app import AlloyRuntimeApp


class ChatTextArea(CopyOnSelectTextArea):
    """Custom TextArea with send key bindings and copy-on-select.

    Key handling for injection popup is done by the parent ChatInput widget.
    """

    BINDINGS = [
        Binding("ctrl+d", "submit", "Send", show=False, priority=True),
        Binding("ctrl+e", "open_editor", "Editor", show=False, priority=True),
        Binding("ctrl+y", "copy_last", "Copy Last", show=False, priority=True),
    ]

    def action_submit(self) -> None:
        """Post a submit request to parent."""
        self.post_message(SendMessageRequested(self.text.strip()))

    def action_open_editor(self) -> None:
        """Request to open current text in external editor."""
        self.post_message(OpenEditorRequested(self.text))

    def action_copy_last(self) -> None:
        """Request to copy last assistant message - bubbles up to ChatScreen."""
        self.post_message(CopyToClipboardRequested("", "_copy_last_request_"))

    async def _on_key(self, event: Key) -> None:
        """Handle keys, letting parent intercept for popup control."""
        # Let the parent handle the key first
        await super()._on_key(event)
        # Then ensure cursor is visible
        self.call_after_refresh(self._ensure_cursor_visible)

    def _ensure_cursor_visible(self) -> None:
        """Scroll the TextArea to ensure the cursor is visible."""
        self.scroll_cursor_visible(animate=False)


class ChatInput(Widget):
    """Widget for composing chat messages with injection autocomplete.

    This widget controls the injection popup - the popup is just a display,
    all keyboard handling happens here. Focus stays on the text area.

    Features:
        - Multi-line text input with auto-grow
        - Send with Ctrl+D
        - Open in external editor with Ctrl+E
        - Slash command parsing (/help, /clear, /copy, etc.)
        - Injection autocomplete (@fragment, @text, @json, @schema)
        - Arrow keys navigate popup while focus stays in input
    """

    app: "AlloyRuntimeApp"

    DEFAULT_CSS = """
    ChatInput {
        height: auto;
    }
    
    ChatInput #input-container {
        height: auto;
    }
    """

    # Commands that are handled directly in ChatInput (don't need screen)
    _IMMEDIATE_COMMANDS = {
        "clear",
        "c",
        "cls",
        "help",
        "h",
        "?",
        "copy",
        "cp",
        "y",
        "markdown",
        "md",
        "plain",  # Render mode toggle
    }

    def __init__(
        self,
        store: ChatStore,
        injection_service: InjectionService | None = None,
    ) -> None:
        """Initialize the chat input."""
        super().__init__()
        self._store = store
        self._injection_service = injection_service
        self._registry = get_default_registry()
        self._unsubscribe: Callable[[], None] | None = None
        # Track current injection trigger for replacement
        self._current_trigger_start: int = 0
        self._current_trigger_end: int = 0
        # Track if popup was dismissed to prevent re-triggering
        self._popup_dismissed_text: str = ""

    def compose(self) -> ComposeResult:
        """Compose the input layout."""
        with Vertical(id="input-container"):
            yield ChatTextArea(id="chat-input")

    def on_mount(self) -> None:
        """Subscribe to store on mount."""
        self._unsubscribe = self._store.subscribe(self._on_state_change)

    def on_unmount(self) -> None:
        """Unsubscribe from store on unmount."""
        if self._unsubscribe:
            self._unsubscribe()

    def _on_state_change(self, state: ChatState) -> None:
        """React to store state changes."""
        # State changes are handled by store subscribers
        # No local status bar to update anymore
        pass

    def _get_popup(self) -> InjectionSuggestionPopup | None:
        """Get the injection popup from the parent ChatPane."""
        try:
            return self.screen.query_one("#injection-popup", InjectionSuggestionPopup)
        except Exception:
            return None

    def on_key(self, event: Key) -> None:
        """Intercept keys when popup is visible for navigation."""
        popup = self._get_popup()
        if not popup or not popup.is_visible:
            return  # Let key propagate normally

        # Handle popup navigation keys
        if event.key == "up":
            event.stop()
            popup.highlight_previous()
        elif event.key == "down":
            event.stop()
            popup.highlight_next()
        elif event.key == "tab" or event.key == "enter":
            event.stop()
            self._accept_suggestion(popup)
        elif event.key == "escape":
            event.stop()
            self._dismiss_popup(popup)

    def _accept_suggestion(self, popup: InjectionSuggestionPopup) -> None:
        """Accept the currently highlighted suggestion."""
        completion = popup.get_selected()
        if not completion:
            self._dismiss_popup(popup)
            return

        text_area = self.query_one("#chat-input", ChatTextArea)
        current_text = text_area.text

        # Replace the partial injection with the completed one
        new_text = (
            current_text[: self._current_trigger_start]
            + completion.text
            + current_text[self._current_trigger_end :]
        )

        # Update text area
        text_area.text = new_text

        # Move cursor to end of inserted text
        new_cursor_pos = self._current_trigger_start + len(completion.text)
        lines = new_text.split("\n")
        row = 0
        remaining = new_cursor_pos
        for i, line in enumerate(lines):
            if remaining <= len(line):
                row = i
                break
            remaining -= len(line) + 1
            row = i + 1

        col = min(remaining, len(lines[row]) if row < len(lines) else 0)
        text_area.move_cursor((row, col))

        # Hide popup
        popup.hide()
        self._popup_dismissed_text = new_text  # Prevent re-trigger

    def _dismiss_popup(self, popup: InjectionSuggestionPopup) -> None:
        """Dismiss the popup without accepting."""
        popup.hide()
        text_area = self.query_one("#chat-input", ChatTextArea)
        self._popup_dismissed_text = text_area.text  # Prevent re-trigger on same text

    @on(TextArea.Changed, "#chat-input")
    def _on_text_changed(self, event: TextArea.Changed) -> None:
        """Check for injection triggers when text changes."""
        text_area = event.text_area
        text = text_area.text

        # Scroll to keep cursor visible
        text_area.call_after_refresh(
            lambda: text_area.scroll_cursor_visible(animate=False)
        )

        # If text changed significantly, allow popup to trigger again
        if text != self._popup_dismissed_text:
            self._popup_dismissed_text = ""

        # Check for injection trigger
        if not self._injection_service:
            return

        # Get cursor position
        cursor_location = text_area.cursor_location
        lines = text.split("\n")
        cursor_pos = sum(len(lines[i]) + 1 for i in range(cursor_location[0]))
        cursor_pos += cursor_location[1]

        partial = detect_partial_injection(text, cursor_pos)
        popup = self._get_popup()

        if partial and text != self._popup_dismissed_text:
            # Store trigger position for replacement
            self._current_trigger_start = partial.start
            self._current_trigger_end = partial.end

            # Get completions
            completions = self._injection_service.get_completions(
                partial.injection_type,
                partial.partial_identifier,
            )

            # Show popup
            if popup:
                popup.show_suggestions(
                    completions,
                    partial.injection_type,
                    filter_text=partial.partial_identifier,
                )
        elif popup and popup.is_visible:
            # No trigger detected, hide popup if visible
            popup.hide()

    def on_send_message_requested(self, event: SendMessageRequested) -> None:
        """Handle send request from TextArea - check for slash commands first."""
        content = event.content

        # Hide popup if visible
        popup = self._get_popup()
        if popup and popup.is_visible:
            popup.hide()

        # Check for slash command
        parse_result = self._registry.parse(content)

        if parse_result.is_command:
            event.stop()
            text_area = self.query_one("#chat-input", ChatTextArea)
            text_area.clear()

            if parse_result.command is None:
                self.post_message(UnknownSlashCommand(content))
                return

            cmd_name = parse_result.command.name
            is_immediate = cmd_name in self._IMMEDIATE_COMMANDS
            logger.debug(
                "slash_command_parsed",
                command=cmd_name,
                is_immediate=is_immediate,
                args=parse_result.args or None,
            )

            if is_immediate:
                context = SlashContext(store=self._store, app=self.app)
                parse_result.command.execute(parse_result.args, context)
                logger.debug("slash_command_executed_immediate", command=cmd_name)
            else:
                self.post_message(
                    SlashCommandExecuted(parse_result.command.name, parse_result.args)
                )
            return

        # Regular message - validate and process
        state = self._store.state
        session = state.session

        if self._store.is_busy():
            self.app.notify("Please wait for the current operation", severity="warning")
            event.stop()
            return

        if not session:
            self.app.notify(
                "No active session. Press Ctrl+N to create one.", severity="warning"
            )
            event.stop()
            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"
            )
            event.stop()
            return

        if not content:
            event.stop()
            return

        # Clear input and let message bubble to parent
        text_area = self.query_one("#chat-input", ChatTextArea)
        text_area.clear()

    def focus_input(self) -> None:
        """Focus the text input."""
        self.query_one("#chat-input", ChatTextArea).focus()

    def clear_input(self) -> None:
        """Clear the text input."""
        self.query_one("#chat-input", ChatTextArea).clear()

    def get_text(self) -> str:
        """Get the current text content."""
        return self.query_one("#chat-input", ChatTextArea).text

    def set_injection_service(self, service: InjectionService) -> None:
        """Set the injection service for autocomplete."""
        self._injection_service = service

    def has_injections(self) -> bool:
        """Check if current text contains injection patterns."""
        return has_injections(self.get_text())
