"""Root Textual application — full-width chat with Agent Johnny 5.

v1.1: Deterministic install runner + conversational agent.

The install runs as a background Python script (subprocess.run for
each step). The agent is always in conversation mode — available
during and after install for questions and troubleshooting.

Two independent background threads:
  - Install thread: runs install_runner.run_install() — deterministic,
    no LLM. Posts InstallOutput + ProgressUpdate to UI.
  - Agent thread: runs AgentBrain.handle_user_message() — LLM-powered
    conversation. Posts AgentReply + AgentToolCall to UI.

They share InstallState (read by agent for system prompt context,
written by install runner for progress tracking).

Orphan protection (unchanged from v0.8.7):
  Three-layer failsafe prevents zombie processes when SSH disconnects.
"""

from __future__ import annotations

import asyncio
import logging
import os
import signal
import sys

# ── Logging to file (safe — does NOT touch sys.stderr) ──
logging.basicConfig(
    filename="/tmp/easyclaw-installer.log",
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s: %(message)s",
    force=True,
)

logger = logging.getLogger(__name__)

from textual.app import App, ComposeResult
from textual.binding import Binding
from textual import work

from .messages import AgentReply, AgentToolCall, AgentTyping, InstallOutput, ProgressUpdate
from .state import InstallState
from .constants import BUILTIN_API_KEY, OPENROUTER_MODEL
from .agent.brain import AgentBrain
from .install_runner import run_install
from .ui.chat_pane import ChatPane
from .ui.status_bar import StatusBar


class EasyClawApp(App):
    """EasyClaw Installer v1.1 — Deterministic install + Agent Johnny 5."""

    ENABLE_COMMAND_PALETTE = False

    CSS = """
    Screen {
        layout: vertical;
    }

    #chat-pane {
        height: 1fr;
    }

    /* No focusable widgets — suppress all focus rings */
    *:focus {
        border: none;
    }
    """

    TITLE = "EasyClaw Installer v1.1"
    SUB_TITLE = "Agent Johnny 5"

    BINDINGS = [
        Binding("ctrl+backslash", "quit", "Quit", priority=True),
    ]

    def __init__(
        self, api_key: str = "", mcp_url: str = "",
        mcp_key: str = "", **kwargs
    ) -> None:
        super().__init__(**kwargs)
        self.state = InstallState()

        # Raw key capture input buffer for chat
        self._input_buffer: str = ""

        # Resolve API key: explicit arg > env var > built-in key
        resolved_key = (
            api_key
            or os.environ.get("OPENROUTER_API_KEY", "")
            or BUILTIN_API_KEY
        )
        resolved_mcp_key = (
            mcp_key
            or os.environ.get("EASYCLAW_MCP_KEY", "")
        )

        self._mcp_url = mcp_url

        self.agent = AgentBrain(
            self.state, resolved_key, mcp_url=mcp_url,
            mcp_key=resolved_mcp_key, model=OPENROUTER_MODEL,
        )

    # ------------------------------------------------------------------
    # Compose
    # ------------------------------------------------------------------

    def compose(self) -> ComposeResult:
        yield ChatPane(id="chat-pane")
        yield StatusBar(id="status-bar")

    # ------------------------------------------------------------------
    # Raw key capture — always routes to chat input
    # ------------------------------------------------------------------

    def on_key(self, event) -> None:
        """Route keystrokes to the chat input buffer."""
        try:
            key = event.key
            char = event.character

            # Enter → submit message
            if key == "enter":
                text = self._input_buffer.strip()
                if text:
                    chat_pane = self.query_one("#chat-pane", ChatPane)
                    chat_pane.add_user_message(text)
                    # Always send to conversational agent
                    # (works both during and after install)
                    self.send_to_agent(text)
                self._input_buffer = ""
                self._update_input_display()
                event.prevent_default()
                event.stop()
                return

            # Backspace → delete last character
            if key == "backspace":
                if self._input_buffer:
                    self._input_buffer = self._input_buffer[:-1]
                    self._update_input_display()
                event.prevent_default()
                event.stop()
                return

            # Escape → clear input buffer
            if key == "escape":
                if self._input_buffer:
                    self._input_buffer = ""
                    self._update_input_display()
                event.prevent_default()
                event.stop()
                return

            # Printable character → append to buffer
            if char and char.isprintable():
                self._input_buffer += char
                self._update_input_display()
                event.prevent_default()
                event.stop()
                return

        except Exception:
            logger.exception("Error in on_key")

    def on_paste(self, event) -> None:
        """Handle bracketed paste — append to input buffer."""
        try:
            text = event.text
            if not text:
                return
            clean = text.replace("\n", " ").replace("\r", "")
            self._input_buffer += clean
            self._update_input_display()
            event.prevent_default()
            event.stop()
        except Exception:
            logger.exception("Error in on_paste")

    def _update_input_display(self) -> None:
        """Push the current input buffer text to the chat pane display."""
        try:
            chat_pane = self.query_one("#chat-pane", ChatPane)
            chat_pane.update_input_display(self._input_buffer)
        except Exception:
            pass

    # ------------------------------------------------------------------
    # Orphan protection — prevent zombie processes on SSH disconnect
    # ------------------------------------------------------------------

    _MAX_RUNTIME = 7200  # 2 hours

    def _setup_orphan_protection(self) -> None:
        """Three-layer failsafe against orphaned processes."""
        if hasattr(signal, "SIGHUP"):
            signal.signal(signal.SIGHUP, self._on_terminal_hangup)
        self.set_interval(30, self._check_terminal_alive)
        self.set_timer(self._MAX_RUNTIME, self._on_max_runtime)

    @staticmethod
    def _on_terminal_hangup(signum: int, frame: object) -> None:
        os._exit(0)

    def _check_terminal_alive(self) -> None:
        try:
            fd = os.open("/dev/tty", os.O_RDONLY | os.O_NOCTTY)
            os.close(fd)
        except OSError:
            os._exit(0)

    def _on_max_runtime(self) -> None:
        os._exit(0)

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

    async def on_mount(self) -> None:
        """Greet user, start install runner, init MCP."""
        # Disable mouse tracking for web terminal compatibility
        try:
            driver = self._driver
            if driver and hasattr(driver, "_disable_mouse_support"):
                driver._disable_mouse_support()
        except Exception:
            pass

        # Orphan protection
        self._setup_orphan_protection()

        # Store main event loop for agent thread scheduling
        self.state.main_loop = asyncio.get_event_loop()

        # Show agent greeting
        chat_pane = self.query_one("#chat-pane", ChatPane)
        chat_pane.add_agent_message(
            "Hi! I'm Johnny 5, your AI install assistant.\n"
            "\n"
            "I'm starting the OpenClaw installation now. "
            "You'll see live progress below.\n"
            "\n"
            "Type a message anytime to ask me questions."
        )

        # Init MCP knowledge base (lazy, non-blocking)
        if self._mcp_url:
            self.init_agent_mcp()

        # Start deterministic install after a short delay (so greeting renders)
        self.set_timer(0.5, lambda: self.begin_install())

    # ------------------------------------------------------------------
    # Workers (BACKGROUND THREADS)
    # ------------------------------------------------------------------

    @work(thread=True)
    def init_agent_mcp(self) -> None:
        """Connect to MCP knowledge base in a background thread."""
        self.agent.ensure_mcp(self)

    @work(thread=True)
    def begin_install(self) -> None:
        """Run the deterministic install in a background thread."""
        run_install(self, self.state)

    @work(thread=True, exclusive=True, group="agent-loop")
    def send_to_agent(self, user_text: str) -> None:
        """Run the agent loop for a user message (conversation only)."""
        self.agent.handle_user_message(user_text, self)

    # ------------------------------------------------------------------
    # Message handlers
    # ------------------------------------------------------------------

    def on_agent_typing(self, message: AgentTyping) -> None:
        chat_pane = self.query_one("#chat-pane", ChatPane)
        chat_pane.show_typing()

    def on_agent_reply(self, message: AgentReply) -> None:
        chat_pane = self.query_one("#chat-pane", ChatPane)
        chat_pane.add_agent_message(message.text)

    def on_agent_tool_call(self, message: AgentToolCall) -> None:
        chat_pane = self.query_one("#chat-pane", ChatPane)
        chat_pane.add_tool_indicator(message.tool_name, message.args)

    def on_install_output(self, message: InstallOutput) -> None:
        chat_pane = self.query_one("#chat-pane", ChatPane)
        chat_pane.add_install_output(message.text, message.stream)

    def on_progress_update(self, message: ProgressUpdate) -> None:
        chat_pane = self.query_one("#chat-pane", ChatPane)
        chat_pane.add_progress_update(
            message.step, message.total, message.title, message.status
        )
        # Update status bar
        status = self.query_one("#status-bar", StatusBar)
        status.set_step(message.step, message.total, message.title, message.status)
