"""
自定义 Textual 组件
- LeftAlignedMarkdown: 左对齐 Markdown 渲染
- PermissionBar: 权限选项组
- ClarifyBar: 用户澄清选项组
- UserInput: 多行输入框
- CommandList: 命令列表显示（支持二级菜单选择模型/会话等）
"""

from typing import Optional, TYPE_CHECKING
from textual.widget import Widget
from textual.widgets import TextArea, Static, RichLog
from textual.containers import VerticalScroll
from textual.widgets.text_area import TextAreaTheme
from textual.binding import Binding
from textual.message import Message
from textual.strip import Strip

if TYPE_CHECKING:
    from textual.events import Key

from rich.panel import Panel
from rich.markdown import Markdown, Heading
from rich.text import Text
from rich.style import Style
from rich import box


# ============================================================================
# 命令列表组件
# ============================================================================

# 需要二级菜单的命令 → 子菜单数据 key
COMMAND_SUB_MENUS = {
    "model": "models",
    "mode": "agents",
    "session": "sessions",
}

# 无参数命令，选中后直接提交
DIRECT_EXECUTE_COMMANDS = {
    "exit", "quit", "clear", "cls", "compact",
    "help", "status", "tools", "todo", "todos",
    "debug", "config", "skills",
}


class CommandList(VerticalScroll):
    """命令列表显示组件 - 支持搜索过滤、二级菜单、滚动"""

    DEFAULT_CSS = """
    CommandList {
        height: auto;
        min-height: 1;
        max-height: 20;
        padding: 0 1 1 1;
    }
    CommandList > .command-list-content {
        height: auto;
    }
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._content = Static(Text(""), classes="command-list-content")
        self._commands: list = []       # [(name, desc, category), ...]
        self._filtered: list = []       # 过滤后的索引列表
        self._selected_index: int = 0
        self._sub_menu: str = ""
        self._sub_options: list = []
        self._extra_data: dict = {}

    def compose(self):
        yield self._content

    def set_extra_data(self, data: dict):
        """设置额外数据（models, agents 等）"""
        self._extra_data = data

    def set_commands(self, commands: list, search: str = ""):
        """设置命令列表并按搜索词过滤"""
        self._commands = commands
        self._sub_menu = ""
        self._sub_options = []
        self.filter(search)

    def filter(self, search: str = ""):
        """按搜索词过滤命令列表"""
        search = search.lower().strip()
        if search:
            self._filtered = [
                i for i, (name, _, _) in enumerate(self._commands)
                if search in name.lower()
            ]
        else:
            self._filtered = list(range(len(self._commands)))
        self._selected_index = 0

        # 无匹配时隐藏命令列表
        if not self._filtered:
            self._content.update(Text(""))
            self.display = False
        else:
            self.display = True
            self._refresh_display()

    def set_sub_menu(self, sub_menu: str, options: list):
        """切换到二级子菜单"""
        self._sub_menu = sub_menu
        self._sub_options = options
        self._selected_index = 0
        self._refresh_display()

    def back_to_main(self):
        """返回主菜单"""
        self._sub_menu = ""
        self._sub_options = []
        self._selected_index = 0
        self._refresh_display()

    def select_next(self):
        """选择下一个"""
        items = self._sub_options if self._sub_menu else self._filtered
        if items:
            self._selected_index = (self._selected_index + 1) % len(items)
            self._refresh_display()

    def select_prev(self):
        """选择上一个"""
        items = self._sub_options if self._sub_menu else self._filtered
        if items:
            self._selected_index = (self._selected_index - 1) % len(items)
            self._refresh_display()

    def get_selected(self):
        """获取当前选中项：主菜单返回 (name, desc, cat)，子菜单返回 str(值)"""
        if self._sub_menu and self._sub_options:
            if 0 <= self._selected_index < len(self._sub_options):
                item = self._sub_options[self._selected_index]
                # 支持 (value, desc) 元组和纯字符串
                return item[0] if isinstance(item, tuple) else item
        elif self._filtered:
            if 0 <= self._selected_index < len(self._filtered):
                return self._commands[self._filtered[self._selected_index]]
        return None

    def is_sub_menu(self) -> bool:
        return bool(self._sub_menu)

    def _scroll_to_selected(self, total: int):
        """滚动到选中项可见"""
        if total <= 0:
            return
        # 每项占 1 行，滚动到选中行
        self.scroll_to(y=max(0, self._selected_index - 5), animate=False)

    def _refresh_display(self):
        """渲染命令列表"""
        if self._sub_menu and self._sub_options:
            self._render_sub_menu()
            return

        if not self._filtered:
            self._content.update(Text("  无匹配命令", style="dim"))
            return

        lines = Text()
        for sel_i, cmd_i in enumerate(self._filtered):
            name, desc, _ = self._commands[cmd_i]
            if sel_i > 0:
                lines.append("\n")
            if sel_i == self._selected_index:
                lines.append(f" ▸ /{name}  ", style="bold")
                lines.append(desc, style="bold dim")
            else:
                lines.append(f"   /{name}  ")
                lines.append(desc, style="dim")
        self._content.update(lines)
        self._scroll_to_selected(len(self._filtered))

    def _render_sub_menu(self):
        """渲染二级子菜单，支持 str 和 (value, desc) 格式"""
        # 动态菜单从 extra_data 刷新
        if self._sub_menu not in ("session_cmds",):
            if self._sub_menu in self._extra_data:
                self._sub_options = self._extra_data[self._sub_menu]

        if not self._sub_options:
            self._content.update(Text("  无可用选项", style="dim"))
            return

        title_map = {
            "models": "模型", "agents": "Agent",
            "sessions": "会话", "session_cmds": "会话操作",
        }
        title = title_map.get(self._sub_menu, "选项")
        # sessions 子菜单：value 是 UUID，不直接展示，只显示 desc
        hide_value = self._sub_menu == "sessions"
        lines = Text()
        lines.append(f" 选择 {title}:\n", style="dim")
        for i, option in enumerate(self._sub_options):
            if i > 0:
                lines.append("\n")
            # 解析 (value, desc) 或纯 str
            if isinstance(option, tuple):
                value, desc = option[0], option[1]
            else:
                value, desc = option, ""
            display = desc if (hide_value and desc) else value
            if i == self._selected_index:
                lines.append(f" ▸ {display}", style="bold")
                if not hide_value and desc:
                    lines.append(f"  {desc}", style="bold dim")
            else:
                lines.append(f"   {display}")
                if not hide_value and desc:
                    lines.append(f"  {desc}", style="dim")
        self._content.update(lines)
        self._scroll_to_selected(len(self._sub_options))

    def clear(self):
        """清除命令列表"""
        self._commands = []
        self._filtered = []
        self._selected_index = 0
        self._sub_menu = ""
        self._sub_options = []
        self._content.update(Text(""))
        self.scroll_home(animate=False)


# ============================================================================
# 自定义 Markdown 渲染器（修复标题居中问题）
# ============================================================================

class LeftAlignedHeading(Heading):
    """左对齐的 Markdown 标题"""

    def __rich_console__(self, console, options):
        text = self.text
        text.justify = "left"
        if self.tag == "h1":
            yield Panel(
                text,
                box=box.HEAVY,
                style="markdown.h1.border",
            )
        else:
            yield text


class LeftAlignedMarkdown(Markdown):
    """左对齐的 Markdown 渲染器"""

    elements = {
        **Markdown.elements,
        "heading_open": LeftAlignedHeading,
    }


# ============================================================================
# 可选择文本的 RichLog（输出区）
# ============================================================================

# ============================================================================
# 选择功能公共函数
# ============================================================================

def _normalize_selection(sel_start, sel_end):
    """归一化选区坐标，确保 start <= end"""
    if not sel_start or not sel_end:
        return None, None, None, None
    s_line, s_col = sel_start
    e_line, e_col = sel_end
    if s_line > e_line or (s_line == e_line and s_col > e_col):
        s_line, s_col, e_line, e_col = e_line, e_col, s_line, s_col
    return s_line, s_col, e_line, e_col


def _highlight_strip(strip, abs_line, s_line, s_col, e_line, e_col, scroll_x=0):
    """对 Strip 施加选区反色高亮"""
    if abs_line < s_line or abs_line > e_line:
        return strip
    line_width = strip.cell_length
    if line_width == 0:
        return strip
    if abs_line == s_line == e_line:
        sel_start = s_col - scroll_x
        sel_end = e_col - scroll_x
    elif abs_line == s_line:
        sel_start = s_col - scroll_x
        sel_end = line_width
    elif abs_line == e_line:
        sel_start = 0
        sel_end = e_col - scroll_x
    else:
        sel_start = 0
        sel_end = line_width
    sel_start = max(0, min(sel_start, line_width))
    sel_end = max(0, min(sel_end, line_width))
    if sel_start >= sel_end:
        return strip
    selection_style = Style(reverse=True)
    parts = []
    if sel_start > 0:
        parts.append(strip.crop(0, sel_start))
    parts.append(strip.crop(sel_start, sel_end).apply_style(selection_style))
    if sel_end < line_width:
        parts.append(strip.crop(sel_end, line_width))
    return Strip.join(parts)


def _copy_to_clipboard(text: str) -> None:
    """写入系统剪贴板（macOS: pbcopy, Linux: xclip）"""
    import subprocess, sys
    try:
        if sys.platform == "darwin":
            subprocess.Popen(
                ["pbcopy"], stdin=subprocess.PIPE,
                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
            ).communicate(text.encode("utf-8"))
        else:
            subprocess.Popen(
                ["xclip", "-selection", "clipboard"], stdin=subprocess.PIPE,
                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
            ).communicate(text.encode("utf-8"))
    except Exception:
        pass


# ============================================================================
# 可选择 Static（状态栏、功能区等非滚动区域）
# ============================================================================

class SelectableStatic(Static):
    """支持拖拽选择复制的 Static - 用于状态栏、功能区等"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._sel_start: Optional[tuple] = None
        self._sel_end: Optional[tuple] = None
        self._selecting: bool = False
        self._has_selection: bool = False

    def on_mouse_down(self, event) -> None:
        if event.button != 1:
            return
        self._sel_start = (event.y, event.x)
        self._sel_end = self._sel_start
        self._selecting = True
        self._has_selection = False
        self.capture_mouse()
        event.stop()
        self.refresh()

    def on_mouse_move(self, event) -> None:
        if not self._selecting:
            return
        self._sel_end = (max(0, event.y), max(0, event.x))
        self._has_selection = True
        event.stop()
        self.refresh()

    def on_mouse_up(self, event) -> None:
        if not self._selecting:
            return
        self._selecting = False
        self.release_mouse()
        if self._has_selection and self._sel_start and self._sel_end:
            text = self._extract_text()
            if text.strip():
                _copy_to_clipboard(text)
        self._sel_start = None
        self._sel_end = None
        self._has_selection = False
        self.refresh()
        event.stop()

    def render_line(self, y: int) -> Strip:
        strip = super().render_line(y)
        if not self._has_selection and not self._selecting:
            return strip
        s_line, s_col, e_line, e_col = _normalize_selection(self._sel_start, self._sel_end)
        if s_line is None:
            return strip
        return _highlight_strip(strip, y, s_line, s_col, e_line, e_col)

    def _extract_text(self) -> str:
        s_line, s_col, e_line, e_col = _normalize_selection(self._sel_start, self._sel_end)
        if s_line is None:
            return ""
        result = []
        h = self.size.height
        for y in range(s_line, min(e_line + 1, h)):
            strip = Static.render_line(self, y)
            cl = strip.cell_length
            if y == s_line == e_line:
                text = strip.crop(s_col, min(e_col, cl)).text
            elif y == s_line:
                text = strip.crop(s_col, cl).text
            elif y == e_line:
                text = strip.crop(0, min(e_col, cl)).text
            else:
                text = strip.text
            result.append(text)
        return "\n".join(result)


# ============================================================================
# 可选择 RichLog（输出区 - 支持滚动和边缘自动滚动）
# ============================================================================

class SelectableRichLog(RichLog):
    """支持鼠标拖拽选择文本的 RichLog

    - MouseDown 开始选区
    - MouseMove（拖拽）更新选区，反色高亮
    - MouseUp 提取文本并自动复制到剪贴板（选中即复制）
    - 点击或按键自动清除选区
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._sel_start: Optional[tuple] = None
        self._sel_end: Optional[tuple] = None
        self._selecting: bool = False
        self._has_selection: bool = False
        # 边缘自动滚动
        self._scroll_timer = None
        self._scroll_direction: int = 0
        self._last_mouse_x: int = 0

    def on_scroll(self, event) -> None:
        """滚动事件 - 更新 App 的底部状态（延迟调用确保 scroll_offset 已更新）"""
        # 使用 call_later 确保在滚动完成后更新状态
        self.call_later(self._update_bottom_status)

    def _update_bottom_status(self) -> None:
        """检查是否在底部（延迟调用）"""
        try:
            app = self.app
            if hasattr(app, '_output_at_bottom'):
                scroll_x, scroll_y = self.scroll_offset
                virtual_height = self.virtual_size.height
                visible_height = self.size.height if self.size else 0
                app._output_at_bottom = scroll_y + visible_height >= virtual_height - 1
        except Exception:
            pass

    # ========== 鼠标事件 ==========

    def on_mouse_down(self, event) -> None:
        if event.button != 1:
            return
        scroll_x, scroll_y = self.scroll_offset
        self._sel_start = (scroll_y + event.y, scroll_x + event.x)
        self._sel_end = self._sel_start
        self._selecting = True
        self._has_selection = False
        self.capture_mouse()
        event.stop()
        self.refresh()

    def on_mouse_move(self, event) -> None:
        if not self._selecting:
            return
        scroll_x, scroll_y = self.scroll_offset
        line = max(0, scroll_y + event.y)
        if len(self.lines) > 0:
            line = min(line, len(self.lines) - 1)
        col = max(0, scroll_x + event.x)
        self._sel_end = (line, col)
        self._has_selection = True
        self._last_mouse_x = event.x
        event.stop()
        self.refresh()

        # 边缘自动滚动检测
        try:
            h = self.size.height
            if h > 0:
                if event.y <= 0 and scroll_y > 0:
                    self._start_auto_scroll(-1)
                elif event.y >= h - 1 and scroll_y + h < len(self.lines):
                    self._start_auto_scroll(1)
                else:
                    self._stop_auto_scroll()
        except Exception:
            pass

    def on_mouse_up(self, event) -> None:
        if not self._selecting:
            return
        self._selecting = False
        self._stop_auto_scroll()
        self.release_mouse()
        if self._has_selection and self._sel_start and self._sel_end:
            text = self._extract_text()
            if text.strip():
                _copy_to_clipboard(text)
        self._sel_start = None
        self._sel_end = None
        self._has_selection = False
        self.refresh()
        event.stop()

    # ========== 渲染高亮 ==========

    def render_line(self, y: int) -> Strip:
        strip = super().render_line(y)
        if not self._has_selection and not self._selecting:
            return strip
        s_line, s_col, e_line, e_col = _normalize_selection(self._sel_start, self._sel_end)
        if s_line is None:
            return strip
        scroll_x, scroll_y = self.scroll_offset
        return _highlight_strip(strip, scroll_y + y, s_line, s_col, e_line, e_col, scroll_x)

    # ========== 文本提取 ==========

    def _extract_text(self) -> str:
        s_line, s_col, e_line, e_col = _normalize_selection(self._sel_start, self._sel_end)
        if s_line is None:
            return ""
        result = []
        for i in range(s_line, min(e_line + 1, len(self.lines))):
            line = self.lines[i]
            cl = line.cell_length
            if i == s_line == e_line:
                text = line.crop(s_col, min(e_col, cl)).text
            elif i == s_line:
                text = line.crop(s_col, cl).text
            elif i == e_line:
                text = line.crop(0, min(e_col, cl)).text
            else:
                text = line.text
            result.append(text)
        return "\n".join(result)

    # ========== 边缘自动滚动 ==========

    def _start_auto_scroll(self, direction: int) -> None:
        if self._scroll_direction == direction and self._scroll_timer is not None:
            return
        self._stop_auto_scroll()
        self._scroll_direction = direction
        self._scroll_timer = self.set_interval(0.05, self._auto_scroll_tick)

    def _stop_auto_scroll(self) -> None:
        if self._scroll_timer is not None:
            self._scroll_timer.stop()
            self._scroll_timer = None
        self._scroll_direction = 0

    def _auto_scroll_tick(self) -> None:
        if not self._selecting:
            self._stop_auto_scroll()
            return
        try:
            if self._scroll_direction == -1:
                self.scroll_up(animate=False)
            elif self._scroll_direction == 1:
                self.scroll_down(animate=False)
            scroll_x, scroll_y = self.scroll_offset
            if self._scroll_direction == -1:
                line = scroll_y
            else:
                line = scroll_y + self.size.height - 1
            if len(self.lines) > 0:
                line = max(0, min(line, len(self.lines) - 1))
            col = max(0, scroll_x + self._last_mouse_x)
            self._sel_end = (line, col)
            self.refresh()
        except Exception:
            self._stop_auto_scroll()


# ============================================================================
# 权限选项组
# ============================================================================

class PermissionBar(Widget):
    """权限请求选项组 - 左右键选择，Enter 确认"""

    can_focus = True

    DEFAULT_CSS = """
    PermissionBar {
        height: 1;
        width: 100%;
        margin-top: 1;
        margin-left: 1;
    }
    """

    CHOICES = [
        ("y", "允许"),
        ("n", "拒绝"),
        ("a", "始终"),
        ("d", "禁止"),
    ]

    class Selected(Message):
        def __init__(self, choice: str) -> None:
            super().__init__()
            self.choice = choice

    BINDINGS = [
        Binding("left", "move_left", show=False),
        Binding("right", "move_right", show=False),
        Binding("enter", "confirm", show=False),
    ]

    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)
        self._selected_index = 0

    def render(self) -> Text:
        text = Text()
        for i, (key, label) in enumerate(self.CHOICES):
            if i > 0:
                text.append("  ")
            if i == self._selected_index:
                text.append(label, style="bold underline")
            else:
                text.append(label)
        return text

    def reset(self) -> None:
        """重置选中状态"""
        self._selected_index = 0

    def action_move_left(self) -> None:
        self._selected_index = (self._selected_index - 1) % len(self.CHOICES)
        self.refresh()

    def action_move_right(self) -> None:
        self._selected_index = (self._selected_index + 1) % len(self.CHOICES)
        self.refresh()

    def action_confirm(self) -> None:
        choice = self.CHOICES[self._selected_index][0]
        self.post_message(PermissionBar.Selected(choice))


# ============================================================================
# 用户澄清选项组
# ============================================================================

class ClarifyBar(Widget):
    """用户澄清请求 - 支持选项选择和自由输入

自由输入... 选项可直接编辑，类似普通输入框
"""

    can_focus = True

    # 纵向展示的阈值（默认启用纵向展示）
    VERTICAL_THRESHOLD = 0

    DEFAULT_CSS = """
    ClarifyBar {
        width: 100%;
        height: auto;
        min-height: 1;
        padding: 1 0 0 0;
    }
    """

    class Selected(Message):
        def __init__(self, value: str) -> None:
            super().__init__()
            self.value = value  # 可以是选项值或用户输入

    BINDINGS = [
        Binding("left", "move_prev", show=False),
        Binding("right", "move_next", show=False),
        Binding("up", "move_prev", show=False),
        Binding("down", "move_next", show=False),
        Binding("enter", "confirm", show=False),
    ]

    def __init__(self, **kwargs) -> None:
        super().__init__(**kwargs)
        self._options: list = []  # 选项列表
        self._allow_input: bool = True  # 是否允许自由输入
        self._selected_index: int = 0
        self._user_input: str = ""
        self._cursor_pos: int = 0
        self._is_editing: bool = False  # 是否在编辑自由输入
        self._is_vertical: bool = False  # 是否纵向展示

    def configure(self, options: list, allow_input: bool = True) -> None:
        """配置选项和输入模式"""
        self._options = options
        self._allow_input = allow_input
        self._selected_index = 0
        self._user_input = ""
        # 没有选项时直接进入自由输入编辑模式
        if not options and allow_input:
            self._is_editing = True
        else:
            self._is_editing = False
        self._cursor_pos = len(self._user_input)

        # 判断是否需要纵向展示（选项文本超过阈值）
        self._is_vertical = self._should_use_vertical()

        self.refresh()

    def _should_use_vertical(self) -> bool:
        """判断是否应该使用纵向展示模式（默认纵向，增加行间距）"""
        # 始终使用纵向展示
        return True

    def _display_width(self, text: str) -> int:
        """估算显示宽度（中文等宽字符按 2 计）"""
        import unicodedata
        width = 0
        for ch in text:
            width += 2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1
        return width

    def _format_input_with_cursor(self) -> str:
        """格式化自由输入（含光标）"""
        if not self._user_input:
            return "[自由输入...]▎"
        left = self._user_input[:self._cursor_pos]
        right = self._user_input[self._cursor_pos:]
        return f"{left}▎{right}"

    @property
    def _total_options(self) -> int:
        """总选项数（包括自由输入）"""
        return len(self._options) + (1 if self._allow_input else 0)

    @property
    def _is_on_free_input(self) -> bool:
        """是否选中自由输入选项"""
        return self._allow_input and self._selected_index >= len(self._options)

    def render(self) -> Text:
        text = Text()

        # 根据是否为纵向模式选择不同的渲染方式
        if self._is_vertical:
            # 纵向模式：每行一个选项，增加行间距
            for i, opt in enumerate(self._options):
                label = opt.get("label", opt.get("value", f"选项{i+1}"))
                index_label = f"{i + 1}. "
                if i > 0:
                    text.append("\n")
                if i == self._selected_index and not self._is_on_free_input:
                    text.append(f"▸ {index_label}{label}", style="bold underline")
                else:
                    text.append(f"  {index_label}{label}")

            # 自由输入选项（纵向模式时显示在最后）
            if self._allow_input:
                text.append("\n")
                if self._is_on_free_input:
                    if self._is_editing:
                        text.append(f"▸ {self._format_input_with_cursor()}", style="bold underline")
                    else:
                        text.append("▸ 自由输入...", style="bold underline")
                else:
                    text.append("  自由输入...", style="dim italic")
        else:
            # 横向模式：一行显示
            for i, opt in enumerate(self._options):
                label = opt.get("label", opt.get("value", f"选项{i+1}"))
                index_label = f"{i + 1}. "
                if i > 0:
                    text.append("  ")
                if i == self._selected_index and not self._is_on_free_input:
                    text.append(f"{index_label}{label}", style="bold underline")
                else:
                    text.append(f"{index_label}{label}")

            if self._allow_input:
                if len(self._options) > 0:
                    text.append("  ", style="dim")

                # 自由输入选项
                if self._is_on_free_input:
                    if self._is_editing:
                        text.append(self._format_input_with_cursor(), style="bold underline")
                    else:
                        text.append("自由输入...", style="bold underline")
                else:
                    text.append("自由输入...", style="dim italic")

        return text

    def reset(self) -> None:
        """重置状态"""
        self._selected_index = 0
        self._user_input = ""
        self._cursor_pos = 0
        self._is_editing = False
        self.refresh()

    def action_move_prev(self) -> None:
        """移动到上一个选项（支持横纵模式）"""
        if self._is_editing:
            if self._is_vertical:
                self._is_editing = False
            elif self._user_input == "":
                self._is_editing = False
            else:
                return
        total = self._total_options
        if total > 0:
            if self._is_vertical:
                # 纵向模式：向上移动
                self._selected_index = (self._selected_index - 1) % total
            else:
                # 横向模式：向左移动
                self._selected_index = (self._selected_index - 1) % total
            # 导航到自由输入时自动进入编辑模式
            if self._is_on_free_input:
                self._is_editing = True
                self._user_input = ""
                self._cursor_pos = 0
            self.refresh()

    def action_move_next(self) -> None:
        """移动到下一个选项（支持横纵模式）"""
        if self._is_editing:
            if self._is_vertical:
                self._is_editing = False
            else:
                return
        total = self._total_options
        if total > 0:
            if self._is_vertical:
                # 纵向模式：向下移动
                self._selected_index = (self._selected_index + 1) % total
            else:
                # 横向模式：向右移动
                self._selected_index = (self._selected_index + 1) % total
            # 导航到自由输入时自动进入编辑模式
            if self._is_on_free_input:
                self._is_editing = True
                self._user_input = ""
                self._cursor_pos = 0
            self.refresh()

    def action_confirm(self) -> None:
        if self._is_on_free_input and self._is_editing:
            # 提交自由输入内容
            if self._user_input.strip():
                self.post_message(ClarifyBar.Selected(self._user_input))
            return

        # 选择了某个选项
        if self._selected_index < len(self._options):
            value = self._options[self._selected_index].get("value")
            self.post_message(ClarifyBar.Selected(value))

    def on_key(self, event: "Key") -> None:
        """处理按键输入"""
        if self._is_on_free_input and self._is_editing:
            if event.key == "escape":
                # 有选项时退出编辑模式回到选项选择，没有选项时忽略
                if self._options:
                    self._is_editing = False
                    self._user_input = ""
                    self._selected_index = 0
                    self.refresh()
                event.prevent_default()
                event.stop()
            elif event.key == "backspace":
                if self._cursor_pos > 0:
                    left = self._user_input[:self._cursor_pos - 1]
                    right = self._user_input[self._cursor_pos:]
                    self._user_input = left + right
                    self._cursor_pos -= 1
                self.refresh()
                event.prevent_default()
                event.stop()
            elif event.key == "left":
                if self._cursor_pos > 0:
                    self._cursor_pos -= 1
                    self.refresh()
                    event.prevent_default()
                    event.stop()
                else:
                    if not self._is_vertical:
                        self._is_editing = False
                        self.action_move_prev()
                        event.prevent_default()
                        event.stop()
            elif event.key == "right":
                if self._cursor_pos < len(self._user_input):
                    self._cursor_pos += 1
                    self.refresh()
                    event.prevent_default()
                    event.stop()
            elif event.key in ("enter", "left", "right", "up", "down", "tab"):
                # 控制键不拦截，交给 Binding 处理
                pass
            elif event.character and event.is_printable:
                left = self._user_input[:self._cursor_pos]
                right = self._user_input[self._cursor_pos:]
                self._user_input = left + event.character + right
                self._cursor_pos += 1
                self.refresh()
                event.prevent_default()
                event.stop()


# ============================================================================
# 多行输入框
# ============================================================================

class UserInput(TextArea):
    """多行用户输入框，Enter 提交，Shift+Enter 换行，支持历史记录"""

    PROMPT = "›"

    # 透明主题：让 TextArea 内部渲染与 App 背景一致
    _TRANSPARENT_THEME = TextAreaTheme(
        name="transparent",
        base_style=Style(),
        cursor_style=Style(reverse=True),
        cursor_line_style=Style(),
        bracket_matching_style=Style(bold=True, underline=True),
        selection_style=Style(reverse=True),
        gutter_style=Style(),
    )

    # 历史记录配置
    MAX_HISTORY = 100  # 最大历史记录数

    DEFAULT_CSS = """
    UserInput {
        height: auto;
        min-height: 1;
        max-height: 8;
        border: none;
        padding: 0 1 0 0;
    }
    """

    class Submitted(Message):
        def __init__(self, value: str) -> None:
            super().__init__()
            self.value = value

    def __init__(self, **kwargs) -> None:
        super().__init__(
            show_line_numbers=False,
            soft_wrap=True,
            tab_behavior="focus",
            **kwargs,
        )
        # 必须先 register 再设置 theme
        self.register_theme(self._TRANSPARENT_THEME)
        self.theme = "transparent"

        # 历史记录
        self._history: list[str] = []
        self._history_index: int = -1  # -1 表示不在浏览历史
        self._draft: str = ""  # 临时保存当前输入（浏览历史时）

        # 命令选择器
        self._command_list: Optional[CommandList] = None
        self._command_list_visible: bool = False
        self._current_command: str = ""  # 子菜单时记录当前命令名
        self._server_url: str = ""  # 服务端地址（用于获取会话列表）
        self._sessions_cache: list = []  # 会话列表缓存

    def set_command_list(self, command_list: CommandList):
        """设置命令列表组件引用"""
        self._command_list = command_list

    def on_mount(self) -> None:
        pass

    def on_mouse_scroll_up(self, event) -> None:
        """鼠标滚轮上：优先滚动输入区"""
        event.stop()
        event.prevent_default()
        try:
            self.scroll_up(animate=False)
        except Exception:
            pass

    def on_mouse_scroll_down(self, event) -> None:
        """鼠标滚轮下：优先滚动输入区"""
        event.stop()
        event.prevent_default()
        try:
            self.scroll_down(animate=False)
        except Exception:
            pass

    def _scroll_output(self, direction: str) -> None:
        """将滚动操作转发给主输出区 RichLog"""
        try:
            from textual.widgets import RichLog
            log = self.screen.query_one("#output-log", RichLog)
            if direction == "pageup":
                log.scroll_page_up(animate=False)
            elif direction == "pagedown":
                log.scroll_page_down(animate=False)
            elif direction == "up":
                log.scroll_up(animate=False)
            elif direction == "down":
                log.scroll_down(animate=False)
        except Exception:
            pass

    def _add_to_history(self, text: str) -> None:
        """添加输入到历史记录"""
        if not text or not text.strip():
            return
        # 避免重复的连续记录
        if self._history and self._history[-1] == text:
            return
        self._history.append(text)
        # 限制历史记录数量
        if len(self._history) > self.MAX_HISTORY:
            self._history.pop(0)

    # ==================== 命令选择器 ====================

    async def _open_command_list(self, search: str = ""):
        """打开命令列表"""
        if self._command_list is None:
            return
        try:
            from interface.commands import get_command_handler
            handler = get_command_handler()
            commands = []
            for name, info in handler.commands.items():
                if info.hidden or name != info.name:
                    continue
                commands.append((name, info.description, info.category.value))
            commands.sort(key=lambda x: x[0])

            models = getattr(self, '_available_models', [])
            agents = [
                ('build', '编码与构建'),
                ('plan', '规划与设计'),
                ('general', '通用对话'),
                ('explore', '代码探索'),
                ('web', '浏览器自动化'),
            ]

            self._command_list.set_extra_data({
                'models': models, 'agents': agents, 'sessions': [],
            })
            self._command_list.set_commands(commands, search)
            self._command_list.display = True
            self._command_list_visible = True

            # 显示功能区
            try:
                panel = self.app.query_one("#panel-area")
                panel.display = True
            except Exception:
                pass
        except Exception:
            pass

    def _hide_command_list(self):
        """隐藏命令列表"""
        if self._command_list:
            self._command_list.display = False
            self._command_list.clear()
        self._command_list_visible = False
        self._current_command = ""

        # 检查并可能隐藏整个功能区
        self._check_and_hide_panel()

    def _check_and_hide_panel(self):
        """检查功能区是否有可见子区域，如果没有则隐藏"""
        try:
            panel = self.app.query_one("#panel-area")
            cmd_list = self.app.query_one("#panel-command-list")
            perm = self.app.query_one("#panel-permission")
            clarify = self.app.query_one("#panel-clarify")
            
            # 如果命令列表隐藏了，检查其他区域是否可见
            if cmd_list.display == False:
                if perm.display == False and clarify.display == False:
                    panel.display = False
        except Exception:
            pass

    async def _update_command_list_state(self):
        """根据当前输入文本，自动显示/过滤/隐藏命令列表"""
        text = self.text
        if text.startswith("/") and "\n" not in text:
            search = text[1:]
            if not self._command_list_visible:
                await self._open_command_list(search)
            elif not self._command_list.is_sub_menu():
                self._command_list.filter(search)
        elif self._command_list_visible:
            self._hide_command_list()

    def _handle_command_selection(self):
        """处理命令列表中的选择确认"""
        # 子菜单中：填充参数并提交
        if self._command_list.is_sub_menu():
            selected = self._command_list.get_selected()
            if selected:
                cmd = self._current_command
                self.text = f"/{cmd} {selected}"
                self.cursor_location = (0, len(self.text))
            self._hide_command_list()
            self._submit_current()
            return

        # 主菜单中：命令列表已隐藏（无匹配项），直接提交当前文本
        if not self._command_list.display or not self._command_list._filtered:
            self._hide_command_list()
            self._submit_current()
            return

        # 主菜单中
        selected = self._command_list.get_selected()
        if not selected:
            # 无匹配项，直接提交当前文本
            self._hide_command_list()
            self._submit_current()
            return

        cmd_name, _, _ = selected

        # 需要子菜单的命令
        sub_menu_type = COMMAND_SUB_MENUS.get(cmd_name)
        if sub_menu_type:
            self._current_command = cmd_name
            self.text = f"/{cmd_name}"
            self.cursor_location = (0, len(self.text))

            # sessions 子菜单：使用预加载的缓存
            if sub_menu_type == "sessions":
                self._command_list._extra_data["sessions"] = self._sessions_cache

            self._command_list.set_sub_menu(sub_menu_type,
                self._command_list._extra_data.get(sub_menu_type, []))
            return

        # 无参数命令：直接提交
        if cmd_name in DIRECT_EXECUTE_COMMANDS:
            self._hide_command_list()
            self.text = f"/{cmd_name}"
            self._submit_current()
            return

        # 其他命令：填充到输入框，等用户补充参数
        self.text = f"/{cmd_name} "
        self.cursor_location = (0, len(self.text))
        self._hide_command_list()

    async def _fetch_sessions(self) -> list:
        """从服务端获取会话列表（前10条），结果缓存到 _sessions_cache"""
        sessions = []
        server_url = getattr(self, '_server_url', '')
        if not server_url:
            return sessions
        try:
            import aiohttp
            async with aiohttp.ClientSession() as http_session:
                async with http_session.get(f"{server_url}/sessions?limit=10") as resp:
                    result = await resp.json()
                    for s in result.get("sessions", [])[:10]:
                        sid = s.get("id", "")
                        preview = s.get("preview", "")
                        time_str = ""
                        created = s.get("created_at", "")
                        if created:
                            try:
                                dt = created.split("T")
                                if len(dt) >= 2:
                                    time_str = dt[0][5:] + " " + dt[1][:5]
                            except Exception:
                                pass
                        parts = []
                        parts.append(f"[{sid[:8]}]")
                        if time_str:
                            parts.append(time_str)
                        title = s.get("title", "")
                        if title and title not in ("New Session", "新会话"):
                            parts.append(title[:50])
                        if preview:
                            preview_clean = preview.replace("\n", " ").replace("\r", "")
                            parts.append(preview_clean[:100])
                        desc = " ".join(parts)
                        sessions.append((sid, desc))
        except Exception:
            pass
        self._sessions_cache = sessions
        return sessions

    def _submit_current(self):
        """提交当前输入框内容"""
        text = self.text.strip()
        self._add_to_history(text)
        self._history_index = -1
        self._draft = ""
        self.clear()
        self.post_message(UserInput.Submitted(text))

    def set_available_models(self, models: list):
        """设置可用模型列表"""
        self._available_models = models

    def set_available_agents(self, agents: list):
        """设置可用 Agent 列表"""
        self._available_agents = agents

    def _navigate_history(self, direction: int) -> None:
        """
        浏览历史记录

        Args:
            direction: -1 表示向上（更早），1 表示向下（更新）
        """
        if not self._history:
            return

        # 第一次按上键时，保存当前输入
        if self._history_index == -1:
            if direction == -1:  # 按上键
                self._draft = self.text
                self._history_index = len(self._history) - 1
            else:  # 按下键，无操作
                return
        else:
            self._history_index += direction
            # 边界检查
            if self._history_index < 0:
                self._history_index = 0
            elif self._history_index >= len(self._history):
                # 回到草稿
                self._history_index = -1
                self.text = self._draft
                self.cursor_location = (0, len(self._draft))
                return

        # 显示历史记录
        self.text = self._history[self._history_index]
        # 移动光标到末尾
        self.cursor_location = (0, len(self.text))

    async def _on_key(self, event) -> None:
        # PageUp/PageDown: 滚动主输出区
        if event.key == "pageup":
            event.stop()
            event.prevent_default()
            self._scroll_output("pageup")
            return
        if event.key == "pagedown":
            event.stop()
            event.prevent_default()
            self._scroll_output("pagedown")
            return

        # 历史浏览（命令列表未显示时）
        if not self._command_list_visible:
            if event.key in ("escape+up", "ctrl+p"):
                event.stop()
                event.prevent_default()
                self._navigate_history(-1)
                return
            if event.key in ("escape+down", "ctrl+n"):
                event.stop()
                event.prevent_default()
                self._navigate_history(1)
                return

        # ===== 命令列表导航 =====
        if self._command_list_visible:
            # 上下选择
            if event.key in ("up", "down"):
                event.stop()
                event.prevent_default()
                if event.key == "up":
                    self._command_list.select_prev()
                else:
                    self._command_list.select_next()
                return

            # Enter: 确认选择
            if event.key in ("enter", "ctrl+m"):
                event.stop()
                event.prevent_default()
                self._handle_command_selection()
                return

            # Escape: 关闭或返回上级
            if event.key == "escape":
                event.stop()
                event.prevent_default()
                if self._command_list.is_sub_menu():
                    self._command_list.back_to_main()
                else:
                    self._hide_command_list()
                    self.text = ""
                return

            # Tab: 补全选中命令名
            if event.key == "tab":
                event.stop()
                event.prevent_default()
                selected = self._command_list.get_selected()
                if selected and not self._command_list.is_sub_menu():
                    name, _, _ = selected
                    self.text = f"/{name}"
                    self.cursor_location = (0, len(self.text))
                    self._command_list.filter(name)
                return

            # Backspace 在子菜单中返回上级
            if event.key == "backspace" and self._command_list.is_sub_menu():
                event.stop()
                event.prevent_default()
                self._command_list.back_to_main()
                return

            # 其他字符键：交给 TextArea 处理，之后更新过滤
            # （不拦截，fall through 到 super()._on_key）

        # 智能上下箭头（命令列表未显示时）
        if not self._command_list_visible and event.key in ("up", "down"):
            # 如果当前处于历史选择状态，上下键只处理历史选择
            if self._history_index != -1:
                event.stop()
                event.prevent_default()
                self._navigate_history(-1 if event.key == "up" else 1)
                return

            # 如果不在历史选择状态，判断触发条件
            cursor = self.cursor_location
            text = self.text
            current_line_start = text.rfind("\n", 0, cursor[1]) + 1
            line_end = text.find("\n", cursor[1])
            if line_end == -1:
                line_end = len(text)

            # 触发条件：输入框为空，或者光标在第一行行首按上，或者光标在最后一行行尾按下
            if event.key == "up" and cursor[1] == current_line_start:
                event.stop()
                event.prevent_default()
                self._navigate_history(-1)
                return
            if event.key == "down" and (cursor[1] == line_end or cursor[1] == line_end + 1):
                event.stop()
                event.prevent_default()
                self._navigate_history(1)
                return

        # Ctrl+D: 退出
        if event.key == "ctrl+d":
            event.stop()
            event.prevent_default()
            self._hide_command_list()
            self.clear()
            self.post_message(UserInput.Submitted("/exit"))
            return

        # 修饰 Enter: 换行，不提交
        if event.key in ("shift+enter", "alt+enter", "meta+enter", "ctrl+enter"):
            event.stop()
            event.prevent_default()
            self.insert("\n")
            return

        # Enter: 提交输入（命令列表未显示时）
        if event.key in ("enter", "ctrl+m"):
            event.stop()
            event.prevent_default()
            self._hide_command_list()
            self._submit_current()
            return

        # 其他按键：重置历史浏览状态
        if self._history_index != -1 and event.key not in ("shift", "ctrl", "alt"):
            self._history_index = -1
            self._draft = ""

        # 交给 TextArea 处理按键
        await super()._on_key(event)

        # 按键处理后，同步判断是否需要更新命令列表
        text = self.text
        if text.startswith("/") and "\n" not in text:
            search = text[1:]
            if not self._command_list_visible:
                await self._open_command_list(search)
            elif not self._command_list.is_sub_menu():
                self._command_list.filter(search)
        elif self._command_list_visible:
            self._hide_command_list()
