from __future__ import annotations

import argparse
import asyncio
import datetime as dt
import os
import re
import shlex
import sys
from dataclasses import dataclass
from pathlib import Path

from .agents import DEFAULT_AGENT, build_agent_memory_file, get_agent, list_agents
from .chat import (
    APIError,
    DEFAULT_MEMORY_FILE,
    DEFAULT_MODEL,
    DEFAULT_OPENROUTER_API_KEY,
    FreeNeuroChatClient,
)

try:
    from textual.app import App, ComposeResult
    from textual.binding import Binding
    from textual.containers import Container, Vertical
    from textual.widgets import Footer, Input, Label, RichLog, Static

    from rich import box
    from rich.console import Group
    from rich.markdown import Markdown
    from rich.panel import Panel
    from rich.text import Text

    TEXTUAL_AVAILABLE = True
except Exception:  # noqa: BLE001
    TEXTUAL_AVAILABLE = False


APP_TITLE = "REVENGE LIBRARY"
UI_VERSION = "1.1.54"

FILE_HINT_RE = re.compile(
    r"^\s*(?:#|//|;|--)?\s*file\s*:\s*(?P<path>.+?)\s*$",
    re.IGNORECASE,
)
FENCE_RE = re.compile(
    r"```(?P<info>[^\n`]*)\n(?P<code>.*?)(?:\n```)",
    re.DOTALL,
)


@dataclass(frozen=True)
class RevengeModel:
    alias: str
    title: str
    model_id: str


REVENGE_MODELS: list[RevengeModel] = [
    RevengeModel("core", "Revenge-Llama-3-70b", "meta-llama/llama-3.1-8b-instruct:free"),
    RevengeModel("dark", "Dark-Bert-V2", "google/gemma-2-9b-it:free"),
    RevengeModel("viper", "Mistral-Viper-X", "mistralai/mistral-7b-instruct:free"),
    RevengeModel("flash", "Gemini-Flash", "deepseek/deepseek-r1:free"),
    RevengeModel("glm", "GLM-4.5-Air", "z-ai/glm-4.5-air:free"),
]

REVENGE_MODEL_BY_ALIAS = {item.alias: item for item in REVENGE_MODELS}
REVENGE_MODEL_IDS = [item.model_id for item in REVENGE_MODELS]

BANNER = r"""
██████╗ ███████╗██╗   ██╗███████╗███╗   ██╗ ██████╗ ███████╗
██╔══██╗██╔════╝██║   ██║██╔════╝████╗  ██║██╔════╝ ██╔════╝
██████╔╝█████╗  ██║   ██║█████╗  ██╔██╗ ██║██║  ███╗█████╗
██╔══██╗██╔══╝  ╚██╗ ██╔╝██╔══╝  ██║╚██╗██║██║   ██║██╔══╝
██║  ██║███████╗ ╚████╔╝ ███████╗██║ ╚████║╚██████╔╝███████╗
╚═╝  ╚═╝╚══════╝  ╚═══╝  ╚══════╝╚═╝  ╚═══╝ ╚═════╝ ╚══════╝
      >>>  R E V E N G E   L I B R A R Y  <<<
"""


@dataclass
class CommandResult:
    continue_chat: bool = True
    message: str = ""
    clear_output: bool = False


def _friendly_api_error(exc: APIError) -> str:
    raw = str(exc)
    if "No endpoints found for" in raw and "Tried:" in raw:
        return (
            "No available free model right now for this key. "
            "Try /model next, /models, or retry later."
        )
    marker = "No endpoints found for "
    if marker in raw:
        start = raw.find(marker) + len(marker)
        end = raw.find(".", start)
        model_name = raw[start:end] if end != -1 else raw[start:]
        model_name = model_name.strip().strip("'\"")
        if model_name:
            return f"Model unavailable: {model_name}. Use /model next or /models."
    return raw


@dataclass
class ChatController:
    api_key: str
    model: str
    base_memory_file: str
    workspace_root: Path
    agent_name: str = DEFAULT_AGENT
    system_prompt_override: str | None = None
    auto_write: bool = True

    client: FreeNeuroChatClient | None = None
    last_answer: str = ""
    last_written_files: list[str] = None
    last_send_events: list[str] = None

    def __post_init__(self) -> None:
        get_agent(self.agent_name)
        self.workspace_root = self.workspace_root.resolve()
        self.model = self._resolve_model(self.model)
        if self.last_written_files is None:
            self.last_written_files = []
        if self.last_send_events is None:
            self.last_send_events = []
        self._refresh_client()

    @property
    def memory_file(self) -> str:
        scoped = build_agent_memory_file(self.base_memory_file, self.agent_name)
        return scoped or self.base_memory_file

    @property
    def system_prompt(self) -> str:
        base = (
            self.system_prompt_override
            if self.system_prompt_override
            else get_agent(self.agent_name).system_prompt
        )
        file_contract = (
            " When you provide code intended for files, always use fenced code blocks "
            "and include the first line in block exactly as: 'file: relative/path.ext'."
        )
        return f"{base}{file_contract}"

    @staticmethod
    def _normalize_token(value: str) -> str:
        return value.strip().lower()

    def _resolve_model(self, value: str) -> str:
        token = (value or "").strip()
        if not token:
            return REVENGE_MODELS[0].model_id

        lowered = token.lower()
        if lowered in REVENGE_MODEL_BY_ALIAS:
            return REVENGE_MODEL_BY_ALIAS[lowered].model_id

        if token in REVENGE_MODEL_IDS:
            return token

        return REVENGE_MODELS[0].model_id

    def model_title(self, model_id: str | None = None) -> str:
        target = model_id or self.model
        for item in REVENGE_MODELS:
            if item.model_id == target:
                return f"{item.title} ({item.alias})"
        return str(target)

    def _refresh_client(self) -> None:
        self.client = FreeNeuroChatClient(
            api_key=self.api_key,
            model=self.model,
            system_prompt=self.system_prompt,
            memory_file=self.memory_file,
            fallback_models=REVENGE_MODEL_IDS,
        )
        self.model = self.client.model

    def _set_model(self, token: str) -> None:
        value = token.strip()
        lowered = value.lower()

        if lowered in {"next", "+"}:
            self._cycle_model(+1)
            return
        if lowered in {"prev", "-"}:
            self._cycle_model(-1)
            return

        if lowered.isdigit():
            idx = int(lowered) - 1
            if idx < 0 or idx >= len(REVENGE_MODELS):
                raise ValueError("model index out of range")
            self.model = REVENGE_MODELS[idx].model_id
            self.client.model = self.model
            return

        if lowered in REVENGE_MODEL_BY_ALIAS:
            self.model = REVENGE_MODEL_BY_ALIAS[lowered].model_id
            self.client.model = self.model
            return

        if value in REVENGE_MODEL_IDS:
            self.model = value
            self.client.model = self.model
            return

        raise ValueError("unknown model. use /models")

    def _cycle_model(self, step: int) -> None:
        if self.model not in REVENGE_MODEL_IDS:
            self.model = REVENGE_MODEL_IDS[0]
        idx = REVENGE_MODEL_IDS.index(self.model)
        self.model = REVENGE_MODEL_IDS[(idx + step) % len(REVENGE_MODEL_IDS)]
        self.client.model = self.model

    def _set_agent(self, token: str) -> None:
        value = token.strip().lower()
        if value == "next":
            profiles = list_agents()
            names = [p.name for p in profiles]
            if self.agent_name not in names:
                self.agent_name = names[0]
            else:
                i = names.index(self.agent_name)
                self.agent_name = names[(i + 1) % len(names)]
            self._refresh_client()
            return

        get_agent(value)
        self.agent_name = value
        self._refresh_client()

    def _set_memory(self, token: str) -> None:
        value = token.strip()
        if not value:
            raise ValueError("usage: /memory <file>")
        self.base_memory_file = value
        self._refresh_client()

    def _set_system(self, text: str) -> None:
        value = text.strip()
        if not value:
            raise ValueError("usage: /system <text|reset>")
        if value.lower() == "reset":
            self.system_prompt_override = None
        else:
            self.system_prompt_override = value
        self._refresh_client()

    def models_text(self) -> str:
        lines = ["AVAILABLE MODELS:"]
        for idx, item in enumerate(REVENGE_MODELS, start=1):
            mark = "*" if item.model_id == self.model else " "
            lines.append(f"{mark} {idx}. {item.title} [{item.alias}] -> {item.model_id}")
        return "\n".join(lines)

    def agents_text(self) -> str:
        lines = ["AVAILABLE AGENTS:"]
        for p in list_agents():
            mark = "*" if p.name == self.agent_name else " "
            lines.append(f"{mark} {p.name:<10} {p.title}")
        return "\n".join(lines)

    def status_text(self) -> str:
        turns = len([m for m in self.client.messages if m.get("role") in {"user", "assistant"}])
        return "\n".join(
            [
                f"{APP_TITLE} STATUS",
                f"agent : {self.agent_name}",
                f"model : {self.model_title()}",
                f"memory: {self.memory_file}",
                f"turns : {turns}",
            ]
        )

    @staticmethod
    def help_text() -> str:
        return "\n".join(
            [
                "COMMANDS:",
                "/help                       - command help",
                "/status                     - show state",
                "/models                     - list models",
                "/model <alias|id|idx|next>  - switch model",
                "/agents                     - list agents",
                "/agent <name|next>          - switch agent",
                "/memory <file>              - switch memory file",
                "/system <text|reset>        - system prompt override",
                "/reset                      - clear chat history",
                "/forget                     - clear + delete memory file",
                "/autowrite <on|off|status>  - auto save files from AI response",
                "/write                      - write files from last AI answer now",
                "/write <path>               - save last AI answer to file",
                "/build                      - build placeholder",
                "/deploy                     - deploy placeholder",
                "/clear                      - clear output area",
                "/exit                       - quit",
            ]
        )

    @staticmethod
    def _extract_path_from_line(line: str) -> str | None:
        match = FILE_HINT_RE.match(line.strip())
        if not match:
            return None
        return match.group("path").strip().strip("`\"'")

    @staticmethod
    def _extract_path_from_fence_info(info: str) -> str | None:
        text = info.strip()
        if not text:
            return None

        for token in text.split():
            if token.startswith(("file=", "path=")):
                _, value = token.split("=", 1)
                value = value.strip().strip("`\"'")
                if value:
                    return value
        return None

    def _safe_target_path(self, rel_path: str) -> Path:
        rel = Path(rel_path.strip())
        if not rel_path.strip():
            raise ValueError("empty file path")
        if rel.is_absolute():
            raise ValueError("absolute paths are not allowed")

        target = (self.workspace_root / rel).resolve()
        root = self.workspace_root
        if target != root and root not in target.parents:
            raise ValueError("path escapes workspace root")
        return target

    def _extract_file_blocks(self, text: str) -> list[tuple[str, str]]:
        blocks: list[tuple[str, str]] = []
        for match in FENCE_RE.finditer(text):
            info = (match.group("info") or "").strip()
            code = match.group("code") or ""

            file_path = self._extract_path_from_fence_info(info)
            lines = code.splitlines()

            if lines:
                first_path = self._extract_path_from_line(lines[0])
                if first_path:
                    file_path = first_path
                    lines = lines[1:]

            if not file_path:
                before = text[: match.start()].splitlines()
                if before:
                    prev = before[-1].strip()
                    prev_path = self._extract_path_from_line(prev)
                    if prev_path:
                        file_path = prev_path

            if not file_path:
                continue

            content = "\n".join(lines).rstrip("\n") + "\n"
            blocks.append((file_path, content))
        return blocks

    def _write_blocks(self, blocks: list[tuple[str, str]]) -> list[str]:
        written: list[str] = []
        for rel_path, content in blocks:
            target = self._safe_target_path(rel_path)
            target.parent.mkdir(parents=True, exist_ok=True)
            target.write_text(content, encoding="utf-8")
            written.append(target.relative_to(self.workspace_root).as_posix())
        return written

    def _write_from_last_answer(self) -> list[str]:
        if not self.last_answer.strip():
            raise ValueError("no previous AI answer to write")
        blocks = self._extract_file_blocks(self.last_answer)
        if not blocks:
            raise ValueError(
                "no file blocks found. Ask AI to include blocks with 'file: path'."
            )
        written = self._write_blocks(blocks)
        self.last_written_files = written
        return written

    def execute_command(self, raw: str) -> CommandResult:
        try:
            parts = shlex.split(raw)
        except ValueError as exc:
            raise ValueError(f"bad command syntax: {exc}") from None

        if not parts:
            return CommandResult()

        cmd = self._normalize_token(parts[0])
        args = parts[1:]

        if cmd in {"exit", "quit", "q"}:
            return CommandResult(continue_chat=False, message="bye")

        if cmd in {"help", "h", "commands"}:
            return CommandResult(message=self.help_text())

        if cmd == "status":
            return CommandResult(message=self.status_text())

        if cmd == "models":
            return CommandResult(message=self.models_text())

        if cmd == "model":
            if not args:
                raise ValueError("usage: /model <alias|id|idx|next|prev>")
            self._set_model(args[0])
            return CommandResult(message=f"model switched to: {self.model_title()}")

        if cmd == "agents":
            return CommandResult(message=self.agents_text())

        if cmd == "agent":
            if not args:
                raise ValueError("usage: /agent <name|next>")
            self._set_agent(args[0])
            return CommandResult(message=f"agent switched to: {self.agent_name}")

        if cmd == "memory":
            if not args:
                raise ValueError("usage: /memory <file>")
            self._set_memory(args[0])
            return CommandResult(message=f"memory switched to: {self.memory_file}")

        if cmd == "system":
            if not args:
                raise ValueError("usage: /system <text|reset>")
            self._set_system(" ".join(args))
            if self.system_prompt_override:
                return CommandResult(message="system prompt override enabled")
            return CommandResult(message="system prompt override disabled")

        if cmd == "reset":
            self.client.reset()
            return CommandResult(message="chat history cleared")

        if cmd == "forget":
            target = self.memory_file
            self.client.reset()
            self.client.clear_memory_file()
            return CommandResult(message=f"chat history cleared and memory removed: {target}")

        if cmd == "autowrite":
            if not args:
                state = "on" if self.auto_write else "off"
                return CommandResult(message=f"autowrite is {state}")

            value = args[0].strip().lower()
            if value in {"on", "1", "true"}:
                self.auto_write = True
                return CommandResult(message="autowrite enabled")
            if value in {"off", "0", "false"}:
                self.auto_write = False
                return CommandResult(message="autowrite disabled")
            if value in {"status", "state"}:
                state = "on" if self.auto_write else "off"
                return CommandResult(message=f"autowrite is {state}")
            raise ValueError("usage: /autowrite <on|off|status>")

        if cmd == "write":
            if not args:
                written = self._write_from_last_answer()
                return CommandResult(
                    message="written files:\n" + "\n".join(f"- {item}" for item in written)
                )

            target_path = self._safe_target_path(args[0])
            if not self.last_answer.strip():
                raise ValueError("no previous AI answer to write")
            target_path.parent.mkdir(parents=True, exist_ok=True)
            target_path.write_text(self.last_answer.rstrip("\n") + "\n", encoding="utf-8")
            rel = target_path.relative_to(self.workspace_root).as_posix()
            self.last_written_files = [rel]
            return CommandResult(message=f"written file: {rel}")

        if cmd == "build":
            return CommandResult(message="build: started (placeholder, integrate your build script)")

        if cmd == "deploy":
            return CommandResult(message="deploy: started (placeholder, integrate your deploy script)")

        if cmd == "clear":
            return CommandResult(message="output cleared", clear_output=True)

        raise ValueError(f"unknown command: /{cmd}")

    def send_chat(self, text: str) -> tuple[str, list[str], list[str]]:
        cleaned = text.strip()
        if not cleaned:
            raise ValueError("empty message")
        answer = self.client.send(cleaned)
        self.model = self.client.model
        self.last_answer = answer
        self.last_send_events = list(getattr(self.client, "last_send_events", []))

        written: list[str] = []
        if self.auto_write:
            blocks = self._extract_file_blocks(answer)
            if blocks:
                written = self._write_blocks(blocks)
        self.last_written_files = written
        return answer, written, self.last_send_events


def _timestamp() -> str:
    return dt.datetime.now().strftime("%H:%M:%S")


def _run_simple(controller: ChatController) -> int:
    print("=" * 90)
    print(f"{APP_TITLE} terminal chat")
    print(f"agent : {controller.agent_name}")
    print(f"model : {controller.model_title()}")
    print(f"memory: {controller.memory_file}")
    print("type /help")
    print("=" * 90)

    while True:
        try:
            raw = input("you > ").strip()
        except (KeyboardInterrupt, EOFError):
            print("bye")
            return 0

        if not raw:
            continue

        if raw.startswith("/"):
            try:
                result = controller.execute_command(raw[1:])
                if result.clear_output:
                    print("\n" * 30)
                if result.message:
                    print(f"[{_timestamp()}] {result.message}")
                if not result.continue_chat:
                    return 0
            except Exception as exc:  # noqa: BLE001
                print(f"error: {exc}")
            continue

        try:
            answer, written, events = controller.send_chat(raw)
            for event in events:
                print(f"[model] {event}")
            print(f"ai  > {answer}")
            if written:
                print("[info] written files:")
                for item in written:
                    print(f"- {item}")
        except APIError as exc:
            events = list(getattr(controller.client, "last_send_events", []))
            for event in events:
                print(f"[model] {event}")
            print(f"api error: {_friendly_api_error(exc)}")
        except Exception as exc:  # noqa: BLE001
            print(f"error: {exc}")


if TEXTUAL_AVAILABLE:

    class RevengeApp(App):
        ENABLE_COMMAND_PALETTE = False

        CSS = """
        Screen {
            background: #050505;
            align: center top;
            color: #e0e0e0;
            overflow: hidden;
        }

        #main-container {
            width: 96%;
            height: 100%;
            max-width: 160;
            layout: vertical;
            padding: 1 2;
            overflow: hidden;
        }

        .banner {
            color: #8b00ff;
            text-align: center;
            margin-bottom: 0;
            text-style: bold;
            height: auto;
        }

        Screen.chat-mode #splash-banner {
            display: none;
        }

        #meta {
            color: #9e9e9e;
            text-style: bold;
            content-align: center middle;
            margin: 0 0 1 0;
            border: none;
        }

        Screen.chat-mode #meta {
            display: none;
        }

        Input {
            width: 100%;
            background: #000000;
            border: tall #323232;
            color: #ffffff;
            padding: 0 1;
            margin: 1 0;
            text-align: left;
        }

        Input:focus {
            border: tall #bf00ff;
            background: #0a0a0a;
        }

        Input > .input--placeholder {
            color: #555555;
            text-style: italic;
        }

        #results {
            height: 1fr;
            min-height: 12;
            margin-top: 0;
            background: #080808;
            border: round #171717;
            padding: 0;
            overflow: hidden;
        }

        #runtime-status {
            height: 1;
            color: #8c8c8c;
            margin: 0 1 0 1;
            content-align: left middle;
        }

        Screen.chat-mode #results {
            height: 1fr;
            border: round #35104d;
        }

        #output-log {
            height: 100%;
            background: transparent;
            border: none;
            color: #e8e8e8;
            scrollbar-color: #bf00ff #0b0b0b;
        }

        .tips {
            color: #666666;
            text-align: center;
            margin-top: 1;
            text-style: italic;
        }

        Screen.chat-mode #splash-tips {
            display: none;
        }

        Screen.chat-mode #main-container {
            padding: 0 1;
        }

        Screen.chat-mode Input {
            margin: 0 0 1 0;
        }

        Footer {
            background: #090909;
            color: #9a9a9a;
        }
        """

        BINDINGS = [
            Binding("ctrl+m", "show_models", "Models"),
            Binding("ctrl+t", "next_model", "Next Model"),
            Binding("ctrl+a", "next_agent", "Next Agent"),
            Binding("ctrl+p", "show_commands", "Commands"),
            Binding("ctrl+q", "quit", "Exit"),
        ]

        def __init__(self, controller: ChatController) -> None:
            super().__init__()
            self.controller = controller
            self.meta_widget: Static | None = None
            self.banner_widget: Static | None = None
            self.tips_widget: Label | None = None
            self.input_widget: Input | None = None
            self.output_log: RichLog | None = None
            self.runtime_status: Static | None = None
            self.chat_mode_active = False
            self.session_started_at = dt.datetime.utcnow().isoformat(timespec="milliseconds") + "Z"
            self._thinking_active = False
            self._thinking_step = 0
            self._thinking_task: asyncio.Task | None = None

        def compose(self) -> ComposeResult:
            with Container(id="main-container"):
                self.banner_widget = Static(BANNER, id="splash-banner", classes="banner")
                yield self.banner_widget
                self.meta_widget = Static(id="meta")
                yield self.meta_widget

                with Vertical(id="results"):
                    self.output_log = RichLog(
                        id="output-log",
                        markup=True,
                        wrap=True,
                        highlight=True,
                        auto_scroll=True,
                        max_lines=3000,
                    )
                    yield self.output_log

                self.runtime_status = Static("", id="runtime-status")
                yield self.runtime_status

                self.input_widget = Input(
                    placeholder="Ask anything... type /help for commands",
                )
                yield self.input_widget

                tip = (
                    f"\n[#ff8800]• Tip[/#ff8800]  {APP_TITLE}  "
                    f"Ctrl+T models  Ctrl+A agents  Ctrl+M list models  v{UI_VERSION}"
                )
                self.tips_widget = Label(tip, id="splash-tips", classes="tips")
                yield self.tips_widget

            yield Footer()

        def on_mount(self) -> None:
            if self.input_widget is not None:
                self.input_widget.focus()
            self._refresh_meta()
            if self._has_conversation_history():
                self._activate_chat_mode()
                self._append_output("Loaded previous conversation from memory.", kind="info")
            else:
                self._append_output(
                    f"Welcome to {APP_TITLE}. Type /help to see commands.",
                    kind="info",
                )
            self._set_runtime_status("ready")

        def _has_conversation_history(self) -> bool:
            if self.controller.client is None:
                return False
            for message in self.controller.client.messages:
                role = str(message.get("role", "")).strip().lower()
                if role in {"user", "assistant"}:
                    return True
            return False

        def _activate_chat_mode(self) -> None:
            if self.chat_mode_active:
                return
            self.chat_mode_active = True
            self.screen.add_class("chat-mode")
            if self.input_widget is not None:
                self.input_widget.placeholder = "Message Revenge Library...  /help"
            if self.output_log is not None:
                self.output_log.clear()
                self.output_log.write(
                    Text(
                        f"# New session - {self.session_started_at}    v{UI_VERSION}",
                        style="bold #e6e6e6",
                    )
                )
                self.output_log.write("")
            self._refresh_meta()

        def _show_results(self) -> None:
            # Results block is always visible to prevent layout jumps.
            return

        def _refresh_meta(self) -> None:
            if self.meta_widget is None:
                return
            text = (
                f"agent: {self.controller.agent_name}  |  "
                f"model: {self.controller.model_title()}  |  "
                f"memory: {self.controller.memory_file}"
            )
            self.meta_widget.update(text)

        def _set_runtime_status(self, text: str) -> None:
            if self.runtime_status is None:
                return
            self.runtime_status.update(Text(text, style="#8c8c8c"))

        async def _animate_thinking(self) -> None:
            frames = ("thinking   ", "thinking.  ", "thinking.. ", "thinking...")
            try:
                while self._thinking_active:
                    frame = frames[self._thinking_step % len(frames)]
                    self._thinking_step += 1
                    self._set_runtime_status(
                        f"{frame} | model: {self.controller.model_title()}"
                    )
                    await asyncio.sleep(0.2)
            except asyncio.CancelledError:
                return

        def _start_thinking(self) -> None:
            if self._thinking_active:
                return
            self._thinking_active = True
            self._thinking_step = 0
            if self._thinking_task is None or self._thinking_task.done():
                self._thinking_task = asyncio.create_task(self._animate_thinking())

        def _stop_thinking(self, final_status: str) -> None:
            self._thinking_active = False
            if self._thinking_task is not None and not self._thinking_task.done():
                self._thinking_task.cancel()
            self._thinking_task = None
            self._set_runtime_status(final_status)

        def _clear_output(self) -> None:
            if self.output_log is not None:
                self.output_log.clear()

        def _append_output(self, text: str, kind: str = "info") -> None:
            self._show_results()
            if self.output_log is None:
                return

            ts = _timestamp()
            safe_text = text.strip()
            if not safe_text:
                return

            if kind == "user":
                line = Text()
                line.append(f"[{ts}] ", style="dim #9a9a9a")
                line.append("you > ", style="bold #bf00ff")
                line.append(safe_text, style="bold #f0f0f0")
                self.output_log.write(
                    Panel(
                        line,
                        border_style="#2e78ff",
                        box=box.SQUARE,
                        style="on #0b0b0b",
                        padding=(0, 1),
                        expand=True,
                    )
                )
                self.output_log.write("")
                return

            if kind == "ai":
                if "```" in safe_text:
                    body_renderable = Group(
                        Text(f"[{ts}] revenge", style="bold #8a8f98"),
                        Markdown(safe_text, code_theme="monokai"),
                    )
                else:
                    line = Text()
                    line.append(f"[{ts}] ", style="dim #9a9a9a")
                    line.append("revenge > ", style="bold #8a8f98")
                    line.append(safe_text, style="#f2f2f2")
                    body_renderable = line
                self.output_log.write(
                    Panel(
                        body_renderable,
                        border_style="#2a2a2a",
                        box=box.SQUARE,
                        style="on #090909",
                        padding=(0, 1),
                        expand=True,
                    )
                )
                self.output_log.write("")
                return

            if kind == "error":
                self.output_log.write(
                    Panel(
                        Group(
                            Text(f"[{ts}] error", style="bold #ff5f87"),
                            Text(safe_text, style="#ffd7e1"),
                        ),
                        border_style="#a0133d",
                        box=box.SQUARE,
                        style="on #14070b",
                        padding=(0, 1),
                        expand=True,
                    )
                )
                self.output_log.write("")
                return

            self.output_log.write(
                Panel(
                    Group(
                        Text(f"[{ts}] info", style="bold #8c8c8c"),
                        Text(safe_text, style="#d7d7d7"),
                    ),
                    border_style="#232323",
                    box=box.SQUARE,
                    style="on #090909",
                    padding=(0, 1),
                    expand=True,
                )
            )
            self.output_log.write("")

        async def _process_command(self, raw_command: str) -> None:
            try:
                result = await asyncio.to_thread(self.controller.execute_command, raw_command)
            except Exception as exc:  # noqa: BLE001
                self._append_output(str(exc), kind="error")
                self._set_runtime_status("command failed")
                return

            if result.clear_output:
                self._clear_output()

            if result.message:
                self._append_output(result.message, kind="info")

            self._refresh_meta()
            self._set_runtime_status(f"ready | model: {self.controller.model_title()}")

            if not result.continue_chat:
                self.exit()

        async def _process_chat(self, query: str) -> None:
            self._activate_chat_mode()
            self._append_output(query, kind="user")
            started_at = dt.datetime.now()
            self._start_thinking()
            try:
                answer, written, events = await asyncio.to_thread(self.controller.send_chat, query)
                for event in events:
                    self._append_output(event, kind="info")
            except APIError as exc:
                events = list(getattr(self.controller.client, "last_send_events", []))
                for event in events:
                    self._append_output(event, kind="info")
                self._append_output(_friendly_api_error(exc), kind="error")
                self._stop_thinking("request failed")
                return
            except Exception as exc:  # noqa: BLE001
                self._append_output(str(exc), kind="error")
                self._stop_thinking("request failed")
                return

            self._append_output(answer, kind="ai")
            if written:
                self._append_output(
                    "written files:\n" + "\n".join(f"- {item}" for item in written),
                    kind="info",
                )
            self._refresh_meta()
            elapsed = (dt.datetime.now() - started_at).total_seconds()
            self._stop_thinking(
                f"ready in {elapsed:.1f}s | model: {self.controller.model_title()}"
            )

        async def on_input_submitted(self, message: Input.Submitted) -> None:
            query = message.value.strip()
            if not query:
                return

            if self.input_widget is not None:
                self.input_widget.value = ""

            if query.startswith("/"):
                await self._process_command(query[1:])
                return

            await self._process_chat(query)

        def action_show_models(self) -> None:
            self.run_worker(self._process_command("models"), exclusive=False)

        def action_next_model(self) -> None:
            self.run_worker(self._process_command("model next"), exclusive=False)

        def action_next_agent(self) -> None:
            self.run_worker(self._process_command("agent next"), exclusive=False)

        def action_show_commands(self) -> None:
            if self.input_widget is not None:
                self.input_widget.value = "/help"
                self.input_widget.focus()


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="revengelibrary",
        description="Revenge Library terminal chat interface.",
    )
    parser.add_argument(
        "--api-key",
        default=os.getenv("OPENROUTER_API_KEY", DEFAULT_OPENROUTER_API_KEY),
        help="OpenRouter API key.",
    )
    parser.add_argument(
        "--model",
        default=os.getenv("REVENGELIBRARY_MODEL", DEFAULT_MODEL),
        help="Model alias or model id. Aliases: core, dark, viper, flash, glm.",
    )
    parser.add_argument(
        "--agent",
        default=os.getenv("REVENGELIBRARY_AGENT", DEFAULT_AGENT),
        help="Agent role for chat.",
    )
    parser.add_argument(
        "--system",
        default=None,
        help="Override system prompt.",
    )
    parser.add_argument(
        "--memory-file",
        default=os.getenv("REVENGELIBRARY_MEMORY_FILE", DEFAULT_MEMORY_FILE),
        help="Base memory file name.",
    )
    parser.add_argument(
        "--root",
        default=os.getenv("REVENGELIBRARY_ROOT", os.getcwd()),
        help="Workspace root for auto-writing files.",
    )
    parser.add_argument(
        "--no-auto-write",
        action="store_true",
        help="Disable automatic file writing from AI code blocks.",
    )
    parser.add_argument(
        "--list-agents",
        action="store_true",
        help="Show agents and exit.",
    )
    parser.add_argument(
        "--list-models",
        action="store_true",
        help="Show models and exit.",
    )
    parser.add_argument(
        "--simple",
        action="store_true",
        help="Run simple line mode.",
    )
    return parser


def main() -> int:
    parser = _build_parser()
    args = parser.parse_args()

    if args.list_agents:
        for profile in list_agents():
            print(f"{profile.name}: {profile.title}")
        return 0

    if args.list_models:
        for idx, item in enumerate(REVENGE_MODELS, start=1):
            print(f"{idx}. {item.title} ({item.alias}) -> {item.model_id}")
        return 0

    try:
        get_agent(args.agent)
    except ValueError as exc:
        parser.error(str(exc))

    root = Path(args.root).expanduser().resolve()
    if not root.exists() or not root.is_dir():
        parser.error(f"invalid --root: {root}")

    controller = ChatController(
        api_key=args.api_key or DEFAULT_OPENROUTER_API_KEY,
        model=args.model,
        base_memory_file=args.memory_file,
        workspace_root=root,
        agent_name=args.agent,
        system_prompt_override=args.system,
        auto_write=not args.no_auto_write,
    )

    if args.simple:
        return _run_simple(controller)

    if not TEXTUAL_AVAILABLE:
        return _run_simple(controller)

    if not (sys.stdin.isatty() and sys.stdout.isatty()):
        return _run_simple(controller)

    app = RevengeApp(controller)
    app.run()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
