# Copyright (c) Microsoft. All rights reserved.

"""Main Textual application for the harness console.

This module provides the HarnessApp - the main Textual application that
composes all UI components and integrates with the agent runner.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from textual import on, work
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Container, Vertical
from textual.css.query import NoMatches
from textual.widgets import Input, Static

from .app_state import (
    BottomPanelMode,
    HarnessAppState,
    OutputEntryType,
)
from .components import (
    AgentModeAndHelp,
    AgentStatus,
    HarnessListSelection,
    HarnessScrollPanel,
    HarnessTextInput,
    PromptRule,
)
from .textual_state_driver import HarnessConsoleUXStateDriver

if TYPE_CHECKING:
    from agent_framework import Agent, AgentSession

    from .agent_runner import HarnessAgentRunner
    from .commands import CommandHandler
    from .observers.base import ConsoleObserver


class HarnessApp(App[None]):
    """Main Textual application for the harness console.

    Composes the scroll panel (conversation history), status bar (spinner, usage),
    mode/help display, and bottom panel (text input, list selection, or streaming
    indicator). Routes user input to the agent runner.
    """

    CSS = """
    Screen {
        background: $background;
    }

    #scroll-panel {
        height: 1fr;
        padding: 0 1;
        background: transparent;
    }

    #bottom-panel {
        height: auto;
    }

    #text-input-container {
        height: 1;
        display: block;
    }

    #list-selection-container {
        height: auto;
        max-height: 12;
        display: none;
    }

    #streaming-indicator {
        height: 1;
        display: none;
    }

    #status-bar {
        height: 1;
    }

    #mode-help {
        height: 1;
    }

    #top-rule {
        height: 1;
    }

    #bottom-rule {
        height: 1;
    }

    #separator-rule {
        height: 1;
    }

    #text-input {
        height: 1;
    }

    .hidden {
        display: none;
    }

    .visible {
        display: block;
    }

    .input-field {
        border: none;
        padding: 0;
        min-height: 1;
        height: 1;
        background: transparent;
    }

    .input-field:focus {
        border: none;
        background: transparent;
    }

    .prompt-container {
        height: 1;
    }

    .prompt-label {
        width: 2;
        min-width: 2;
        height: 1;
    }
    """

    BINDINGS = [
        Binding("ctrl+c", "quit", "Quit", show=False),
        Binding("ctrl+q", "quit", "Quit", show=False),
    ]

    def __init__(
        self,
        agent: Agent,
        observers: list[ConsoleObserver],
        session: AgentSession | None = None,
        mode_colors: dict[str, str] | None = None,
        initial_mode: str | None = None,
        placeholder: str = "Type a message and press Enter...",
        title: str = "Harness Console",
        max_context_window_tokens: int | None = None,
        max_output_tokens: int | None = None,
        command_handlers: list[CommandHandler] | None = None,
    ) -> None:
        """Initialize the harness console application.

        Args:
            agent: The agent to run.
            observers: List of console observers.
            session: Optional agent session.
            mode_colors: Optional mode color mapping.
            initial_mode: Initial agent mode.
            placeholder: Input placeholder text.
            title: Application title.
            max_context_window_tokens: Optional max context window tokens for usage display.
            max_output_tokens: Optional max output tokens for usage display.
            command_handlers: Optional list of command handlers. If None, auto-detected.
        """
        super().__init__()
        self.title = title
        self._agent = agent
        self._observers = observers
        self._session = session
        self._mode_colors = mode_colors
        self._initial_mode = initial_mode
        self._placeholder = placeholder
        self._max_context_window_tokens = max_context_window_tokens
        self._max_output_tokens = max_output_tokens

        # Build command handlers
        if command_handlers is None:
            from .commands import build_default_command_handlers

            self._command_handlers = build_default_command_handlers(agent, mode_colors=mode_colors)
        else:
            self._command_handlers = command_handlers

        # Compute help text from command handlers
        help_parts = [h.get_help_text() for h in self._command_handlers if h.get_help_text() is not None]
        help_text = ", ".join(help_parts) if help_parts else None

        # State and driver
        self._app_state = HarnessAppState(
            placeholder=placeholder,
            mode_text=initial_mode,
            help_text=help_text,
        )
        self._ux_driver = HarnessConsoleUXStateDriver(
            app_state=self._app_state,
            on_state_changed=self._on_state_changed,
            mode_colors=mode_colors,
        )

        # Agent runner (created after init)
        self._runner: HarnessAgentRunner | None = None

    @property
    def ux_driver(self) -> HarnessConsoleUXStateDriver:
        """Get the UX state driver."""
        return self._ux_driver

    @property
    def runner(self) -> HarnessAgentRunner | None:
        """Get the agent runner."""
        return self._runner

    def compose(self) -> ComposeResult:
        """Compose the application layout."""
        with Vertical():
            # Main scroll panel for conversation history
            yield HarnessScrollPanel(id="scroll-panel")

            # Blank line separating scroll content from status area
            yield Static(" ", id="separator-rule")

            # Status bar (spinner + usage)
            yield AgentStatus(id="status-bar")

            # Top rule (mode-colored)
            yield PromptRule(id="top-rule")

            # Bottom panel - switches between text input, list selection, streaming
            with Container(id="bottom-panel"):
                # Text input (default)
                with Container(id="text-input-container"):
                    text_input = HarnessTextInput(id="text-input")
                    text_input.placeholder = self._placeholder
                    yield text_input

                # List selection (for follow-up questions)
                with Container(id="list-selection-container"):
                    yield HarnessListSelection(id="list-selection")

            # Bottom rule (mode-colored)
            yield PromptRule(id="bottom-rule")

            # Mode and help
            yield AgentModeAndHelp(id="mode-help")

    def on_mount(self) -> None:
        """Initialize after mount."""
        # Create agent runner now that everything is set up
        from .agent_runner import HarnessAgentRunner

        self._runner = HarnessAgentRunner(
            agent=self._agent,
            observers=self._observers,
            state_driver=self._ux_driver,
            max_context_window_tokens=self._max_context_window_tokens,
            max_output_tokens=self._max_output_tokens,
        )

        # Set initial mode
        if self._initial_mode:
            self._ux_driver.current_mode = self._initial_mode

        # Focus the text input
        try:
            text_input = self.query_one("#text-input", HarnessTextInput)
            text_input.focus_input()
        except NoMatches:
            pass

        # Set initial rule colors and mode display
        self._sync_mode_help()

    # --- Event handlers ---

    @on(HarnessTextInput.Submitted)
    def on_text_submitted(self, event: HarnessTextInput.Submitted) -> None:
        """Handle text input submission."""
        text = event.value.strip()
        if not text:
            return

        if self._app_state.pending_questions:
            # Answer the current follow-up question
            self._handle_follow_up_answer(text)
        elif self._app_state.mode == BottomPanelMode.STREAMING:
            # Input submitted while the agent is streaming — enqueue it for
            # injection into the ongoing run. Handled synchronously so it does
            # not cancel the exclusive turn worker.
            if self._runner is not None:
                self._runner.on_streaming_input(text, self._session)
        elif text.startswith("/"):
            # Try command handlers
            self._try_command_handlers(text)
        else:
            # Normal user input - run agent turn
            self._run_agent_turn(text)

    @work(exclusive=True, thread=False)
    async def _try_command_handlers(self, text: str) -> None:
        """Try each command handler; fall through to agent if none match."""
        session = self._session
        if session is None:
            # No session — fall through to agent turn
            self._run_agent_turn(text)
            return

        for handler in self._command_handlers:
            if await handler.try_handle(text, session, self._ux_driver):
                # Command handled — check for shutdown/session swap signals
                self._process_command_signals()
                return

        # No handler matched — treat as normal agent input
        self._run_agent_turn(text)

    def _process_command_signals(self) -> None:
        """Check and process signals set by command handlers."""
        if self._app_state.shutdown_requested:
            self.exit()
            return

        if self._app_state.replaced_session is not None:
            self._session = self._app_state.replaced_session  # type: ignore[assignment]
            self._app_state.replaced_session = None
            self._ux_driver.append_info_line("Session replaced.")

        self._sync_ui_from_state()

    @on(HarnessListSelection.Selected)
    def on_list_selected(self, event: HarnessListSelection.Selected) -> None:
        """Handle list selection."""
        self._handle_follow_up_answer(event.value)

    # --- Agent turn ---

    @work(exclusive=True, thread=False)
    async def _run_agent_turn(self, text: str) -> None:
        """Run an agent turn in a background worker."""
        if self._runner is None:
            return

        await self._runner.run_turn(text, session=self._session)

        # After turn completes, check for follow-up questions
        self._sync_ui_from_state()

    # --- Follow-up question handling ---

    @work(exclusive=True, thread=False)
    async def _handle_follow_up_answer(self, answer: str) -> None:
        """Handle a user's answer to a follow-up question."""
        if not self._app_state.pending_questions:
            return

        question = self._app_state.pending_questions[0]

        # Call the continuation
        result_message = await question.continuation(answer, self._ux_driver)

        # Add result to accumulated responses
        if result_message is not None:
            self._ux_driver.add_follow_up_response(result_message)

        # Advance to next question
        self._ux_driver.advance_follow_up_question()

        # If no more questions, resume the agent with accumulated responses
        if not self._app_state.pending_questions:
            responses = self._ux_driver.take_follow_up_responses()
            if responses and self._runner:
                await self._runner.start_agent_turn(responses, session=self._session)

        self._sync_ui_from_state()

    # --- State synchronization ---

    def _on_state_changed(self) -> None:
        """Called by state driver when state changes - schedule UI sync.

        Since the agent runner uses @work(thread=False), state changes happen
        on the main event loop. We use call_later to batch updates.
        """
        self.call_later(self._sync_ui_from_state)

    def _sync_ui_from_state(self) -> None:
        """Synchronize UI components with current application state."""
        state = self._app_state

        # Update scroll panel with new entries
        self._sync_scroll_panel()

        # Update bottom panel mode
        self._sync_bottom_panel(state.mode)

        # Hide status bar and mode/help during list selection (matching C#)
        is_list_mode = state.mode == BottomPanelMode.LIST_SELECTION
        self._sync_chrome_visibility(not is_list_mode)

        # Update status bar
        self._sync_status_bar()

        # Update mode/help display
        self._sync_mode_help()

    def _sync_scroll_panel(self) -> None:
        """Sync the scroll panel with output entries."""
        try:
            panel = self.query_one("#scroll-panel", HarnessScrollPanel)
        except NoMatches:
            return

        entries = self._app_state.output_entries
        rendered_count = getattr(self, "_rendered_entry_count", 0)

        if rendered_count < len(entries):
            # There are new entries to render
            for entry in entries[rendered_count:]:
                if entry.type == OutputEntryType.STREAMING_TEXT:
                    panel.set_streaming_entry(entry)
                else:
                    # End any active streaming before appending other entry types
                    panel.end_streaming()
                    panel.append_entry(entry)
            self._rendered_entry_count = len(entries)
        elif rendered_count == len(entries) and entries:
            # Same count — check if the last entry is a streaming entry that was mutated
            last_entry = entries[-1]
            if last_entry.type == OutputEntryType.STREAMING_TEXT:
                panel.set_streaming_entry(last_entry)

    def _sync_bottom_panel(self, mode: BottomPanelMode) -> None:
        """Switch the bottom panel between text input, list, and streaming."""
        try:
            text_container = self.query_one("#text-input-container")
            list_container = self.query_one("#list-selection-container")
        except NoMatches:
            return

        if mode == BottomPanelMode.TEXT_INPUT:
            text_container.display = True
            list_container.display = False
            # Restore the normal placeholder and focus to text input
            try:
                text_input = self.query_one("#text-input", HarnessTextInput)
                text_input.placeholder = self._placeholder
                text_input.focus_input()
            except NoMatches:
                pass
        elif mode == BottomPanelMode.LIST_SELECTION:
            text_container.display = False
            list_container.display = True
            self._sync_list_selection()
        elif mode == BottomPanelMode.STREAMING:
            text_container.display = True
            list_container.display = False
            # Hint that typed input will be queued for injection into the run.
            try:
                text_input = self.query_one("#text-input", HarnessTextInput)
                text_input.placeholder = "type to queue a message for the agent…"
            except NoMatches:
                pass

    def _sync_list_selection(self) -> None:
        """Sync the list selection widget with state."""
        try:
            list_widget = self.query_one("#list-selection", HarnessListSelection)
        except NoMatches:
            return

        state = self._app_state
        list_widget.title = state.list_selection_title or ""
        list_widget.options = list(state.list_selection_options)
        list_widget.allow_custom_text = state.list_selection_custom_text_placeholder is not None

        if state.list_selection_custom_text_placeholder:
            try:
                custom_input = list_widget.query_one("#custom-input", Input)
                custom_input.placeholder = state.list_selection_custom_text_placeholder
            except Exception:
                pass

        # Focus the option list so keyboard navigation works immediately
        list_widget.focus_list()

    def _sync_status_bar(self) -> None:
        """Sync the status bar with state."""
        try:
            status = self.query_one("#status-bar", AgentStatus)
        except NoMatches:
            return

        state = self._app_state
        status.show_spinner = state.show_spinner
        status.usage_text = state.usage_text or ""
        status.queued_text = " ".join(state.queued_items)

    def _sync_mode_help(self) -> None:
        """Sync the mode/help display and rule colors with state."""
        try:
            mode_help = self.query_one("#mode-help", AgentModeAndHelp)
        except NoMatches:
            return

        state = self._app_state
        mode_help.mode = state.mode_text or ""
        mode_help.mode_color = state.mode_color or "blue"
        mode_help.help_text = state.help_text or ""

        # Sync rule colors to match mode
        color = state.mode_color or "cyan"
        try:
            top_rule = self.query_one("#top-rule", PromptRule)
            top_rule.rule_color = color
        except NoMatches:
            pass

        try:
            bottom_rule = self.query_one("#bottom-rule", PromptRule)
            bottom_rule.rule_color = color
        except NoMatches:
            pass

    def _sync_chrome_visibility(self, visible: bool) -> None:
        """Show or hide chrome elements (status bar, mode/help).

        During list selection mode, these are hidden to give more vertical
        space to the scroll panel and list picker.

        Args:
            visible: Whether chrome elements should be visible.
        """
        import contextlib

        with contextlib.suppress(NoMatches):
            self.query_one("#status-bar", AgentStatus).display = visible
        with contextlib.suppress(NoMatches):
            self.query_one("#mode-help", AgentModeAndHelp).display = visible

    # --- Rendering count tracking ---

    _rendered_entry_count: int = 0
