"""Root Textual application — 60/40 split-pane terminal + agent chat.

v0.9.0: PTY monitor architecture. The left pane runs the official
OpenClaw installer in a real PTY. Agent Johnny 5 monitors from the
right pane and helps when things go wrong.

Concurrency boundaries:
  - Terminal (TerminalWidget): async PTY I/O via add_reader() on main loop.
  - Agent (AgentBrain): @work(thread=True) in a BACKGROUND THREAD.
    Uses httpx.Client (sync) for streaming, subprocess.run for tools.
    All UI updates routed via app.call_from_thread().

Input strategy (v0.9.0 — Tab-based focus switching):
  - TERMINAL mode (default): keystrokes → PTY subprocess
  - CHAT mode: keystrokes → agent input buffer (raw on_key capture)
  - Tab key toggles between modes
  - Ctrl+C in terminal mode → \\x03 to PTY (NOT quit app)
  - Ctrl+\\ → quit app (escape hatch)

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
import time

# ── 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, TerminalExited
from .state import InstallState
from .constants import (
    BUILTIN_API_KEY,
    OPENROUTER_MODEL,
    MONITOR_INTERVAL,
    MONITOR_STALL_TIMEOUT,
)
from .agent.brain import AgentBrain
from .ui.log_pane import LogPane
from .ui.terminal_widget import TerminalWidget
from .ui.chat_pane import ChatPane
from .ui.status_bar import StatusBar


class EasyClawApp(App):
    """EasyClaw Agentic Installer v0.9.0 — PTY monitor + Agent Johnny 5."""

    ENABLE_COMMAND_PALETTE = False

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

    #log-pane {
        width: 3fr;
        height: 1fr;
        border-right: thick #444444;
    }

    #chat-pane {
        width: 2fr;
        height: 1fr;
    }

    /* Focused pane gets a bright border indicator */
    .pane-focused {
        border: heavy #00aaff;
    }

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

    TITLE = "EasyClaw Installer v0.9.0"
    SUB_TITLE = "Agent Johnny 5 — Monitoring Mode"

    # Ctrl+\ to quit (Ctrl+C goes to PTY in terminal mode)
    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()

        # Focus mode: "terminal" (default) or "chat"
        self._focus_mode: str = "terminal"

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

        # Track last output time for stall detection
        self._last_output_check: 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._api_key = resolved_key
        self._model = OPENROUTER_MODEL
        self._mcp_url = mcp_url
        self._mcp_key = resolved_mcp_key

        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 LogPane(id="log-pane")
        yield ChatPane(id="chat-pane")
        yield StatusBar(id="status-bar")

    # ------------------------------------------------------------------
    # Tab-based focus switching + raw key capture
    # ------------------------------------------------------------------

    def on_key(self, event) -> None:
        """Route keystrokes based on focus mode.

        TERMINAL mode: keystrokes → PTY subprocess
        CHAT mode: keystrokes → agent input buffer
        Tab/F2: toggle focus mode
        Ctrl+C: in terminal mode → \\x03 to PTY; in chat mode → ignored

        CRITICAL: If the PTY has exited and focus is still on terminal,
        auto-switch to chat so the user isn't trapped in a dead pane.
        This is the #1 UX issue — web terminals (Hostinger) eat the Tab
        key, so users couldn't reach the chat pane at all.
        """
        try:
            key = event.key
            char = event.character

            # Tab or F2 → toggle focus between terminal and chat
            # (F2 is the reliable alternative — Tab is intercepted by
            # browser-based web terminals like Hostinger)
            if key in ("tab", "f2"):
                self._toggle_focus()
                event.prevent_default()
                event.stop()
                return

            # ── TERMINAL MODE (PTY alive) ──
            if self._focus_mode == "terminal":
                terminal = self._get_terminal()

                # If terminal is gone or dead, auto-switch to chat
                # so user isn't trapped in a dead pane
                if terminal is None or not terminal.is_alive:
                    self._switch_to_chat()
                    # Fall through to chat handling below
                else:
                    # Ctrl+C → send interrupt to PTY (NOT quit app)
                    if key == "ctrl+c":
                        terminal.write_input("\x03")
                        event.prevent_default()
                        event.stop()
                        return

                    # Ctrl+D → send EOF to PTY
                    if key == "ctrl+d":
                        terminal.write_input("\x04")
                        event.prevent_default()
                        event.stop()
                        return

                    # Enter → send newline to PTY
                    if key == "enter":
                        terminal.write_input("\r")
                        event.prevent_default()
                        event.stop()
                        return

                    # Backspace → send backspace to PTY
                    if key == "backspace":
                        terminal.write_input("\x7f")
                        event.prevent_default()
                        event.stop()
                        return

                    # Escape → send escape to PTY
                    if key == "escape":
                        terminal.write_input("\x1b")
                        event.prevent_default()
                        event.stop()
                        return

                    # Arrow keys
                    if key == "up":
                        terminal.write_input("\x1b[A")
                        event.prevent_default()
                        event.stop()
                        return
                    if key == "down":
                        terminal.write_input("\x1b[B")
                        event.prevent_default()
                        event.stop()
                        return
                    if key == "right":
                        terminal.write_input("\x1b[C")
                        event.prevent_default()
                        event.stop()
                        return
                    if key == "left":
                        terminal.write_input("\x1b[D")
                        event.prevent_default()
                        event.stop()
                        return

                    # Printable character → send to PTY
                    if char and char.isprintable():
                        terminal.write_input(char)
                        event.prevent_default()
                        event.stop()
                        return

                    # Key not handled by terminal — don't fall through
                    return

            # ── CHAT MODE (or fell through from dead terminal) ──
            if self._focus_mode == "chat":
                # Enter → submit message to agent
                if key == "enter":
                    text = self._input_buffer.strip()
                    if text:
                        chat_pane = self.query_one("#chat-pane", ChatPane)
                        chat_pane.add_user_message(text)
                        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 — route based on focus mode."""
        try:
            text = event.text
            if not text:
                return

            if self._focus_mode == "terminal":
                # Send pasted text directly to PTY
                terminal = self._get_terminal()
                if terminal is not None:
                    terminal.write_input(text)
            else:
                # Chat mode — append to input buffer
                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 _toggle_focus(self) -> None:
        """Switch focus between terminal and chat panes."""
        if self._focus_mode == "terminal":
            self._focus_mode = "chat"
        else:
            self._focus_mode = "terminal"
        self._update_focus_visuals()
        logger.info("Focus toggled to: %s", self._focus_mode)

    def _switch_to_chat(self) -> None:
        """Force-switch focus to chat mode.

        Called automatically when the PTY exits so the user isn't
        trapped typing into a dead terminal.
        """
        if self._focus_mode == "chat":
            return  # Already there
        self._focus_mode = "chat"
        self._update_focus_visuals()
        logger.info("Auto-switched to chat (terminal dead)")

    def _update_focus_visuals(self) -> None:
        """Update status bar and pane borders for current focus mode."""
        # Update status bar
        status = self.query_one("#status-bar", StatusBar)
        status.set_focus_mode(self._focus_mode)

        # Visual feedback — update pane borders
        log_pane = self.query_one("#log-pane")
        chat_pane = self.query_one("#chat-pane")

        if self._focus_mode == "terminal":
            log_pane.add_class("pane-focused")
            chat_pane.remove_class("pane-focused")
        else:
            chat_pane.add_class("pane-focused")
            log_pane.remove_class("pane-focused")

    def _get_terminal(self) -> TerminalWidget | None:
        """Get the TerminalWidget instance."""
        try:
            return self.query_one("#terminal", TerminalWidget)
        except Exception:
            return None

    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:
        """Start the PTY terminal, greet the user, and 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()

        # Get terminal widget reference and store in state
        terminal = self._get_terminal()
        self.state.terminal = terminal

        # Set initial focus visual indicator
        log_pane = self.query_one("#log-pane")
        log_pane.add_class("pane-focused")

        # Show agent greeting in chat
        chat_pane = self.query_one("#chat-pane", ChatPane)
        chat_pane.add_agent_message(
            "I'm Johnny 5, your AI install assistant.\n"
            "\n"
            "The official OpenClaw installer is running in the "
            "terminal on the left. I'm watching for any issues.\n"
            "\n"
            "Press [Tab] to switch between terminal and chat.\n"
            "Type into the terminal to interact with the installer.\n"
            "Switch to chat to ask me questions."
        )

        # Spawn the official installer in the PTY
        if terminal is not None:
            await terminal.spawn(
                "bash -c '"
                "echo \"=== EasyClaw Installer v0.9.0 ===\"; "
                "echo \"Starting OpenClaw installation...\"; "
                "echo; "
                # Bootstrap: install Node.js + CLI if needed, then run onboard
                "if ! command -v node >/dev/null 2>&1; then "
                "  echo \"Installing Node.js 22...\"; "
                "  curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && "
                "  apt-get install -y nodejs; "
                "fi; "
                "echo \"Node.js: $(node --version 2>/dev/null || echo not found)\"; "
                # Install @openclaw/cli from our tarball
                "if ! command -v openclaw >/dev/null 2>&1; then "
                "  echo \"Installing @openclaw/cli...\"; "
                "  npm install -g https://easyclaw.help/packages/openclaw-cli-0.1.0.tgz; "
                "fi; "
                "echo; "
                "echo \"Running OpenClaw onboard wizard...\"; "
                "echo \"─────────────────────────────────\"; "
                "openclaw onboard --install-daemon"
                "'"
            )
            status = self.query_one("#status-bar", StatusBar)
            status.set_terminal_status("Running")
            self.state.is_running = True
            self.state.last_output_time = time.time()

        # Start monitoring loop
        self.set_interval(MONITOR_INTERVAL, self._monitor_terminal)

        # Init MCP knowledge base
        if self._mcp_url:
            self.init_agent_mcp()

    # ------------------------------------------------------------------
    # Terminal monitoring (proactive error/stall detection)
    # ------------------------------------------------------------------

    def _monitor_terminal(self) -> None:
        """Periodic check on terminal output for errors or stalls."""
        terminal = self._get_terminal()
        if terminal is None:
            return

        # Check if process exited
        if not terminal.is_alive and self.state.is_running:
            self.state.is_running = False
            exit_code = terminal.exit_code
            status = self.query_one("#status-bar", StatusBar)

            # Auto-switch to chat so user can talk to Johnny 5
            self._switch_to_chat()

            if exit_code == 0:
                self.state.is_complete = True
                status.set_terminal_status("Complete")
                self._agent_notify(
                    "The installation completed successfully! "
                    "Run `openclaw health --full` to verify everything is working."
                )
            else:
                self.state.has_error = True
                status.set_terminal_status(f"Exited ({exit_code})")
                self.agent_proactive_intervention(
                    "terminal_exit",
                    f"The installer process exited with code {exit_code}."
                )
            return

        if not self.state.is_running:
            return

        # Get recent output for error scanning
        recent = terminal.get_recent_output(max_lines=20)

        # Update last output time
        current_check = recent[-200:] if recent else ""
        if current_check != self._last_output_check:
            self._last_output_check = current_check
            self.state.last_output_time = time.time()

        # Error pattern detection
        error_patterns = [
            "Error:", "ERROR:", "FAILED", "fatal:", "Fatal:",
            "Permission denied", "No such file or directory",
            "command not found", "npm ERR!", "E: Unable to locate",
            "EACCES", "ENOENT", "SIGTERM", "Traceback",
        ]

        if not self.state.agent_investigating:
            for pattern in error_patterns:
                if pattern in recent:
                    self.agent_proactive_intervention(
                        "terminal_error", recent[-500:]
                    )
                    break

        # Stall detection — no new output for MONITOR_STALL_TIMEOUT
        elapsed = time.time() - self.state.last_output_time
        if (
            elapsed > MONITOR_STALL_TIMEOUT
            and not self.state.agent_investigating
            and self.state.is_running
        ):
            self.agent_proactive_intervention(
                "terminal_stall",
                f"No terminal output for {int(elapsed)} seconds. "
                "The installer may be stuck or waiting for input."
            )

    def _agent_notify(self, text: str) -> None:
        """Send a notification message through the agent chat."""
        chat_pane = self.query_one("#chat-pane", ChatPane)
        chat_pane.add_agent_message(text)

    # ------------------------------------------------------------------
    # Agent worker (BACKGROUND THREAD)
    # ------------------------------------------------------------------

    @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, exclusive=True, group="agent-loop")
    def send_to_agent(self, user_text: str) -> None:
        """Run the agent loop in a background thread."""
        self.agent.handle_user_message(user_text, self)

    @work(thread=True, exclusive=True, group="agent-loop")
    def agent_proactive_intervention(
        self, trigger: str, context: str
    ) -> None:
        """Agent auto-diagnoses a terminal issue (background thread)."""
        self.state.agent_investigating = True
        try:
            self.agent.handle_terminal_issue(trigger, context, self)
        finally:
            self.state.agent_investigating = False

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