"""AiCippY Textual TUI — A7THAVA-powered agent with streaming, auto-execution, workspace awareness."""

from __future__ import annotations

import asyncio
import json
import platform
import re
import subprocess
import time
from pathlib import Path
from typing import Any

from textual import on, work
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal, Vertical, VerticalScroll
from textual.css.query import NoMatches
from textual.reactive import reactive
from textual.widgets import Button, Static, TextArea

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

DISPLAY_NAMES: dict[str, str] = {"odin": "OdiN", "arjuna": "ArjunA"}
MEMORY_FILE = Path.home() / ".aicippy" / "session_memory.json"
MAX_AUTO_EXEC_TURNS = 50
SHELL_RE = re.compile(r"```(?:powershell|bash|sh|shell|cmd)\n(.*?)```", re.DOTALL)
FILE_RE = re.compile(r"```(?:typescript|python|tsx|ts|py|json|css|html|yaml|toml)\n(.*?)```", re.DOTALL)


def _dn(key: str) -> str:
    """Return display name for agent key."""
    return DISPLAY_NAMES.get(key.lower(), key) if key else "OdiN"


# ---------------------------------------------------------------------------
# Session Memory
# ---------------------------------------------------------------------------


class SessionMemory:
    """Persistent memory across restarts."""

    def __init__(self) -> None:
        self._data: dict[str, Any] = {}
        self._load()

    def _load(self) -> None:
        try:
            if MEMORY_FILE.exists():
                self._data = json.loads(MEMORY_FILE.read_text("utf-8"))
        except Exception:
            self._data = {}

    def save(self) -> None:
        try:
            MEMORY_FILE.parent.mkdir(parents=True, exist_ok=True)
            MEMORY_FILE.write_text(json.dumps(self._data, indent=2), "utf-8")
        except Exception:
            pass

    def get(self, key: str, default: Any = None) -> Any:
        return self._data.get(key, default)

    def set(self, key: str, value: Any) -> None:
        self._data[key] = value
        self.save()

    def context_str(self) -> str:
        """Return env context for agent prompts."""
        return "\n".join(f"  {k}: {v}" for k, v in self._data.items() if not k.startswith("_"))


def _detect_env(mem: SessionMemory) -> None:
    """Auto-detect machine environment."""
    mem.set("os", f"{platform.system()} {platform.release()} ({platform.machine()})")
    mem.set("python", platform.python_version())
    mem.set("computer", platform.node())
    mem.set("user", str(Path.home().name))
    mem.set("cwd", str(Path.cwd()))
    mem.set("project", Path.cwd().name)

    # Scan workspace tree (top 2 levels)
    try:
        tree: list[str] = []
        cwd = Path.cwd()
        for p in sorted(cwd.iterdir()):
            if p.name.startswith(".") or p.name in {"node_modules", "__pycache__", "dist", "build", ".git", "venv"}:
                continue
            tree.append(p.name + ("/" if p.is_dir() else ""))
            if p.is_dir():
                try:
                    for sub in sorted(p.iterdir())[:10]:
                        if not sub.name.startswith("."):
                            tree.append(f"  {sub.name}")
                except PermissionError:
                    pass
        mem.set("workspace_tree", "\n".join(tree[:60]))
    except Exception:
        pass

    # AWS check
    try:
        r = subprocess.run(["aws", "sts", "get-caller-identity", "--query", "Account", "--output", "text"],
                           capture_output=True, text=True, timeout=10, check=False)
        if r.returncode == 0 and r.stdout.strip():
            mem.set("aws_account", r.stdout.strip())
            mem.set("aws_configured", True)
    except Exception:
        mem.set("aws_configured", False)

    # Docker check
    try:
        r = subprocess.run(["docker", "info", "--format", "{{.ServerVersion}}"],
                           capture_output=True, text=True, timeout=5, check=False)
        if r.returncode == 0 and r.stdout.strip():
            mem.set("docker", r.stdout.strip())
    except Exception:
        pass

    # Git check
    try:
        r = subprocess.run(["git", "branch", "--show-current"],
                           capture_output=True, text=True, timeout=5, check=False)
        if r.returncode == 0 and r.stdout.strip():
            mem.set("git_branch", r.stdout.strip())
    except Exception:
        pass

    mem.save()


# ---------------------------------------------------------------------------
# CSS
# ---------------------------------------------------------------------------

# ZooZoo animated frames — mood-based ASCII art
ZOOZOO_FRAMES: dict[str, list[str]] = {
    "idle": ["╭◉‿◉╮", "╭◉‿◉╮✨", "╭◉ᴗ◉╮", "╭◉ᴗ◉╮💭"],
    "thinking": ["╭◉_◉╮💭", "╭◉•◉╮💭", "╭◉_◉╮🔍", "╭◉•◉╮🔍"],
    "working": ["╭◉▿◉╮⚡", "╭◉△◉╮⚡", "╭◉▿◉╮🔧", "╭◉△◉╮🔧"],
    "success": ["╭◉‿◉╮✅", "╭◉‿◉╮🎉", "╭◉‿◉╮✨", "╭◉‿◉╮💚"],
    "error": ["╭◉_◉╮❌", "╭◉╭◉╮😰", "╭◉_◉╮🔴", "╭◉╭◉╮💔"],
    "hello": ["╭◉‿◉╮👋", "╭◉ᴗ◉╮🤗", "╭◉‿◉╮💫", "╭◉ᴗ◉╮🌟"],
}
ZOOZOO_GREETINGS = [
    "Hey! I see you! 👋",
    "Hi there! Ready to help! ✨",
    "AiCippY at your service! 🚀",
    "ZooZoo says hi! What can I do? 💡",
    "Click me again, I dare you! 😄",
    "OdiN and I are ready! Let's go! ⚡",
]

APP_CSS = """
Screen { background: #0d1117; }

/* Header */
#header-bar {
    dock: top; height: 3; background: #161b22;
    border-bottom: solid #30363d; padding: 0 1;
}
#header-left {
    width: 1fr; height: 3;
    content-align: left middle; padding: 0 1;
}
#header-right {
    width: auto; height: 3;
    content-align: right middle; padding: 0 1;
}
#zoozoo {
    width: auto; height: 3;
    content-align: center middle; padding: 0 1;
    color: #22d3ee;
}
#zoozoo:hover { color: #00ff88; }

/* Info */
#info-bar {
    dock: top; height: 1;
    background: #0d1117; color: #6b7280; padding: 0 2;
}

/* Chat */
#chat-scroll {
    background: #0d1117; padding: 0 1; scrollbar-size: 1 1;
}
.msg-user {
    margin: 1 0 0 4; padding: 1 2;
    background: #1a3a5c; color: #e5e7eb; border: round #264d73;
}
.msg-agent {
    margin: 1 4 0 0; padding: 1 2;
    background: #1c1c2e; color: #d1d5db; border: round #30363d;
}
.msg-system { margin: 0 0; padding: 0 2; color: #a855f7; }
.msg-success { color: #00ff88; padding: 0 2; }
.msg-exec {
    margin: 0 1; padding: 0 2; color: #22d3ee;
    background: #0f1923; border: round #1e3a50;
}
.msg-stream { margin: 0 4 0 0; padding: 0 2; color: #9ca3af; }
.msg-zoozoo {
    margin: 0 2; padding: 0 2; color: #fbbf24;
    text-style: italic;
}

/* Input */
#input-area {
    dock: bottom; height: auto; max-height: 12;
    background: #161b22; border-top: solid #30363d;
    padding: 1 1;
}
#input-row {
    height: auto; min-height: 3; max-height: 8;
    padding: 0 1;
}
#prompt-input {
    width: 1fr; min-height: 3; max-height: 6;
    border: round #30363d; background: #0d1117;
    color: #e5e7eb;
}
#prompt-input:focus { border: round #6366f1; }
#send-btn {
    width: 8; min-width: 8; height: 3;
    margin: 0 0 0 1; background: #6366f1;
    color: #fff; text-style: bold; border: none;
}
#send-btn:hover { background: #4f46e5; }
#chips-row { height: 1; padding: 0 2; }
.chip {
    width: auto; margin: 0 1 0 0; padding: 0 1;
    background: #1e293b; color: #9ca3af;
    border: round #30363d;
}
.chip:hover { background: #334155; color: #22d3ee; }
#status-bar {
    dock: bottom; height: 1;
    background: #080b10; color: #6b7280; padding: 0 2;
}
"""


# ---------------------------------------------------------------------------
# Widgets
# ---------------------------------------------------------------------------


class ZooZooWidget(Static):
    """Animated ZooZoo mascot with click interaction."""

    mood: reactive[str] = reactive("idle")
    _frame: reactive[int] = reactive(0)
    _click_count: int = 0

    def __init__(self) -> None:
        super().__init__(id="zoozoo")
        self._timer_handle: Any = None

    def on_mount(self) -> None:
        """Start animation timer."""
        self._timer_handle = self.set_interval(0.8, self._next_frame)

    def _next_frame(self) -> None:
        frames = ZOOZOO_FRAMES.get(self.mood, ZOOZOO_FRAMES["idle"])
        self._frame = (self._frame + 1) % len(frames)
        self.update(f"[bold #22d3ee]{frames[self._frame]}[/]")

    def on_click(self) -> None:
        """ZooZoo speaks when clicked."""
        import random
        self._click_count += 1
        greeting = random.choice(ZOOZOO_GREETINGS)  # noqa: S311
        self.mood = "hello"
        # Post greeting to chat
        try:
            app = self.app
            if hasattr(app, "_add_zoozoo_msg"):
                app._add_zoozoo_msg(greeting)
        except Exception:
            pass

    def set_mood(self, mood: str) -> None:
        """Change mascot mood."""
        if mood in ZOOZOO_FRAMES:
            self.mood = mood


class HeaderBar(Horizontal):
    """Title bar."""

    def __init__(self, model: str, user: str) -> None:
        super().__init__(id="header-bar")
        self._model = model
        self._user = user

    def compose(self) -> ComposeResult:
        try:
            from aicippy import __version__
        except Exception:
            __version__ = "?"
        brand = "[bold #00ff88]Ai[/][bold #22d3ee]Cipp[/][bold #00ff88]Y[/]"
        left_text = (
            f"{brand} [#6b7280]v{__version__}[/]"
            f"  Agent: [bold #f59e0b]{self._model}[/]"
        )
        right_text = ""
        if self._user:
            right_text += f"[#22d3ee]{self._user}[/]  "
        right_text += "[bold #00ff88]● LIVE[/]"
        yield Static(left_text, id="header-left")
        yield Static(right_text, id="header-right")
        yield ZooZooWidget()


class InfoBar(Static):
    """Info line."""

    def __init__(self, mem: SessionMemory) -> None:
        proj = mem.get("project", "?")
        cwd = mem.get("cwd", str(Path.cwd()))
        home = str(Path.home())
        short = cwd.replace(home, "~", 1) if cwd.startswith(home) else cwd
        aws = f"  [bold #f59e0b]AWS:{mem.get('aws_account', '')}[/]" if mem.get("aws_configured") else ""
        branch = f"  [bold #a78bfa]⎇ {mem.get('git_branch', '')}[/]" if mem.get("git_branch") else ""
        super().__init__(
            f"[#6b7280]project:[/] [bold #22d3ee]{proj}[/]  [#6b7280]CWD:[/] [bold #ffd93d]{short}[/]{branch}{aws}",
            id="info-bar",
        )


class ChatMessage(Static):
    """Chat bubble."""

    pass


class StreamingMessage(Static):
    """Live streaming text that updates in-place."""

    pass


class CommandChip(Button):
    """Chip."""

    def __init__(self, cmd: str) -> None:
        super().__init__(cmd, classes="chip")


class StatusBar(Static):
    """Status bar."""

    tokens: reactive[int] = reactive(0)
    latency: reactive[int] = reactive(0)
    session_start: reactive[float] = reactive(0.0)
    turns: reactive[int] = reactive(0)
    status: reactive[str] = reactive("Ready")

    def __init__(self) -> None:
        super().__init__(id="status-bar")
        self.session_start = time.time()

    def render(self) -> str:
        dur = int(time.time() - self.session_start)
        m, s = divmod(dur, 60)
        h, m = divmod(m, 60)
        ds = f"{h}h{m}m" if h else f"{m}m{s}s"
        lat = f"{self.latency}ms" if self.latency > 0 else "--"
        return (
            f"  {self.status}"
            f"    ⚡ {self.tokens:,} tokens"
            f"  │  {lat}"
            f"  │  {self.turns} turns"
            f"  │  {ds}"
        )


# ---------------------------------------------------------------------------
# App
# ---------------------------------------------------------------------------


class AiCippyApp(App):
    """TUI App."""

    TITLE = "AiCippY"
    CSS = APP_CSS
    BINDINGS: list[Binding] = [  # noqa: RUF012
        Binding("ctrl+c", "quit", "Quit", show=False),
        Binding("ctrl+l", "clear_chat", "Clear"),
        Binding("escape", "focus_input", "Focus", show=False),
    ]

    def __init__(self, model: str = "odin", user_email: str = "", initial_prompt: str = "") -> None:
        super().__init__()
        self._model = _dn(model)
        self._user_email = user_email
        self._initial_prompt = initial_prompt
        self._agent_client: Any = None
        self._tokens = 0
        self._turns = 0
        self._conversation: list[dict[str, str]] = []
        self._memory = SessionMemory()
        self._streaming_widget: StreamingMessage | None = None

    def compose(self) -> ComposeResult:
        yield HeaderBar(model=self._model, user=self._user_email)
        yield InfoBar(self._memory)

        with VerticalScroll(id="chat-scroll"):
            ts = time.strftime("%H:%M:%S")
            _os = self._memory.get("os", platform.system())
            _py = platform.python_version()
            yield ChatMessage(
                f"[#4b5563]{ts}[/]  [bold #a855f7]●[/] [#a855f7]{self._model} Agent initialized  \\[A7THAVA][/]",
                classes="msg-system",
            )
            yield ChatMessage(
                f"[#4b5563]{ts}[/]  [#6b7280]{_os} | Py {_py}[/]",
                classes="msg-system",
            )
            if self._memory.get("aws_configured"):
                _aid = self._memory.get("aws_account", "?")
                yield ChatMessage(f"[#4b5563]{ts}[/]  [bold #00ff88]✓ AWS: {_aid}[/]", classes="msg-success")
            if self._memory.get("git_branch"):
                _br = self._memory.get("git_branch", "?")
                yield ChatMessage(f"[#4b5563]{ts}[/]  [bold #a78bfa]⎇ {_br}[/]", classes="msg-system")

        with Vertical(id="input-area"):
            with Horizontal(id="input-row"):
                yield TextArea(id="prompt-input", language=None, theme="monokai", show_line_numbers=False)
                yield Button("SEND", id="send-btn", variant="primary")
            with Horizontal(id="chips-row"):
                for cmd in ["/build", "/deploy", "/test", "/status", "/clear"]:
                    yield CommandChip(cmd)

        yield StatusBar()

    def on_mount(self) -> None:
        self.query_one("#prompt-input", TextArea).focus()
        self._detect_env_bg()
        if self._initial_prompt:
            self.call_after_refresh(self._send_initial)

    @work(thread=True)
    def _detect_env_bg(self) -> None:
        _detect_env(self._memory)

    def _send_initial(self) -> None:
        ta = self.query_one("#prompt-input", TextArea)
        ta.load_text(self._initial_prompt)
        self._handle_send()

    def _handle_send(self) -> None:
        ta = self.query_one("#prompt-input", TextArea)
        text = ta.text.strip()
        if not text:
            return
        ta.clear()
        if text.lower() in {"quit", "exit", "q", "/quit", "/exit"}:
            self.exit()
            return
        self._add_user_msg(text)
        self._conversation.append({"role": "user", "content": text})
        self._set_status("Processing...")
        self._set_zoozoo_mood("thinking")
        self._invoke_agent(text)

    def on_key(self, event: Any) -> None:
        if event.key in {"ctrl+j", "ctrl+enter"}:
            self._handle_send()
            event.prevent_default()
            event.stop()

    @on(Button.Pressed, "#send-btn")
    def _on_send_click(self) -> None:
        self._handle_send()

    @on(Button.Pressed, ".chip")
    def _on_chip_click(self, event: Button.Pressed) -> None:
        cmd = str(event.button.label)
        if cmd == "/clear":
            self.action_clear_chat()
        elif cmd == "/status":
            self._add_sys(f"Environment:\n{self._esc(self._memory.context_str())}")
        else:
            ta = self.query_one("#prompt-input", TextArea)
            ta.load_text(cmd + " ")
            ta.focus()

    # -- Helpers --

    @staticmethod
    def _esc(text: str) -> str:
        """Escape Rich markup."""
        return text.replace("[", "\\[")

    def _set_status(self, text: str) -> None:
        try:
            self.query_one(StatusBar).status = text
        except NoMatches:
            pass

    def _set_zoozoo_mood(self, mood: str) -> None:
        try:
            self.query_one(ZooZooWidget).set_mood(mood)
        except NoMatches:
            pass

    def _add_zoozoo_msg(self, text: str) -> None:
        ts = time.strftime("%H:%M:%S")
        sc = self.query_one("#chat-scroll", VerticalScroll)
        m = ChatMessage(
            f"[#4b5563]{ts}[/]  [bold #fbbf24]🤖 ZooZoo:[/]"
            f" [italic #fbbf24]{self._esc(text)}[/]",
            classes="msg-zoozoo",
        )
        sc.mount(m)
        m.scroll_visible()

    def _scroll_bottom(self) -> None:
        try:
            sc = self.query_one("#chat-scroll", VerticalScroll)
            sc.scroll_end(animate=False)
        except NoMatches:
            pass

    # -- Messages --

    def _add_user_msg(self, text: str) -> None:
        ts = time.strftime("%H:%M:%S")
        sc = self.query_one("#chat-scroll", VerticalScroll)
        m = ChatMessage(f"[bold #86efac]You ❯[/]  {self._esc(text)}\n[#4b5563 italic]{ts}[/]", classes="msg-user")
        sc.mount(m)
        m.scroll_visible()

    def _add_agent_msg(self, text: str) -> None:
        ts = time.strftime("%H:%M:%S")
        sc = self.query_one("#chat-scroll", VerticalScroll)
        m = ChatMessage(
            f"[bold #22d3ee]{self._model} ❯[/]  {self._esc(text)}\n[#4b5563 italic]{ts}[/]",
            classes="msg-agent",
        )
        sc.mount(m)
        m.scroll_visible()

    def _add_sys(self, text: str) -> None:
        ts = time.strftime("%H:%M:%S")
        sc = self.query_one("#chat-scroll", VerticalScroll)
        m = ChatMessage(f"[#4b5563]{ts}[/]  [bold #a855f7]●[/] [#a855f7]{text}[/]", classes="msg-system")
        sc.mount(m)
        m.scroll_visible()

    def _add_exec(self, cmd: str, output: str, ok: bool) -> None:
        ts = time.strftime("%H:%M:%S")
        sc = self.query_one("#chat-scroll", VerticalScroll)
        icon = "[bold #00ff88]✓[/]" if ok else "[bold #ef4444]✗[/]"
        m = ChatMessage(
            f"[#4b5563]{ts}[/]  {icon} [bold #22d3ee]$ {self._esc(cmd[:80])}[/]\n"
            f"[#9ca3af]{self._esc(output[:400]) if output else '(ok)'}[/]",
            classes="msg-exec",
        )
        sc.mount(m)
        m.scroll_visible()

    def _start_stream(self) -> None:
        """Create a streaming message widget for live token display."""
        sc = self.query_one("#chat-scroll", VerticalScroll)
        self._streaming_widget = StreamingMessage(
            f"[bold #22d3ee]{self._model} ❯[/]  [#6b7280]...[/]",
            classes="msg-stream",
        )
        sc.mount(self._streaming_widget)
        self._streaming_widget.scroll_visible()

    def _update_stream(self, text: str) -> None:
        """Update streaming message with new text."""
        if self._streaming_widget:
            self._streaming_widget.update(
                f"[bold #22d3ee]{self._model} ❯[/]  {self._esc(text)}"
            )
            self._streaming_widget.scroll_visible()

    def _end_stream(self) -> None:
        """Finalize streaming message."""
        if self._streaming_widget:
            self._streaming_widget.remove_class("msg-stream")
            self._streaming_widget.add_class("msg-agent")
            ts = time.strftime("%H:%M:%S")
            if self._streaming_widget.renderable:
                current = str(self._streaming_widget.renderable)
                self._streaming_widget.update(f"{current}\n[#4b5563 italic]{ts}[/]")
            self._streaming_widget = None

    # -- Agent invocation with streaming + auto-execution --

    @work(thread=True)
    def _invoke_agent(self, text: str) -> None:
        """Call agent with streaming, auto-execute commands, loop."""
        try:
            from aicippy.agents.bedrock_agent import BedrockAgentClient

            if self._agent_client is None:
                self._agent_client = BedrockAgentClient()

            # Build context
            env = self._memory.context_str()
            tree = self._memory.get("workspace_tree", "")
            ctx = (
                f"[Environment]\n{env}\n"
                f"[Workspace Tree]\n{tree}\n\n"
            )

            turn = 0
            current = ctx + text

            while turn < MAX_AUTO_EXEC_TURNS:
                turn += 1
                self._turns += 1
                start_t = time.time()

                # Start streaming widget
                self.call_from_thread(self._start_stream)
                self.call_from_thread(self._set_status, f"OdiN thinking (turn {turn})...")

                # Stream tokens
                streamed: list[str] = []

                def on_token(t: str) -> None:
                    streamed.append(t)
                    full = "".join(streamed)
                    self.call_from_thread(self._update_stream, full)

                # Call agent
                loop = asyncio.new_event_loop()
                try:
                    result = loop.run_until_complete(
                        self._agent_client.invoke(current, on_token=on_token)
                    )
                finally:
                    loop.close()

                elapsed = int((time.time() - start_t) * 1000)
                self._tokens += len(result.split()) if result else 0

                # Update status bar
                try:
                    sb = self.query_one(StatusBar)
                    sb.tokens = self._tokens
                    sb.latency = elapsed
                    sb.turns = self._turns
                except NoMatches:
                    pass

                # Finalize streaming widget with actual result
                if result:
                    self.call_from_thread(self._update_stream, result)
                self.call_from_thread(self._end_stream)

                if not result:
                    self.call_from_thread(self._add_sys, "No response.")
                    self.call_from_thread(self._set_zoozoo_mood, "error")
                    break

                self._conversation.append({"role": "assistant", "content": result})

                # Detect shell blocks
                blocks = SHELL_RE.findall(result)
                if not blocks:
                    self.call_from_thread(self._set_status, "Ready")
                    self.call_from_thread(self._set_zoozoo_mood, "success")
                    break

                # Execute commands
                outputs: list[str] = []
                for block in blocks:
                    lines = [
                        c.strip() for c in block.strip().split("\n")
                        if c.strip() and not c.strip().startswith("#")
                    ]
                    for cmd in lines:
                        self.call_from_thread(self._set_status, f"Running: {cmd[:40]}...")
                        self.call_from_thread(self._set_zoozoo_mood, "working")
                        try:
                            if platform.system() == "Windows":
                                r = subprocess.run(
                                    ["powershell", "-ExecutionPolicy", "Bypass", "-NoProfile", "-Command", cmd],
                                    capture_output=True, text=True, timeout=60, check=False,
                                )
                            else:
                                r = subprocess.run(
                                    ["bash", "-c", cmd],
                                    capture_output=True, text=True,
                                    timeout=60, check=False,
                                )
                            out = (r.stdout or "").strip()
                            err = (r.stderr or "").strip()
                            output = out if out else err
                            ok = r.returncode == 0
                            self.call_from_thread(self._add_exec, cmd, output, ok)
                            outputs.append(f"$ {cmd}\nExit: {r.returncode}\n{output[:500]}")
                        except subprocess.TimeoutExpired:
                            self.call_from_thread(self._add_exec, cmd, "TIMEOUT", False)
                            outputs.append(f"$ {cmd}\nTIMEOUT")
                        except Exception as exc:
                            self.call_from_thread(self._add_exec, cmd, str(exc), False)
                            outputs.append(f"$ {cmd}\nERROR: {exc}")

                # Feed back
                feedback = "\n\n".join(outputs)
                current = f"[Results]\n{feedback}\n\nContinue or say DONE."
                self._conversation.append({"role": "user", "content": current})

                # Check DONE
                if "DONE" in result.upper().split("\n")[-3:]:
                    self.call_from_thread(self._set_status, "Ready")
                    break

            self.call_from_thread(self._set_status, "Ready")

        except Exception as exc:
            self.call_from_thread(self._add_sys, f"Error: {self._esc(str(exc))}")
            self.call_from_thread(self._set_status, "Error")
            self.call_from_thread(self._set_zoozoo_mood, "error")

        self.call_from_thread(self._focus_input)

    def _focus_input(self) -> None:
        try:
            self.query_one("#prompt-input", TextArea).focus()
        except NoMatches:
            pass

    def action_clear_chat(self) -> None:
        sc = self.query_one("#chat-scroll", VerticalScroll)
        sc.remove_children()
        self._conversation.clear()
        self._add_sys("Cleared. Memory preserved.")

    def action_focus_input(self) -> None:
        self._focus_input()

    def action_quit(self) -> None:
        self._memory.save()
        self.exit()


# ---------------------------------------------------------------------------
# Entry
# ---------------------------------------------------------------------------


async def start_tui_session(
    model: str = "odin", user_email: str = "",
    initial_prompt: str = "", **_kw: object,
) -> None:
    """Launch TUI."""
    app = AiCippyApp(model=model, user_email=user_email, initial_prompt=initial_prompt)
    await app.run_async()
