"""
Dundun Textual App - 主应用窗口

三区域布局：
1. 主输出区 (RichLog) - 所有服务端消息、动画、Thinking
2. 功能栏 (PermissionBar) - 权限请求（正常隐藏）
3. 输入区 (UserInput + status-bar) - 输入框 + 底部状态
"""

import os
import sys
import asyncio
from typing import Optional, Dict, Any

from rich.console import RenderableType
from rich.text import Text

from textual.app import App, ComposeResult
from textual.widgets import RichLog, Static
from textual.containers import Horizontal, Vertical
from textual.binding import Binding
from textual.css.query import NoMatches

from interface.styles import Styles, Icons
from interface.widgets import PermissionBar, ClarifyBar, UserInput, CommandList, SelectableRichLog, SelectableStatic


# 客户端全局工作路径 - 由 connected 事件从服务端获取
SERVER_WORKING_PATH: str = ""


class DundunApp(App):
    """Dundun Agent 的 Textual TUI 应用"""

    TITLE = ""
    CSS = f"""
    Screen {{
        layout: vertical;
    }}
    #output-log {{
        height: 1fr;
        border: none;
        padding: 0;
        overflow-x: hidden;
        scrollbar-size-vertical: 0;
    }}
    #bottom-area {{
        height: auto;
        dock: bottom;
    }}
    #input-area {{
        height: auto;
        border-top: solid {Styles.BORDER};
        border-bottom: solid {Styles.BORDER};
        background: $background;
    }}
    #input-prompt {{
        width: auto;
        height: auto;
        padding: 0 0 0 1;
        color: green;
        text-style: bold;
    }}
    #user-input {{
        width: 1fr;
        border: none;
        background: $background;
    }}
    /* 功能区 - 统一容器，按需显示 */
    #panel-area {{
        display: none;
        height: auto;
        background: $background;
        border-top: solid {Styles.BORDER};
        padding: 0 0 0 0;
    }}
    #panel-permission {{
        display: none;
        height: auto;
    }}
    #panel-permission-info {{
        height: auto;
        padding: 0;
    }}
    #panel-permission-bar {{
        height: 1;
        padding: 0;
    }}
    #panel-clarify {{
        display: none;
        height: auto;
    }}
    #panel-clarify-info {{
        height: auto;
        padding: 0;
    }}
    #panel-clarify-bar {{
        height: auto;
        padding: 0;
        margin-top: 1;
    }}
    #panel-command-list {{
        display: none;
        height: auto;
        max-height: 20;
        padding: 0 1;
    }}
    #status-bar {{
        height: 1;
        margin: 0;
    }}
    #status-left {{
        width: 1fr;
        height: 1;
        padding: 0 1;
        overflow: hidden;
    }}
    #status-right {{
        width: 1fr;
        height: 1;
        padding: 0 1;
        overflow: hidden;
        text-align: right;
    }}
    """

    ENABLE_COMMAND_PALETTE = False

    # 全局快捷键
    BINDINGS = [
        Binding("pageup", "scroll_output_up", "上翻", show=False, priority=True),
        Binding("pagedown", "scroll_output_down", "下翻", show=False, priority=True),
    ]

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._input_future: Optional[asyncio.Future] = None
        self._permission_mode: bool = False
        self._permission_future: Optional[asyncio.Future] = None
        self._clarify_future: Optional[asyncio.Future] = None
        self._width: int = 80
        self._thinking_timer = None
        self._thinking_frame: int = 0
        # RichLog 内瞬态行追踪（用于 pop/replace）
        self._has_live_line: bool = False
        self._live_line_count: int = 0
        # 输出区是否在底部（用于控制自动滚动）
        self._output_at_bottom: bool = True
        # 状态栏缓存（用于刷新时保留 model/agent）
        self._status_model: str = ""
        self._status_agent: str = ""
        self._status_session_id: str = ""
        self._context_tokens: int = 0       # 上下文 token 数
        self._context_max: int = 0          # 模型最大 token 数
        self._context_percent: float = 0.0  # 上下文使用百分比
        # Ctrl+C 中断回调（由 cli.py 注入）
        self._interrupt_callback: Optional[callable] = None

    def _handle_exception(self, error: Exception) -> None:
        """处理未捕获的异常 - 打印到控制台和日志"""
        import traceback
        error_msg = traceback.format_exception(type(error), error, error.__traceback__)
        error_text = "".join(error_msg)
        # 打印到 stderr
        print(f"\n✗ App Error: {error}", file=sys.stderr)
        print(f"Traceback:\n{error_text}", file=sys.stderr, flush=True)
        # 调用父类处理
        super()._handle_exception(error)

    def set_interrupt_callback(self, callback: callable):
        """设置 Ctrl+C 中断回调"""
        self._interrupt_callback = callback

    def action_help_quit(self) -> None:
        """Ctrl+C：中断当前任务"""
        if self._interrupt_callback:
            self._interrupt_callback()

    def action_quit(self) -> None:
        """覆盖 Ctrl+Q 退出行为，也改为中断当前任务"""
        if self._interrupt_callback:
            self._interrupt_callback()

    def compose(self) -> ComposeResult:
        """三区域布局：输出区 + 功能区（按需展示）+ 输入区 + 状态栏"""
        yield SelectableRichLog(
            id="output-log",
            highlight=False,
            markup=False,
            wrap=True,
            auto_scroll=False,  # 禁用默认自动滚动，完全手动控制
        )
        yield Vertical(
            # 功能区 - 统一容器，按需显示
            Vertical(
                # 权限请求
                Vertical(
                    SelectableStatic(id="panel-permission-info"),
                    PermissionBar(id="panel-permission-bar"),
                    id="panel-permission",
                ),
                # 用户澄清
                Vertical(
                    SelectableStatic(id="panel-clarify-info"),
                    ClarifyBar(id="panel-clarify-bar"),
                    id="panel-clarify",
                ),
                # 命令列表
                CommandList(id="panel-command-list"),
                id="panel-area",
            ),
            # 输入区
            Horizontal(
                Static(f"{UserInput.PROMPT} ", id="input-prompt"),
                UserInput(id="user-input"),
                id="input-area",
            ),
            # 状态栏
            Horizontal(
                SelectableStatic(id="status-left"),
                SelectableStatic(id="status-right"),
                id="status-bar",
            ),
            id="bottom-area",
        )

    def on_mount(self) -> None:
        self._width = self.size.width
        self._refresh_status_bar()
        try:
            self.query_one("#output-log", RichLog).can_focus = False
        except NoMatches:
            pass
        try:
            user_input = self.query_one("#user-input", UserInput)
            user_input.focus()
            # 传递 CommandList 引用给 UserInput
            command_list = self.query_one("#panel-command-list", CommandList)
            user_input.set_command_list(command_list)
        except NoMatches:
            pass

    def on_resize(self, event) -> None:
        self._width = self.size.width
        self._refresh_status_bar()

    # ========== 滚动操作 ==========

    def action_scroll_output_up(self) -> None:
        """PageUp - 主输出区向上翻页"""
        try:
            log = self.query_one("#output-log", RichLog)
            log.scroll_page_up(animate=False)
        except NoMatches:
            pass

    def action_scroll_output_down(self) -> None:
        """PageDown - 主输出区向下翻页"""
        try:
            log = self.query_one("#output-log", RichLog)
            log.scroll_page_down(animate=False)
        except NoMatches:
            pass

    # ========== 用户输入事件 ==========

    def on_user_input_submitted(self, event: UserInput.Submitted) -> None:
        # 用户提交输入后，立即滚动输出区到底部，确保用户能看到自己的输入
        try:
            log = self._get_log()
            log.scroll_end(animate=False)
        except NoMatches:
            pass

        value = event.value
        if self._input_future and not self._input_future.done():
            self._input_future.set_result(value)

    def fill_input(self, text: str) -> None:
        """填充输入框并聚焦"""
        try:
            user_input = self.query_one("#user-input", UserInput)
            user_input.text = text
            user_input.focus()
            # 将光标移到末尾
            user_input.cursor_position = len(text)
        except NoMatches:
            pass

    # ========== 输出方法 ==========

    def _is_output_at_bottom(self) -> bool:
        """检查输出区是否在底部"""
        try:
            log = self.query_one("#output-log", RichLog)
            scroll_x, scroll_y = log.scroll_offset
            virtual_height = log.virtual_size.height
            visible_height = log.size.height if log.size else 0
            # 底部容差为 1 行
            return scroll_y + visible_height >= virtual_height - 1
        except NoMatches:
            return True

    def _get_log(self) -> RichLog:
        """获取 RichLog 组件，并更新底部状态"""
        try:
            log = self.query_one("#output-log", RichLog)
            # 更新底部状态
            self._output_at_bottom = self._is_output_at_bottom()
            return log
        except NoMatches:
            raise NoMatches("#output-log")

    def clear_output(self) -> None:
        """清空输出区所有内容"""
        try:
            log = self._get_log()
            log.clear()
            self._output_at_bottom = True
        except NoMatches:
            pass

    def write_renderable(self, renderable: RenderableType, expand: bool = False) -> None:
        try:
            log = self._get_log()
            # 写入时重新检查底部状态
            is_at_bottom = self._is_output_at_bottom()
            log.write(renderable, expand=expand)
            # 只有在底部时才手动滚动到底部
            if is_at_bottom:
                log.scroll_end(animate=False)
        except NoMatches:
            pass

    def write_text(self, content: str, style: str = "") -> None:
        text = Text(content, style=style) if style else Text(content)
        self.write_renderable(text)

    def write_markup(self, markup: str) -> None:
        try:
            log = self._get_log()
            is_at_bottom = self._is_output_at_bottom()
            log.write(Text.from_markup(markup))
            if is_at_bottom:
                log.scroll_end(animate=False)
        except NoMatches:
            pass

    def write_blank(self) -> None:
        self.write_renderable(Text(""))

    def write_separator(self, char: str = "─", style: str = Styles.BORDER) -> None:
        try:
            log = self.query_one("#output-log", RichLog)
            w = log.content_region.width or (self._width - 2)
        except NoMatches:
            w = self._width - 2
        self.write_renderable(Text(char * max(w, 1), style=style))

    # ========== 瞬态行（在 RichLog 内 pop/write 替换）==========

    def update_live(self, renderable: RenderableType) -> None:
        """在 RichLog 底部写入/替换瞬态内容"""
        try:
            log = self.query_one("#output-log", RichLog)
            self._pop_live_lines(log)

            is_at_bottom = self._is_output_at_bottom()
            before = len(log.lines)
            log.write(renderable)
            self._live_line_count = len(log.lines) - before
            self._has_live_line = True

            # 只有在底部时才滚动到底部
            if is_at_bottom:
                log.scroll_end(animate=False)
        except NoMatches:
            pass

    def clear_live(self) -> None:
        """移除 RichLog 底部的瞬态行"""
        if not self._has_live_line:
            return
        try:
            log = self.query_one("#output-log", RichLog)
            self._pop_live_lines(log)
            self._has_live_line = False
            self._live_line_count = 0
        except NoMatches:
            pass

    def _pop_live_lines(self, log: RichLog) -> None:
        if self._live_line_count <= 0:
            return
        if self._live_line_count >= len(log.lines):
            log.lines.clear()
        else:
            log.lines = log.lines[:-self._live_line_count]
        self._live_line_count = 0
        log._line_cache.clear()
        from textual.geometry import Size
        log.virtual_size = Size(log.virtual_size.width, len(log.lines))
        log.refresh()

    # ========== Thinking 动画 ==========

    def start_thinking(self) -> None:
        self._thinking_frame = 0
        thinking_text = Text()
        thinking_text.append(f"{Icons.SPINNER_FRAMES[0]} ", style="bold dark_red")
        thinking_text.append("Thinking...", style="bold dark_red")
        self.update_live(thinking_text)
        self._thinking_timer = self.set_interval(0.08, self._update_thinking_frame)

    def _update_thinking_frame(self) -> None:
        self._thinking_frame = (self._thinking_frame + 1) % len(Icons.SPINNER_FRAMES)
        thinking_text = Text()
        thinking_text.append(
            f"{Icons.SPINNER_FRAMES[self._thinking_frame]} ",
            style="bold dark_red",
        )
        thinking_text.append("Thinking...", style="bold dark_red")
        self.update_live(thinking_text)

    def stop_thinking(self) -> None:
        if self._thinking_timer is not None:
            self._thinking_timer.stop()
            self._thinking_timer = None
        self.clear_live()

    # ========== 输入方法 ==========

    async def get_input(self) -> str:
        loop = asyncio.get_running_loop()
        self._input_future = loop.create_future()
        try:
            w = self.query_one("#user-input", UserInput)
            w.disabled = False
            w.focus()
        except NoMatches:
            pass
        try:
            return await self._input_future
        except asyncio.CancelledError:
            return ""
        finally:
            self._input_future = None

    def disable_input(self) -> None:
        try:
            self.query_one("#user-input", UserInput).disabled = True
        except NoMatches:
            pass

    def enable_input(self) -> None:
        try:
            w = self.query_one("#user-input", UserInput)
            w.disabled = False
            w.focus()
        except NoMatches:
            pass

    # ========== 输入框焦点管理 ==========

    def _disable_input_focus(self) -> None:
        """禁用输入框焦点，防止抢走键盘事件"""
        try:
            user_input = self.query_one("#user-input", UserInput)
            user_input.can_focus = False
        except NoMatches:
            pass

    def _restore_input_focus(self) -> None:
        """恢复输入框焦点"""
        try:
            user_input = self.query_one("#user-input", UserInput)
            user_input.can_focus = True
            user_input.focus()
        except NoMatches:
            pass

    # ========== 权限交互 ==========

    def on_permission_bar_selected(self, event: PermissionBar.Selected) -> None:
        if self._permission_future and not self._permission_future.done():
            self._permission_future.set_result(event.choice)

    # ========== 会话选择交互 ==========

    async def ask_permission_async(
        self,
        permission: str,
        target: str,
        description: str = "",
        args: Dict[str, Any] = None,
    ) -> str:
        # 更新权限信息到 permission-info（多行命令保持多行显示，第二行起缩进4个空格）
        try:
            info = self.query_one("#panel-permission-info", Static)
            # 多行命令：保持多行，第二行起缩进4个空格
            if target and '\n' in target:
                lines = target.split('\n')
                # 第一行：标题 + 第一行命令
                info_text = Text("🔐 权限请求: ")
                info_text.append(permission, style="bold cyan")
                info_text.append(" (")
                info_text.append(lines[0])
                info_text.append(")")
                # 后续行：缩进4个空格
                for line in lines[1:]:
                    info_text.append("\n")
                    info_text.append("    ")
                    info_text.append(line)
            else:
                # 单行命令：保持原有格式
                safe_target = target.replace('\n', '\\n') if target else ""
                info_text = Text("🔐 权限请求: ")
                info_text.append(permission, style="bold cyan")
                if safe_target:
                    info_text.append(" (")
                    info_text.append(safe_target)
                    info_text.append(")")
            info.update(info_text)
        except NoMatches:
            pass

        # 禁用输入框焦点，防止抢走键盘事件
        self._disable_input_focus()

        # 显示功能区和权限区域，聚焦选项栏
        try:
            panel = self.query_one("#panel-area")
            panel.display = True
            perm = self.query_one("#panel-permission")
            perm.display = True
            perm_bar = self.query_one("#panel-permission-bar", PermissionBar)
            perm_bar.reset()
            perm_bar.focus()
        except NoMatches:
            pass

        # 权限区域显示后，根据位置决定是否滚动到底部
        try:
            log = self.query_one("#output-log", RichLog)
            if self._output_at_bottom:
                log.scroll_end(animate=False)
        except NoMatches:
            pass

        loop = asyncio.get_running_loop()
        self._permission_future = loop.create_future()
        try:
            choice = await self._permission_future
        except asyncio.CancelledError:
            choice = "n"
        finally:
            self._permission_future = None

        # 隐藏权限区域，恢复焦点
        try:
            self.query_one("#panel-permission").display = False
            self._restore_input_focus()
        except NoMatches:
            pass

        # 检查是否还有其他可见的子区域，如果没有则隐藏整个功能区
        self._check_and_hide_panel()

        # 权限区域隐藏后，根据位置决定是否滚动到底部
        try:
            log = self.query_one("#output-log", RichLog)
            if self._output_at_bottom:
                log.scroll_end(animate=False)
        except NoMatches:
            pass

        choice_labels = {"y": "允许", "n": "拒绝", "a": "始终允许", "d": "始终禁止"}
        return choice

    def _check_and_hide_panel(self) -> None:
        """检查功能区是否有可见子区域，如果没有则隐藏整个功能区"""
        try:
            panel = self.query_one("#panel-area")
            # 检查各个子区域是否可见
            perm = self.query_one("#panel-permission")
            clarify = self.query_one("#panel-clarify")
            cmd_list = self.query_one("#panel-command-list")
            
            if (perm.display == False and 
                clarify.display == False and 
                cmd_list.display == False):
                panel.display = False
        except NoMatches:
            pass

    # ========== 用户澄清交互 ==========

    def on_clarify_bar_selected(self, event: ClarifyBar.Selected) -> None:
        if self._clarify_future and not self._clarify_future.done():
            self._clarify_future.set_result(event.value)

    async def ask_clarify_async(
        self,
        question: str,
        options: list,
        allow_input: bool = True,
    ) -> str:
        """用户澄清请求 - 显示问题、选项，支持自由输入"""
        # 更新澄清提示信息（问题已在输出区展示，功能区只显示操作提示）
        try:
            info = self.query_one("#panel-clarify-info", Static)
            hint = "选择选项" + ("或自由输入" if allow_input else "") + "，按 Enter 确认"
            info.update(Text.from_markup(f"[dim]{hint}[/dim]"))
        except NoMatches:
            pass

        # 禁用输入框焦点，防止抢走键盘事件
        self._disable_input_focus()

        # 配置并显示功能区和澄清区域
        try:
            panel = self.query_one("#panel-area")
            panel.display = True
            clarify = self.query_one("#panel-clarify")
            clarify.display = True
            clarify_bar = self.query_one("#panel-clarify-bar", ClarifyBar)
            clarify_bar.configure(options, allow_input)
            clarify_bar.focus()
        except NoMatches:
            pass

        # 滚动到底部（根据位置决定）
        try:
            log = self.query_one("#output-log", RichLog)
            if self._output_at_bottom:
                log.scroll_end(animate=False)
        except NoMatches:
            pass

        loop = asyncio.get_running_loop()
        self._clarify_future = loop.create_future()
        try:
            value = await self._clarify_future
        except asyncio.CancelledError:
            value = ""
        finally:
            self._clarify_future = None

        # 隐藏澄清区域，恢复焦点
        try:
            self.query_one("#panel-clarify").display = False
            self._restore_input_focus()
        except NoMatches:
            pass

        # 检查是否还有其他可见的子区域
        self._check_and_hide_panel()

        # 隐藏后根据位置决定是否滚动到底部
        try:
            log = self.query_one("#output-log", RichLog)
            if self._output_at_bottom:
                log.scroll_end(animate=False)
        except NoMatches:
            pass

        return value

    # ========== 状态栏 ==========

    def update_status_bar(
        self, model: str = "", agent: str = "",
        context_tokens: int = 0, context_max: int = 0, context_percent: float = 0.0,
        session_id: str = "",
    ) -> None:
        if model:
            self._status_model = model
        if agent:
            self._status_agent = agent
        if session_id:
            self._status_session_id = session_id
        self._context_tokens = context_tokens
        self._context_max = context_max
        self._context_percent = context_percent
        self._refresh_status_bar()

    def _refresh_status_bar(self) -> None:
        # 左侧：路径 + session_id
        left = Text()
        if SERVER_WORKING_PATH:
            left.append("Path: ", style=Styles.DIM)
            left.append(SERVER_WORKING_PATH, style=Styles.TEXT)
        if self._status_session_id:
            if SERVER_WORKING_PATH:
                left.append("  ", style=Styles.DIM)
            left.append("Session: ", style=Styles.DIM)
            left.append(self._status_session_id[:8], style=Styles.WARNING)
        # 右侧：agent + model + 上下文使用量
        right = Text()
        if self._status_agent:
            right.append("Mode: ", style=Styles.DIM)
            right.append(self._status_agent, style=Styles.TOOL_NAME)
        if self._status_model:
            if self._status_agent:
                right.append("  ", style=Styles.DIM)
            right.append("Model: ", style=Styles.DIM)
            right.append(self._status_model, style=Styles.SUCCESS)
        if self._context_max > 0:
            if self._status_agent or self._status_model:
                right.append("  ", style=Styles.DIM)
            # 根据百分比选择值颜色
            percent = self._context_percent
            if percent >= 80:
                val_style = Styles.ERROR
            elif percent >= 60:
                val_style = Styles.WARNING
            else:
                val_style = Styles.SUCCESS
            # 格式化 token 数（k 单位）
            ctx_str = self._format_tokens(self._context_tokens)
            max_str = self._format_tokens(self._context_max)
            right.append("Ctx: ", style=Styles.DIM)
            right.append(f"{ctx_str}/{max_str} ({percent:.0f}%)", style=val_style)
        try:
            self.query_one("#status-left", Static).update(left)
            self.query_one("#status-right", Static).update(right)
        except NoMatches:
            pass

    @staticmethod
    def _format_tokens(n: int) -> str:
        """格式化 token 数为简短形式：1234 -> 1.2k, 1234567 -> 1.2M"""
        if n >= 1_000_000:
            return f"{n / 1_000_000:.1f}M"
        elif n >= 1_000:
            return f"{n / 1_000:.1f}k"
        return str(n)

    def update_available_models(self, models: list):
        """更新可用模型列表"""
        try:
            user_input = self.query_one("#user-input", UserInput)
            user_input.set_available_models(models)
        except NoMatches:
            pass
