"""UI 渲染器：组装字符串，不做 IO。
- BaseRenderer 定义接口
- DefaultRenderer 返回与现状 100% 一致的文本（保证默认主题逐字节兼容现有测试）
- HackerRenderer 实现方案 4：复古 CRT 风格（ASCII LOGO + [TAG] 前缀 + 单色主绿 + 少量强调色）
"""
from __future__ import annotations

from typing import Optional

from .themes import Theme, is_hacker_compact, CFG_KEY_CRT_SCANLINE
from ..utils.text import hhmmss, pad_ansi

if False:  # TYPE_CHECKING 防循环引用
    from abyss.config.legacy import Config


# --- ANSI 颜色常量（Hacker theme 统一使用，避免 \033 在多处字符串里散落） ---
GRN = "\033[32m"         # 正常绿（ok 图标/AI文本/主体）
GRN_DIM = "\033[32;2m"   # 暗绿（次要信息、分隔线、标签前缀）
GRN_BOLD = "\033[1;32m"  # 亮绿（用户输入/OK 文本）
YEL = "\033[33m"         # 黄（THINK 关键字/进度/PROGRESS、强调提示）
YEL_BOLD = "\033[1;33m"  # 亮黄（标题/中断提示）
RED = "\033[31m"         # 红（错误/失败）
RED_BOLD = "\033[1;31m"
BLU = "\033[34m"         # 蓝（TOOL BATCH 关键字）
BLU_BOLD = "\033[1;34m"
DIM = "\033[2m"          # 灰色（回显/次要提示）
RST = "\033[0m"          # 重置

# Default theme（保持 main.py 现状的颜色值 — 直接抄原字符串里的 \033 常量）
DEF_PREFIX_GRAY = "\033[2m"
DEF_OK = "\033[32m"
DEF_ERR = "\033[31m"
DEF_WARN_YEL = "\033[33m"
DEF_SHELL_YEL = "\033[33m"
DEF_FILE_BLU = "\033[36m"
DEF_SEARCH_MAG = "\033[35m"
DEF_THINK_GRAY = "\033[2;90m"
DEF_AI_BLU = "\x1b[36m"
DEF_PROMPT_GRN = "\x1b[1;32m"
DEF_RST = "\033[0m"


# ---------------------------------------------------------------------------
# 抽象基类
# ---------------------------------------------------------------------------

class BaseRenderer:
    """所有渲染方法返回 str（已含 ANSI 和结尾换行符）。调用方负责 click.echo(text)。

    契约：
    - 不做 IO，纯函数，可直接单元测试
    - 不读全局 sys.stdout/isatty（由外层传入 cols / hms / 上下文）
    - 所有方法在 DefaultRenderer 返回与现状一致的字符串（含颜色、缩进、换行）
    """

    # -------- 启动阶段 --------
    def banner(self, version: str, model: str, api_ok: bool,
               project_type: str, git_branch: str, files_n: int,
               session_id: str, thinking_enabled: bool) -> str:
        raise NotImplementedError

    def resume_prompt(self, msg_count: int) -> str:
        raise NotImplementedError

    # -------- 回合阶段 --------
    def cmd_echo(self, slash_command: str) -> str:
        raise NotImplementedError

    def user_turn(self, msg_text: str, hms: Optional[str] = None) -> str:
        raise NotImplementedError

    def api_key_hint(self) -> str:
        raise NotImplementedError

    def ai_turn_header(self, think_tokens: int, think_sec: float) -> str:
        raise NotImplementedError

    def ai_think_prefix(self) -> str:
        """流式 think 开始前一次性输出的前缀。"""
        raise NotImplementedError

    def ai_content_prefix(self) -> str:
        """流式 AI 内容开始前一次性输出的前缀（含颜色）。"""
        raise NotImplementedError

    def ai_content_reset(self) -> str:
        """每 chunk 前后输出，用于防止颜色溢出。多数主题返回 '' 或 \\\\033[0m。"""
        return RST + "" if False else "\033[0m"

    def interrupted(self) -> str:
        """Esc 中断提示。"""
        raise NotImplementedError

    def api_error(self, msg: str) -> str:
        """API 连接错误提示。"""
        raise NotImplementedError

    # -------- 工具调用 --------
    def tool_op_preview(self, op_i: int, op_n: int, tool_name: str,
                        tool_args: dict, prefix: str = "") -> str:
        """工具开始前一次性显示。`prefix` 缩进对齐字符，用于嵌套。"""
        raise NotImplementedError

    def tool_op_result(self, op_i: int, op_n: int, tool_name: str, tool_args: dict,
                       status: str,  # ok | warn | err
                       detail: Optional[str] = None,
                       duration: Optional[float] = None,
                       prefix: str = "") -> str:
        raise NotImplementedError

    def tool_batch_header(self, round_i: int, round_n: int, tool_count: int,
                          expected_sec: Optional[float] = None) -> str:
        raise NotImplementedError

    def tool_batch_footer(self, results: list) -> str:
        """一批工具全部执行完毕后一次性输出的收尾行（如方案4的 └─ results: ... 汇总行）。

        Args:
            results: list[dict]，每项含 keys: op_i, op_n, tool_name, tool_args,
                     status(ok/warn/err), detail(可选), duration(可选)
        Returns:
            要追加到末尾的字符串（含换行）。默认主题返回 ""（保持现状逐工具逐行出结果）。
        """
        return ""

    def tool_result_tree(self, i, n, name, args, status, duration_ms,
                         error_msg: Optional[str] = None) -> str:
        """Hacker 风格「单工具独立占行」结果。默认主题空实现。"""
        return ""

    # -------- TODO --------
    def todo_box(self, title: str, items: list[dict]) -> str:
        raise NotImplementedError

    # -------- 结束阶段 --------
    def ai_summary(self, ok_items: list[str],
                   err_items: Optional[list[str]] = None) -> str:
        """AI 最后总结列表。默认主题返回 ''（不增加任何行，保持现状不加 summary）。"""
        return ""

    def round_summary(self, wall_sec: float, tool_calls_n: int,
                      round_i: int, round_n: int,
                      cost_cny: Optional[float] = None) -> str:
        """回合结尾 summary。默认主题返回 ''（保持现状不加 summary 行）。"""
        return ""

    # -------- prompt --------
    def cli_prompt(self, session_id: Optional[str] = None) -> str:
        """终端 prompt 前缀（传给 ansi_prompt 的 prefix 参数）。"""
        raise NotImplementedError

    # -------- slash /help 帮助 --------
    def help_list(self) -> str:
        """命令帮助列表（/help 或 CTRL+H 触发）。默认主题返回空字符串（保持原 _HELP_TEXT）。"""
        return ""


# ---------------------------------------------------------------------------
# DefaultRenderer（保持现状逐字节一致）
# ---------------------------------------------------------------------------

class DefaultRenderer(BaseRenderer):
    """逐字拷贝 main.py 现状字符串，保证默认主题 100% 兼容。"""

    # -------- 启动 --------
    def banner(self, version: str, model: str, api_ok: bool,
               project_type: str, git_branch: str, files_n: int,
               session_id: str, thinking_enabled: bool) -> str:
        # main.py:L1699 原始输出：模型: xxx  |  / 命令  @ 文件  |  Esc 中断任务
        return f"模型: {model}  |  / 命令  @ 文件  |  Esc 中断任务\n"

    def resume_prompt(self, msg_count: int) -> str:
        # main.py:L1697 原始：会话已恢复，输入消息继续对话\n
        return f"{DEF_PREFIX_GRAY}会话已恢复，输入消息继续对话{DEF_RST}\n"

    # -------- 回合 --------
    def cmd_echo(self, slash_command: str) -> str:
        # main.py:L1713 原始：  \033[2m> /xxx\033[0m
        return f"  {DEF_PREFIX_GRAY}> {slash_command}{DEF_RST}\n"

    def user_turn(self, msg_text: str, hms: Optional[str] = None) -> str:
        # Default 主题：不输出用户消息（用户消息本来就是终端 stdin 输入，用户已经看见了）
        return ""

    def api_key_hint(self) -> str:
        # main.py:L1730 原始
        return "请先配置 API Key: /config set api-key <your-key>\n"

    def ai_turn_header(self, think_tokens: int, think_sec: float) -> str:
        # Default 主题：不在 think 前打 header（思考流式直接打 "思考: "）
        return ""

    def ai_think_prefix(self) -> str:
        # main.py:L627 原始：  \033[2;90m思考:
        return f"  {DEF_THINK_GRAY}思考: "

    def ai_content_prefix(self) -> str:
        # main.py:L653-654 原始： ansi_gray_line() + "\x1b[36m"
        # ansi_gray_line() 返回 "  " + gray "──────── ..." 分隔线
        gray_line = f"  {DEF_PREFIX_GRAY}{'─' * 60}{DEF_RST}\n"
        return f"{gray_line}{DEF_AI_BLU}"

    def interrupted(self) -> str:
        # main.py:L712 原始
        return f"\n  {DEF_WARN_YEL}(已中断当前任务){DEF_RST}\n"

    def api_error(self, msg: str) -> str:
        # main.py:L718（简化版：err 文本 + 红色）
        return f"\n  {DEF_ERR}{msg}{DEF_RST}\n"

    # -------- 工具：op_preview + op_result --------
    def tool_op_preview(self, op_i: int, op_n: int, tool_name: str,
                        tool_args: dict, prefix: str = "") -> str:
        # 严格对应 main.py:L403-416 的 _describe_tool 原输出
        p = prefix or "  "
        if tool_name == "shell_exec":
            return f"{p}{DEF_SHELL_YEL}$ {tool_args.get('command', '')}{DEF_RST}\n"
        if tool_name in ("file_write", "file_snapshot"):
            return f"{p}{DEF_FILE_BLU}写入文件: {tool_args.get('path', '')}{DEF_RST}\n"
        if tool_name == "file_read":
            return f"{p}{DEF_FILE_BLU}读取文件: {tool_args.get('path', '')}{DEF_RST}\n"
        if tool_name == "file_edit":
            return f"{p}{DEF_FILE_BLU}编辑文件: {tool_args.get('path', '')}{DEF_RST}\n"
        if tool_name in ("SearchCodebase", "search_tool", "Grep", "Glob"):
            return f"{p}{DEF_SEARCH_MAG}搜索: {tool_args.get('query', tool_args.get('pattern', ''))}{DEF_RST}\n"
        if tool_name == "todo_write":
            return f"{p}{DEF_FILE_BLU}更新任务清单{DEF_RST}\n"
        return f"{p}[tool] {tool_name}\n"

    def tool_op_result(self, op_i: int, op_n: int, tool_name: str, tool_args: dict,
                       status: str, detail: Optional[str] = None,
                       duration: Optional[float] = None,
                       prefix: str = "") -> str:
        # 对应 main.py:L477-L511 的结果输出（简化：ok → 完成；err→ 失败；search 计数）
        p = prefix or "  "
        if status == "ok":
            if tool_name == "shell_exec":
                # shell_exec 特殊：先 stdout/stderr，再 "完成"
                head = ""
                if detail:
                    head = f"{detail}"
                    if not head.endswith("\n"):
                        head += "\n"
                    head += f"{p}{DEF_OK}完成{DEF_RST}\n"
                else:
                    head = f"{p}{DEF_OK}完成 (无输出){DEF_RST}\n"
                return head
            if tool_name in ("SearchCodebase", "search_tool", "Grep", "Glob"):
                try:
                    count = int(detail or "0")
                except ValueError:
                    count = 0
                return f"{p}{DEF_OK}搜索完成 ({count}条){DEF_RST}\n"
            return f"{p}{DEF_OK}完成{DEF_RST}\n"
        # err / warn
        msg = f": {detail}" if detail else ""
        color = DEF_ERR if status == "err" else DEF_WARN_YEL
        return f"{p}{color}失败{msg}{DEF_RST}\n"

    def tool_batch_header(self, round_i: int, round_n: int, tool_count: int,
                          expected_sec: Optional[float] = None) -> str:
        # main.py 目前没有 batch header 行（只有单工具前缀），返回空
        return ""

    # -------- TODO --------
    def todo_box(self, title: str, items: list[dict]) -> str:
        # 对应 todo_write.py 现状：空行 + 标题 + 每项 "    ICON content" + 空行
        lines: list[str] = [""]
        lines.append(f"  {DEF_RST}{title}{DEF_RST}")
        for it in items:
            s = (it.get("status") or "pending").lower()
            if s == "done":
                ic, col = "✓", DEF_OK
            elif s == "progress":
                ic, col = "◐", DEF_WARN_YEL
            else:
                ic, col = "○", DEF_PREFIX_GRAY
            lines.append(f"    {col}{ic} {it.get('content', '')}{DEF_RST}")
        lines.append("")
        return "\n".join(lines) + "\n" if lines else "\n\n"

    # -------- prompt --------
    def cli_prompt(self, session_id: Optional[str] = None) -> str:
        """Default 主题 prompt 前缀。保持原 "> "；若有 session_id 追加右侧灰色 [ABCD] 标识。

        注意：必须与 BaseRenderer / HackerRenderer 的签名完全一致（接受 session_id 可选参数），
        否则 main.py:L2764 统一调用 R.cli_prompt(_SESSION_SID) 时 DefaultRenderer 会
        TypeError("takes 1 positional argument but 2 were given") 崩在进入交互循环前，
        表现为「配置完 API Key 后自动退出 / 再次打开闪一下就没了」。
        """
        # main.py:L1705 原始："> "（ansi_prompt(prefix="> ")）
        base = "> "
        if not session_id:
            return base
        sid4 = (str(session_id)[-4:]).upper()
        # 灰色 [ABCD]，与 DEF_PREFIX_GRAY 保持同色，最后重置色彩回终端默认色
        return f"{base}{DEF_PREFIX_GRAY}[{sid4}]{DEF_RST} "


# ---------------------------------------------------------------------------
# HackerRenderer（方案 4：复古 CRT 风格）
# ---------------------------------------------------------------------------

class HackerRenderer(BaseRenderer):
    """方案4 · 复古 CRT 黑客风：[TAG] 前缀 + 主绿单色 + ▶/▢/✓ 三态 + │/└─ 树形。"""

    # 三态文字左对齐宽度（与 HTML 一致：IN PROGRESS=11 / pending=7 / DONE=4 → 统一 11）
    _STATUS_WIDTH = 11

    def __init__(self, cfg: Optional["Config"] = None):
        self._cfg = cfg
        self._crt_scanline = False
        self._session_sid4 = "LOCAL"
        self._brand_word = "abyss"
        if cfg is not None:
            self._crt_scanline = bool(cfg.get(CFG_KEY_CRT_SCANLINE))
            brand = cfg.get("ui.brand") or cfg.get("brand")
            if brand:
                self._brand_word = str(brand).strip() or "abyss"

    # -------- 内部工具 --------
    def _tag(self, tag: str, hms: Optional[str] = None) -> str:
        """生成行首标签。

        - 有 hms：   `[HH:MM:SS USR]` （回合标签，占固定 14 列 + ANSI）
        - 无 hms：   `[ SYS ]`        （banner/summary 的纯空格装饰版）
        """
        if hms is None:
            # banner/summary 空格装饰版
            return f"{GRN_DIM}[ {tag} ]{RST}"
        t = f"[{hms} {tag}]"
        return f"{GRN_DIM}{t}{RST}"

    def _sep(self, char: str = "-", cols: int = 80) -> str:
        import shutil
        try:
            c = shutil.get_terminal_size(fallback=(cols, 24)).columns
        except Exception:
            c = cols
        return f"{GRN_DIM}{char * c}{RST}\n"

    def _maybe_scan(self, line: str, lineno: int) -> str:
        if not self._crt_scanline:
            return line
        if lineno % 2 == 0:
            return line
        return f"\033[48;5;234m{line}{RST}"

    # -------- 启动 --------
    def banner(self, version: str, model: str, api_ok: bool,
               project_type: str, git_branch: str, files_n: int,
               session_id: str, thinking_enabled: bool) -> str:
        if session_id:
            self._session_sid4 = (str(session_id)[-4:]).upper()
        now = hhmmss()
        lines: list[str] = []
        compact = is_hacker_compact()
        sid4 = self._session_sid4

        if not compact:
            # figlet "abyss" - standard 字体（pyfiglet 生成，字形权威）
            lines += [
                rf"{GRN_BOLD}       _                           {RST}",
                rf"{GRN_BOLD}  __ _| |__  _   _ ___ ___         {RST}",
                rf"{GRN_BOLD} / _` | '_ \| | | / __/ __|        {RST}",
                rf"{GRN_BOLD}| (_| | |_) | |_| \__ \__ \        {RST}",
                rf"{GRN_BOLD} \__,_|_.__/ \__, |___/___/  {GRN}v{version}{RST}",
                rf"{GRN_BOLD}             |___/               {RST}",
            ]
        else:
            lines.append(f"{GRN_BOLD}abyss v{version}{RST}")

        # ==== 分隔线（HTML 中 banner 下方是 ===）
        lines.append(self._sep("=").rstrip("\n"))

        def add_sys(text: str, lineno: int) -> None:
            # banner 用空格版 [ SYS ]（与 HTML 严格一致）
            line = f"{self._tag('SYS')} {text}"
            lines.append(self._maybe_scan(line, lineno))

        ln = len(lines)
        api_text = f"{GRN_BOLD}[OK]{RST}" if api_ok else f"{RED_BOLD}[FAIL]{RST}"
        # 照 HTML 4 行 SYS：
        add_sys(f"{GRN}establishing secure channel ...{RST} {api_text}", ln); ln += 1
        status = f"{GRN_BOLD}ONLINE{RST}" if api_ok else f"{RED_BOLD}OFFLINE{RST}"
        add_sys(f"{GRN}model: {YEL}{model}{GRN}  status: {status}", ln); ln += 1
        add_sys(f"{GRN}cwd: {YEL}{project_type}{GRN}/git:{YEL}{git_branch}{GRN}/{YEL}{files_n}f "
                f"{GRN_DIM}·{GRN} session: {GRN_BOLD}{sid4}{RST}", ln); ln += 1
        add_sys(f"{GRN}press {YEL_BOLD}ESC{GRN} to interrupt {GRN_DIM}·{GRN} "
                f"{YEL_BOLD}CTRL+H{GRN} for help{RST}", ln); ln += 1
        lines.append(self._sep("=").rstrip("\n"))

        return "\n".join(lines) + "\n"

    def resume_prompt(self, msg_count: int) -> str:
        # 用空格版 [ SYS ]，与 banner 风格一致
        tag = self._tag("SYS")
        return f"{tag} {GRN}session resumed, {msg_count} messages buffered. awaiting input.{RST}\n"

    # -------- 回合 --------
    def cmd_echo(self, slash_command: str) -> str:
        """若以 / 开头：/xxx 命令名部分用 YEL_BOLD 高亮（/xxx 高亮，参数保持原用户输入的 cmd_echo 格式。"""
        s = (slash_command or "").strip()
        t = self._tag("CMD", hhmmss())
        if s.startswith("/"):
            sp = s.split(None, 1)
            cmd_part = sp[0] if sp else s
            rest = sp[1] if len(sp) > 1 else ""
            if rest:
                return f"{t}  {GRN_DIM}>>>{RST} {YEL_BOLD}{cmd_part}{RST} {GRN}{rest}{RST}\n"
            else:
                return f"{t}  {GRN_DIM}>>>{RST} {YEL_BOLD}{cmd_part}{RST}\n"
        return f"{t} {GRN_DIM}>>> {slash_command}{RST}\n"

    def user_turn(self, msg_text: str, hms: Optional[str] = None) -> str:
        """[USR] 单独一行 + 下一行「  >>> 内容」（严格照 HTML）。"""
        ts = hms or hhmmss()
        tag = self._tag("USR", ts)
        parts = msg_text.splitlines() or [""]
        out_lines: list[str] = [tag]
        # 第一行：2 空格缩进 + >>> 首段
        out_lines.append(f"  {GRN_BOLD}>>> {parts[0]}{RST}")
        # 续行：  ...  对齐
        for p in parts[1:]:
            out_lines.append(f"  {GRN_DIM}...{RST}  {p}")
        return "\n".join(out_lines) + "\n"

    def api_key_hint(self) -> str:
        t = self._tag("SYS")
        return f"{t} {YEL_BOLD}API KEY NOT CONFIGURED{RST} {GRN}→ run: {YEL}/config set api-key <KEY>{RST}\n"

    def ai_turn_header(self, think_tokens: int, think_sec: float) -> str:
        """[SYS] THINK depth=1 tokens=1284 t=4.2s（与 HTML 一致）。"""
        t = self._tag("SYS", hhmmss())
        return (f"{t} {YEL_BOLD}THINK{GRN} depth=1 tokens={YEL}{think_tokens}{GRN} "
                f"t={YEL}{think_sec:.1f}{GRN}s{RST}\n")

    def ai_think_prefix(self) -> str:
        # think 行流式：直接用暗绿（在 ai_turn_header 之后紧跟，无需再打标签）
        return f"{GRN_DIM}"

    def ai_content_prefix(self) -> str:
        """[A.I] 单独一行 + 下一行「  >> 内容...」开头。"""
        t = self._tag("A.I", hhmmss())
        return f"\n{t}\n  {GRN_BOLD}>>{RST} {GRN}"

    def ai_content_reset(self) -> str:
        return RST

    def interrupted(self) -> str:
        t = self._tag("SYS", hhmmss())
        return f"\n{t} {YEL_BOLD}! INTERRUPTED by user (ESC){RST}\n"

    def api_error(self, msg: str) -> str:
        t = self._tag("SYS", hhmmss())
        return f"\n{t} {RED_BOLD}API ERROR{RED} {msg}{RST}\n"

    # -------- 工具：batch + op --------
    def tool_batch_header(self, round_i: int, round_n: int, tool_count: int,
                          expected_sec: Optional[float] = None) -> str:
        """TOOL BATCH 标题：不再显示 /MAX_TOOL_ROUNDS（通常 100，误导用户以为要 100 轮）。
        只显示 round_i 序号 + (N ops)。"""
        t = self._tag("EXE", hhmmss())
        extra = ""
        if expected_sec:
            extra = f" {GRN_DIM}≈{expected_sec:.1f}s{RST}"
        return (f"{t} {BLU_BOLD}TOOL BATCH {GRN}{round_i}{RST}"
                f" {GRN_DIM}({tool_count} ops){RST}{extra}\n")

    def tool_op_preview(self, op_i: int, op_n: int, tool_name: str,
                        tool_args: dict, prefix: str = "") -> str:
        """工具开始前一次性显示。HTML 格式：│ op 1/3 file_read  src/abyss/history/file_history.py
        - 格式严格：`op n/m` + 工具名（空格左对齐）+ 2 空格 + 直接值（无键名 path:/q:）
        - 对于 file_read/main.py#Lxxx 片段：路径后直接缀 #Lxx-Lxx
        """
        pad = prefix if prefix else "  "
        bar = f"{pad}{GRN_DIM}│{RST}"
        head = f"{bar} {DIM}op {op_i}/{op_n}{RST} {BLU_BOLD}{tool_name}{RST}"
        # 参数严格按 HTML：直接值，不带键名
        if tool_name == "shell_exec":
            arg = tool_args.get("command", "")
            head += f"  {GRN_DIM}${RST} {GRN}{arg}{RST}"
        elif tool_name in ("file_read", "file_write", "file_edit", "file_snapshot",
                           "Read", "Write", "file_snapshot"):
            arg = tool_args.get("path", "")
            offset = tool_args.get("offset")
            limit = tool_args.get("limit")
            extra = ""
            if offset is not None or limit is not None:
                o = int(offset or 0)
                extra = f"{GRN_DIM}#L{o}{RST}"
                if limit is not None:
                    extra = f"{GRN_DIM}#L{o}-L{o + int(limit)}{RST}"
            head += f"  {GRN}{arg}{RST}{extra}"
        elif tool_name in ("SearchCodebase", "search_tool", "Grep", "Glob", "web_search",
                           "WebSearch", "Browser", "browser_use"):
            arg = tool_args.get("query") or tool_args.get("pattern") or \
                  tool_args.get("description") or tool_args.get("url") or ""
            head += f"  {GRN}{arg}{RST}"
        elif tool_name == "todo_write":
            n = len(tool_args.get("todos", []) or [])
            head += f"  {GRN}{n} tasks{RST}"
        elif tool_name == "RunCommand" or tool_name == "shell_exec":
            arg = tool_args.get("command", "")
            head += f"  {GRN_DIM}${RST} {GRN}{arg}{RST}"
        else:
            # 通用情况：把 dict 里 value 串起来，去掉键名前缀，取前 80 字
            vals = [str(v) for v in tool_args.values() if v is not None]
            combined = " ".join(vals) if vals else str(tool_args)
            head += f"  {GRN}{combined[:80]}{RST}"
        return f"{head}\n"

    def tool_op_result(self, op_i: int, op_n: int, tool_name: str, tool_args: dict,
                       status: str, detail: Optional[str] = None,
                       duration: Optional[float] = None,
                       prefix: str = "") -> str:
        # Hacker 风格：逐工具不单独输出结果；统一攒到 tool_batch_footer + tool_result_tree
        return ""

    def tool_result_tree(self, i, n, name, args, status, duration_ms,
                         error_msg: Optional[str] = None) -> str:
        """工具执行完毕的一行树形输出（HTML 格式）：
          │ op 1/3 shell_exec  $ Get-Date -Format 'yyyy-MM-dd'  [ ■ ok   322ms ]
          │ op 2/3 web_fetch   https://xxxxx.html  [ ✗ fail   763ms ] → Client error '403 Forbidden'
        严格格式：op n/m（无方括号）+ name 空格对齐 + 直接值（无键名）+ 状态徽章。
        """
        pad = "  "
        bar = f"{pad}{GRN_DIM}│{RST}"
        head = f"{bar} {DIM}op {i}/{n}{RST} {BLU_BOLD}{name:<11s}{RST} "
        # args 格式化（严格同 tool_op_preview：直接值，无键名前缀）
        if name == "shell_exec":
            arg = args.get("command", "") if isinstance(args, dict) else str(args)
            head += f" {GRN_DIM}${RST} {GRN}{arg}{RST}"
        elif name in ("file_read", "file_write", "file_edit", "file_snapshot",
                      "Read", "Write", "Glob", "Grep"):
            arg = args.get("path", "") if isinstance(args, dict) else str(args)
            offset = args.get("offset") if isinstance(args, dict) else None
            limit = args.get("limit") if isinstance(args, dict) else None
            extra = ""
            if offset is not None or limit is not None:
                o = int(offset or 0)
                extra = f"{GRN_DIM}#L{o}{RST}"
                if limit is not None:
                    extra = f"{GRN_DIM}#L{o}-L{o + int(limit)}{RST}"
            head += f" {GRN}{arg}{RST}{extra}"
        elif name in ("SearchCodebase", "search_tool", "web_search", "WebSearch",
                      "Grep", "Glob"):
            arg = (args.get("query") or args.get("pattern") or args.get("url")
                   or "") if isinstance(args, dict) else ""
            head += f" {GRN}{arg}{RST}"
        elif name == "todo_write":
            n_items = len(args.get("todos", []) or []) if isinstance(args, dict) else 0
            head += f" {GRN}{n_items} tasks{RST}"
        elif name in ("web_fetch", "WebFetch", "browser_use", "Browser"):
            arg = (args.get("url") or args.get("description")
                   or "") if isinstance(args, dict) else ""
            head += f" {GRN}{arg[:80]}{RST}"
        else:
            s = str(args)[:80] if not isinstance(args, (dict, list)) else \
                " ".join(str(v) for v in (args.values() if isinstance(args, dict)
                         else args))[:80]
            head += f" {GRN}{s}{RST}"
        # 状态徽章 [ ■ ok   Xms ]
        st = (status or "ok").lower()
        if st == "ok":
            icon, col, word = "■", GRN_BOLD, "ok"
        elif st == "warn":
            icon, col, word = "!", YEL_BOLD, "warn"
        else:
            icon, col, word = "✗", RED_BOLD, "fail"
        ms_txt = f"{int(duration_ms):>4d}ms" if duration_ms is not None else ""
        tail = f" {GRN_DIM}[{RST} {col}{icon}{RST} {col}{word:<4s}{RST} {GRN_DIM}{ms_txt}{RST} {GRN_DIM}]{RST}"
        line = head + tail
        # 失败追加 error_msg 片段
        if st != "ok" and error_msg:
            m = str(error_msg).strip().splitlines()[:1]
            if m:
                line += f" {GRN_DIM}→{RST} {GRN_DIM}{m[0][:70]}{RST}"
        return line + "\n"

    def tool_batch_footer(self, results: list) -> str:
        """└─ results: ✓ OK (0.6s) · ✓ OK (0.3s) · ! WARN msg（严格照方案4 HTML）。"""
        pad = "  "
        bar = f"{pad}{GRN_DIM}└─{RST}"
        parts: list[str] = []
        for r in results:
            st = (r.get("status") or "ok").lower()
            if st == "ok":
                icon, col, word = "✓", GRN_BOLD, "OK"
            elif st == "warn":
                icon, col, word = "!", YEL_BOLD, "WARN"
            else:
                icon, col, word = "✗", RED_BOLD, "FAIL"
            seg = f"{col}{icon} {word}{RST}"
            dur = r.get("duration")
            if dur is not None:
                seg += f" {GRN_DIM}({float(dur):.1f}s){RST}"
            det = r.get("detail")
            if det:
                short = str(det).strip().splitlines()[:1]
                if short:
                    seg += f" {GRN_DIM}→{RST} {short[0][:60]}"
            parts.append(seg)
        if not parts:
            return ""
        joined = f" {GRN_DIM}·{RST} ".join(parts)
        return f"{bar} {GRN_DIM}results:{RST} {joined}\n"

    # -------- TODO --------
    def todo_box(self, title: str, items: list[dict]) -> str:
        """方案4 PLAN / PLAN UPDATE 风格：
        ▶ [01/04] IN PROGRESS  fix ...
        ▢ [02/04] pending      add redo() ...
        ✓ [01/04] DONE         fix ...
        """
        t = self._tag("A.I", hhmmss())
        done = sum(1 for it in items if (it.get("status") or "").lower() == "done")
        total = len(items)

        # 自动识别标题：若传进来的是 todo_write 默认的"任务计划/任务进度"，统一替换成 PLAN / PLAN UPDATE
        t_low = (title or "").lower()
        if done > 0 and done < total:
            pretty_title = "PLAN UPDATE"
            suffix = f"{GRN_DIM}({done}/{total} done){RST}"
        elif done == total and total > 0:
            pretty_title = "PLAN UPDATE"
            suffix = f"{GRN_DIM}({done}/{total} done){RST}"
        else:
            pretty_title = "PLAN"
            suffix = f"{GRN_DIM}({total} tasks){RST}"
        # 如果用户明确传自定义标题（比如非中文 PLAN/UPDATE 关键字），尊重原标题
        if title and not any(k in t_low for k in ("任务", "todo", "plan", "进度")):
            pretty_title = title

        header = f"{t} {YEL_BOLD}{pretty_title}{RST} {suffix}\n"

        W = self._STATUS_WIDTH
        body = ""
        for i, it in enumerate(items, 1):
            s = (it.get("status") or "pending").lower()
            if s == "done":
                ic, col, word = "✓", GRN_BOLD, "DONE"
            elif s in ("in_progress", "progress"):
                ic, col, word = "▶", YEL_BOLD, "IN PROGRESS"
            else:
                ic, col, word = "▢", GRN_DIM, "pending"
            word_padded = word.ljust(W)
            num = f"[{i:02d}/{total:02d}]"
            body += (f"  {col}{ic}{RST} {GRN_DIM}{num}{RST}  "
                     f"{col}{word_padded}{RST}  {GRN}{it.get('content', '')}{RST}\n")
        return header + body

    # -------- 结束 --------
    def ai_summary(self, ok_items: list[str],
                   err_items: Optional[list[str]] = None) -> str:
        """[A.I] DONE - /redo complete: 列表（[ok] / [fail]）。"""
        t = self._tag("A.I", hhmmss())
        out: list[str] = []
        if ok_items:
            out.append(f"{t} {GRN_BOLD}DONE{RST} {GRN_DIM}-{RST} {GRN_BOLD}{ok_items[0]}{RST}")
            for item in ok_items[1:]:
                out.append(f"    {GRN}[ok]{RST} {GRN}{item}{RST}")
        else:
            out.append(f"{t} {GRN_BOLD}DONE{RST}")
        if err_items:
            for item in err_items:
                out.append(f"    {RED}[fail]{RST} {item}")
        return "\n".join(out) + "\n"

    def round_summary(self, wall_sec: float, tool_calls_n: int,
                      round_i: int, round_n: int,
                      cost_cny: Optional[float] = None) -> str:
        """----- 分隔 + [hh:mm:ss SYS] wall time/tools/round/cost 行（prompt 前视觉分隔）。"""
        mm = int(wall_sec // 60)
        ss = int(wall_sec % 60)
        parts: list[str] = [self._sep("-").rstrip("\n")]
        tag1 = self._tag("SYS", hhmmss())
        line = (f"{tag1} {GRN}wall time: {YEL}{mm:02d}:{ss:02d}{GRN} · "
                f"tools: {YEL}{tool_calls_n}{GRN} · round: {YEL}{round_i}/{round_n}{RST}")
        if cost_cny is not None:
            line += f" · {GRN}cost: {YEL}~¥{cost_cny:.3f}{RST}"
        parts.append(line)
        return "\n".join(parts) + "\n"

    # -------- prompt --------
    def cli_prompt(self, session_id: Optional[str] = None) -> str:
        """abyss@ABCD:~$ （ABCD = session_id[-4:]，大写）。"""
        if session_id:
            sid4 = (str(session_id)[-4:]).upper()
            self._session_sid4 = sid4
        else:
            sid4 = self._session_sid4
        return f"{GRN_BOLD}abyss@{sid4}{GRN_DIM}:~${RST} "

    # -------- slash /help 帮助 --------
    _HELP_GROUPS: list[tuple[str, list[tuple[str, str]]]] = [
        ("BASIC", [
            ("/help", "显示本帮助（或按 CTRL+H）"),
            ("/clear", "清空当前会话上下文"),
            ("/exit, /quit", "退出 CLI"),
            ("/session", "会话管理（保存/加载/删除）"),
        ]),
        ("SESSION", [
            ("/retry", "重试上一条 user 消息"),
            ("/compact [指令]", "手动压缩上下文"),
            ("/undo", "撤销最近一轮 AI 文件编辑（批量回退）"),
            ("/redo", "恢复最近一次被 /undo 撤销的修改"),
            ("/stats", "显示会话统计（消息 / token / 工具调用）"),
            ("/logs [选项]", "查看日志（-n/-l/-c/-t/--since/--trace/--stats）"),
        ]),
        ("GIT", [
            ("/diff [staged]", "显示 Git 工作区或暂存区差异"),
            ("/git blame <file> [起-止]", "行级作者归因"),
            ("/git log [N]", "最近 N 条提交（默认 10）"),
            ("/git stats", "按作者统计提交数"),
            ("/git show <hash>", "显示指定 commit 摘要"),
        ]),
        ("CRON", [
            ("/cron list", "列出所有定时任务"),
            ("/cron create <spec> <cmd>", "创建定时任务（5 段 cron spec）"),
            ("/cron delete <id>", "删除指定任务"),
            ("/cron cleanup", "清理过期任务"),
        ]),
        ("AI ANALYSIS", [
            ("/bug-hunter <描述>", "启动 bug 猎手分析模式"),
            ("/pr-review", "审查最近一次 git commit"),
            ("/security-scan <路径>", "安全扫描指定路径"),
            ("/explain <路径>", "解释指定文件"),
            ("/advisor <问题>", "顾问模式（咨询架构/方案）"),
        ]),
        ("EXTENSION", [
            ("/skills", "列出已加载的 Skill"),
            ("/mcp", "列出 MCP server 和工具"),
            ("/hooks", "列出已配置的 Hook"),
            ("/curator", "Skill 生命周期管理"),
            ("/knowledge ...", "知识库（list/search/show/pin/archive/stats）"),
        ]),
        ("MEMORY", [
            ("/memory", "列出最近 50 条记忆"),
            ("/memory search <q>", "关键词搜索记忆"),
            ("/memory show <id>", "显示某条记忆完整内容"),
            ("/memory save <标题> | <内容>", "手动保存记忆"),
            ("/memory delete <id>", "删除某条记忆"),
            ("/memory stats", "统计分类与存储路径"),
        ]),
        ("METHODOLOGY", [
            ("/diagnose <问题>", "启动调试纪律流程（6 阶段）"),
            ("/tdd <功能>", "启动 TDD 红-绿-重构流程"),
            ("/review", "审查当前 diff 的过度工程"),
            ("/audit", "全仓库过度工程审计"),
            ("/architecture", "扫描架构深化机会"),
            ("/prototype <问题>", "构建原型回答设计问题"),
            ("/domain", "领域建模，提炼术语和决策"),
        ]),
        ("CONFIG", [
            ("/model", "交互式切换模型（上下键选 + 回车）"),
            ("/thinking", "查看或开关思考模式（on/off）"),
            ("/show-reasoning", "开关思考过程在屏幕上的显示"),
            ("/config", "查看当前配置"),
            ("/config set api-key <key>", "设置 API Key"),
            ("/config set model <name>", "设置模型"),
            ("/config set thinking <on|off>", "开关思考模式"),
            ("/config set max-tokens <n>", "设置单次响应最大 token 数"),
            ("/config unset <key>", "删除某项配置（恢复默认值）"),
            ("/version", "显示版本号"),
        ]),
        ("REFERENCE SYNTAX", [
            ("@file.py", "引用整个文件（受 500 行截断保护）"),
            ("@file.py:L10", "引用第 10 行"),
            ("@file.py:L10-L20", "引用第 10-20 行"),
            ("@src/main.py:L1395-L1420", "支持相对路径 + 行范围"),
        ]),
        ("SHORTCUTS", [
            ("输入 /", "弹出补全菜单（上下键选 + Tab 补全）"),
            ("输入 @file", "引用文件到上下文（支持 :L10-L20 行范围）"),
            ("↑ / ↓", "翻历史消息"),
            ("Esc", "取消当前补全菜单或中断任务"),
            ("CTRL+H", "显示本帮助"),
        ]),
    ]

    def help_list(self) -> str:
        """输出 Hacker 风格的帮助（[CMD] TAG 分组 + │ 缩进的 2 列风格，避免使用边框）。"""
        out: list[str] = []
        # 顶部标题
        t = self._tag("CMD", hhmmss())
        out.append(f"{t} {GRN_BOLD}ABYSS COMMAND REFERENCE{RST} {GRN_DIM}(press CTRL+H anytime){RST}")
        out.append(self._sep("-").rstrip("\n"))
        # 分 2 列输出：左列 [CMD] GRP_NAME，右列 │ cmd  desc
        for grp_name, cmds in self._HELP_GROUPS:
            # 组标题
            out.append(f"{GRN_DIM}[{RST} {GRN_BOLD}{grp_name:<11s}{RST} {GRN_DIM}]{RST}")
            for cmd, desc in cmds:
                # 每命令：2空格 │ cmd (14宽对齐) + description
                cmd_padded = cmd.ljust(32)
                out.append(f"  {GRN_DIM}│{RST} {YEL_BOLD}{cmd_padded[:32]}{RST} {GRN_DIM}│{RST} {GRN}{desc}{RST}")
            # 分组间空一行（但最后一组不加）
            out.append("")
        if out and out[-1] == "":
            out.pop()
        # 底部提示
        out.append(self._sep("-").rstrip("\n"))
        t2 = self._tag("CMD")
        out.append(f"{t2} {GRN}cfg: {YEL}~/.abyss/{RST}  {GRN_DIM}·{RST}  "
                   f"{GRN}theme: {GRN_BOLD}{'HACKER · 复古 CRT'}{RST}  {GRN_DIM}·{RST}  "
                   f"{GRN}输入 {YEL_BOLD}/{RST}{GRN} + {YEL_BOLD}TAB{RST} {GRN}弹出补全{RST}")
        return "\n".join(out) + "\n"
